allcommands.ps1

<#
    .SYNOPSIS
 
    Add's ACLs to a folder.
 
    .DESCRIPTION
 
    Adds the acl to the specified folder - will also attempt to repair cannonical ordering issue
 
    .PARAMETER FolderPath
 
    Folder to set ACLs on.
 
    .PARAMETER User
 
    User to grant permission to.
 
    .PARAMETER Permission
 
    Permission to add
 
    .EXAMPLE
 
    Add-AclFolder -Folder ".\test" -User "hqcatalyst\dev.test" -Permission "Read"
#>

function Add-AclFolder {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateScript({
            if (!(Test-Path $_ -PathType Container)) {
                Write-DosMessage -Level "Fatal" -Message "FolderPath either $_ does not exist or is not a folder. Please enter valid folder path."
            }
            else {
                $true
            }
        })]
        [string] $FolderPath,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $User,
        [Parameter(Mandatory=$true)]
        [ValidateSet("Read", "Write")]
        [string] $Permission
    )

    Write-DosMessage -Level "Verbose" -Message "Adding ACL to give $Permission to $User on folder $FolderPath"
    $directoryAcl = (Get-Item $FolderPath).GetAccessControl("Access")
    $accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule($User, $Permission, "ContainerInherit,ObjectInherit", "None", "Allow")

    try{
        $directoryAcl.AddAccessRule($accessRule)
    }
    catch [System.InvalidOperationException]{
        Write-DosMessage -Level "Warning" -Message "Error attempting to add ACL to list on $FolderPath, attempting to repair"
        Repair-AclCanonicalOrder -Acl $directoryAcl -Path $FolderPath
        $directoryAcl.AddAccessRule($accessRule)
    }

    try{
        Write-DosMessage -Level "Verbose" -Message "Adding ACL to give $Permission to $User on folder $FolderPath"
        Set-Acl -Path $FolderPath $directoryAcl
    }
    catch [System.InvalidOperationException]{
        Write-DosMessage -Level "Warning" -Message "Error attempting to add ACL to $FolderPath, attempting to repair"
        Repair-AclCanonicalOrder -Acl $directoryAcl -Path $FolderPath
        Set-Acl -Path $FolderPath $directoryAcl
    }

}


function Assert-DependencySemVerRequirementsMet {
    [CmdletBinding()]
    param (
        [string]$ServiceName,
        [array]$DiscoveryServiceEntries,
        [array]$CurrentDependencies
    )

    $allDependenciesMet = $true

    foreach ($dependency in $CurrentDependencies ) {
        $installedDependencyBuildNumber = ($DiscoveryServiceEntries | Where-Object { $_.ServiceName -eq $dependency.serviceName }).BuildNumber

        $s1 = [SemVer]::New($installedDependencyBuildNumber)
        $s2 = [SemVer]::New($dependency.serviceBuildVersion)

        $dependencyMet = Assert-SatisfiesSemVer -SemVer1 $s1 -SemVer2 $s2
        if (!$dependencyMet) {
            Write-DosMessage -Level "Warning" -Message "$($dependency.serviceName) dependency not met. Expected $($dependency.serviceBuildVersion), existing $($installedDependencyBuildNumber)"
            $allDependenciesMet = $false
        }
        else {
            Write-DosMessage -Level "Information" -Message "$($dependency.serviceName) dependency met. Expected $($dependency.serviceBuildVersion), existing $($installedDependencyBuildNumber)"
        }
    }

    return $allDependenciesMet
}



function Get-PossibleConstraints {
    return @('~', '^', '=', 'v', '>', '<', '>=', '<=')
}

function Compare-Numbers {
    [CmdletBinding()]
    param (
        $A,
        $B
    )

    if ([int]$A -eq [int]$B) { return 0 }
    elseif ([int]$A -lt [int]$B) { return -1 }
    else { return 1 }
}

enum VersionIdentifier {
    Major
    Minor
    Patch
}

class SemVer {
    [string]$Constraint
    [string]$Major
    [string]$Minor
    [string]$Patch

    SemVer(
        [string]$Version
    ) {
        $this.Parse($Version)
    }

    [void]Parse([string]$Version) {
        $possibleOperators = Get-PossibleConstraints
        $splitVersion = @()

        if ($Version[0] -in $possibleOperators) {
            $versionSubstring = ""
            if (($Version[0] -eq '<') -or ($Version[0] -eq '>') -and ($Version[1] -eq '=')) {
                $this.Constraint = "$($Version[0])$($Version[1])"
                $versionSubstring = $Version.Substring(2, $Version.length - 2)
            }
            else {
                $this.Constraint = $Version[0]
                $versionSubstring = $Version.Substring(1, $Version.length - 1)
            }

            $splitVersion = $versionSubstring.split('.')
        }
        else {
            $splitVersion = $Version.split('.')
        }
        
        if ($null -ne $splitVersion[0]) {
            $this.Major = "$($splitVersion[0])"
        }
        if ($null -ne $splitVersion[1]) {
            $this.Minor = "$($splitVersion[1])"
        }
        if ($null -ne $splitVersion[2]) {
            $this.Patch = "$($splitVersion[2])"
        }
    }

    [int]Compare([SemVer] $other) {
        if (Compare-Numbers -A $this.Major -B $other.Major) {
            return Compare-Numbers -A $this.Major -B $other.Major
        }
        elseif (Compare-Numbers -A $this.Minor -B $other.Minor) {
            return Compare-Numbers -A $this.Minor -B $other.Minor
        }
        else {
            return Compare-Numbers -A $this.Patch -B $other.Patch
        }
    }

    [string]ToString() {
        return "$($this.Major).$($this.Minor).$($this.Patch)"
    }

    [void]Increment([VersionIdentifier] $Identifier) {
        switch ($Identifier) {
            Major {
                # 1.2.3 -> 2.0.0
                $this.Major = (+$this.Major + 1).ToString()
                $this.Minor = "0"
                $this.Patch = "0"
            }
            Minor {
                # 1.2.3 -> 1.3.0
                $this.Minor = (+$this.Minor + 1).ToString()
                $this.Patch = "0"
            }
            Patch {
                # 1.2.3 -> 1.2.4
                $this.Patch = (+$this.Patch + 1).ToString()
            }
        }
    }
}

function Assert-Equal {
    [CmdletBinding()]
    param (
        [SemVer]$SemVer1,
        [SemVer]$SemVer2
    )

    # $SemVer2
    # 1 >=1.0.0 <2.0.0
    # 1.0 >=1.0.0 <1.1.0.
    # 1.0.0 1.0.0 exact

    $lessThanSemVer = [SemVer]::new($SemVer2.ToString())

    if ($SemVer2.Patch) {
        return $SemVer1.Compare($SemVer2) -eq 0 
    }
    elseif ($SemVer2.Minor) {
        $lessThanSemVer.Increment([VersionIdentifier]::Minor)
    }
    else {
        $lessThanSemVer.Increment([VersionIdentifier]::Major)
    }

    return (Assert-GreaterThanEqual $SemVer1 $SemVer2) -and (Assert-LessThan $SemVer1 $lessThanSemVer)
}

function Assert-LessThan {
    [CmdletBinding()]
    param (
        [SemVer]$SemVer1,
        [SemVer]$SemVer2
    )

    return $SemVer1.Compare($SemVer2) -lt 0
}

function Assert-LessThanEqual {
    [CmdletBinding()]
    param (
        [SemVer]$SemVer1,
        [SemVer]$SemVer2
    )

    $lteSemVer = [SemVer]::new($SemVer2.ToString())

    if ($SemVer2.Patch) {
        return $SemVer1.Compare($SemVer2) -le 0
    }
    elseif ($SemVer2.Minor) {
        $lteSemVer.Increment([VersionIdentifier]::Minor)
    }
    else {
        $lteSemVer.Increment([VersionIdentifier]::Major)
    }

    return $SemVer1.Compare($lteSemVer) -lt 0
}

function Assert-GreaterThan {
    [CmdletBinding()]
    param (
        [SemVer]$SemVer1,
        [SemVer]$SemVer2
    )

    $gtSemVer = [SemVer]::new($SemVer2.ToString())

    if ($SemVer2.Patch) {
        return $SemVer1.Compare($SemVer2) -gt 0
    }
    elseif ($SemVer2.Minor) {
        $gtSemVer.Increment([VersionIdentifier]::Minor)
    }
    else {
        $gtSemVer.Increment([VersionIdentifier]::Major)
    }

    return $SemVer1.Compare($gtSemVer) -ge 0
}

function Assert-GreaterThanEqual {
    [CmdletBinding()]
    param (
        [SemVer]$SemVer1,
        [SemVer]$SemVer2
    )

    return $SemVer1.Compare($SemVer2) -ge 0
}

function Assert-TildeConstraint {
    [CmdletBinding()]
    param (
        [SemVer]$SemVer1,
        [SemVer]$SemVer2
    )

    # $SemVer2
    # ~1: >=1.0.0 <2.0.0.
    # ~1.1: >=1.1.0 <1.2.0.
    # ~1.1.1: >=1.1.1 <1.2.0.

    $lessThanSemVer = [SemVer]::new($SemVer2.ToString())

    if ($SemVer2.Patch -or $SemVer2.Minor) {
        # ~1.1.1: >=1.1.1 <1.2.0.
        # ~1.1: >=1.1.0 <1.2.0.
        $lessThanSemVer.Increment([VersionIdentifier]::Minor)
    }
    else {
        # ~1: >=1.0.0 <2.0.0.
        $lessThanSemVer.Increment([VersionIdentifier]::Major)
    }

    return (Assert-GreaterThanEqual $SemVer1 $SemVer2) -and (Assert-LessThan $SemVer1 $lessThanSemVer)
}

function Assert-CaretConstraint {
    [CmdletBinding()]
    param (
        [SemVer]$SemVer1,
        [SemVer]$SemVer2
    )

    # $SemVer2
    # ^1 >=1.0.0 <2.0.0
    # ^1.1 >=1.1.0 <2.0.0
    # ^1.1.1 >=1.1.1 <2.0.0

    # ^0 >=0.0.0 <1.0.0
    # ^0.0 >=0.0.0 <0.1.0
    # ^0.0.0 >=0.0.0 <0.0.1

    $lessThanSemVer = [SemVer]::new($SemVer2.ToString())

    if (($SemVer2.Patch -eq 0) -and ($SemVer2.Minor -eq 0) -and ($SemVer2.Major -eq 0)) {
        # ^0.0.0 >=0.0.0 <0.0.1
        $lessThanSemVer.Increment([VersionIdentifier]::Patch)
    }
    elseif (($SemVer2.Minor -eq 0) -and ($SemVer2.Major -eq 0)) {
        # ^0.0 >=0.0.0 <0.1.0
        $lessThanSemVer.Increment([VersionIdentifier]::Minor)
    }
    else {
        # ^1 >=1.0.0 <2.0.0
        # ^0 >=0.0.0 <1.0.0
        $lessThanSemVer.Increment([VersionIdentifier]::Major)
    }

    return (Assert-GreaterThanEqual $SemVer1 $SemVer2) -and (Assert-LessThan $SemVer1 $lessThanSemVer)
}

function Assert-XRangeConstraint {
    [CmdletBinding()]
    param (
        [SemVer]$SemVer1,
        [SemVer]$SemVer2
    )

    # $SemVer2
    # * any
    # 1.* >=1.0.0 <2.0.0
    # 1.1.* >=1.1.0 <1.2.0

    $lessThanSemVer = [SemVer]::new($SemVer2.ToString())
    $semVer2ReplaceStar = [SemVer]::new($SemVer2.ToString())

    if ($SemVer2.Patch -eq '*') {
        # 1.1.* >=1.1.0 <1.2.0
        $lessThanSemVer.Increment([VersionIdentifier]::Minor)
        $semVer2ReplaceStar.Patch = "0"
    }
    elseif ($SemVer2.Minor -eq '*') {
        # 1.* >=1.0.0 <2.0.0
        $lessThanSemVer.Increment([VersionIdentifier]::Major)
        $semVer2ReplaceStar.Patch = "0"
        $semVer2ReplaceStar.Minor = "0"
    }
    else {
        # *
        return $true
    }

    return (Assert-GreaterThanEqual $SemVer1 $semVer2ReplaceStar) -and (Assert-LessThan $SemVer1 $lessThanSemVer)
}

function Assert-SatisfiesSemVer {
    [CmdletBinding()]
    param (
        [SemVer]$SemVer1,
        [SemVer]$SemVer2
    )

    if ($SemVer2.Constraint) {
        Switch ($SemVer2.Constraint) {
            '=' {
                return Assert-Equal -SemVer1 $SemVer1 -SemVer2 $SemVer2
            }
            'v' {
                return Assert-Equal -SemVer1 $SemVer1 -SemVer2 $SemVer2
            }
            '<' {
                return Assert-LessThan -SemVer1 $SemVer1 -SemVer2 $SemVer2
            }
            '<=' {
                return Assert-LessThanEqual -SemVer1 $SemVer1 -SemVer2 $SemVer2
            }
            '>' {
                return Assert-GreaterThan -SemVer1 $SemVer1 -SemVer2 $SemVer2
            }
            '>=' {
                return Assert-GreaterThanEqual -SemVer1 $SemVer1 -SemVer2 $SemVer2
            }
            '~' {
                return Assert-TildeConstraint -SemVer1 $SemVer1 -SemVer2 $SemVer2
            }
            '^' {
                return Assert-CaretConstraint -SemVer1 $SemVer1 -SemVer2 $SemVer2
            }
        }
    }

    if (($SemVer2.Major -eq '*') -or ($SemVer2.Minor -eq '*') -or ($SemVer2.Patch -eq '*')) {
        return Assert-XRangeConstraint -SemVer1 $SemVer1 -SemVer2 $SemVer2
    }

    return Assert-Equal -SemVer1 $SemVer1 -SemVer2 $SemVer2
}


<#
    .SYNOPSIS
 
    Validates ConfigStore object to be sure configuration values will be returned accuratelly and appropriately.
 
    .DESCRIPTION
 
    Checks if config store object properties are provided with valid values.
 
    .PARAMETER ConfigStore
 
    Accepts a powershell hashtable or dictionary with key value pairs following the example, $configStore = @{Type = "File"; Format = "XML"; Path = "C:\Path\To\File"}.
 
    .EXAMPLE
 
    Confirm-ConfigStore -ConfigStore $configHashtable
#>


function Confirm-ConfigStore {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [hashtable] $ConfigStore
    )

    Write-DosMessage -Level "Debug" -Message "Validating Config Store object."

    $isValid = $true

    $isValid = Confirm-GenericConfigStore -ConfigStore $ConfigStore

    if ($ConfigStore.Type -eq "File") {
        $isValid = Confirm-FileConfigStore -ConfigStore $ConfigStore
    }

    if ($ConfigStore.Type -eq "External") {
        $isValid = Confirm-ExternalConfigStore -ConfigStore $ConfigStore
    }

    if ($isValid -eq $true) {
        Write-DosMessage -Level "Debug" -Message "Config Store is valid."
    }
    
    return $isValid
}

function Confirm-GenericConfigStore {
    param (
        $ConfigStore
    )

    $isValid = $true

    $validConfigTypes = @("File", "External")

    if (!($validConfigTypes -contains $ConfigStore.Type)){
        Write-DosMessage -Level "Warning" -Message "$($ConfigStore.Type) is not a supported configuration type. Supported types include, $validConfigTypes"
        $isValid = $false
    }

    $validConfigFormats = @("XML", "AzureTable")

    if (!($validConfigFormats -contains $ConfigStore.Format)){
        Write-DosMessage -Level "Warning" -Message "$($ConfigStore.Format) is not a supported configuration format. Supported formats include, $validConfigFormats"
        $isValid = $false
    }

    if ([string]::IsNullOrEmpty($ConfigStore.Type)){
        Write-DosMessage -Level "Warning" -Message "ConfigStore 'Type' attribute is missing. Please provide a valid value."
        $isValid = $false
    }   

    if ([string]::IsNullOrEmpty($ConfigStore.Format)){
        Write-DosMessage -Level "Warning" -Message "ConfigStore 'Format' attribute is missing. Please provide a valid value."
        $isValid = $false
    }

    return $isValid
}

function Confirm-FileConfigStore {
    param (
        $ConfigStore
    )

    $isValid = $true
    $validFileConfigFormats = @("XML")

    if (!($validFileConfigFormats -contains $ConfigStore.Format)){
        Write-DosMessage -Level "Warning" -Message "$($ConfigStore.Format) is not a supported 'File' type configuration format. Supported formats include, $validFileConfigFormats"
        $isValid = $false
    }

    if ([string]::IsNullOrEmpty($ConfigStore.Path)){
        Write-DosMessage -Level "Warning" -Message "ConfigStore 'Path' attribute cannot be empty when ConfigStore type is 'File'. Please provide an appropriate path."
        $isValid = $false
    }

    if (![string]::IsNullOrEmpty($ConfigStore.Path) -and !(Test-Path $ConfigStore.Path)){
        Write-DosMessage -Level "Warning" -Message "Path $($ConfigStore.Path) does not exist or user does not have access. Please enter a valid path in the ConfigStore object."
        $isValid = $false
    }

    if (![string]::IsNullOrEmpty($ConfigStore.Path) -and !(Test-Path $ConfigStore.Path -PathType Leaf)){
        Write-DosMessage -Level "Warning" -Message "Path $($ConfigStore.Path) is not a file. Please enter a valid path in the ConfigStore object."
        $isValid = $false
    }

    return $isValid
}

function Confirm-ExternalConfigStore {
    param (
        $ConfigStore
    )

    $isValid = $true
    $validExternalConfigFormats = @("AzureTable")

    if (!($validExternalConfigFormats -contains $ConfigStore.Format)){
        Write-DosMessage -Level "Warning" -Message "$($ConfigStore.Format) is not a supported 'External' type configuration format. Supported formats include, $validFileConfigFormats"
        $isValid = $false
    }

    if ([string]::IsNullOrEmpty($ConfigStore.Uri)){
        Write-DosMessage -Level "Warning" -Message "ConfigStore 'Uri' attribute cannot be empty when ConfigStore type is 'External'. Please provide an appropriate Uri."
        $isValid = $false
    }

    return $isValid
}


function Confirm-IsBoolean {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $name,
        [Parameter(Mandatory = $true)]
        [string] $type,
        $value
    )
    $result = @{check = "IsBoolean"; name = $name; type = $type; value = $value }

    if ($value -isnot [boolean]) {
        $result.Add("errorFlag", 1)
        $result.Add("level", "Fatal")
        $result.Add("message", "You must specify a valid boolean value (ex: `$true or `$false) for the ""$name"" configuration.")
    }
    else {
        $result.Add("errorFlag", 0)
    }
    return (New-CheckResult @result)
}



function Confirm-IsNotNull {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $name,
        [Parameter(Mandatory = $true)]
        [string] $type,
        $value
    )
    $result = @{check = "IsNotNull"; name = $name; type = $type; value = $value}

    if ([string]::IsNullOrEmpty($value)) {
        $result.Add("errorFlag", 1)
        $result.Add("level", "Fatal")
        $result.Add("message", "A null or empty ""$name"" value was found as a configuration.")
    }
    else {
        $result.Add("errorFlag", 0)
    }
    return (New-CheckResult @result)
}



function Confirm-IsValidConnection {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $name,
        [Parameter(Mandatory = $true)]
        [string] $type,
        $value
    )
    $result = @{check = "IsValidConnection"; name = $name; type = $type; value = $value}

    if ($value -is [Hashtable] -and $value.ContainsKey("sqlConnection") -and $value.ContainsKey("sqlTestCommand")) {
        # confirm string is a valid
        try {
            $connection = New-Object System.Data.SqlClient.SQLConnection($value.sqlConnection)
            # confirm access to connection
            try {
                $connection.Open()
                try {
                    $command = New-Object System.Data.SqlClient.SqlCommand($value.sqlTestCommand, $connection)
                    $out = $command.ExecuteReader()
                    if (($out | Measure-Object).Count -eq 0) {
                        throw
                    }
                    $result.Add("errorFlag", 0)
                }
                catch {
                    $result.Add("errorFlag", 1)
                    $result.Add("level", "Fatal")
                    $result.Add("message", "Test sql command failed '$($value.sqlTestCommand)' please check database connection settings.")
                }
            }
            catch {
                $result.Add("errorFlag", 1)
                $result.Add("level", "Fatal")
                $result.Add("message", "Could not connect to '$($value.sqlConnection)' please check database connection settings.")
            }
            finally {
                $connection.Close();
            }
        }
        catch {
            $result.Add("errorFlag", 1)
            $result.Add("level", "Fatal")
            $result.Add("message", "Invalid connection string '$($value.sqlConnection)'.")
        }
    }
    else {
        $result.Add("errorFlag", 1)
        $result.Add("level", "Fatal")
        $result.Add("message", "Valid connection string requires a hashtable with both sqlConnection and sqlTestCommand keys (example: connection = @{sqlConnection=""Data Source=<server>;Initial Catalog=<database>;Integrated Security=True;"";sqlTestCommand=""SELECT <test> FROM <schema><table>""}")
    }
    return (New-CheckResult @result)
}



function Confirm-IsValidDir {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $name,
        [Parameter(Mandatory = $true)]
        [string] $type,
        [string] $value
    )
    $result = @{check = "IsValidDir"; name = $name; type = $type; value = $value }

    if (!(Test-Path (Split-Path $value -Parent))) {
        $result.Add("errorFlag", 1)
        $result.Add("level", "Fatal")
        $result.Add("message", """$value"" does not have a valid directory. Please specify a valid directory for the ""$name"" configuration")
    }
    else {
        $result.Add("errorFlag", 0)
    }
    return (New-CheckResult @result)
}



function Confirm-IsValidEndpoint {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $name,
        [Parameter(Mandatory = $true)]
        [string] $type,
        $value
    )
    $result = @{check = "IsValidEndpoint"; name = $name; type = $type; value = $value }

    try {
        Invoke-WebRequest -Uri $value -Method GET -UseDefaultCredentials -UseBasicParsing
        $result.Add("errorFlag", 0)
    }
    catch [System.Net.WebException] {
        $result.Add("errorFlag", 1)
        $result.Add("level", "Fatal")
        $result.Add("message", "There was an error communicating with the configured $name endpoint. Request: $value. Status Code: $($_.Exception.Response.StatusCode.value__). Message: $($_.Exception.Response.StatusDescription)")
    }
    return (New-CheckResult @result)
}



function Confirm-IsValidPath {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $name,
        [Parameter(Mandatory = $true)]
        [string] $type,
        [string] $value
    )
    $result = @{check = "IsValidPath"; name = $name; type = $type; value = $value }

    if (!(Test-Path $value)) {
        $result.Add("errorFlag", 1)
        $result.Add("level", "Fatal")
        $result.Add("message", """$value"" is not a valid path. Please specify a valid path for the ""$name"" configuration")
    }
    else {
        $result.Add("errorFlag", 0)
    }
    return (New-CheckResult @result)
}



function Confirm-IsValidValue {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string[]] $validateSet,
        [Parameter(Mandatory = $true)]
        [string] $name,
        [Parameter(Mandatory = $true)]
        [string] $type,
        [string] $value
    )
    $result = @{check = "IsValidValue"; name = $name; type = $type; value = $value }

    if ($validateSet -notcontains $value) {
        $result.Add("errorFlag", 1)
        $result.Add("level", "Fatal")
        $result.Add("message", """$value"" is not a valid value. Please specify one of these valid configuration values ""$($validateSet -join ", ")""")
    }
    else {
        $result.Add("errorFlag", 0)
    }
    return (New-CheckResult @result)
}


function Get-AzureTableStorageInputsFromUri {
    param (
        $storageUri
    )

    $uriString = [System.Uri]$storageUri
    # Host returns "<stroage_account>.table.core.windows.net"
    $storageAccountName = $uriString.Host.Substring(0,$uriString.Host.IndexOf("."))
    Write-DosMessage -Level "Debug" -Message "Extracting storage account name from storage URI. Storage Account Name: $storageAccountName"

    # AbsolutePath returns "/<table_name>"
    $tableName = $uriString.AbsolutePath.Substring(1)
    Write-DosMessage -Level "Debug" -Message "Extracting table name from storage URI. Table Name: $tableName"

    # Query returns everyting after "?" in the url
    $sasToken = $uriString.Query
    Write-DosMessage -Level "Debug" -Message "Extracting SAS token from storage URI."

    return @{ storageSas = $sasToken; tableName = $tableName; storageAccountName = $storageAccountName }
}


<#
    .SYNOPSIS
 
    Private function for the Get-DosConfigScopes, but just for the azuretable type.
 
    .DESCRIPTION
 
    Private function for the Get-DosConfigScopes, but just for the azuretable type.
 
#>

function Get-DosConfigScopesAzureTable
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [hashtable] $ConfigStore
    )


    Write-DosMessage -Level "Verbose" -Message "Grabbing the scopes from the external config store.."
    try {
        $values = Get-DosConfigValuesAzureTable -ConfigStore $ConfigStore
        return $values.scope | select-object -unique
    }
    catch {
        Write-DosMessage -Level "Fatal" -Message "Error grabbing the scopes from the external config store. Exception: $($_.Exception)"
        return $null
    }

}


<#
    .SYNOPSIS
 
    Private function for the Get-DosConfigScopes, but just for the xml type.
 
    .DESCRIPTION
 
    Private function for the Get-DosConfigScopes, but just for the xml type.
 
#>

function Get-DosConfigScopesXml
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [hashtable] $ConfigStore
    )


    Write-DosMessage -Level "Verbose" -Message "Attempting to parse XML content from $installConfigPath."
    try {
        $installConfigXml = [xml](Get-Content $ConfigStore.Path)
    }
    catch {
        Write-DosMessage -Level "Warning" -Message "Error parsing XML content from $installConfigPath. Exception: $($_.Exception)"
        return $null
    }

    $scopeArray = $installConfigXml.installation.settings.scope.name | select-object -unique

    return $scopeArray

}


<#
    .SYNOPSIS
 
    Private function for the Get-DosConfigValues, but just for the AzureTable type.
 
    .DESCRIPTION
 
    Private function for the Get-DosConfigValues, but just for the AzureTable type.
 
#>

function Get-DosConfigValuesAzureTable {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        $ConfigStore,
        $Scope
    )

    $filter = ""
    if ($Scope) {
        # parse requested scope to be returned
        Write-DosMessage -Level "Debug" -Message "Adding $Scope scope filter"
        $filter = "`$filter=(PartitionKey eq '$Scope')"
    }

    $header = @{
        Accept = 'application/json;odata=nometadata'
    }

    try {
        $oldProgressPreference = $progresspreference
        $global:progressPreference = 'silentlyContinue'
        $progresspreference = 'SilentlyContinue'
        $result = Invoke-WebRequest -Method GET -Uri "$($ConfigStore.Uri)&$filter" -Headers $header -UseBasicParsing
        $oldProgressPreference = $oldProgressPreference
        $global:progressPreference = $oldProgressPreference
        $progresspreference = $oldProgressPreference
    }
    catch {
        Write-DosMessage -Level "Fatal" -Message "Failed to retrieve configuration values from $($configStore.Type) configstore. Excetion $($_.Exception)."
    }

    $configValues = ($result.Content | ConvertFrom-Json).value
    <#
     
    PartitionKey RowKey Timestamp Value
    ------------ ------ --------- -----
    common clientEnvironment 2020-09-23T23:30:28.8063443Z
    common clientName 2020-09-23T23:08:49.5847473Z dfma
    common testVariable 2020-09-23T23:28:52.2270564Z test123
     
    #>


    #convert the array of PSCustomObjects to an array of PSCustomObjects with names that we want.
    if([string]::IsNullOrEmpty($Scope)){
        #return an array of pscustomobjects
        $returnArray = @()
        foreach($row in $configValues){
            $myCustomRow=  [PSCustomObject] @{
                Name = $row.RowKey
                Value = $row.Value
                Scope = $row.PartitionKey
                Timestamp = $row.Timestamp
            }
            $returnArray += $myCustomRow
        }
        return $returnArray

    } else {
        #return a hashtable
        $returnHashtable = @{}
        foreach($row in $configValues){
            $returnHashtable[$row.RowKey] = $row.Value
        }

        return $returnHashtable
    }

}


<#
    .SYNOPSIS
 
    Private function for the Get-DosConfigValues, but just for the Xml type.
 
    .DESCRIPTION
 
    Private function for the Get-DosConfigValues, but just for the Xml type.
 
#>


function Get-DosConfigValuesXml
{
    param(
        [Parameter(Mandatory=$true)]
        [string] $ConfigSection,
        [ValidateScript({
            if (!(Test-Path $_)) {
                Write-DosMessage -Level "Fatal" -Message "Path $_ does not exist. Please enter valid path to the install.config."
            }
            if (!(Test-Path $_ -PathType Leaf)) {
                Write-DosMessage -Level "Fatal" -Message "Path $_ is not a file. Please enter a valid path to the install.config."
            }
            return $true
        })]  
        [string] $InstallConfigPath = "install.config"
    )

    Write-DosMessage -Level "Verbose" -Message "Attempting to parse XML content from $installConfigPath."
    try {
        $installConfigXml = [xml](Get-Content $installConfigPath)
    }
    catch {
        Write-DosMessage -Level "Warning" -Message "Error parsing XML content from $installConfigPath. Exception: $($_.Exception)"
        return $null
    }
    
    Write-DosMessage -Level "Verbose" -Message "Searching XML content for $configSection scoped values."
    $sectionSettings = $installConfigXml.installation.settings.scope | Where-Object {$_.name -eq $configSection}

    if($null -eq $sectionSettings){
        Write-DosMessage -Level "Warning" -Message "The '$ConfigSection' scope doesn't exist in '$installConfigPath'."
        return $null
    }

    $installationSettings = @{}

    Write-DosMessage -Level "Verbose" -Message "Reading scoped values if they are not null or empty."
    foreach($variable in $sectionSettings.variable){
        if($variable.name){
            $installationSettings.Add($variable.name, $variable.value)
        }
    }

    if ($installationSettings.Count -eq 0){
        Write-DosMessage -Level "Warning" -Message "There were no configuration values provided in '$ConfigSection' scope."
    }

    return $installationSettings
}


<#
    .SYNOPSIS
 
    Checks Registry for .net core
 
    .DESCRIPTION
 
    Checks the array of .net core versions
 
#>

function Get-DotNetCoreVersion {
    param (
        [PSCustomObject] $Value,
        [PSCustomObject] $Registry
    )
    if($Registry) {
        $item = $null
        ForEach ($item in $Registry) {
            if($item.DisplayVersion -like "$($Value.softwareVersion)*") {
                Write-Host "Registry:"$item.DisplayVersion, " Manifest:"$Value.softwareVersion
                break
            } 
        }
        It "dependent software $($Value.softwareName) $($Value.softwareVersion) version check" {
            $item.DisplayVersion | Should -BeLike "$($Value.softwareVersion)*"
        }
    }
}


function Get-DotNetVersion {
    param (
        [PSCustomObject] $Value
    )
    $netVersion = @{
        378389 = [version]'4.5'
        378675 = [version]'4.5.1'
        378758 = [version]'4.5.1'
        379893 = [version]'4.5.2'
        393295 = [version]'4.6'
        393297 = [version]'4.6'
        394254 = [version]'4.6.1'
        394271 = [version]'4.6.1'
        394802 = [version]'4.6.2'
        394806 = [version]'4.6.2'
        460798 = [version]'4.7'
        460805 = [version]'4.7'
        461308 = [version]'4.7.1'
        461310 = [version]'4.7.1'
        461808 = [version]'4.7.2'
        461814 = [version]'4.7.2'
        528040 = [version]'4.8'
        528049 = [version]'4.8'
    }
    $netVersionArray = @()
    foreach ($item in $netVersion.GetEnumerator()) {
        #If the variable software i.e '4.6.2' is equal to the Value of '4.6.2' in the Hash table then set the
        #variable softwareConverted to the value of the Key or i.e '394806''
        if($Value.softwareVersion -eq $item.Value) {
            #Append to array as there are multiple DWORDs for each Value of our Table
            $netVersionArray += $item.Key
        } else {
            #Do Nothing (currently looping through entire Hash Table to make sure we dont miss anything)
            continue
        }
    }
    #This looks directly into the registry and compares against our netVersionArray to make sure the requested .NET version matches the DWORD inside our Windows registry
    $itemProperty = Get-ItemProperty -Path "HKLM:\Software\Microsoft\NET Framework Setup\NDP\v4\full\*" | 
        Where-Object { $_.Release -ige $netVersionArray[0] -Or $_.Release -ige $netVersionArray[1] } | 
        Select-Object Release
    $releaseProperty = [string]$itemProperty
    $dword = $releaseProperty.Replace("@{Release=", "").Replace("}", "")
    Write-Host "Registry:"$dword, " Manifest:"$Value.softwareVersion
    $versionNumber = $null
    foreach ($item in $netVersion.GetEnumerator()) {
            if($dword -eq $item.Key) {
                    $versionNumber = $item.Value
                    break
            }
        }
    It ".net dependent software version check" {
        $versionNumber| Should -Not -BeNullOrEmpty
        $versionNumber | Should -BeGreaterOrEqual $Value.softwareVersion
    }
}


<#
    .SYNOPSIS
 
    Retrieves a list of applications from the windows registry.
 
    .DESCRIPTION
 
    Scans the windows registry for installed applications and returns a list of summary objects.
 
    .EXAMPLE
 
    $x = Get-InstalledApps ()
 
    .OUTPUTS
 
    Returns an array of registered applications. Each item in the array contains the application's DisplayName, Publisher, InstallDate, DisplayVersion and UninstallString
    32 Bit NOTE: IF this function is called from a 32 bit process, the apps returned may differ from the list when called fomr a 64 bit process!
        This is becuase microsfot redirects 32 bit apps in the registry.
        See: https://docs.microsoft.com/en-us/windows/desktop/winprog64/registry-reflection
         
#>


function Get-InstalledApps
{
    if (![Environment]::Is64BitProcess) {
        $regpath = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'
    }
    else {
        $regpath = @(
            'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'
            'HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
        )
    }
    Get-ItemProperty $regpath | .{process{if($_.DisplayName -and $_.UninstallString) { $_ } }} | Select DisplayName, Publisher, InstallDate, DisplayVersion, UninstallString |Sort DisplayName
}



<#
.SYNOPSIS
Creates a new IntegrationServices object
 
.DESCRIPTION
Instantiates an IntegrationServices object using a specified connection string
 
.PARAMETER ConnectionString
Connection string to target Integration Services
 
.INPUTS
None. You cannot pipe objects to Get-IntegrationServices.
 
.OUTPUTS
Integration Services object
 
.EXAMPLE
PS> Get-IntegrationServices
#>

function Get-IntegrationServices {
    [cmdletbinding(SupportsShouldProcess=$true)]
    [OutputType()]
    param([parameter(Mandatory=$false)][string]$ConnectionString = 'Data Source=localhost;Initial Catalog=EDWAdmin;Integrated Security=True')

    $connection = New-Object Data.SqlClient.SqlConnection $ConnectionString

    if ($pscmdlet.ShouldProcess($CatalogName, "Provisioning SSIS catalog")) {
        $connection.Open()
        $integrationServices = New-Object "Microsoft.SqlServer.Management.IntegrationServices.IntegrationServices" $connection  
    }

    return $integrationServices
}


function Get-SqlServerVersion {
    $sqlVersionDict = @{
        13 = 2016
        11 = 2012
    }
    
    $sqlVersion = Get-ItemProperty HKLM:\SOFTWARE\Microsoft\MSSQLServer\MSSQLServer\CurrentVersion | Select-Object -ExpandProperty "CurrentVersion"
    $sqlVersion = [int]$sqlVersion.split('.')[0]

    $currentSqlVersion = $sqlVersionDict[$sqlVersion]

    if ($null -eq $currentSqlVersion) {
        Write-DosMessage -Level 'Error' -Message 'SQL Server Version not in Dictionary'
    }

    return $currentSqlVersion
}


function Invoke-DependentSoftwareCheck {
    param (
        [array] $Data
    )
    Describe "DependentSoftwareCheck" {
        ForEach ($value in $Data) {
            # Continue if sqlVersion is not specified or is different than sqlVersion of machine or if the $value is $null
            if ($null -eq $value -or ($value.PSObject.Properties['sqlVersion'] -and $value.sqlVersion -ne $(Get-SqlServerVersion))) {
                continue
            }
            $w64 = Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | where-Object DisplayName -like "*$($value.softwareName)*"
            $w32 = Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*  | where-Object DisplayName -like "*$($value.softwareName)*"
            if ($value.softwareName -like "*.net framework*") {
                Get-DotNetVersion $value
            }  elseif ($value.softwareName -like "*.net core*" -and $w64){
                Get-DotNetCoreVersion $value $w64
            } elseif ($value.softwareName -like "*.net core*" -and $w32) {
                Get-DotNetCoreVersion $value $w32
            } else {  
                if($w64) {
                    Get-RegistryAndLocationCheck $w64 $value
                } elseif ($w32) {
                    Get-RegistryAndLocationCheck $w32 $value
                } else {
                    Write-Host "Software not found in Registry - Manifest Path: $($value.softwareLocation)"
                    It "Dependent software $($value.softwareName) $($value.softwareVersion) location exists" {
                        $value.softwareLocation | Should exist 
                    }
                }
            }
        }
    }
}

function Get-RegistryAndLocationCheck {
    param (
        [PSCustomObject] $Registry,
        [PSCustomObject] $Value
    ) 
    if ($Registry) {
        Write-Host "Registry:"$Registry.DisplayName," Manifest:"$Value.softwareName
        It "Dependent software $($Value.softwareName) $($Value.softwareVersion) exists in registry" {
            $Registry.DisplayName | Should -Match "$($Value.softwareName)*"
        }   
        if ($Value.versionCheckType -eq 'exact') {
            Write-Host "Registry:"$Registry.DisplayVersion, " Manifest:"$Value.softwareVersion
            It "Dependent software $($Value.softwareName) $($Value.softwareVersion) version check" {
                $Registry.DisplayVersion  | Should -BeExactly $Value.softwareVersion
            }
        } elseif ($Value.versionCheckType -eq 'min') {
            Write-Host "Registry:"$Registry.DisplayVersion, " Manifest:"$Value.softwareVersion
            It "Dependent software $($Value.softwareName) $($Value.softwareVersion) version check" {
                $Registry.DisplayVersion  | Should -BeGreaterOrEqual $Value.softwareVersion
            }
        }
    } 
} 


<#
    .SYNOPSIS
    Checks whether the OS Version from a manifest json file is equal to that on the machine this is being ran on
 
    .DESCRIPTION
 
    .PARAMETER Data
    Accepts an array of OS Version checktype objects that have been converted from JSON
#>


function Invoke-OsVersionCheck {
    param (
        [array] $AllowedVersions
    )

    $currentOsVersion = (Get-ComputerInfo -Property osname).osname
    Describe "Os Version Check" {
        $matchedOSVersion = $null

        ForEach ($value in $AllowedVersions) {
            if ($currentOsVersion -like "*$value*") {
                $matchedOSVersion = $value
                break
            }
        }

        It "'$currentOsVersion' is an allowed OS" {
            $matchedOSVersion | Should -Not -BeNullOrEmpty
            $currentOsVersion | Should -BeLike "*$matchedOSVersion*"
        }
    }
}


<#
    .SYNOPSIS
 
    Checks whether the Powershell Version from a manifest json file is equal to that on the machine this is being ran on
 
    .DESCRIPTION
 
 
    .PARAMETER Data
 
    Accepts an array of powershell objects that have been converted from JSON
 
 
#>


function Invoke-PowershellVersionCheck {
    param (
        [array] $Data
    )
    $currentVersion = "$($psversiontable.psversion.major).$($psversiontable.psversion.minor)"
    Write-Host "Powershell version is $currentVersion)"
    Describe "PowershellVersionCheck" {
        ForEach ($value in $Data) {
            if ($null -eq $value) {
                continue
            }
             
            #process check
            if ($value.versionCheckType -eq 'min'){
                It "Version of powershell minimum version $($value.powershellVersion)" {
                    $currentVersion | Should -BeGreaterOrEqual $value.powershellVersion
                }
            } elseif ($value.versionCheckType -eq 'exact') {
                It "Version of powershell exactly $($value.powershellVersion)" {
                    $currentVersion | Should -BeExactly $value.powershellVersion
                }
            }
        }
    }
}


<#
    .SYNOPSIS
 
    Checks whether the Windows Features from a manifest json file are installed and enabled on the executing environmnet
 
    .DESCRIPTION
 
 
    .PARAMETER Data
 
    Accepts an array of powershell objects that have been converted from JSON
#>


function Invoke-WindowsFeatureCheck {
    param (
        [array] $Data
    )

    $currentOsVersion = (Get-ComputerInfo -Property osname).osname
    $amIAServer = $currentOsVersion.StartsWith("Microsoft Windows Server")
    $amIWindowsTen = $currentOsVersion.StartsWith("Microsoft Windows 10")

    Describe "Windows Feature Check" {
        ForEach ($value in $Data) {
            if ($amIAServer) {
                if ($value.os -like "*Server*") {
                    ForEach ($feature in $value.featureList) {
                        It "'$feature' windows feature is installed & enabled" {
                            Get-WindowsFeature -Name $feature | Should -Not -BeNullOrEmpty
                            (Get-WindowsFeature -Name $feature)."InstallState" | Should -Be "Installed"
                        }
                    }
                }
                else {
                    Write-Warning "Check designed for $($value.os) does not match the Current OS: $currentOsVersion. Skipping Test"
                }
            } elseif ($amIWindowsTen) {
                if ($value.os -like "*10*") {
                    ForEach ($feature in $value.featureList) {
                        It "'$feature' windows feature is installed & enabled" {
                            Get-WindowsOptionalFeature -Online -FeatureName $feature | Should -Not -BeNullOrEmpty
                            (Get-WindowsOptionalFeature -Online -FeatureName $feature).State | Should -Be "Enabled"
                        }
                    }
                }
                else {
                    Write-Warning "Check designed for $($value.os) does not match the Current OS: $currentOsVersion. Skipping Test"
                }
            }
            else {
                Write-Warning "Unrecognized OS: $currentOsVersion"
            }
        }
    }
}


<#
    .SYNOPSIS
 
    Creates an IIS Application Pool with the specified options.
 
    .DESCRIPTION
 
    Uses the WebAdministration powershell module to create IIS app pools.
 
    .PARAMETER IISAppPoolName
 
    Name of App Pool to create.
 
    .PARAMETER IdentityCredential
 
    PSCredential with a username and password.
 
    .PARAMETER RuntimeDotNetVersion
 
    Runtime version to be used in app pool creation. Defaults to 'v4.0'
 
    .PARAMETER PipelineMode
 
    Managed pipeline mode to be used in app pool creation. Defaults to 'Integrated'
 
    .EXAMPLE
 
    New-AppPool -IISAppPoolName "CatalystAppPool" -IdentityCredential $psCredential
#>


function New-AppPool {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateLength(1,64)]
        [ValidateScript({
            if ($_ -match '[^a-zA-Z0-9]') {
                Write-DosMessage -Level "Error" -Message "$_ must only contain alphanumeric values. Please remove special characters."
            }
            else {
                $true
            }
        })]
        [string] $IISAppPoolName,
        [PSCredential] $IdentityCredential,
        [string] $RuntimeDotNetVersion = "v4.0",
        [string] $PipelineMode = "Integrated"
    )

    # Tests if current session has elevated permissions required to create IIS App Pool
    Test-ElevatedPermission

    # TODO: Validate bitness for powershell session. Import-Module WebAdministration most likely requires 64 bit

    Import-Module WebAdministration

    if ($IdentityCredential) {
        Write-DosMessage -Level "Information" -Message "Attempting to validate credential"
        if (!((Confirm-DosCredential -Credential $IdentityCredential).isValid)) {
            Write-DosMessage -Level "Fatal" -Message "Username or password is not valid"
        }
    }

    if(!(Test-Path "IIS:\AppPools\$IISAppPoolName" -PathType Container))
    {
        Write-DosMessage -Level "Information" -Message "Creating AppPool $IISAppPoolName."
        $appPool = New-WebAppPool $IISAppPoolName
        $appPool | Set-ItemProperty -Name "managedRuntimeVersion" -Value "$RuntimeDotNetVersion"
        $appPool | Set-ItemProperty -Name "managedPipelineMode" -Value "$PipelineMode"
        Set-AppPoolSettings -IISAppPoolName $IISAppPoolName -IdentityCredential $IdentityCredential
        $appPool.Stop()
    }else{
        Write-DosMessage -Level "Error" -Message "AppPool: $IISAppPoolName already exists."
        return
    }         
    
}


function New-CheckResult {
    [CmdletBinding()]
    param (
        $name,
        $type,
        $value,
        $check,
        $errorFlag,
        $level,
        $message
    )
    $result = New-Object PSObject
    $result | Add-Member -Type NoteProperty -Name name -Value $name
    $result | Add-Member -Type NoteProperty -Name type -Value $type
    $result | Add-Member -Type NoteProperty -Name value -Value $value
    $result | Add-Member -Type NoteProperty -Name check -Value $check
    $result | Add-Member -Type NoteProperty -Name errorFlag -Value $errorFlag
    $result | Add-Member -Type NoteProperty -Name level -Value $level
    $result | Add-Member -Type NoteProperty -Name message -Value $message    
    return $result
}



<#
    .SYNOPSIS
    NON PUBLIC - creates the xml file for the XML/File config store
     
    .DESCRIPTION
    creates the xml file for the XML/File config store
     
    .PARAMETER configStore
    The xml object that represents the install.config
     
    .EXAMPLE
    New-DosConfigStoreXml -configStore $configStore
     
    .NOTES
    General notes
#>

function New-DosConfigStoreXml {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    param(
        [Parameter(Mandatory=$true)]
        [hashtable] $configStore
    )

    if(Test-Path -Path $configStore.Path){
        Write-DosMessage -Level Warning -Message "$($configStore.Path) already exists. No config store created"
    } else {
        $folder = split-path -path $configStore.Path
        if(-not(Test-Path -Path $folder)){
            Write-DosMessage -Level Information -Message "Creating $folder."
            if($PSCmdlet.ShouldProcess("Create $folder")){
                New-Item -ItemType directory -Path $folder | Out-Null
            }
            
        }
        if(-not(Test-Path -Path $configStore.Path)){
            Write-DosMessage -Level Information -Message "Creating $($configStore.Path) with template settings."
            $installationConfigTemplate = "<installation>`n`t<settings>`n`t</settings>`n</installation>"
            if($PSCmdlet.ShouldProcess("Create $($configStore.Path))")){
                New-Item -Path $configStore.Path -ItemType "file" -Value $installationConfigTemplate -Force | Out-Null
            }
        }
        
    }

    return $configStore
}


<#
.SYNOPSIS
Creates a new SSIS catalog
 
.DESCRIPTION
Adds a new SSIS catalog with specified name and encryption key
 
.PARAMETER IntegrationServices
IntegrationServices object where catalog should be created
 
.PARAMETER CatalogEncryptionKey
Key to use for encrypting catalog, if it must be created
 
.PARAMETER CatalogName
Name of SSIS catalog to contain project
 
.INPUTS
None. You cannot pipe objects to New-SsisCatalog
 
.OUTPUTS
Created SSIS catalog
 
.EXAMPLE
PS> New-SsisCatalog -IntegrationServices $integrationServices -CatalogEncryptionKey 'password'
#>

function New-SsisCatalog {
    [cmdletbinding(SupportsShouldProcess=$true)]
    [OutputType()]
    param(
        [parameter(Mandatory=$true)]$IntegrationServices,
        [parameter(Mandatory=$true)][string]$CatalogEncryptionKey,
        [parameter(Mandatory=$false)][string]$CatalogName = 'SSISDB')

    if ($pscmdlet.ShouldProcess($CatalogName, "Provisioning SSIS catalog")) {
        $catalog = New-Object "Microsoft.SqlServer.Management.IntegrationServices.Catalog" ($IntegrationServices, $CatalogName, $CatalogEncryptionKey)  
        $catalog.Create()  
    }

    return $catalog
}


<#
.SYNOPSIS
Creates a new SSIS folder
 
.DESCRIPTION
Adds a new SSIS folder with specified name
 
.PARAMETER SsisCatalog
IntegrationServices Catalog object where folder should be created
 
.PARAMETER FolderName
Name of folder to create
 
.INPUTS
None. You cannot pipe objects to New-SsisFolder
 
.OUTPUTS
Created SSIS folder
 
.EXAMPLE
PS> New-SsisFolder -SsisCatalog $catalog -FolderName 'Catalyst'
#>

function New-SsisFolder {
    [cmdletbinding(SupportsShouldProcess=$true)]
    [OutputType()]
    param(
        [parameter(Mandatory=$true)]$SsisCatalog,
        [parameter(Mandatory=$true)][string]$FolderName)

    if ($pscmdlet.ShouldProcess($FolderName, "Creating SSIS folder")) {
        $folder = New-Object "Microsoft.SqlServer.Management.IntegrationServices.CatalogFolder" ($SsisCatalog, $FolderName, "Folder to contain SSIS projects")
        $folder.Create()
    }

    return $folder
}


<#
.SYNOPSIS
Creates a new SSIS project
 
.DESCRIPTION
Deploys an ISPAC into an existing SSIS catalog and folder
 
.PARAMETER SsisFolder
IntegrationServices Folder object where project should be created
 
.PARAMETER ProjectName
Name of project to create
 
.PARAMETER IspacPath
Path to ISPAC to deploy to initialize project
 
.INPUTS
None. You cannot pipe objects to New-SsisProject
 
.OUTPUTS
None.
 
.EXAMPLE
PS> New-SsisProject -SsisFolder $folder -ProjectName 'CatalystLoader' -IspacPath "C:\a\place"
#>

function New-SsisProject {
    [cmdletbinding(SupportsShouldProcess=$true)]
    [OutputType()]
    param(
        [parameter(Mandatory=$true)]$SsisFolder,
        [parameter(Mandatory=$true)][string]$ProjectName,
        [parameter(Mandatory=$true)][string]$IspacPath)

    if ($pscmdlet.ShouldProcess($ProjectName, "Deploying SSIS project")) {
        [byte[]] $projectFile = [System.IO.File]::ReadAllBytes($IspacPath)
        $SsisFolder.DeployProject($ProjectName, $projectFile)
    }
}


<#
    .SYNOPSIS
 
    Publishes a .net core web applications.
 
    .DESCRIPTION
 
    Creates the necessary folder and expands the archive containing the .net core web app to the folder. Also creates the appropriate IIS Site and associates the specified App Pool with the site.
 
    .PARAMETER WebApplicationPackagePath
 
    Path to the zip file containing the .net core web applications
 
    .PARAMETER AppPoolName
 
    Application pool to associate with the web application. This must already exist
 
    .PARAMETER IISWebSite
 
    IIS Site to install the application to. Defaults to "Default Web Site" if not specified
 
    .PARAMETER AppName
 
    Application name - used for both the site AND the folder created underneath the IISWebSite root
 
    .PARAMETER PathsToPreserve
 
    Array of paths to preserve during a deployment, such as logs, relative to the install directory in IIS, so they are not removed during the upgrade of an application. Ignored for new installs.
 
 
    .EXAMPLE
 
    Publish-DotNetCoreWebApp -WebApplicationPackagePath $WebAppPackagePath -AppPoolCredentials $AppPoolCredential -AppName $AppName -IISWebSite $IISWebSite -AppPoolName $AppPoolName -PathsToPreserve @("logs")
#>

function Publish-DotNetCoreWebApp{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateScript({
            if (!(Test-Path $_)) {
                Write-DosMessage -Level "Fatal" -Message "WebApplicationPackagePath $_ does not exist. Please enter valid path."
            }
            else {
                $true
            }
        })]
        [string] $WebApplicationPackagePath,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [ValidateLength(1,64)]
        [ValidateScript({
            if ($_ -match '[^a-zA-Z0-9]') {
                Write-DosMessage -Level "Fatal" -Message "$_ must only contain alphanumeric values. Please remove special characters."
            }
            else {
                $true
            }
        })]
        [string] $AppPoolName,
        [string] $IISWebSite = "Default Web Site",
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $AppName,
        [string[]] $PathsToPreserve
    )

    # Tests if current session has elevated permissions required to create IIS App Pool
    Test-ElevatedPermission

    Import-Module WebAdministration

    #Stop app pool if it's running (upgrade scenario)
    [string] $appPoolUser = $null
    if(Test-Path "IIS:\AppPools\$AppPoolName" -PathType Container){
    
        $currentState = Get-WebAppPoolState -Name $AppPoolName
        if($currentState.Value -ne "Stopped"){
            Write-DosMessage -Level "Information" -Message "App pool $AppPoolName not stopped, current state $currentState"
            Stop-WebAppPool -Name $AppPoolName
            Wait-AppPoolState -AppPoolName $AppPoolName -AppPoolState "Stopped"
        }

        $appPool = Get-Item "IIS:\AppPools\$AppPoolName"

        $appPoolUser = $appPool.processModel.userName
        
    }
    else{
        Write-DosMessage -Level "Error" -Message "No app pool found named $AppPoolName"
        return
    }

    #Get/Create folder

    $physicalWebPath = Join-Path (Get-IISWebSitePath -WebSiteName $IISWebSite) $AppName

    if(!(Test-Path $physicalWebPath)){
        Write-DosMessage -Level "Information" -Message "Creating directory $physicalWebPath"
        New-Item -Path $physicalWebPath -ItemType Directory | Out-Null
    }
    else {
        Write-DosMessage -Level "Information" -Message "Directory $physicalWebPath exists, removing previous installation files"

        if ($PathsToPreserve) {
            Get-ChildItem -Path "$physicalWebPath\*" -Exclude $PathsToPreserve | Remove-Item -Recurse -Force
        }
        else {
            Remove-Item -Path "$physicalWebPath\*" -Recurse -Force
        }
    }

    #Folder ACL ops
    if (!([string]::IsNullOrEmpty($appPoolUser))) {
        Write-DosMessage -Level "Information" -Message "Adding read and write to $physicalWebPath for $appPoolUser"
        Add-AclFolder -FolderPath $physicalWebPath -User $appPoolUser -Permission "Read"

        Add-AclFolder -FolderPath $physicalWebPath -User $appPoolUser -Permission "Write"
    }

    #Extract zip file

    Write-DosMessage -Level "Information" -Message "Extracting $WebApplicationPackagePath to $physicalWebPath"
    Expand-DosArchive -ArchiveFile $WebApplicationPackagePath -DestinationPath $physicalWebPath -Overwrite

    Write-DosMessage -Level "Information" -Message "Creating web site $AppName on site $IISWebSite using application pool $AppPoolName"
    New-WebApplication -Name $AppName -Site $IISWebSite -PhysicalPath $physicalWebPath -ApplicationPool $AppPoolName -Force | Out-Null

    Start-WebAppPool -Name $AppPoolName

}


[string] $script:WebDeployExecutableName = "msdeploy.exe"

# alternatively the directory is located in registry: HKLM\SOFTWARE\Microsoft\IIS Extensions\MSDeploy
[Array] $script:MsdeployLocations = @([System.IO.Path]::Combine(([System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::ProgramFilesX86)), "IIS", "Microsoft Web Deploy V3"))

<#
    .SYNOPSIS
 
    Publishes the target Web Deploy Web application.
 
    .DESCRIPTION
 
    Uses WebDeploy to deploy specified web application with specified parameters
 
    .PARAMETER WebDeployPackageFilePath
 
    File path to web deploy zip file to publish to server. Mandatory
 
    .PARAMETER WebDeployParameterFilePath
 
    File path to the set parameter XML file that contains transforms for the web.config file (connection strings, etc)
 
    .PARAMETER WebParameters
 
    Arraylist object to the set parameters for the web.config file (connection strings, etc)
 
    .EXAMPLE
 
    Publish-DosWebApp -WebDeployPackageFilePath ".\test.zip" -WebDeployParameterFilePath ".\test.params.xml"
#>

function Publish-WebDeployWebApp {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string] $WebDeployPackageFilePath,
        [string] $WebDeployParameterFilePath,
        [System.Collections.ArrayList] $WebParameters,
        [string] $AppName,
        [string] $IISWebSite = "Default Web Site",
        [string] $AppPoolName,
        [string[]] $PathsToPreserve
    )

    Write-DosMessage -Level "Verbose" -Message "Confirming process is running with elevated permissions."
    Test-ElevatedPermission

    if(!(Test-Path $WebDeployPackageFilePath)){
        Write-DosMessage -Level "Error" -Message "Unable to locate $WebDeployPackageFilePath"
        return
    }
    
    Write-DosMessage -Level "Verbose" -Message "Generating WebDeploy arguments with '$WebDeployPackageFilePath' file path."
    # --% escapes out the rest of the line. otherwise we would need to add a tick(`) for semi-colons, apostrophes, and quotes.
    [string] $webDeployArguments = "--%"`
                               +" -source:package='$WebDeployPackageFilePath'"`
                               +" -dest:auto,includeAcls=""False"""`
                               +" -verb:sync"`
                               +" -disableLink:AppPoolExtension"`
                               +" -disableLink:ContentExtension"`
                               +" -disableLink:CertificateExtension"

    Write-DosMessage -Level "Verbose" -Message "Ensuring that WebDeploy parameter file path exists."
    if(!([string]::IsNullOrEmpty($WebDeployParameterFilePath))){
        if ((Test-Path $WebDeployParameterFilePath) -and $WebParameters){
            Write-DosMessage -Level "Error" -Message "It is not supported to provide both an XML settings file and deploy arg objects. Provide only one."
            Return
        }

        if(!(Test-Path $WebDeployParameterFilePath)){
            Write-DosMessage -Level "Error" -Message "Unable to locate $WebDeployParameterFilePath"
            Return
        }
        else{
            $webDeployArguments = $webDeployArguments + " -setParamFile:""$WebDeployParameterFilePath"""
        }
        
    }
    else{
        foreach ($param in $WebParameters)
        {
            $webDeployArguments = $webDeployArguments + $(" -setParam:name='{0}',value='{1}'" -f $param.Name, $param.Value)
        }
    }

    Write-DosMessage -Level "Information" -Message "Attempting to retrieve WebDeploy executable path."
    [string] $webDeployExecutablePath = Resolve-FilePath -PathsToSearch $script:MsdeployLocations -FilePattern $script:WebDeployExecutableName

    if([String]::IsNullOrEmpty($webDeployExecutablePath)){
        Write-DosMessage -Level "Error" -Message "Unable to locate web deploy, unable to publish DOS application"
        return
    }

    if ($PathsToPreserve) {
        foreach ($path in $PathsToPreserve) {
            $webDeployArguments += " -skip:skipaction='Delete',objectName='dirPath',absolutepath='$path$'"
            $webDeployArguments += " -skip:skipaction='Delete',objectName='filePath',absolutepath='$path\\.*$'"
        }
    }

    Write-DosMessage -Level "Verbose" -Message "Running: $webDeployExecutablePath $webDeployArguments"
    $output = Start-CommandAndReturnOutput -Command  "& ""$webDeployExecutablePath"" $webDeployArguments | Out-String"

    if ([string]::IsNullOrEmpty($output)) {
        Write-DosMessage -Level "Fatal" -Message "Web Deploy output returned empty or null. Please validate Web Deploy 3.5 is correctly installed."
        return
    }

    $physicalWebPath = Join-Path (Get-IISWebSitePath -WebSiteName $IISWebSite) $AppName
    $appPool = Get-Item "IIS:\AppPools\$AppPoolName"
    $appPoolUser = $appPool.processModel.userName

    Write-DosMessage -Level "Information" -Message "Adding read and write to $physicalWebPath for $appPoolUser"
    if (!([string]::IsNullOrEmpty($appPoolUser))) {
        Add-AclFolder -FolderPath $physicalWebPath -User $appPoolUser -Permission "Read"
        Add-AclFolder -FolderPath $physicalWebPath -User $appPoolUser -Permission "Write"
    }

    Write-DosMessage -Level "Verbose" -Message $output
}


<#
    .SYNOPSIS
 
    Private function for the Remove-DosConfigSection function, but just for the azuretable type.
 
    .DESCRIPTION
 
    Private function for the Remove-DosConfigSection function, but just for the azuretable type.
 
#>

function Remove-DosConfigSectionAzureTable {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")]
    [CmdletBinding()]
    param (
        $configStore,
        $configSection
    )

    $configValues = Get-DosConfigValuesAzureTable -ConfigStore $configStore -Scope $configSection

    $responseObject = $null

    foreach ($configValue in $configValues) {
        Write-DosMessage -Level "Debug" -Message "Attepmting to remove $($configValue.Scope).$($configValue.Name)"
        $responseObject = Remove-DosConfigValueAzureTable -ConfigStore $configStore -configSection "$($configValue.Scope)" -configSetting "$($configValue.Name)"
    
        if ($responseObject.StatusCode -eq 204) {
            Write-DosMessage -Level "Debug" -Message "Successfully removed $($configValue.Scope).$($configValue.Name) from $($configStore.Type) configstore."
        }
    }

    return $responseObject
}


<#
    .SYNOPSIS
 
    Private function for the Remove-DosConfigSection function, but just for the Xml type.
 
    .DESCRIPTION
 
    Private function for the Remove-DosConfigSection function, but just for the Xml type.
 
#>

function Remove-DosConfigSectionXml {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")]
    [CmdletBinding()]
    param (
        $configStore,
        $configSection
    )

    $installConfigXml = [xml](Get-Content "$($configStore.Path)")

    $sectionSettings = $installConfigXml.installation.settings.scope | Where-Object {$_.name -eq $configSection}

    if (-not($sectionSettings) -or ($sectionSettings.Count -eq 0)) {
        #if the scope doesn't exist no problem, do nothing
        Write-DosMessage -Level Information -Message "$($configStore.Path) didn't have a $configSection scope. No action taken."
        return
    }

    #make the config section lowercase and then get the node to delete (case insensitive)
    $configSection = $configSection.ToLower()
    $nodeToDelete = $installConfigXml.selectnodes("/installation/settings/scope[translate(@name,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='$configSection']")

    if ($nodeToDelete) {
        $nodeToDelete | Foreach-Object{$_.parentnode.removechild($_)} | out-null
    }
    else {
        Write-DosMessage -Level "Warning" -Message "$($configStore.Path) had a $configSection.$configSetting value but we couldn't find it with XPath. No action taken."
        $installConfigXml = $null
    }

    return $installConfigXml
}




<#
    .SYNOPSIS
 
    Private function for the Remove-DosConfigValue function, but just for the AzureTable type.
 
    .DESCRIPTION
 
    Private function for the Remove-DosConfigValue function, but just for the AzureTable type.
 
#>

function Remove-DosConfigValueAzureTable {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")]
    [CmdletBinding()]
    param (
        $configStore,
        $configSection,
        $configSetting
    )

    Write-DosMessage -Level "Debug" -Message "Attempting to remove $configSection.$configSetting from $($configStore.Type) configstore."

    $tableStorageInputs = Get-AzureTableStorageInputsFromUri -storageUri "$($configStore.Uri)"

    $resource = "$($tableStorageInputs.tableName)(PartitionKey='$configSection',RowKey='$configSetting')"
    $deleteUri = "https://$($tableStorageInputs.storageAccountName).table.core.windows.net/$resource$($tableStorageInputs.storageSas)"

    $header = @{
        Accept         = "application/json;odata=nometadata"
        'If-Match'     = "*"
    }
    
    $responseObject = $null

    try {
        $responseObject = Invoke-WebRequest -Method DELETE -Uri "$deleteUri" -Headers $header -UseBasicParsing
    }
    catch {
        Write-DosMessage -Level "Fatal" -Message "Failed to remove $($configValue.PartitionKey).$($configValue.RowKey) configuration from $($configStore.Type) config store. Exception: $($_.Exception)"
    }

    if ($responseObject.StatusCode -eq 204) {
        Write-DosMessage -Level "Debug" -Message "Successfully removed $configSection.$configSetting from $($configStore.Type) configstore."
    }

    return $responseObject
}


<#
    .SYNOPSIS
 
    Private function for the Remove-DosConfigValue function, but just for the Xml type.
 
    .DESCRIPTION
 
    Private function for the Remove-DosConfigValue function, but just for the Xml type.
 
#>

function Remove-DosConfigValueXml {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")]
    [CmdletBinding()]
    param (
        $configStore,
        $configSection,
        $configSetting
    )

    Write-DosMessage -Level "Debug" -Message "Attempting to remove $configSection.$configSetting from $($configStore.Path)."

    $installConfigXml = [xml](Get-Content "$($configStore.Path)")

    $sectionSettings = $installConfigXml.installation.settings.scope | Where-Object {$_.name -eq $configSection}

    if (-not($sectionSettings) -or ($sectionSettings.Count -eq 0)) {
        #if the scope doesn't exist no problem, do nothing
        Write-DosMessage -Level 'Debug' -Message "$($configStore.Path) didn't have a $configSection scope. No action taken."
        return
    }

    $existingSetting = $sectionSettings.variable | Where-Object {$_.name -eq $configSetting}

    if(!$existingSetting){
        #if the existing variable in the scope doesn't exist, do nothing
        Write-DosMessage -Level 'Debug' -Message "$($configStore.Path) didn't have a $configSection.$configSetting value. No action taken."
        return
    }

    $configSection = $configSection.ToLower()
    $configSetting = $configSetting.ToLower()
    $nodeToDelete = $installConfigXml.selectnodes("/installation/settings/scope[translate(@name,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='$configSection']/variable[translate(@name,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='$configSetting']")

    if ($nodeToDelete) {
        $nodeToDelete | Foreach-Object{$_.parentnode.removechild($_)} | out-null
    }
    else {
        Write-DosMessage -Level "Warning" -Message "$($configStore.Path) had a $configSection.$configSetting value but we couldn't find it with XPath. No action taken."
        $installConfigXml = $null
    }
    return $installConfigXml
}


<#
    .SYNOPSIS
 
    Looks for the v1/v2/v3/vN in the URL and removes it.
 
    .DESCRIPTION
 
    There are various times when we want the version number removed from the URL, thus we have this helper function to make it
    easy to remove the version number.
 
    .PARAMETER Url
 
    Name the URL
 
    .EXAMPLE
 
    Remove-VersionFromLocalPath -Url 'https://www.example.com/DiscoveryService/v1/Services
#>

function Remove-VersionFromLocalPath {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [string] $Url
    )

    $uri = [System.Uri]$Url

    $uriLocalPath = $($uri.LocalPath) -replace "/v(\d+)([/]?)", "/"
    $uriLocalPath = $uriLocalPath -replace "//","/"

    if($($uri.LocalPath).EndsWith("/") -and -not($uriLocalPath.endswith("/"))){
        #the original ended with a slash, and the new one doesn't, add a slash
        $uriLocalPath = "$uriLocalPath/"
    }

    if(-not($($uri.LocalPath).EndsWith("/")) -and $uriLocalPath.endswith("/")){
        #the original didn't end with a slash, but the new one does.
        #removing trailing slashes, remove it.
        $uriLocalPath = $uriLocalPath.substring(0,$uriLocalPath.Length-1)
    }

    $newUrl = "$($uri.Scheme)://$($uri.Host)$uriLocalPath$($uri.Query)"

    #This was the original code, but it didn't handle all the cases that I wanted it to handle.
    # So the following line was replaced by all the code above.
    #$NewUrl = $Url -replace "/v(\d+)([/]?)", "/"
        
    if ($Url -ne $NewUrl) {
        Write-DosMessage -Level "Debug" -Message "Removed version from url. From: $Url To: $NewUrl"
    }
    return $newUrl
}


<#
    .SYNOPSIS
 
    Repairs non-cannonical ACLs
 
    .DESCRIPTION
 
    Attempts to reorder ACEs in the specified ACL.
 
    .PARAMETER Acl
 
    ACL needing repair
 
    .PARAMETER Path
 
    Path to the item needing ACL repairs - needed because sometimes the ACL passed in doesn't have an associated path.
 
    .EXAMPLE
 
    Repair-AclCanonicalOrder -Acl $x
#>

function Repair-AclCanonicalOrder {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [System.Security.AccessControl.DirectorySecurity] $Acl,
        [string] $Path
    )
    
    if ($Acl.AreAccessRulesCanonical) {
        Write-DosMessage -Level "Debug" -Message "Acls are canonical"
        return
    }

    Write-DosMessage -Level "Debug" -Message "Acls are not canonnical, attempting to fix Acls"

    # Convert ACL to a raw security descriptor:
    $RawSD = New-Object System.Security.AccessControl.RawSecurityDescriptor($Acl.Sddl)

    # Create a new, empty DACL
    $NewDacl = New-Object System.Security.AccessControl.RawAcl(
        [System.Security.AccessControl.RawAcl]::AclRevision,
        $RawSD.DiscretionaryAcl.Count  # Capacity of ACL
    )

    # Put in reverse canonical order and insert each ACE (I originally had a different method that
    # preserved the order as much as it could, but that order isn't preserved later when we put this
    # back into a DirectorySecurity object, so I went with this shorter command)
    $RawSD.DiscretionaryAcl | Sort-Object @{E={$_.IsInherited}; Descending=$true}, AceQualifier | ForEach-Object {
        $NewDacl.InsertAce(0, $_)
    }

    # Replace the DACL with the re-ordered one
    $RawSD.DiscretionaryAcl = $NewDacl

    # Commit those changes back to the original SD object (but not to disk yet):
    $Acl.SetSecurityDescriptorSddlForm($RawSD.GetSddlForm("Access"))

    # Commit changes
    $Acl | Set-Acl -Path $Path
}


#List of common locations for assemblies
[System.Collections.ArrayList] $script:commonFilePaths = @("$PSScriptRoot\..\assemblies","C:\Program Files (x86)\Microsoft SQL Server")

<#
    .SYNOPSIS
 
    Attempts to resolve the the assembly file name specified to a fully qualified path.
 
    .DESCRIPTION
 
    Looks through the commonAssemblyFilePaths for files matching the specified file name.
 
    .PARAMETER FileName
 
    Assembly file name. Ex. "Microsoft.Test.dll"
 
    .EXAMPLE
 
    Resolve-AssemblyFilePath -AssemblyFileName ".\test.dll"
 
    .OUTPUTS
 
    $null if no file found, otherwise the fully qualitifed path to the file.
 
#>

function Resolve-CommonFilePath{
    [CmdletBinding()]
    param(
        [string] $AssemblyFileName
    )

    return Resolve-FilePath -PathsToSearch $script:commonFilePaths -FilePattern $AssemblyFileName
}


<#
    .SYNOPSIS
 
    Attempts to resolve the the assembly file name specified to a fully qualified path.
 
    .DESCRIPTION
 
    Looks through the commonAssemblyFilePaths for files matching the specified file name.
 
    .PARAMETER AssemblyFileName
 
    Assembly file name. Ex. "Microsoft.Test.dll"
 
    .EXAMPLE
 
    Resolve-FilePath -PathsToSearch "C:\Path\To\Search" -FilePatter "Executable.exe"
 
    .OUTPUTS
 
    $null if no file found, otherwise the fully qualitifed path to the file.
 
#>

function Resolve-FilePath{
    [CmdletBinding()]
    param(
        [Array] $PathsToSearch,
        [string] $FilePattern
    )

    Write-DosMessage -Level "Verbose" -Message "Attempting to retrieve file path that matches '$FilePattern'."

    foreach($filePath in $PathsToSearch){
        try {
            Write-DosMessage -Level "Verbose" -Message "Searching in '$filePath'"
            $files = Get-ChildItem -Path $filePath -Filter $FilePattern -Recurse | Where-Object { $_.PSIsContainer -ne $true }
        }
        catch {
            Write-DosMessage -Level "Error" -Message "Error retrieving path that matches pattern '$FilePattern'. Exception: $($_.Exception)."
        }
        if(($files -ne $null) -and ($files.Length -gt 0)){
            Write-DosMessage -Level "Verbose" -Message "Found $($files.Length) files under $assemblyFilePath for $AssemblyFileName"
            return $files[0].FullName
        }
    }

    Write-DosMessage -Level "Verbose" -Message "Unable to find path that matches pattern '$FilePattern'. Returning null."
    return $null

}

<#
    .SYNOPSIS
 
    Adds to the common search paths used when attempting to resolve an assembly file location.
 
    .DESCRIPTION
 
    Adds a path to the list of paths to search.
 
    .EXAMPLE
 
    Add-CommonPath -Path "C:\Windows"
 
#>

function Add-CommonPath{
    [CmdletBinding()]
    param(
        [string] $Path
    )

    $script:commonFilePaths.Add($Path)

}

<#
    .SYNOPSIS
 
    Clears the common search paths.
 
    .DESCRIPTION
 
    Clears the search path variable.
 
    .EXAMPLE
 
    Clear-CommonPath
 
#>

function Clear-CommonPath{
    
    $script:commonFilePaths = @()
}



<#
    .SYNOPSIS
 
    Configure an IIS Application Pool with the specified settings.
 
    .DESCRIPTION
 
    Uses the WebAdministration powershell module to create IIS app pools.
 
    .PARAMETER IISAppPoolName
 
    Name of App Pool to configure.
 
    .PARAMETER IdentityCredential
 
    PSCredential with a username and password.
 
    .EXAMPLE
 
    Set-AppPoolSettings -IISAppPoolName "CatalystAppPool" -IdentityCredential $psCredential
#>


function Set-AppPoolSettings {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateLength(1, 64)]
        [ValidateScript( {
                if ($_ -match '[^a-zA-Z0-9]') {
                    Write-DosMessage -Level "Error" -Message "$_ must only contain alphanumeric values. Please remove special characters."
                }
                else {
                    $true
                }
            })]
        [string] $IISAppPoolName,
        [PSCredential] $IdentityCredential,
        [switch] $NoCredential
    )  

    $poolpath = "IIS:\AppPools\$IISAppPoolName"
    try {
        $appPool = Get-Item -Path $poolpath -ErrorAction Stop
    } Catch {
        Write-DosMessage -Level "Fatal" -Message "Failed to get application pool. Exception: $($_.Exception)"
        Break
    }

    Write-DosMessage -Level "Information" -Message "Configuring AppPool $IISAppPoolName."

    if ($IdentityCredential) {
        if (!((Confirm-DosCredential -Credential $IdentityCredential).isValid)) {
            Write-DosMessage -Level "Fatal" -Message "Username or password is not valid"
        }

        if (![string]::IsNullOrEmpty($IdentityCredential.UserName) -and $IdentityCredential.GetNetworkCredential().Password -ne $null) {
            Write-DosMessage -Level "Information" -Message "Configuring '$IISAppPoolName' app pool's identity with the credentials provided."
            $appPool.processModel.userName = $IdentityCredential.UserName
            $appPool.processModel.password = $IdentityCredential.GetNetworkCredential().Password
        }

        # IdentityType 3 references 'SpecificUser' https://docs.microsoft.com/en-us/iis/configuration/system.applicationhost/applicationpools/add/processmodel
        if (!($appPool.processModel.identityType -eq "SpecificUser")) {
            Write-DosMessage -Level "Information" -Message "Identity Type was not set to SpecificUser, setting to 3"
            $appPool.processModel.identityType = 3
        }
    }
    else {
        Write-DosMessage -Level "Information" -Message "No identity credential was provided. '$IISAppPoolName' identity configuration will not be altered."
    }

    if ($NoCredential.IsPresent) {
        Write-DosMessage -Level "Information" -Message "NoCredential parameter was provided. Attempting to configure '$IISAppPoolName' identity type."
        if (!($appPool.processModel.identityType -eq "ApplicationPoolIdentity")) {
            Write-DosMessage -Level "Information" -Message "Identity Type was not set to ApplicationPoolIdentity, setting to 4"
            $appPool.processModel.identityType = 4
        }
        Write-DosMessage -Level "Information" -Message "'$IISAppPoolName' identity type successfully set to 'ApplicationPoolIdentity'"
    }
    
    
    if ($appPool.processModel.loaduserprofile -eq $false) {   
        Write-DosMessage -Level "Verbose" -Message "loaduserprofile was not set to true, setting to true" 
        $appPool.processModel.loaduserprofile = $true
    }
    if (!($appPool.startMode -eq "alwaysrunning")) {
        Write-DosMessage -Level "Verbose" -Message "startmode was not set to alwaysrunning, setting to alwaysrunning"
        $appPool.startMode = "alwaysrunning"
    }
    if (!($appPool.processmodel.idletimeout -eq [TimeSpan]::FromMinutes(0))) {
        $appPool.processmodel.idletimeout = [TimeSpan]::FromMinutes(0)
    }
    if (!($appPool.processmodel.idletimeoutaction -eq "suspend")) {
        Write-DosMessage -Level "Verbose" -Message "idletimeoutaction was not set to suspend, setting to suspend"
        $appPool.processmodel.idletimeoutaction = "suspend"
    }
    if (!($appPool.cpu.action -eq "ThrottleUnderLoad")) {
        Write-DosMessage -Level "Verbose" -Message "cpu limit action was not set, setting ThrottleUnderLoad to 10%"
        $appPool.cpu.action = "ThrottleUnderLoad"
        $appPool.cpu.limit = 10000
    }
    try {
        $appPool | Set-Item -Verbose -ErrorAction Stop
    } catch {
        Write-DosMessage -Level "Fatal" -Message "Failed to set application pool settings. Exception: $($_.Exception)"
        Break
    }
    
    
}


<#
    .SYNOPSIS
 
    Private function for the Set-DosConfigValue function, but just for the AzureTable type.
 
    .DESCRIPTION
 
    Private function for the Set-DosConfigValue function, but just for the AzureTable type.
 
#>

function Set-DosConfigValueAzureTable {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")]
    [CmdletBinding()]
    param (
        $configStore,
        $configSection,
        $configSetting,
        $configValue
    )

    Write-DosMessage -Level "Debug" -Message "Attempting to add $configSection.$configSetting=$configValue into $($ConfigStore.Format)."

    # Check if value already exists
    $configValues = Get-DosConfigValues -ConfigStore $configStore -Scope $configSection
    $currentValue = $configValues.$configSetting
    
    $tableStorageInputs = Get-AzureTableStorageInputsFromUri -storageUri $ConfigStore.Uri

    $headers = @{
        Accept = 'application/json;odata=nometadata'
    }

    $entity = @{
        PartitionKey = "$configSection"
        RowKey = "$configSetting"
        Value = "$configValue"
    }

    $body = $entity | ConvertTo-Json
    
    if ($currentValue -eq $configValue) {
        Write-DosMessage -Level "Debug" -Message "$configSection.$configSetting is already configured with $configValue"
        $responseObject = $null
    }
    else {
        #upsert
        Write-DosMessage -Level "Debug" -Message "Changing $configSection.$configSetting from '$($currentValue)' to '$($configValue)'"
        $resource = "$($tableStorageInputs.tableName)(PartitionKey='$configSection',RowKey='$configSetting')"
        $putUri = "https://$($tableStorageInputs.storageAccountName).table.core.windows.net/$resource$($tableStorageInputs.storageSas)"

        try {
            $oldProgressPreference = $progresspreference
            $global:progressPreference = 'silentlyContinue'
            $progresspreference = 'SilentlyContinue'
            
            $responseObject = Invoke-WebRequest -Method PUT -Uri $putUri -Headers $headers -Body $body -ContentType 'application/json' -UseBasicParsing

            $progresspreference = $oldProgressPreference
            $global:progressPreference = $oldProgressPreference

        }
        catch {
            Write-DosMessage -Level "Fatal" -Message "Failed to set $configSection.$configSetting=$configValue on $($configStore.Type) configStore. Exception: $($_.Exception)"
        }
    }
    
    if ($responseObject.StatusCode -eq 204) {
        Write-DosMessage -Level "Debug" -Message "Successfully set $configSection.$configSetting in $($configStore.Type) configstore."
    }

    return $responseObject
}


<#
    .SYNOPSIS
 
    Private function for the Set-DosConfigValue function, but just for the Xml type.
 
    .DESCRIPTION
 
    Private function for the Set-DosConfigValue function, but just for the Xml type.
 
#>

function Set-DosConfigValueXml {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")]
    [CmdletBinding()]
    param (
        $configStore,
        $configSection,
        $configSetting,
        $configValue,
        $KeepExisting
    )
    Write-DosMessage -Level "Debug" -Message "Attempting to add $configSection.$configSetting=$configValue with KeepExisting=($KeepExisting) into $($configStore.Path)."

    $somethingChanged = $false

    $installConfigXml = [xml](Get-Content "$($configStore.Path)")
    $sectionSettings = $installConfigXml.installation.settings.scope | Where-Object {$_.name -eq $configSection}
    if (!$sectionSettings) {
        #if the scope doesn't exist, create it
        Write-DosMessage -Level "Debug" -Message "Scope ""$configSection"" doesn't exist, creating it."
        $sectionSettings = $installConfigXml.CreateElement("scope")
        $nameAttribute = $installConfigXml.CreateAttribute("name")
        $nameAttribute.Value = $configSection

        $sectionSettings.Attributes.Append($nameAttribute) | Out-Null
        $installConfigXml.installation.SelectSingleNode("settings").AppendChild($sectionSettings) | Out-Null
        $somethingChanged = $true
    }

    $existingSetting = $sectionSettings.variable | Where-Object {$_.name -eq $configSetting}
    if(!$existingSetting){
        #if the existing variable in the scope doesn't exist, create it.
        $setting = $installConfigXml.CreateElement("variable")
        
        $nameAttribute = $installConfigXml.CreateAttribute("name")
        $nameAttribute.Value = $configSetting
        $setting.Attributes.Append($nameAttribute) | Out-Null

        #now that the existing variable is created, set the value.
        $valueAttribute = $installConfigXml.CreateAttribute("value")
        $valueAttribute.Value = $configValue
        $setting.Attributes.Append($valueAttribute) | Out-Null
        Write-DosMessage -Level "Debug" -Message "Adding setting ""$configSetting"" with value ""$configValue"" to the ""$configSection"" scope"

        $sectionSettings.AppendChild($setting) | Out-Null
        $somethingChanged = $true
    } elseif([string]::IsNullOrEmpty($existingSetting.value)){
        #the current value is null or empty, so we are going to overwrite it, regardless if it says keepexisting or not.
        Write-DosMessage -Level "Debug" -Message "No existing value found for setting ""$configSetting"" in scope ""$configSection"", populating with ""$configValue"""
        $existingSetting.value = $configValue
        $somethingChanged = $true
    } elseif (-not([string]::IsNullOrEmpty($existingSetting.value))) {
        #There is an existing setting
        #That existing setting has a value.


        if($KeepExisting){
            #Don't change the value and let the user know.
            Write-DosMessage -Level "Debug" -Message "Existing value ""$($existingSetting.value)"" found for setting ""$configSetting"" in scope ""$configSection"" but KeepExisting was passed in, leaving value as-is."
        } else {
            #Do change the value and let the user know.
            Write-DosMessage -Level "Debug" -Message "Existing value ""$($existingSetting.value)"" found for setting ""$configSetting"" in scope ""$configSection"", replacing with ""$configValue"""
            $existingSetting.value = $configValue
            $somethingChanged = $true
        }
        
    } else {
        Write-DosMessage -Level "Fatal" -Message "You've reached an else block that you shouldn't have been able to reach. The cake is a lie."
    }

    if (!$somethingChanged) {
        $installConfigXml = $null
    }
    return $installConfigXml
}


<#
    .SYNOPSIS
 
    Alters IIS authentication type based off parameter values provided by user.
 
    .DESCRIPTION
 
    Pull current web.config values, unlocking the configurations, and altering based of provided values.
 
    .PARAMETER AuthenticationType
 
    Array of strings allowing for either 'Windows', 'Anonymous' or both.
 
    .PARAMETER SiteName
 
    Name of IIS website being used.
 
    .PARAMETER ApplicationName
 
    Name of IIS application being altered.
 
    .EXAMPLE
 
    Set-IISAuthentication -AuthenticationType 'Windows' -SiteName 'Default Web Site' -ApplicationName 'HCApp'
#>


function Set-IISAuthentication {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateSet("Windows", "Anonymous")]
        [string[]] $AuthenticationType,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $SiteName,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $ApplicationName
    )

    Add-Assembly -Assemblies "$env:systemroot\system32\inetsrv\Microsoft.Web.Administration.dll"
    $manager = New-Object Microsoft.Web.Administration.ServerManager      

    if ($AuthenticationType.Contains("Anonymous")){
        Edit-AuthenticationType -AuthenticationType "Anonymous" -SiteName $SiteName -AppName $ApplicationName -ApplicationHostManager $manager -Enable
    }
    else {
        Edit-AuthenticationType -AuthenticationType "Anonymous" -SiteName $SiteName -AppName $ApplicationName -ApplicationHostManager $manager
    }
    
    if ($AuthenticationType.Contains("Windows")){
        Edit-AuthenticationType -AuthenticationType "Windows" -SiteName $SiteName -AppName $ApplicationName -ApplicationHostManager $manager -Enable
    }
    else {
        Edit-AuthenticationType -AuthenticationType "Windows" -SiteName $SiteName -AppName $ApplicationName -ApplicationHostManager $manager
    }

    $manager.CommitChanges()
}

<#
    .SYNOPSIS
 
    Used to alter authentication type of an IIS web site/application
 
    .DESCRIPTION
 
 
 
    .PARAMETER AuthenticationType
 
    Authentication Mode that will be altered
 
    .PARAMETER SiteName
 
    Name of the IIS Website
 
    .PARAMETER AppName
 
    Name of the IIS application
 
    .PARAMETER Enable
 
    Switch toggling whether the specified Authentication will be enabled or disabled
 
    .EXAMPLE
 
    Alter-AuthenticationType -AuthenticationType "Windows" -SiteName "TestSite" -AppName "TestApp" -ApplicationConfiguration $config -Enable
#>


function Edit-AuthenticationType {
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [ValidateSet("Windows", "Anonymous")]
        [string] $AuthenticationType,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $SiteName,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $AppName,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [Microsoft.Web.Administration.ServerManager] $ApplicationHostManager,
        [switch] $Enable
    )

    if ($AuthenticationType -eq  "Windows") {
        $authenticationString = "windowsAuthentication"
    }
    else {
        $authenticationString = "anonymousAuthentication"
    }

    $config = $ApplicationHostManager.GetApplicationHostConfiguration()
    $section = $config.GetSection("system.webServer/security/authentication/$authenticationString")
    $section.OverrideMode = "Allow"  
    $ApplicationHostManager.CommitChanges()  
    # When Invoke-Pester is called on the Publish-DosWebApplication Integration tests there seems to be inconsistent behavior when ran in and out of a debug session.
    Start-Sleep -s 3
    Write-DosMessage -Level "Information" -Message "Unlocked system.webServer/security/authentication/$authenticationString for configuration"

    Set-WebConfigurationProperty -Filter "/system.webServer/security/authentication/$authenticationString" -Name Enabled -Value $Enable.IsPresent -PSPath "IIS:\Sites\$SiteName\$AppName"
    
    if ($Enable.IsPresent){
        Write-DosMessage -Level "Information" -Message "Enabled $AuthenticationType Authentication on $SiteName/$AppName"
    }
    else {
        Write-DosMessage -Level "Information" -Message "Disabled $AuthenticationType Authentication on $SiteName/$AppName"
    }
}


function Start-CommandAndReturnOutput {
    [CmdletBinding()]
    param(
        [ValidateNotNullOrEmpty()]
        [string] $Command
    )

    Write-DosMessage -Level "Verbose" -Message "Running: $Command"

    $output = Invoke-Expression "$Command"

    return $output
}


<#
    .SYNOPSIS
 
    NON-PUBLIC Validates if the assembly is loaded into the current PSSession
 
    .DESCRIPTION
 
    Checkes if the specified DLL is already loaded.
 
    .PARAMETER AssemblyFile
 
    DLL file to check if it is loaded
 
    .PARAMETER ExactVersion
 
    Specifies if we require an exact version match. If not, we will return true if a higher level version is loaded than the specified version
 
    .EXAMPLE
 
    Test-AssemblyLoaded -assemblyFile "C:\Sql\Microsoft.Sql.Smo.Dll"
 
    .OUTPUTS
 
    True if the assembly or higher version is loaded (if exactVersion = $false). Throws an exception if a down level version is found to be loaded (this can cause issues)
 
#>

function Test-AssemblyLoaded{
    [CmdletBinding()]
    param(
        [string] $AssemblyFile,
        [bool] $ExactVersion = $false

    )

    if(!(Test-Path $AssemblyFile)){
        Write-DosMessage -Level "Error" -Message "Can't find $AssemblyFile"
    }

    [System.Reflection.AssemblyName] $targetName = [System.Reflection.AssemblyName]::GetAssemblyName($AssemblyFile)

    Write-DosMessage -Level "Verbose" -Message "Assembly info $targetName for $AssemblyFile"

    [Array] $loadedAssemblies = [AppDomain]::CurrentDomain.GetAssemblies()

    foreach($loadedAssembly in $loadedAssemblies){
        [System.Reflection.AssemblyName] $loadedName = $loadedAssembly.GetName()
        #Have to do deep comparison - Equals and -eq just compare references
        if(($loadedName.Name -eq $targetName.Name) -and ($loadedName.Version -eq $targetName.Version)){
            Write-DosMessage -Level "Verbose" -Message "Exact assembly $($loadedName.Name) already loaded"
            return $true
        }

        if(!$ExactVersion){
            if($loadedName.Name -eq $targetName.Name){
                if($loadedName.Version -ge $targetName.Version){
                    Write-DosMessage -Level "Verbose" -Message "Found assembly $($loadedName.Name) with version $($loadedName.Version) which is greater or equal to targeted version $($targetName.Version)"
                    return $true
                }
                else{
                    Write-DosMessage -Level "Error" -Message "Older assembly version loaded than $($targetName.Name) with specified version $($targetName.Version)"
                }
            }
        }
    }

    return $false
}



<#
    .SYNOPSIS
 
    Tests if the powershell session is running with elevated permissions.
 
    .DESCRIPTION
 
    Tests if the powershell session is running with elevated permissions.
#>


function Test-ElevatedPermission {
    $elevated = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
    if (-not $elevated ) {
        Write-DosMessage -Level "Fatal" -Message "This procedure requires elevated permissions."
    }
}


<#
    .SYNOPSIS
 
    Checks for a specific version of an installed application.
 
    .DESCRIPTION
 
    Checks the windows registry for an installed application has the exact version specified.
 
    .PARAMETER appName
 
    Application name pattern. The function uses "like" to match against the DisplayName in the registry.
 
    .PARAMETER supportedVersion
 
    The specific version to check. This is a string paramreter and should match the registered version exactly.
 
    .EXAMPLE
 
    Test-PrerequisiteExact -appName "Dot Net Runtime" -supportedVersion "1.2.3.4"
 
    .OUTPUTS
 
    boolean. True if the application is installed at the same exact version. A newer version will return false.
#>


function Test-PrerequisiteExact {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        $appName,         
        $supportedVersion
    )
    $installedAppResults = Get-InstalledApps | Where-Object {$_.DisplayName -like $appName}
    if($null -eq $installedAppResults){
        return $false;
    }

    if($null -eq $supportedVersion)
    {
        return $true;
    }

    $supportedVersionAsSystemVersion = [System.Version]$supportedVersion

    Foreach($version in $installedAppResults)
    {
        $installedVersion = [System.Version]$version.DisplayVersion
        if($installedVersion -eq $supportedVersionAsSystemVersion)
        {
            return $true;
        }
    }
    return $false;
}


<#
    .SYNOPSIS
 
    Checks for a specific version of an installed application.
 
    .DESCRIPTION
 
    Checks the windows registry for an installed application has the exact version specified.
 
    .PARAMETER appName
 
    Application name pattern. The function uses "like" to match against the DisplayName in the registry.
 
    .PARAMETER supportedVersion
 
    The specific version to check. This is a string paramreter and should match the registered version exactly.
 
    .EXAMPLE
 
    Test-PrerequisiteExact -appName "Dot Net Runtime" -supportedVersion "1.2.3.4"
 
    .OUTPUTS
 
    boolean. True if the application is installed at the same or newer version.
#>


function Test-PrerequisiteSameOrNewer {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        $appName,         
        $supportedVersion
    )
    $installedAppResults = Get-InstalledApps | Where-Object {$_.DisplayName -like $appName}
    if($null -eq $installedAppResults){
        return $false;
    }

    if($null -eq $supportedVersion)
    {
        return $true;
    } 

    $supportedVersionAsSystemVersion = [System.Version]$supportedVersion

    Foreach($version in $installedAppResults)
    {
        $installedVersion = [System.Version]$version.DisplayVersion
        
        if($installedVersion.Major -eq $supportedVersionAsSystemVersion.Major) {
            if($installedVersion.Minor -eq $supportedVersionAsSystemVersion.Minor) {
                if($installedVersion.Build -ge $supportedVersionAsSystemVersion.Build) {
                    Write-DosMessage -Level "Information" -Message "Found $($appName) version $($installedVersion.ToString())"
                    return $true;
                }
            }
        }        
    }
    return $false;
}


<#
    .SYNOPSIS
 
    Waits for the application pool to enter the specified state
 
    .DESCRIPTION
 
    Waits for the application pool to enter the specified state. Needed to allow files to be overwritten during upgrade
 
    .PARAMETER AppPoolName
 
    App pool to wait for state change on
 
    .PARAMETER AppPoolState
 
    State to wait for the app pool to enter. Current allowed values of "Started" and "Stopped"
 
    .PARAMETER TimeOut
 
    Amount of time to wait before throwing an error. Default is 2 minutes (240 seconds)
 
    .EXAMPLE
 
    Wait-AppPoolStateChange -AppPoolName "Identity" -AppPoolState "Started"
#>


function Wait-AppPoolState{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [ValidateLength(1,64)]
        [ValidateScript({
            if ($_ -match '[^a-zA-Z0-9]') {
                Write-DosMessage -Level "Fatal" -Message "$_ must only contain alphanumeric values. Please remove special characters."
            }
            else {
                $true
            }
        })]
        [string] $AppPoolName,
        [Parameter(Mandatory=$true)]
        [ValidateSet("Started", "Stopped")]
        [string] $AppPoolState,
        [int] $TimeOut = 240
    )

    Test-ElevatedPermission

    Import-Module WebAdministration

    $currentState = Get-WebAppPoolState -Name $AppPoolName
    Write-DosMessage -Level "Information" -Message "Waiting for app pool '$AppPoolName' to enter the '$AppPoolState' state"
    $loopTimes = 0
    while($currentState.Value -ne $AppPoolState) {
        Write-DosMessage -Level "Debug" -Message "Waiting 1 second"
        Start-Sleep 1
        $currentState = Get-WebAppPoolState -Name $AppPoolName
        if($loopTimes -ge $TimeOut) {
            Write-DosMessage -Level "Error" -Message "Timed out waiting for $AppPoolName to enter state $AppPoolState - timeout $TimeOut seconds"
            return
        }
        $loopTimes++
    }

    Write-DosMessage -Level "Information" -Message "App Pool $AppPoolName successfully entered state $AppPoolState"
}


<#
    .SYNOPSIS
 
    Adds the target assembly - essentially loads the assembly
 
    .DESCRIPTION
 
    Attempts to load the specified DLL files (assemblies). Will attempt to resolve from a couple of common paths during an attempt to load
 
    .PARAMETER Assemblies
 
    DLL files to attempt to load
 
    .OUTPUTS
 
    $true if the specified assemblies are already loaded, $false if the assemblies aren't loaded due to not being found or other issues.
 
    .EXAMPLE
 
    Add-Assembly -Assemblies "Microsoft.Sql.Smo.Dll"
 
#>

function Add-Assembly {
    [CmdletBinding()]
    param(
        [Object[]] $Assemblies,
        [string] $AssemblyFilter = "*.dll"
    )

    if(($null -eq $Assemblies) -or ([String]::IsNullOrEmpty($Assemblies[0]))){
        Write-DosMessage -Level "Error" -Message "Must specify one or more assemblies to load"
        return $false
    }

    foreach($assemblyFilePath in $Assemblies){

        Write-DosMessage -Level "Verbose" -Message "Attempting to load assembly $assemblyFilePath"
    
        if([String]::IsNullOrEmpty($assemblyFilePath)){
            Write-DosMessage -Level "Error" -Message "Specified assembly file path is null or empty"
            return $false
        }

        [string] $targetAssemblyFilePath = $assemblyFilePath
        
        if(!(Test-Path -Path $targetAssemblyFilePath)){
            Write-DosMessage -Level "Verbose" -Message "Unable to resolve $assemblyFilePath, searching common folder"
            $targetAssemblyFilePath = Resolve-CommonFilePath -AssemblyFileName $targetAssemblyFilePath
        }

        if([String]::IsNullOrEmpty($targetAssemblyFilePath)){
            Write-DosMessage -Level "Error" -Message "Unable to find specified assembly $assemblyFilePath"
            return $false
        }

        Write-DosMessage -Level "Verbose" -Message "Found $targetAssemblyFilePath for specified file $assemblyFilePath"
        
        if(!(Test-AssemblyLoaded -assemblyFile $targetAssemblyFilePath)){

            try{
                Write-DosMessage -Level "Verbose" -Message "Attempting to load $targetAssemblyFilePath"
                Add-Type -Path $targetAssemblyFilePath
            }
            catch{
                Write-DosMessage -Level "Error" -Message "Unable to load. Exception: $($_.Exception)"
            }
        
        }
    }

    Write-DosTelemetry -Message "Add-Assembly called."
}


<#
    .SYNOPSIS
 
    Adds or updates a registration with the Discovery Service
 
    .DESCRIPTION
 
    Uses the Discovery Service /Services endpoint to POST a new service or update an existing registration.
 
    .PARAMETER DiscoveryPostBody
 
    An hashtable that is used in the body of the request to create or update a registration.
    The following are required members on said object: ServiceName, Version, ServiceUrl, DiscoveryType, IsHidden
 
    DiscoveryPostBody has the following types:
    Required:
    - ServiceName (string)
    - Version (integer)
    - ServiceUrl (string)
    Optional:
    - DiscoveryServiceId (integer) only required when updating a service
    - Heartbeat (string)
    - DiscoveryType (string, usually "Service" or "Application")
    - IsHidden (boolean)
    - Icon (string)
    - FriendlyName (string)
    - Description (string)
    - BuildNumber (string)
    - ShallowHealthCheckUrl (string)
    - DeepHealthCheckUrl (string)
 
    Since this function just posts to the DiscoveryService Services endpoint, please see the Discovery Service documentation for additional information
 
    .PARAMETER ConfigStore
 
    An object that describes the location of the installation configuration file.
    Used to get the url for the discovery service from the common scope.
    Accepts a powershell hashtable or dictionary with key value pairs following the example, $configStore = @{Type = "File"; Format = "XML"; Path = "C:\Path\To\File"}.
 
    .EXAMPLE
 
    Add-DosDiscoveryRegistration -DiscoveryPostBody $postBody -ConfigStore $configHashTable
 
#>

function Add-DosDiscoveryRegistration {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [hashtable]$DiscoveryPostBody,
        [Parameter(Mandatory = $true)]
        [hashtable]$ConfigStore,
        [string] $AccessToken
    )
    begin {
        function Confirm-BodyParameters {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification="Values should be plural.")]
            [CmdletBinding()]
            param(
                [Parameter(Mandatory = $true)]
                [hashtable]$PostBody,
                [string] $DiscoveryServiceUrl
            )

            $DiscoveryServiceUrl = $DiscoveryServiceUrl.Substring(0,$DiscoveryServiceUrl.IndexOf('/v'))
            $discoverySwaggerJson = Invoke-RestMethod -Method GET -Uri "$DiscoveryServiceUrl/swagger/swagger.json"

            $requiredParams = $discoverySwaggerJson.definitions.ApiService.required

            $missingRequiredParams = $requiredParams | Where-Object { !$PostBody.ContainsKey($_) }

            if ($missingRequiredParams.Length -gt 0) {
                Write-DosMessage -Level "Fatal" -Message "These parameters are required but missing from the discovery registration payload: $($missingRequiredParams -join ", ")"
            }

            $allParams = $discoverySwaggerJson.definitions.ApiService.properties.PSObject.properties
            
            $allParams = $allParams | ForEach-Object { $_.Name } 

            $optionalParams = $allParams | Where-Object { !$requiredParams.Contains($_) }

            # Exception being made due to invalid swagger api documentation on what is actually optional/required
            # In other words, DiscoveryType and IsHidden are required but not documented as such. DiscoveryServiceId is auto-generated ID column
            $optionalParamsExceptions = @(
                "DiscoveryServiceId"
                "DiscoveryType"
                "IsHidden"
            )

            $optionalParams = $optionalParams | Where-Object { !$optionalParamsExceptions.Contains($_) }

            $missingOptionalParams = $optionalParams | Where-Object { !$PostBody.ContainsKey($_) }

            foreach ($missingOptional in $missingOptionalParams) {
                $PostBody.$missingOptional = ""
            }

            if ($null -eq $PostBody.IsHidden) {
                $PostBody.IsHidden = $true
            }

            if ($null -eq $PostBody.DiscoveryType) {
                $PostBody.DiscoveryType = "Service"
            }
    
            if ($PostBody.Icon -eq [DBNull]::Value) {
                $PostBody.Icon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGwAAABsCAYAAACPZlfNAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMTZEaa/1AAARf0lEQVR4Xu2deYyURRrGXV3dqHHVGHc12XjsJhv/UGM8spt4ZDUeCN73bVSIGo8ooKgIion3hSDHeCHXDMoxHiOKcigiCoo3qMBwDAJeIIjAOAxT+/xqqoZv+pqe7q+756gnedPd31Ff1fvU8b5vVX29XUBAQEB6vPzyyztNnDjxH+PHjz9U34/R58kvvfRSN4TvHCsvLz+Ua7jW3RZQaEyZMmXXcePGHSUiekgGS6ZJlku2igiTjejaen0uc/cO0vfuIvXIUaNG7eoeE5ArysrKdnQE9ZNiZ+lzs1c8MmHCBPPKK6+Yd955x8yaNcvMnTvXfPbZZ+bLL780X3/9tRW+c4xz77//vr2We7g3mpbS3qTPmZK7JUfMmDHjzy4bAZkgZe1QUVFxrBRIC1qNMhG1AvPWW2+Zjz/+2FRXV5s1a9aYP/74w+QK7iUN0iJN0uYZ/nl69irJIFWYo++5557tXfYCPDS+/E0K6y0lLfJKoyV8+OGHZtmyZXmRky1qa2vN0qVL7TMrKyuj5C3UZy997u2y23khJRwoGSqhS7Jd1QcffGBWrlxp6uvrnSqLD579/fff267WtzzlcaNksMg8wGW/82DMmDH7q/BlUkQdynj99dfN/PnzzebNm53K2g42bdpk8/baa6/5VlcrGSbZzxWn40Ld3G4iaoAKaw2IN954w44jW7dudeppuyCP5JU8k3d6BUn/DmthagDvqgIup7CvvvqqWbx4cbsgKhHkedGiRbYMjjhciy6umO0fkyZN2kvjwBgVroEx6vPPPzd1dXWu+O0XGEK4DJRJhDVIRlZVVe3pit0+IaL+J6JWUBPxgX799VdX3I4DykTZKKOkRgQe54rffqBybK8a11dSTw3EiW2P3V+2oGw46ViUKvMWSZ924785w6KSGodl9dNPP7lidXxQVm9NSgfj27xBIgd4X2V2HhmeNm1amzTTCw3cgKlTp/ouci5BAaeetgV1ff9UBqvJKNGCUjq+pQZlnz17tidtsVrbgU5NbQMi6yBlbCUZxApsaGhwWe+8QAdYkY60moqKin87dZUWrmVZsjAuAprjq6++aiJNutrfqa00YMxSc7fdYCArPSKkLZbs49RXXGAN6uHWwKAbDMgM3z2qgs8pKyvbxamxONDz8bOs6Y6BEdAyGNO8ISLdTSiqn6YH9uXBmO6d2RpsLdAVOnOk9XHqLCwIN+lh9TiIndHPyhf4aS5wvEW6PNaptTAgkKsHrSDc1JkiGHHjxx9/9GGspdLn7k698UMPGEtzDhZh/iD2iC4lo5164wXzWUq8gch0cIzzBwHjt99+m1bG1Ey882kuoLucrrAjTpGUCqzccl1jtWRnp+78ocSY1g/+VgEQ8c/uderODyyYUYKbsWw6wkxxWwMz1+hWhG0gcuTUnjuUEKub7BqMgMKANSKulQ13as8NSuBAJVTHSqGOPGNcauBQs9RP+q6rqKjIfd2jEhgK8yzvCigs0LFrZYOd+lsHZkp18yaYD62r8NiyZYuPgKyvrKzcw9GQPURWbxhn1WtAcYCuXSvr7WjIDrppB920CL8rxAuLB+KM+GXS/4JWRfPZ8gPTbEwIKC7YgKHG0lBeXv4fR0fL0A2DIYxdJAHFxYoVK3y3OMjRkRnsPtTFq9mfFea6ig+MDxl8ELZCP//kaEkP9aFHwnCYSS4d0D0cjBs37nBHS3qIWfYU252PAaUBO0HhQHK3oyU9dNEsLJVibFMNSA1vLarxzHC0pAavVtBFm9mkHVA6MN84efJkWthaSfp3i/CaBZoiO+vzxYYNG8ybb75pli9f7o40AkNGFcNs3LjRHWk85qcZxowZY2bOnGlrWSZMnz7d7kMG/lnffPON/R0F6aQ7995775k5c+a4X82B0r799lszadIkM3r0aKvAH374wZ1tBGmSdir55Zdf3FW5AQ7QhxrQIY6eZOhkDy6KI3ZIP3ziiSeaa665xlo+HuzW79atW5Oy165da2644QZz8cUXmyeeeMI8/fTT5rrrrjPnn39+xqUI5513niUeUCl41rXXXmt/R0FvcdJJJ5mhQ4e6I43g+eTjjDPOML///rs72gjyKMfVnHPOOebhhx82Q4YMMXfccYc59dRT7e4UD45fdNFF5sEHH0wSyp8PIrHFHo6eZOik9b+YCc0XZPi0004zN954oxk1apQ72pwwavGdd95pbrnllmYtithlWVmZOffcc5u1xCgSCUOZl156qZ2q8CD9m2++2VxxxRVJhD333HPm/vvvNzfddJNtRVHw7MsuuyxJD3J5zPXXX2/LACDsrrvust/jBi3UEZbeH9PJaYSj4jA4IKxr167mu+++s7UYhxBECWP1ELU/VbySkNiZZ55pu6JUSEXYsGHDzJNPPmmPgZqaGnvd448/3owwykcL/vTTT236PXr0aFqnwrmzzjrLTne0hEIShp4gTDLV0ZMMEbYchzkOQBhKBM8884xtRSglShivFILUdA76rbfemtQyPFIRRjdy9tlnN1W4F154wQwaNMgMHDiwWTos5qTVkR+6QyrGggUL7DmW71GJosMC+SPfXnx+IYyu/LHHHmsmlDcO0PLFyRJHT3OIyZ10ciurouJAlDC6NbqYqqqqZoQx4ENYuumb3r17t4ow0qGLY0MdyxlQJoqPEua7Sbq99evXW3nggQesAN/qo4TxZhzy6cW3egi7+uqr7cb0qDDhGwfcqqraTz75ZEdH0zZMbHw9nX2JVhyIEgawehjEV69e3UTYqlWrrHKwxhKBwmktkJwKqQgDKKtnz572ed4IiRLGUodTTjnFph0ViPjtt99shUrsEjnGOYQ0/blCdokALkRYvazU5LUectR436DtpuJAImHgoYceMn379jWnn356k9GBcrHAErtFajWKpAWkQjrC1q1bZ9Pv169fkzERJYwukjzwPC9UDqxZyg8g4vLLL096Nl3tlVde2VSJCk0YXChPvC7jIEfTNujEMWQYfygOpCIMq4tWhgkOYYAZAcYTaq5qk511ve+++2wt/+ijj+w1qZCOMIApTivGPwOeMH5DZqp0Ifeqq66y3+nCb7vtNnPhhReaF1980RKEVYkVSpfrl6l7YjmXKHH4st43lX/8X0fTNqiF8fZOu4Q4DkDOs88+635tA+MW4wctwQOLkK4MxWLRKYMtOp64Cr4r5VkoyYPxJ+ov4SCz1WfJkiXWEElszYD8DB8+vGmhLNcwHwgpjz76qDUkNJY0G285T1lSSabKli38cm5V5BMcTdugg904GdbNtx3AhSMseSl3IKztISNhcXeJAfkjY5eoE7EaHQH5wxsdakzJ6zt4VTgn4zLrA/JHRrM+bsc5IH94x1m8JL8qQgcJTdXHFZoCBHzxb37++Wf7G0cZ38nH7RJBBgnIesybN8/6ZdwXBUFd3AB/nNggQeZMwAcjLwsXLnRHGkEaLHrhHEIsFZM8OkvAFBCOfKqgOMf8vYmCO5EPfGhKbkJyaAqItGVxBX8B8Tki9VF/DH8G5zMRhH0IwuIvARRxySWXWEc30QllmoNou/eJcLrx3TIBUkgLhzhaAfC3jj/+eBsZwQd85JFHbHwQZ977ZEzZEM6K+o4eBI8JBBDxwGGPytixY91VuQHSRVi1oycZOhnb9Aq1kmgDUQIm+YjHAVodhScSEgUtBoI8CRBHwPj55583/fv3t8c8WksYpEAAzjQxw+h6S09YdIrHxzF9JCUbwlLFQ/MB+mKIEicZp1f4e4tYJjCVlo0TohAI890Dtfv22283gwcPtr8B1zAxGJ3oZGJz5MiRtvuDeCqAR2sJo5tlMpSKyEwykQiPVIRBAj0DkQ1QCsIYRuBC8pSjJxk62Z2LolMLuQBF0joYWwBrInr16tXUFRHSIabod3Qy/hAL9BWFOCMtwYenuDdKZmsJGzBgQFPwF8uLOKRv8Z4wWjFLFOgSiQ9yvc9vtl0iwe2o5OMiMasAFypXd0dPMmTv20Wk+QYuUQpdCl0PSsfIoMAEaQFrPOj+vIFDBB2levgJT+5F6MuZ20K5oDWEcX+XLl2s8vjOQhry5iuTJ4yul8UzzHUx5hLoZZ07yIawESNGJC3E8bPsucCZ9JTrYEdPMnjlqbqyTfkuc2P6gmg7XaEXBnym8D1YHQUpFBgF+ggLaztoARASvR+l+9XIrSGM1s3akmhaPI/uGqTqEgFWIdF7nlHsLpGWzZguLtaItMx/oaULZqqlNXUZrQU1GOUmdquY7BdccEFTuszs0u3xLKwy3/0wWwxhngwPxjwqAsiWMLpcpkgS14X4PNLi0xFGhcJQKQVhkYWk0xwt6aGC85dMSVZctqBrYKVUIlAMRECIB90gBgWWKYA0pu9TrYmgAqBkBuNUhNFiuM8L81hUElyFVHvc+vTpY5fUecIYu5h6oWtkNRWtEj8IQBgz41Sa6DPoMj1hmPHRcwjpRZf4ZQt0DwcirK+jJT104RFcnOtmCEzndAFkTHUWiXowP0VN9kvc+MR3YaxJBGQylmGgsO4fv8q3SloQ6USFMvC8d999116TCFoETjmk0236+3g+x1m+4IExFE3bC+MMrTjVOYRWnwth5Jv8Sw5ztKSHai/bjVbRh1P7AooLKgCVUWTV6GfL242ACLP+mJ/GDygeIhv6Bjo6Woaa8tHc5M3agOLBBXwbxMFRjo6WwYZo3bQQSyW6hDqgsIhYh5is2XWHHmphvWhlqZZRBxQGkSUBPR0N2UM37S3ZiNWX6BMFxA+sSWZKRNg6Wa1/dTS0DiLM7mbJN7YY0DJ87FCSPtjbEmTaH6AEagmThFZWOOA+uZeD1U7I998jRBh/0Gm9/YDCgFlwdCzChji15w4ltJ8S2oT376dDAuIDc3Ru7FoviedvPkRYf2pAWAIXP5hcRbeSll/xkC3ctEt4SXPMIEaJ3yWyFk+ePPkvTt3xQIR1kdjXoAcDJH+gQ2YDRNZWkXayU3O8EGEjab5hOXf++OKLL2xXKJ2OcOqNH1VVVXvqITU04/BXHrmDCVTXFVbL4NjNqbcw0Dh2nGrFFiIgIc7YerBAFYtbOqwjyO7UWljoYX1ozswehzmz7IGusAHQnaSXU2fh4aL543kws6N+5jcgPdARy/vQmXTHApTWRePzxZQpU3bVw+eSgeCfZQZksU/AkTU7dhM+W5SXl/9dmeCPOO0fcwakRmRj3iLJ3k59pYEywL9H1ATSkkHL8mRJ+HO3/ZzaSgv+VNqTRvcYxrRGsiJhp6XS0b+cutoGmBZQxmz3iCHSma1Hyh4xMBbps220rEQoY/sog3PIKGvXO6Ofhp+Fu+PIml3yMasllJWV7aJMTiDDOIgsye4soKyU2ZE1rmTWYGvh/DSca/761g68HTlgTNmIDVJWlbtO5e6lw8X1s+KACnCsCoB1ZCPTcWwUbGugTC7qjlQXLdxUKKgQu0tGi7gGaiBWZBxbcksNyoAV6IK4W1W+EQUP5BYTKhDzadXURPp51oi0R0uSPLMGw03rI4tFWmHms0oNEbaz5F7JBgrLSiGW0OWyw6PYII8sRSPPjijWYNzdbgyLfDBx4sR9RdpwCQO0bXGsMG6LbgB5YkWub1HKc61kiL7Hs2CmPYE/6FThWaxKbbXjARsw2MFRylbH6jDyQF7cGIWskzyV97rBjoDKyso9RBx/4bhAnw0oiD1SbCpkNyK1vJDhLtLmGTyLKI3bn0Vr4m/n50t65rx8uiMD/41/ppOCBklWoDSEWs4uS95uwJjHDs1c92AD7mXrLWMSuyhZ2RxpSUiNnj+QLT+6vP35U6UAipLCDpfy2HM9XcIfx0SVarfQ4gOxpwrF4zLgpDPmIHznGOe4hmu5JzEdkbNGMk3SV78PCyTFACmSl5cdIukhoQWi4CUSjADeeGa70jRC11bvrq2WTNWxp1QhuksO1vfMr1YIiA+8hF/jy74yBg5St0Z3eoIEnw85gWOcEyn7pH0rWkBAQICw3Xb/B+xpzGHxSTPuAAAAAElFTkSuQmCC"
            }
        }
        function Get-ConfigStoreValue {
            param(
                [Parameter(Mandatory = $true)]
                [hashtable]$ConfigStore,
                [Parameter(Mandatory = $true)]
                [string]$Scope,
                [Parameter(Mandatory = $true)]
                [string] $ValueName
            )

            $commonConfig = Get-DosConfigValues -ConfigStore $ConfigStore -Scope "$Scope"
            $requestedValue = $commonConfig.$ValueName
            if ($null -eq $requestedValue) {
                Throw "The $ValueName variable was not found in the common scope of the configuration file"
            }

            return $requestedValue
        }

        function Add-CurrentUserToDiscoveryServiceUserRole {
            param(
                [Parameter(Mandatory = $true)]
                [hashtable]$ConfigStore
            )

            $commonConfig = Get-DosConfigValues -ConfigStore $configStore -Scope 'common'
            Write-DosMessage -Level 'Information' -Message "Granting DiscoveryServiceUser role to the current user, $(whoami), in the '$($commonConfig.metadataDbName)' database, on '$($commonConfig.metadataSqlServerInstanceAddress)'"
            
            $MetadataConnectionString = "Server=$($commonConfig.metadataSqlServerInstanceAddress);Database=$($commonConfig.metadataDbName);Trusted_Connection=True;MultipleActiveResultSets=True;Connect Timeout=10"

            $sqlQuery = '
                DECLARE @RoleID INT;
 
                IF NOT EXISTS(SELECT * FROM [CatalystAdmin].[IdentityBASE] WHERE [IdentityNM] = SYSTEM_USER)
                    INSERT INTO [CatalystAdmin].[IdentityBASE] ([IdentityNM]) VALUES (SYSTEM_USER)
 
                DECLARE @IdentityID INT = (SELECT [IdentityID] FROM [CatalystAdmin].[IdentityBASE] WHERE [IdentityNM] = SYSTEM_USER)
                 
                SET @RoleID = (SELECT [RoleID] FROM [CatalystAdmin].[RoleBASE] WHERE [RoleNM] = ''DiscoveryServiceUser'')
                 
                IF NOT EXISTS(SELECT * FROM [CatalystAdmin].[IdentityRoleBASE] WHERE [RoleID] = @RoleID AND [IdentityID] = @IdentityID)
                    INSERT INTO [CatalystAdmin].[IdentityRoleBASE] ([IdentityID], [RoleID]) VALUES(@IdentityID, @RoleID)'


            Invoke-DosSqlQuery -ConnectionString $MetadataConnectionString -Query $sqlQuery -NonQuery

        }

        function Get-IdentityAccessToken {
            [CmdletBinding()]
            param(
                [Parameter(Mandatory = $true)]
                [string] $identityServiceUrl,
                [Parameter(Mandatory = $true)]
                [string] $clientId,
                [Parameter(Mandatory = $true)]
                [string] $scope,
                [Parameter(Mandatory = $true)]
                [string] $secret
            )

            
            $url = "$identityServiceUrl/connect/token"
            $body = @{
                client_id     = "$clientId"
                grant_type    = "client_credentials"
                scope         = "$scope"
                client_secret = "$secret"
            }
            try {
                $accessTokenResponse = Invoke-RestMethod -Method Post -Uri $url -Body $body
                return $accessTokenResponse.access_token
            }
            catch {
                $exception = $_.Exception
                $errorMessage = Get-ErrorFromResponse -response $exception.Response
                Write-DosMessage -Level "Error" -Message "There was an error getting the access token at $url : $errorMessage. Halting installation."
                throw $exception
            }
        }

        function Get-ErrorFromResponse {
            param(
                [Parameter(Mandatory = $true)]
                $response   
            )
            $result = $response.GetResponseStream()
            $reader = New-Object System.IO.StreamReader($result)
            $reader.BaseStream.Position = 0
            $reader.DiscardBufferedData()
            $responseBody = $reader.ReadToEnd();
        
            return $responseBody
        }

        function Add-DiscoveryRegistrationSql {
            param (
                [string] $connectionString,
                [hashtable] $postBody
            )
            $query = "DECLARE @InsertBuildNumberTXT nvarchar(100) = ''
                    DECLARE @InsertListBuildNumberTXT nvarchar(50) = ''
                    DECLARE @UpdateBuildNumberTXT nvarchar(100) = ''
                    DECLARE @InsertOrUpdateService varchar(max) = ''
                     
                    IF EXISTS (SELECT * FROM sys.columns WHERE name = 'BuildNumberTXT' AND OBJECT_ID = OBJECT_ID('CatalystAdmin.DiscoveryServiceBASE'))
                    BEGIN
                    SET @InsertBuildNumberTXT = ',''' + @BuildNumber + '''';
                    SET @InsertListBuildNumberTXT = ',[BuildNumberTXT]';
                    SET @UpdateBuildNumberTXT = ',BuildNumberTXT = ''' + @BuildNumber + '''';
                    END
                     
                    IF EXISTS
                    (
                        SELECT *
                        FROM [CatalystAdmin].[DiscoveryServiceBASE]
                        WHERE ServiceNM = @ServiceName AND ServiceVersion = @Version
                    )
                    BEGIN
                        SET @InsertOrUpdateService = '
                        UPDATE [CatalystAdmin].[DiscoveryServiceBASE]
                        SET [ServiceUrl] = ''' + @ServiceUrl + ''',
                            DiscoveryTypeCD = ''' + @DiscoveryType + ''',
                            HiddenFLG = ' + CAST(@IsHidden AS NVARCHAR(1)) + ',
                            FriendlyNM = ''' + @FriendlyName + ''',
                            IconTXT = ''' + @Icon + ''',
                            ShallowHealthCheckUrl = ''' + @ShallowHealthCheckUrl + ''',
                            DeepHealthCheckUrl = ''' + @DeepHealthCheckUrl + ''',
                            DescriptionTXT = ''' + @Description + '''' + @UpdateBuildNumberTXT + '
                        WHERE ServiceNM = ''' + @ServiceName + ''' AND ServiceVersion = ' + CAST(@Version AS NVARCHAR(1)) + ';'
                    END
                    ELSE
                    BEGIN
                        SET @InsertOrUpdateService = '
                        INSERT INTO [CatalystAdmin].[DiscoveryServiceBASE]
                        ([ServiceNM],
                        [ServiceUrl],
                        [ServiceVersion],
                        [DiscoveryTypeCD],
                        [HiddenFLG],
                        [FriendlyNM],
                        [IconTXT],
                        [ShallowHealthCheckUrl],
                        [DeepHealthCheckUrl],
                        [DescriptionTXT]' +
                        @InsertListBuildNumberTXT + '
                        )
                        VALUES
                        ('''
                        + @ServiceName + ''','''
                        + @ServiceUrl + ''','
                        + CAST(@Version AS NVARCHAR(20)) + ','''
                        + @DiscoveryType + ''','
                        + CAST(@IsHidden AS NVARCHAR(1)) + ','''
                        + @FriendlyName + ''','''
                        + @Icon + ''','''
                        + @ShallowHealthCheckUrl + ''', '''
                        + @DeepHealthCheckUrl + ''', '''
                        + @Description + ''''
                        + @InsertBuildNumberTXT + '
                        );'
                    END
                     
                    EXEC(@InsertOrUpdateService);"

            
            Invoke-DosSqlQuery -ConnectionString $connectionString -Query $query -NonQuery -Parameters $postBody
        }
    }
    process {
        
        $discoveryServiceUrl = Get-ConfigStoreValue -ConfigStore $ConfigStore -Scope "common" -ValueName "discoveryService"

        $postBody = $DiscoveryPostBody.PSObject.Copy()

        Confirm-BodyParameters -PostBody $postBody -DiscoveryServiceUrl $discoveryServiceUrl

        if ($AccessToken) {
            # Leaving commented for future work, if/when discovery can operate service to service
            # if ($AnonymousAuth.IsPresent) {
            # $identityServiceUrl = Get-ConfigStoreValue -ConfigStore $ConfigStore -Scope "common" -ValueName "identityService"
            # $fabricInstallerSecret = Get-ConfigStoreValue -ConfigStore $ConfigStore -Scope "common" -ValueName "fabricInstallerSecret"
            # $encryptionCertificateThumbprint = Get-ConfigStoreValue -ConfigStore $ConfigStore -Scope "identity" -ValueName "encryptionCertificateThumbprint"
            # $decryptedFabricInstallerSecret = Unprotect-DosInstallerSecret -CertificateThumprint $encryptionCertificateThumbprint -EncryptedInstallerSecretValue $fabricInstallerSecret
            # $accessToken = Get-IdentityAccessToken -identityServiceUrl $identityServiceUrl -clientId "fabric-installer" -scope "fabric/identity.manageresources" -secret $decryptedFabricInstallerSecret
                
            # $postHeaders += @{ "Authorization" = "Bearer $accessToken" }
            # }
            try {
                Write-DosMessage -Level "Information" -Message "Attempting to register with Discovery via direct sql"
                $sqlServerInstanceAddress = Get-ConfigStoreValue -ConfigStore $ConfigStore -Scope "common" -ValueName "metadataSqlServerInstanceAddress"
                $metadataDatabase = Get-ConfigStoreValue -ConfigStore $ConfigStore -Scope "common" -ValueName "metadataDbName"
                $connectionStringMetadata = "Server=$sqlServerInstanceAddress;Database=$metadataDatabase;Trusted_Connection=True;MultipleActiveResultSets=True;"
                Add-DiscoveryRegistrationSql -connectionString $connectionStringMetadata -postBody $postBody
                Write-DosMessage -Level "Information" -Message "Successfully registered with Discovery via direct sql"
            }
            catch {
                Write-DosMessage -Level "Fatal" -Message "Failed to register with Discovery via direct sql. Exception: $($_.Exception.Message)"
            }
        }
        else {
            Add-CurrentUserToDiscoveryServiceUserRole -configstore $ConfigStore
            $postHeaders = @{
                "Accept"       = "application/json"
                "Content-Type" = "application/json"
            }

            $postBodyJson = ConvertTo-Json $postBody -Depth 100        
        
            Write-DosMessage -Level "Information" -Message "Registering $($postBody.ServiceName) v$($postBody.Version) with Discovery Service"

            try {
                $discoveryRegistrationParams = @{
                    Method                = "Post"
                    Uri                   = "$discoveryServiceUrl/Services"
                    Body                  = $postBodyJson
                    Headers               = $postHeaders
                    UseDefaultCredentials = $true
                }
                Invoke-RestMethod @discoveryRegistrationParams | Out-Null
                Write-DosMessage -Level "Information" -Message "Registration Successful"

                $postBody.Remove("Icon")
                Write-DosMessage -Level "Information" -Message "Registration summary: $(($postBody | ConvertTo-Json -Compress).ToString())"
            }
            catch {
                if ($_.Exception.Message -like '*403*') {
                    Write-DosMessage -Level "Warning" -Message "Please make sure the current user, $(whoami), has the right DiscoveryService permissions"
                }
                Write-DosMessage -Level "Fatal" -Message "Registration failed: $($_.Exception.Message)"
            }
        }
    }
}


<#
.SYNOPSIS
Adds a SQL login to a database role
 
.DESCRIPTION
Adds a SQL user to a database role
 
.PARAMETER InstanceName
Full instance name of SQL server
 
.PARAMETER ConnectionString
Connection string to a SQL instnace
 
.PARAMETER SqlConnection
SQL connection to target SQL Server
 
.PARAMETER DatabaseName
Database in which the role exists
 
.PARAMETER UserName
The full name of the user to add to the role
 
.PARAMETER RoleName
The full name of the database role
 
.PARAMETER Force
Will force the addition of the user to the database role even if it is not needed.
 
 
.INPUTS
None
 
.OUTPUTS
None
 
.EXAMPLE
PS> Add-DosSqlDatabaseRoleMembership ...
#>

function Add-DosSqlDatabaseRoleMembership {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Passing through the -WhatIf and -Confirm to the Invoke-DosSqlQuery - this is a supported scenario")]
    [cmdletbinding(SupportsShouldProcess=$true)]  
    [OutputType()]
    param(
        [parameter(Mandatory=$true,ParameterSetName='Connection')][Data.SqlClient.SqlConnection]$SqlConnection,
        [parameter(Mandatory=$true,ParameterSetName='ConnectionString')][string]$ConnectionString,
        [parameter(Mandatory=$true,ParameterSetName='AdHoc')][string]$InstanceName,
        [parameter(Mandatory=$true,ParameterSetName='AdHoc')][string]$DatabaseName,
        [parameter(Mandatory=$false,ParameterSetName='AdHoc')][PSCredential]$Credential,
        [parameter(Mandatory=$true)][string]$UserName,
        [parameter(Mandatory=$true)][string[]]$RoleName,
        [switch] $force
    )

    #remove this from the parameters
    $PSBoundParameters.Remove('ErrorAction') | Out-Null

    if(!$force){
        #query for all roles first:
        $roleQuery = "SELECT DP1.name AS DatabaseRoleName,
            isnull (DP2.name, 'No members') AS DatabaseUserName
            FROM sys.database_role_members AS DRM
            RIGHT OUTER JOIN sys.database_principals AS DP1
                ON DRM.role_principal_id = DP1.principal_id
            LEFT OUTER JOIN sys.database_principals AS DP2
                ON DRM.member_principal_id = DP2.principal_id
            WHERE DP1.type = 'R'
            ORDER BY DP1.name; "


        $queryResult = Invoke-DosSqlQuery @PSBoundParameters -Query $roleQuery
    }
    

    foreach($role in $RoleName){
        $userExistsInRole = $queryResult | where-object -property 'DatabaseRoleName' -eq $role | where-object -property 'DatabaseUserName' -eq $UserName
        if(!$userExistsInRole -or $force.IsPresent){
            $query += "ALTER ROLE [$role] ADD MEMBER [$UserName]"
        } else {
            Write-DosMessage -Level 'Information' -Message "User '$UserName' already has role '$role' in database."
        }
        
    }

    if($query){
        #if ($pscmdlet.ShouldProcess($Query, "Executing SQL Query")){
            Write-DosMessage -Level 'Information' -Message "Executing ALTER ROLE Query: $query"
            Invoke-DosSqlQuery @PSBoundParameters -Query $query -NonQuery | Out-Null
        #}
    }
    
}


function Add-IISUrlRewriteRule {
    <#
        .SYNOPSIS
        Add URL rewrite rule
 
        .DESCRIPTION
        Adds a given URL rewrite rule to IIS with a Redirect action. The IIS URL Rewrite extension must be installed in
        order to use this function.
 
        .PARAMETER IISWebSite
        The IIS site from which to remove the application. Defaults to "Default Web Site"
 
        .PARAMETER RuleName
        A unique name for the rule.
 
        .PARAMETER MatchUrl
        A regular expression that specifies the URL match pattern.
 
        .PARAMETER RedirectUrl
        The URL in which to redirect on a match.
 
        .PARAMETER UseRewrite
        Use a Rewrite action instead of a Redirect. Defaults to $false.
 
        .PARAMETER StopProcessing
        Enable the StopProcessing flag. When the rule action is performed (i.e. the rule matched) and the StopProcessing flag is turned on,
        it means that no more subsequent rules will be processed and the request will be passed to the IIS request pipeline.
        Defaults to $true.
 
        .EXAMPLE
        Add-IISUrlRewriteRule -RuleName "Atlas4-Atlas-Redirect" -MatchUrl "^Atlas4(.*)" -RedirectUrl "/Atlas" -UseRewrite $false -StopProcessing $true -IISWebSite "Default Web Site"
 
        .NOTES
        Rules are always appended to the list of existing rules, which are evaluated in order by IIS.
        Rules may be viewed from within the "URL Rewrite" feature within IIS Manager.
 
        Refer to the "URL Rewrite Module Configuration Reference" at
        https://docs.microsoft.com/en-us/iis/extensions/url-rewrite-module/url-rewrite-module-configuration-reference
        for further details.
    #>


    Param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $RuleName,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $MatchUrl,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $RedirectUrl,
        [bool] $UseRewrite = $false,
        [bool] $StopProcessing = $true,
        [string] $IISWebSite = "Default Web Site"
    )
    Import-Module WebAdministration -Force
    $iisPath = "IIS:\Sites\$($IISWebSite)"
    $ruleFilter = "/system.webserver/rewrite/rules/rule[@name='$RuleName']"
    $rulesXPath = "/system.webserver/rewrite/rules"

    $exists = Get-WebConfigurationProperty -PSPath $iisPath -Filter $ruleFilter -Name *
    if ($exists) {
        Write-DosMessage -Level "Information" -Message "Removing existing URL Rewrite rule '$ruleFilter'."
        Clear-WebConfiguration -PSPath $iisPath -Filter $ruleFilter
    }

    $actionType = "Redirect"
    if ($UseRewrite) {
        $actionType = "Rewrite"
    }

    Write-DosMessage -Level "Information" -Message "Adding URL Rewrite rule '$RuleName'."
    Add-WebConfigurationProperty -PSPath $iisPath -Filter $rulesXPath -Name "." -value @{ name = $RuleName; patternSyntax = "ECMAScript"; stopProcessing = $StopProcessing }
    Set-WebConfigurationProperty -PSPath $iisPath -Filter "$ruleFilter" -Name "match" -value @{ url = $MatchUrl }
    Set-WebConfigurationProperty -PSPath $iisPath -Filter "$ruleFilter" -Name "action" -value @{ type = $actionType; url = $RedirectUrl; appendQueryString = "false"; }
}



<#
.SYNOPSIS
    Asserts if an application install meets dependency version requirements.
.DESCRIPTION
    Returns true if all dependencies for an application meet SemVer requirements. Returns false otherwise.
.PARAMETER DiscoveryServiceUrl
    The parameter DiscoveryServiceUrl is used to define the Discovery Service URL.
.PARAMETER DependencyManifestPath
    The parameter DependencyManifestPath is used to define the location of the dependency manifest file for the service/app being installed.
 
    dependency.manifest example
    {
        "$schema": "./InstallReadinessTool.schema.json",
        "manifestName": "Analytics Manifest",
        "operationMode": "validate",
        "readinessChecks": [
            {
                "name": "Authorization Service Dependency",
                "checkType": "dependentService",
                "serviceName": "AuthorizationService",
                "serviceVersion": "1",
                "serviceBuildVersion": "^1.7"
            },
            {
                "name": "Identity Service Dependency",
                "checkType": "dependentService",
                "serviceName": "IdentityService",
                "serviceVersion": "1",
                "serviceBuildVersion": "^1.7"
            }
        ]
    }
.PARAMETER ServiceName
    The parameter ServiceName is used to define the service name to lookup in Discovery Service.
.EXAMPLE
    $vars = @{
        DiscoveryServiceUrl = "https://test/DiscoveryService/v1"
        DependencyManifestPath = "."
        ServiceName = "TestService"
    }
 
    Assert-DosValidDependencySet @vars
 
.EXAMPLE
    Assert-DosValidDependencySet -DiscoveryServiceUrl "https://test/DiscoveryService/v1" -DependencyManifestPath "." -ServiceName "TestService"
#>


function Assert-DosValidDependencySet {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]$DiscoveryServiceUrl,
        [Parameter(Mandatory=$true)]
        [ValidateScript({Test-Path $_})]
        [string]$DependencyManifestPath,
        [Parameter(Mandatory=$true)]
        [string]$ServiceName
    )

    $currentDependencies = Get-CurrentDependencyList -Path $DependencyManifestPath
    $discoveryServiceEntries = Get-ServiceList -CurrentDependencies $currentDependencies.readinessChecks -DiscoveryServiceUrl $DiscoveryServiceUrl

    if (!(Assert-DependencySemVerRequirementsMet -ServiceName $ServiceName -DiscoveryServiceEntries $discoveryServiceEntries -CurrentDependencies $currentDependencies.readinessChecks)) {
        return $false
    }

    return $true 
}

function Get-CurrentDependencyList {
    [CmdletBinding()]
    param (
        [string]$Path
    )

    return Get-Content -Path $Path | ConvertFrom-Json
}

function Get-ServiceList {
    [CmdletBinding()]
    param (
        [array]$CurrentDependencies,
        [string]$DiscoveryServiceUrl
    )
    $discoveryResponses = @()
    foreach ($dependency in $CurrentDependencies) {
        $discoveryResponse = Get-DosService -DiscoveryServiceUrl $DiscoveryServiceUrl -ServiceName $dependency.serviceName -ServiceVersion $dependency.serviceVersion
        $discoveryResponses += $discoveryResponse
    }
    return $discoveryResponses
}



<#
.SYNOPSIS
    Compares the scopes, variables, and values in two configstores.
.DESCRIPTION
    Compares the scopes, variables, and values in two configstores. Note: the config stores do not need to be of the same type.
.PARAMETER Configstore1
    The first configstore.
.PARAMETER Configstore2
    The second config store
.PARAMETER skipSensitiveVariables
    A switch parameter that will tell the Compare-DosConfigStore function to ignore sensitive variables when doing the compare.
     
.EXAMPLE
    $uri = 'https://mystoragetable.table.core.windows.net/Common?sp=raud&st=2020-10-04T21:34:00Z&se=2020-10-21T21:33:00Z&sv=2019-12-12&sig=cDMUXyvpozHJYkTfdQBhKiQ3PL05jV8gLvCXqc6GOgI%3D&tn=Common'
    $econfigstore = @{Type = "External"; Format = "AzureTable"; Uri = "$uri"}
    $configStore = @{Type = "File"; Format = "XML"; Path = "c:\Program Files\Health Catalyst\install.config"}
 
    Compare-DosConfigStore -configstore1 $econfigstore -configstore2 $configstore
#>

function Compare-DosConfigStore {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [hashtable]$Configstore1,
        [Parameter(Mandatory=$true)]
        [hashtable]$Configstore2,
        [switch]$skipSensitiveVariables
    )

    $equal = $true

    $sensitiveVariables = @('common.fabricInstallerSecret')

    for($i=0; $i -le 1; $i++){
        if($i -eq 0){
            $c1 = $Configstore1
            $c2 = $Configstore2
            $first = 'first'
            $second = 'second'
        }else{
            $c1 = $Configstore2
            $c2 = $Configstore1
            $first = 'second'
            $second = 'first'
        }

        $scopesConfigStore1 = get-dosconfigscopes -configstore $c1
        $scopesConfigStore2 = get-dosconfigscopes -configstore $c2
        
        #for each scope
        foreach($scope1 in $scopesConfigStore1){
            if($scopesConfigStore2 -contains $scope1){
                $myScopeHashTable1 = get-dosconfigvalues -ConfigStore $c1 -Scope $scope1
                $myScopeHashTable2 = get-dosconfigvalues -ConfigStore $c2 -Scope $scope1
                
                foreach($key in $myScopeHashTable1.keys){
                    if($skipSensitiveVariables -and $sensitiveVariables -contains "$scope1.$key"){
                        #because we are skipping sensitive variables _and_ at this point in the foreach we are comparing a sensitive variable, just continue.
                        continue
                    }
                    if($myScopeHashTable2.keys -contains $key){
                        if($myScopeHashTable1.$key -ne $myScopeHashTable2.$key){
                            $equal = $false
                            Write-DosMessage -Level 'Warning' -Message "Value for variable '$key' in scope '$scope1' in the $first configstore doesn't match the value in the $second configstore ($($myScopeHashTable1.$key) != $($myScopeHashTable2.$key))."
                        }
                    } else {
                        $equal = $false
                        Write-DosMessage -Level 'Warning' -Message "Variable '$key' in scope '$scope1' is in the $first configstore, but not the $second configstore."
                    }
                }

            } else {
                $equal = $false
                Write-DosMessage -Level 'Warning' -Message "Scope '$scope1' is in the $first configstore, but not the $second configstore."
            }
            
        }
    }
    
    return $equal

}


<#
.SYNOPSIS
    Compares the version of an install to the currently installed version.
.DESCRIPTION
    Returns 1 if application upgrade version is greater than the currently installed version or if an entry for the current install is not found.
    Returns 0 if application upgrade version is equal to the currently installed version.
    Returns -1 if application upgrade version is less than the currently installed version.
.PARAMETER DiscoveryServiceUrl
    The parameter DiscoveryServiceUrl is used to define the Discovery Service URL.
.PARAMETER ServiceName
    The parameter ServiceName is used to define the service name to lookup in Discovery Service.
.PARAMETER ServiceVersion
    The parameter ServiceVersion is used to define the service version to lookup in Discovery Service.
.PARAMETER InstallBuildNumber
    The parameter InstallBuildNumber is used to define the installer build number.
.EXAMPLE
    $vars = @{
        DiscoveryServiceUrl = "https://test/DiscoveryService/v1"
        ServiceName = "TestService"
        ServiceVersion = "1"
        InstallBuildNumber = "1.2.3"
    }
 
    Compare-DosServiceVersion @vars
 
.EXAMPLE
    Compare-DosServiceVersion -DiscoveryServiceUrl "https://test/DiscoveryService/v1" -ServiceName "TestService" -ServiceVersion "1" -InstallBuildNumber "1.2.3"
#>


function Compare-DosServiceVersion {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]$DiscoveryServiceUrl,
        [Parameter(Mandatory=$true)]
        [string]$ServiceName,
        [Parameter(Mandatory=$true)]
        [string]$ServiceVersion,
        [Parameter(Mandatory=$true)]
        [string]$InstallBuildNumber
    )

    $service = Get-DosService -DiscoveryServiceUrl $DiscoveryServiceUrl -ServiceName $ServiceName -ServiceVersion $ServiceVersion -Exists

    if (!$service.Exists) {
        # treat as though new version is greater
        return 1;
    }

    $currentBuildNumber = $service.BuildNumber
    $comparison = Compare-BuildNumber -CurrentBuildNumber $currentBuildNumber -InstallBuildNumber $InstallBuildNumber
    
    if ($comparison -lt 0) {
        Write-DosMessage -Level "Warning" -Message "Installer version of $($ServiceName) $($InstallBuildNumber) is not greater than current version $($currentBuildNumber)"
    }

    return $comparison
}

function Compare-BuildNumber {
    [CmdletBinding()]
    param (
        [string]$CurrentBuildNumber,
        [string]$InstallBuildNumber
    )

    Write-DosMessage -Level "Information" -Message "Comparing upgrade build number to installed package"

    $currentBuildNumberWithoutDecimals = $CurrentBuildNumber.Split('.') -join ''
    $installBuildNumberWithoutDecimals = $InstallBuildNumber.Split('.') -join ''

    return $installBuildNumberWithoutDecimals.CompareTo($currentBuildNumberWithoutDecimals)
}


<#
    .SYNOPSIS
 
    Validates the install.config file against the configuration.manifest file
 
    .DESCRIPTION
 
    For the scopes given, removes variables from the installation config that are not also found in the manifest.
    Validates that all variables in the manifest are also in the installation config.
 
    .PARAMETER InstallationConfigStore
 
    An object that describes the location of the installation configuration file.
    Accepts a powershell hashtable or dictionary with key value pairs following the example, $configStore = @{Type = "File"; Format = "XML"; Path = "C:\Path\To\File"}.
 
    .PARAMETER ManifestConfigStore
 
    An object that describes the location of the configuration manifest file.
    Accepts a powershell hashtable or dictionary with key value pairs following the example, $configStore = @{Type = "File"; Format = "XML"; Path = "C:\Path\To\File"}.
 
    .PARAMETER CleanupScopes
 
    An array of scopes for which variables should be removed from the installation config file if they are not also found in the manifest.
 
    .EXAMPLE
 
    Confirm-DosConfigStoreContent -InstallationConfigStore $installationConfigHashTable -ManifestConfigStore $manifestConfigHashTable -CleanupScopes @( "common" )
#>

function Confirm-DosConfigStoreContent {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [hashtable]$InstallationConfigStore,
        [Parameter(Mandatory=$true)]
        [hashtable]$ManifestConfigStore,
        [array] $CleanupScopes
    )
    begin {
        function ConvertFrom-ContentToObject {
            [CmdletBinding()]
            param (
                [Parameter(Position = 0, Mandatory = $true)]
                [hashtable]$ConfigStore
            )

            $scopes = Get-DosConfigScopes -ConfigStore $ConfigStore
            $variables = @()
            foreach ($scope in $scopes) {
                $values = Get-DosConfigValues -ConfigStore $ConfigStore -Scope $scope
                foreach ($value in $values.GetEnumerator()) {
                    $variable = New-Object PSObject
                    $variable | Add-Member -Type NoteProperty -Name scope_variable -Value "$($scope).$($value.Name)"
                    $variable | Add-Member -Type NoteProperty -Name scope -Value ($scope)
                    $variable | Add-Member -Type NoteProperty -Name variable -Value ($value.Name)
                    $variable | Add-Member -Type NoteProperty -Name value -Value ($value.Value)
                    $variables += $variable
                }
            }
            return $variables
        }
        function Write-XmlToScreen {
            [CmdletBinding()]
            param (
                [xml]$xml
            )
            $StringWriter = New-Object System.IO.StringWriter;
            $XmlWriter = New-Object System.Xml.XmlTextWriter $StringWriter;
            $XmlWriter.Formatting = "indented";
            $xml.WriteTo($XmlWriter);
            $XmlWriter.Flush();
            $StringWriter.Flush();
            Write-Output $StringWriter.ToString();
        }
    }
    process {
        $configVariables = ConvertFrom-ContentToObject -ConfigStore $InstallationConfigStore
        $manifestVariables = ConvertFrom-ContentToObject -ConfigStore $ManifestConfigStore

        # clean up outdated variables from the filter scope
        if ($CleanupScopes) {
            foreach ($cleanupScope in $CleanupScopes) {
                foreach ($configVariable in $configVariables | Where-Object scope -eq $cleanupScope) {
                    if ($manifestVariables.scope_variable -notcontains $configVariable.scope_variable) {
                        Remove-DosConfigValue -configStore $InstallationConfigStore -configSection $cleanupScope -configSetting $configVariable.variable
                    }
                }
            }
        }
        
        $missingVariables = @()
        foreach ($manifestVariable in $manifestVariables) {
            if ($configVariables.scope_variable -notcontains $manifestVariable.scope_variable) {
                $missingVariables += $manifestVariable
            }
        }
        if ($missingVariables) {
            $missingScopes = $missingVariables | Group-Object scope

            $text = @"
MISSING CONFIGURATIONS REPORT
Manifest: $($ConfigManifestPath)
Generated: $(Get-Date)
 
After doing a comparison of the configuration manifest with the install.config store
it appears that there are some missing configurations that need to be added to this
computers install.config store before there can be a successful installation.
 
Below are the missing variables with template/default values that are to be only used
as a guide. Please review the values carefully to ensure they are correct and then
copy and paste them to the install.config file.
 
COPY TO LOCATION: $($ConfigStorePath)
"@

    
            [xml]$doc = New-Object System.Xml.XmlDocument
            $doc.AppendChild($doc.CreateComment($text)) | Out-Null
            $installation = $doc.CreateElement("installation")
            $settings = $doc.CreateElement("settings")

            foreach ($missingScope in $missingScopes) {
                $scope = $doc.CreateElement("scope")
                $scope.SetAttribute("name", $missingScope.Name)
        
                foreach ($missingVariable in $missingScope.Group) {
                    $variable = $doc.CreateElement("variable")
                    $variable.SetAttribute("name", $missingVariable.variable)
                    $variable.SetAttribute("value", $missingVariable.value)
                    $scope.AppendChild($variable) | Out-Null
                }
                $settings.AppendChild($scope) | Out-Null
            }
        
            $installation.AppendChild($settings) | Out-Null
            $doc.AppendChild($installation) | Out-Null            

            Write-DosMessage -Level "Fatal" -Message "$(Write-XmlToScreen $doc)"
        }    
        Write-DosMessage -Level "Information" -Message "All required configurations found in the install.config"
    }
}



function Confirm-DosConfiguration {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [Hashtable] $config,
        [Parameter(Mandatory = $true)]
        [Hashtable] $checkList
    )
    begin {
        $results = @();
        function Invoke-Check {
            [CmdletBinding()]
            param (
                [Parameter(Position = 0, Mandatory = $true)]
                [string] $name,
                [Parameter(Position = 1)]
                [object[]] $checkList
            )
            $result = @();
            $splat = @{
                name  = $name
                type  = $config["_type_"][$name]
                value = $config[$name]
            }
            switch -Wildcard ($checkList) {
                "isNotNull" { $result += Confirm-IsNotNull @splat }
                "isValidPath" { $result += Confirm-IsValidPath @splat }
                "isValidDir" { $result += Confirm-IsValidDir @splat }
                "isBoolean" { $result += Confirm-IsBoolean @splat }
                "isValidConnection" { $result += Confirm-IsValidConnection @splat }                
                "isValidEndpoint" { $result += Confirm-IsValidEndpoint @splat }
                "isValidValue=(*)" {
                    $list = $checkList.Split("'*',", [System.StringSplitOptions]::RemoveEmptyEntries)
                    [string[]]$list = $list[(1..($list.Length - 2))]
                    $splat.Add('validateSet', $list)
                    $result += Confirm-IsValidValue @splat
                }
            }

            return $result
        }        
    }
    process {
        foreach ($item in $checkList.Keys) {
            if ($config.ContainsKey($item)) {
                $results += Invoke-Check $item $checkList[$item]
            }
            else {
                Write-DosMessage -Level "Warning" -Message "Unable to find $item as a configuration. Please remove from checklist."
            }
        }
    }
    end {
        $errors = $results | Where-Object errorFlag -eq 1
        $warnings = $results | Where-Object errorFlag -eq -1
        $success = $results | Where-Object errorFlag -eq 0

        $errorsCnt = ($errors | Measure-Object).Count
        $warningsCnt = ($warnings | Measure-Object).Count
        $successCnt = ($success | Measure-Object).Count

        if ($warnings) {
            $msg = ($warnings | Sort-Object type, name | Format-Table name, type, check, @{Label = "result"; Expression = {"warning"}}, message -Wrap)
            Write-DosMessage -Level "Warning" -Message "WARNINGS: $warningsCnt >>>`n$(($msg | Out-String).trim())"
        }
        if ($errors) {
            if ($errors | Where-Object type -eq "store") {
                $msg = ($errors | Where-Object type -eq "store" | Sort-Object type, name | Format-Table name, type, check, @{Label = "result"; Expression = {"failed"}}, message -Wrap)
                $errorsCnt = ($errors | Where-Object type -eq "store" | Measure-Object).Count
            }
            else {
                $msg = ($errors | Sort-Object type, name | Format-Table name, type, check, @{Label = "result"; Expression = {"failed"}}, message -Wrap)
            }
            Write-DosMessage -Level "Fatal" -Message "ERRORS: $errorsCnt >>>`n$(($msg | Out-String).trim())"
        }

        Write-DosMessage -Level "Information" -Message "Configuration checks summary - Success: $successCnt, Warnings: $warningsCnt, Errors: $errorsCnt"
    }
}



<#
    .SYNOPSIS
 
    Checks if the credential provided is valid
 
    .DESCRIPTION
 
    Checks that the provided credential (username/password pairing) is valid and returns you and object with the validity response ($returnObject.isValid) and the validated credential ($returnObject.credential).
    Note: If the isValid property of the return object is $false, the credential property will be invalid and should be handled accordingly.
 
    .PARAMETER Credential
 
    Credential object used to check if the username and password combination are correct
 
    .PARAMETER promptOnInvalid
 
    Switch - If provided and credential is invalid, will prompt for the correct password.
 
    .EXAMPLE
 
    $validationResponse = (Confirm-DosCredential -Credential $credential).isValid
    $returnedCredential = (Confirm-DosCredential -Credential $credential -PromptOnInvalid).credential
#>

function Confirm-DosCredential {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [PSCredential] $Credential,
        [switch] $PromptOnInvalid
    )
    $iisUser = $Credential.UserName
    $isValid = Assert-DosCredential($Credential)
    if(!$isValid){
        if ($PromptOnInvalid) {
            Write-DosMessage -Level "Warning" -Message "Incorrect credentials for $iisUser"
            Write-DosMessage -Level "Verbose" -Message "PromptOnInvalid parameter provided. Please input appropriate credential information."
            $count = 0
            do {
                $count++
                $Credential = Read-DosCredential -UserName $Credential.UserName
                $isValid = Assert-DosCredential($Credential)
                if(!$isValid){
                    Write-DosMessage -Level "Warning" -Message "Credential you provide is incorrect for user: $iisUser. Please try again."
                }
            }until ($isValid -or ($count -ge 3))
            if (!$isValid) {
                Write-DosMessage -Level "Error" -Message "Maximum number of credential validation attempts reached for user: $iisUser"
                Write-DosMessage -Level "Verbose" -Message "Credential is invalid for $iisUser. Returning false."
            }
        }else {
            Write-DosMessage -Level "Error" -Message "Incorrect credentials provided for user: $iisUser"
        }
    }
    
    $credentialValidationResult = @{ isValid = $isValid; credential = $Credential }

    return $credentialValidationResult
}


    
<#
    .SYNOPSIS
 
    Helper Function: Checks if the credential provided is valid
 
    .DESCRIPTION
 
    Uses System.DirectoryServices.AccountManagement.ContextType to check that the credential provided is valid
 
    .PARAMETER Credential
 
    Credential object used to check if the username and password combination are correct
 
    .EXAMPLE
 
    Assert-DosCredential -Credential $credential
#>


function Assert-DosCredential {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [PSCredential] $Credential
    )
    
    Write-DosMessage -Level "Information" -Message "Checking credential validity."

    $contextName = $Credential.GetNetworkCredential().Domain
    [System.Reflection.Assembly]::LoadWithPartialName("System.DirectoryServices.AccountManagement") | Out-Null

    if($contextName -eq $(hostname)){
        #validate locally
        Write-DosMessage -Level "Debug" -Message "Local credential."
        $principalContext = "machine"
        $pc = New-Object System.DirectoryServices.AccountManagement.PrincipalContext($principalContext, $contextName)
        $isValid = $pc.ValidateCredentials($Credential.GetNetworkCredential().UserName, $Credential.GetNetworkCredential().Password)
        return $true
    } else {
        Write-DosMessage -Level "Debug" -Message "Domain credential."
        $principalContext = "domain"
        $pc = New-Object System.DirectoryServices.AccountManagement.PrincipalContext($principalContext, $contextName)
        $isValid = $pc.ValidateCredentials($Credential.GetNetworkCredential().UserName, $Credential.GetNetworkCredential().Password, [System.DirectoryServices.AccountManagement.ContextOptions]::Negotiate)
    }

    return $isValid
}


<#
    .SYNOPSIS
 
    Helper Function: Prompts for username and password for a credential object
 
    .DESCRIPTION
 
    Prompts for username and password and returns a credential object
 
    .PARAMETER Credential
 
    Credential object used to check if the username and password combination are correct
 
    .EXAMPLE
 
    Prompt-DosCredential -UserName 'testaccount' -Password 'password'
#>

function Read-DosCredential {
    param (
        [string] $UserName,
        [securestring] $UserPassword 
    )

    if (!$UserName){
        $UserName = Read-Host "Enter the domain\username to use for the credential"
    }
    if (!$UserPassword) {
        $UserPassword = Read-Host "Enter the password for $UserName" -AsSecureString
    }

    Write-DosMessage -Level "Information" -Message "Creating credential from inputs provided."

    $Credential = New-Object -TypeName "System.Management.Automation.PSCredential" -ArgumentList $UserName, $UserPassword
    return $Credential
}


<#
    .SYNOPSIS
    Copies the scopes, variables, and values from the $SourceConfigStore to the $DestinationConfigStore. Note: it will not create the ConfigStores.
     
    .DESCRIPTION
     
     
    .PARAMETER SourceConfigStore
    The source configstore you want to copy from.
     
    .PARAMETER DestinationConfigStore
    The destination configstore you want to copy from.
 
    .PARAMETER skipSensitiveVariables
    A switch parameter that will tell the copy-dosconfigstore function to skip the copying of sensitive variables. Note: when copying from a File based
    configstore to an external configstore, skipSensitiveVariables is always set to true.
     
    .EXAMPLE
    $uri = 'https://mystoragetable.table.core.windows.net/Common?sp=raud&st=2020-10-04T21:34:00Z&se=2020-10-21T21:33:00Z&sv=2019-12-12&sig=cDMUXyvpozHJYkTfdQBhKiQ3PL05jV8gLvCXqc6GOgI%3D&tn=Common'
    $econfigstore = @{Type = "External"; Format = "AzureTable"; Uri = "$uri"}
    $configStore = @{Type = "File"; Format = "XML"; Path = "c:\Program Files\Health Catalyst\install.config"}
    New-DosConfigStore -configStore $configStore
 
    Copy-DosConfigStore -SourceConfigStore $econfigstore -DestinationConfigStore $configstore
 
#>

function Copy-DosConfigStore {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [hashtable] $SourceConfigStore,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [hashtable] $DestinationConfigStore,
        [switch]$skipSensitiveVariables,
        [switch]$keepExisting
    )
    # Confirm External ConfigStore
    if(!(Confirm-ConfigStore -ConfigStore $SourceConfigStore)) {
        Write-DosMessage -Level "Fatal" -Message "SourceConfigStore is invalid"
        return
    }
    if(!(Confirm-ConfigStore -ConfigStore $DestinationConfigStore)) {
        Write-DosMessage -Level "Fatal" -Message "DestinationConfigStore is invalid"
        return
    }

    $sensitiveVariables = @('common.fabricInstallerSecret')

    $okayToCopySensitiveValues = $false
    if($skipSensitiveVariables){
        #do not change $okayToCopySensitiveValues because $skipSensitiveVariables is true (and thus $okayToCopySensitiveValues remains false)

    } else {
        #$skipSensitiveVariables was no passed in, so we can see if it is okay to copy sensitive variables
        if($DestinationConfigStore.Type -eq "File"){
            $okayToCopySensitiveValues = $true
        }
    }


    #get all the scopes for the SourceConfigStore
    $scopes = Get-DosConfigScopes -ConfigStore $SourceConfigStore
    #foreach scope in the SourceConfigStore, call the set-dosconfigvalue on the destination config store.

    foreach($scope in $scopes){
        $valuesForScope = Get-DosConfigValues -ConfigStore $SourceConfigStore -Scope $scope
        foreach($key in $($valuesForScope.keys | sort-object)){
            if($okayToCopySensitiveValues){
                Set-DosConfigValue -ConfigStore $DestinationConfigStore -configSection $scope -configSetting $key -configValue $valuesForScope[$key] -KeepExisting:$keepExisting.IsPresent
            } else {
                if($sensitiveVariables -contains "$scope.$key"){
                    #do not copy
                    Write-DosMessage -Level "Warning" -Message "Unable to copy senstive variable $scope.$key"
                } else {
                    Set-DosConfigValue -ConfigStore $DestinationConfigStore -configSection $scope -configSetting $key -configValue $valuesForScope[$key] -KeepExisting:$keepExisting.IsPresent
                }
            }
            
        }
        
    }
}


<#
    .SYNOPSIS
 
    Extracts the target zip file to the target directory.
 
    .DESCRIPTION
 
    Either uses Expand-Archive in PS 5.0+ or [System.IO.Compression.ZipFile] and associated calls in PS 4.0
 
    .PARAMETER ArchiveFile
 
    Target zip file to extract
 
    .PARAMETER DestinationPath
 
    Destination directory to extract the zip file into.
 
    .PARAMETER OverWrite
 
    Forces overwriting existing files. If not specified and similar files already exists in the target directory, errors will be displayed for each file NOT overwritten
 
    .EXAMPLE
 
    Expand-DosArchive -ArchiveFile "x.zip" -DestinationPath "c:\inetpub\wwwroot\x" -OverWrite
 
#>

function Expand-DosArchive{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [ValidateScript({
            if (!(Test-Path $_)) {
                Write-DosMessage -Level "Error" -Message "ArchiveFile $_ does not exist. Please enter valid path."
            }
            else {
                $true
            }
        })]
        [string] $ArchiveFile,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $DestinationPath,
        [switch] $OverWrite
    )

    Expand-Archive -Path $ArchiveFile -DestinationPath $DestinationPath -Force:$OverWrite.IsPresent
    Write-DosTelemetry -Message "Expand-DosArchive called."
}



<#
    .SYNOPSIS
 
    Obtains the path to the root of the install directory.
 
    .DESCRIPTION
 
    Traverses up the path until it finds the root of the install directory. E.g. C:\install\DosInstaller2016_20.2.2019.11\DosInstaller2016
 
    .EXAMPLE
 
    Get-DosBaseInstallerPath
 
#>

#replaces get-baseinstallerpath

function Get-DosBaseInstallerPath {
    $parentPath = Get-Location
    $installerPath = $parentPath
    $rootDrivePath = (Split-Path -Path $installerPath -Qualifier) + "\"

    do {
        if(Get-ChildItem $installerPath | Where-Object {$_.Name -eq "CatalystSetup.exe"})
        {
            break
        } else
        {
            $installerPath = Split-Path -Path $installerPath -Parent
        }
        if($installerPath -eq $rootDrivePath) {
            Write-DosMessage -Level "Warning" -Message "Could not resolve the base path to the installer starting from $parentPath."
        }
    } 
    until ($installerPath -eq $rootDrivePath) 

    return $installerPath
}


<#
    .SYNOPSIS
        Fixes default PowerShell url dispatch decoding
     
    .DESCRIPTION
        PowerShell by default has a url parser setting which decodes a backslash before the request dispatch.
        This has caused problems when attempting to send web requests with urls that contain backslashes in them.
        This function fixes that issue and returns a url that can be passed to Invoke-WebRequest or Invoke-RestMethod.
     
    .PARAMETER url
        The URL string of a given web request.
     
    .EXAMPLE
        PS C:\> Get-DosCleanUri -url "http://localhost/DOMAIN\My User"
     
    .NOTES
        This function was created by following a helpful article found on Stack Overflow
        Topic: "Percent-encoded slash (“/”) is decoded before the request dispatch"
        Article: https://stackoverflow.com/a/30927141
#>

function Get-DosCleanUri {
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true,
            Position = 0)]
        [string]$url
    )
    
    try {
        $escapedUrl = Format-UriString -Url $url
        $escapedUrl.PathAndQuery | Out-Null
        $m_Flags = Get-MFlagsFieldDotNet
        [uint64]$flags = Get-MFlagsValue -Url $escapedUrl -mFlags $m_Flags
        Set-MFlagsValue -Url $escapedUrl -mFlags $m_Flags -Flags $flags
        Write-DosMessage -Level "Information" -Message "Url cleaned to prevent decoding on dispatch ($($escapedUrl.OriginalString))."
    }
    catch {
        Write-DosMessage -Level "Error" -Message "An error occurred while attempting to clean the url ($($escapedUrl.OriginalString)). Exception: $($_.Exception)."
    }

    return $escapedUrl
}

function Format-UriString {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string]$Url
    )

    [Uri]$escapedUrl = [System.Uri]::EscapeUriString($Url)

    return $escapedUrl
}

function Get-MFlagsFieldDotNet {
    [CmdletBinding()]
    $m_Flags = [Uri].GetField("m_Flags", $([Reflection.BindingFlags]::Instance -bor [Reflection.BindingFlags]::NonPublic))
    
    return $m_Flags
}

function Get-MFlagsValue {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [Uri]$Url,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        $mFlags
    )
    [uint64]$flags = $mFlags.GetValue($Url)
    
    return $flags
}

function Set-MFlagsValue {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [Uri]$Url,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        $mFlags,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        $Flags
    )
    $mFlags.SetValue($Url, $($Flags -bxor 0x30))
}


<#
    .SYNOPSIS
        Returns the scopes within a config store.
     
    .DESCRIPTION
        Returns the scopes within a config store.
     
    .PARAMETER ConfigStore
        The configstore object for which you want the scopes.
     
    .EXAMPLE
        Get-DosConfigScopes -configstore $configstore
#>

function Get-DosConfigScopes
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification="Values should be plural.")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [hashtable] $ConfigStore
    )


    if(-not(Confirm-ConfigStore -ConfigStore $ConfigStore)){
        Write-DosMessage -Level 'Fatal' -Message "Invalid ConfigStore. Unable to Get-DosConfigScopes"
        return
    }


    if ($ConfigStore.Type -eq "File") {
        return Get-DosConfigScopesXml -ConfigStore $ConfigStore
    }

    if ($ConfigStore.Type -eq "External") {
        return Get-DosConfigScopesAzureTable -ConfigStore $ConfigStore
    }

}


<#
    .SYNOPSIS
 
    Gets configuration values from a configuration store of a certain type and format.
 
    .DESCRIPTION
 
    Given a user has a valid Configuration Store object, when the user asks for all values for a particular scope, values are returned in a hash table.
    If user provides an invalid Configuration Store object, warning messages will be displayed & logged, and Get-DosConfigValues will return $null.
 
    .PARAMETER ConfigStore
 
    Accepts a powershell hashtable or dictionary with key value pairs following the example, $configStore = @{Type = "File"; Format = "XML"; Path = "C:\Path\To\File"}.
 
    .PARAMETER Scope
 
    The configuration values you wish to return. Passing in an application specific scope will return values within the configuration that are contained in that application scope.
    Additionally, users can pass in a "common" scope to return values contained in the common scope.
 
    .EXAMPLE
 
    Get-DosConfigScopeValues -ConfigStore $configHashtable -Scope "common"
    Get-DosConfigScopeValues -ConfigStore $configHashtable -Scope "terminology"
#>


function Get-DosConfigValues {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification="Values should be plural.")]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [hashtable] $ConfigStore,
        [Alias("ConfigSection", "ConfigScope")]
        [string] $Scope
    )

    $validConfigStore = Confirm-ConfigStore -ConfigStore $ConfigStore

    if (!$validConfigStore) {
        Write-DosMessage -Level "Warning" -Message "ConfigStore object is invalid. No configuration values will be returned."
        return $null
    }

    Write-DosMessage -Level "Verbose" -Message "ConfigStore object is valid."

    $configValues = $null

    if ($ConfigStore.Type -eq "External") {
        Write-DosMessage -Level "Debug" -Message "Attempting to retrieve configuration values from external config store using external uri"
        $configValues = Get-DosConfigValuesAzureTable -ConfigStore $ConfigStore -Scope $Scope 
    }

    if ($ConfigStore.Type -eq "File") {
        if ([string]::IsNullOrEmpty($Scope)) {
            Write-DosMessage -Level "Fatal" -Message "The required paramter 'Scope' was not provided. Please provide the required parameter and try agian."
        }
        Write-DosMessage -Level "Debug" -Message "Retrieving '$Scope' scoped configuration values from '$($ConfigStore.Path)'"
        $configValues = Get-DosConfigValuesXml -ConfigSection $Scope -InstallConfigPath $ConfigStore.Path
    }

    Write-DosTelemetry -Message "Get-DosConfigValues called with the following parameters. Scope: $Scope. ConfigStore: $($ConfigStore | Out-String)"

    return $configValues
}


<#
    .SYNOPSIS
 
    Replacement for dbatools Get-DbaDatabase
 
    .DESCRIPTION
 
    Replacement for dbatools Get-DbaDatabase
 
#>

function Get-DosDbaDatabase {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $SqlInstance,
        [Parameter(Mandatory = $true)]
        [string] $Database
    )

    $MasterConnectionString = "Server=$SqlInstance;Database=master;Trusted_Connection=True;MultipleActiveResultSets=True;Connect Timeout=10"
    $sysDbQuery = "select name as [Name]
            , recovery_model_desc as RecoveryModel
            , suser_sname( owner_sid ) as [Owner]
        FROM sys.databases
        WHERE name = @Database"


    $queryResult = Invoke-DosSqlQuery -ConnectionString $MasterConnectionString -Query $sysDbQuery -Parameters @{Database = $database}
    $queryResultCount = $($queryResult | Measure-Object).Count
    if($queryResultCount -gt 1){
        Write-DosMessage -Level 'Error' -Message "Get-DosDbaDatabase query resulted in $queryResultCount rows when we only expected 0 or 1."
    }

    return $queryResult

}



<#
    .SYNOPSIS
 
    Returns a service url from Discovery Service.
 
    .DESCRIPTION
 
    Given a valid Discovery Service url and valid service name, Get-DosServiceUrl will return a service url string.
    If a valid service version is provided, Get-DosServiceUrl will reutrn the service url string for the specified version.
    If a valid credential is provided, Get-DosServiceUrl will query Discovery Service with the credential provided, else it will use the local powershell session user.
 
    .PARAMETER DiscoveryServiceUrl
 
    REQUIRED
    Accepts a valid (not null or empty) Discovery Service Url string.
 
    .PARAMETER ServiceName
 
    REQUIRED
    Accepts a valid (not null or empty) service name that will be used to query Discovery Service
 
    .PARAMETER ServiceVersion
 
    REQUIRED
    Accepts a valid (not null or empty) service name that will be used to query Discovery Service
 
    .PARAMETER Credential
 
    Accepts a valid (not null or empty) service name that will be used to query Discovery Service
 
    .PARAMETER Exists
 
    Switch - If provided, appends an 'Exists' property to the output object and does not write a fatal message if the ServiceName is not found.
 
    .EXAMPLE
 
    Get-DosService -DiscoveryServiceUrl "https://server/DiscoveryService/v1" -ServiceName "AnalyticsService" -ServiceVersion 1
 
    $credential = Get-Credential
    Get-DosService -DiscoveryServiceUrl "https://server/DiscoveryService/v1" -ServiceName "AnalyticsService" -ServiceVersion 1 -Credential $credential
 
    Get-DosService -DiscoveryServiceUrl "https://server/DiscoveryService/v1" -ServiceName "AnalyticsService" -ServiceVersion 1 -Exists
#>

function Get-DosService {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        [string] $DiscoveryServiceUrl,
        [Parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        [string] $ServiceName,
        [int] $ServiceVersion,
        [pscredential] $Credential,
        [switch]$Exists
    )

    if ($ServiceVersion) {
        Write-DosMessage -Level "Verbose" -Message "Service Version provided. Request formed with specified version."
        $discoveryRequest = "$DiscoveryServiceUrl/Services(ServiceName='$ServiceName',Version=$ServiceVersion)"
    }
    else {
        Write-DosMessage -Level "Verbose" -Message "Service Version was not provided. Request formed without specified version."
        $discoveryRequest = "$DiscoveryServiceUrl/Services?`$filter=ServiceName eq $ServiceName"
    }
    
    $discoveryResponse = @{ }

    if ($null -ne $Credential) {
        try {
            Write-DosMessage -Level "Information" -Message "Attempting to retrieve Discovery Service object from Discovery Service at '$discoveryRequest' using '$($Credential.UserName)' credential."
            $discoveryResponse = Invoke-RestMethod -Method Get -Uri $discoveryRequest -Credential $Credential
            Write-DosMessage -Level "Information" -Message "Successfully retrieved Discovery Service object from Discovery Service."
        }
        catch {
            $statusCode = $_.Exception.Response.StatusCode.value__
            if ($statusCode -eq "400" -and $Exists.IsPresent) {
                $discoveryResponse.Exists = $false
                return $discoveryResponse
            }
            Write-DosMessage -Level "Fatal" -Message "Error retrieving service registration with '$($Credential.UserName)' for '$ServiceName'. Please confirm Discovery Service installation and/or credential permissions. Exception: $($_.Exception)."
        }
    }
    else {
        try {
            Write-DosMessage -Level "Information" -Message "Attempting to retrieve Discovery Service object from Discovery Service at '$discoveryRequest' using '$env:UserName' credential."
            $discoveryResponse = Invoke-RestMethod -Method Get -Uri $discoveryRequest -UseDefaultCredentials
            Write-DosMessage -Level "Information" -Message "Successfully retrieved Discovery Service object from Discovery Service."
        }
        catch {
            $statusCode = $_.Exception.Response.StatusCode.value__
            if ($statusCode -eq "400" -and $Exists.IsPresent) {
                $discoveryResponse.Exists = $false
                return $discoveryResponse
            }
            Write-DosMessage -Level "Fatal" -Message "Error retrieving service registration with '$($Credential.UserName)' for '$ServiceName'. Please confirm Discovery Service installation and/or credential permissions. Exception: $($_.Exception)."
        }
    }

    if ($Exists.IsPresent) {
        $discoveryResponse | Add-Member -NotePropertyName "Exists" -NotePropertyValue $true
    }
    
    return $discoveryResponse
}


function Get-DosServiceUrl {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        [string] $DiscoveryServiceUrl,
        [Parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        [string] $ServiceName,
        [Parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        [int] $ServiceVersion,
        [pscredential] $Credential
    )

    if ($Credential) {
        $discoveryResponse = Get-DosService -DiscoveryServiceUrl $DiscoveryServiceUrl -ServiceName $ServiceName -ServiceVersion $ServiceVersion -Credential $Credential
    }
    else {
        $discoveryResponse = Get-DosService -DiscoveryServiceUrl $DiscoveryServiceUrl -ServiceName $ServiceName -ServiceVersion $ServiceVersion
    }

    Write-DosMessage -Level "Verbose" -Message "Pulling 'ServiceUrl' from the Discovery Service response."
    $serviceUrl = Get-ServiceUrlString -DiscoveryServiceResponse $discoveryResponse

    Write-DosMessage -Level "Information" -Message "Checking if a value was returned for '$ServiceName version $ServiceVersion'."
    if ([string]::IsNullOrWhiteSpace($serviceUrl)) {
        Write-DosMessage -Level "Fatal" -Message "The service $ServiceName is not registered with the Discovery service. Make sure that '$ServiceName $ServiceVersion' is registered with Discovery Service. Exception: $($_.Exception)"
    }

    Write-DosMessage -Level "Information" -Message "Returning url '$serviceUrl' for '$ServiceName' service."

    return $serviceUrl
}

function Get-ServiceUrlString {
    [CmdletBinding()]
    param (
        $DiscoveryServiceResponse
    )

    $serviceUrl = $DiscoveryServiceResponse.ServiceUrl

    return $serviceUrl
}


<#
    .SYNOPSIS
 
    Returns the physical root of the specified web site
 
    .DESCRIPTION
 
    Uses the WebAdministration powershell module to locate the web site's physical root
 
    .PARAMETER WebSiteName
 
    Name of the IIS site - defaults to "Defautl Web Site"
 
    .EXAMPLE
 
    Get-IISWebSitePath -WebSiteName $IISWebSite
#>


function Get-IISWebSitePath{
    [CmdletBinding()]
    param(
        [string] $WebSiteName = "Default Web Site"
    )

    Import-Module WebAdministration

    Test-ElevatedPermission

    try{
        [Microsoft.IIs.PowerShell.Framework.ConfigurationElement] $webSite = Get-Item "IIS:\Sites\$WebSiteName"
        return [System.Environment]::ExpandEnvironmentVariables($webSite.PhysicalPath)
    }
    catch{
        Write-DosMessage -Level Error -Message "Unable to get inforomation for $WebSiteName : error $($_.Exception)"
    }
}


<#
    .SYNOPSIS
 
    Downloads the specified Uri using a WebRequest to the specified OutFile
 
    .DESCRIPTION
 
    Wraps the PowerShell native Invoke-WebRequest and performs the request without the default progress bar behavior.
 
    .PARAMETER Uri
 
    Specifies the Uniform Resource Identifier (URI) of the Internet resource to which the web request is sent. Enter a URI.
    This parameter supports HTTP, HTTPS, FTP, and FILE values.
 
    This parameter is required.
 
    .PARAMETER OutFile
 
    Specifies the output file for which this cmdlet saves the response body. Enter a path and file name. If you omit the path,
    the default is the current location.
 
    .PARAMETER NoCache
 
    When supplied will provide the "Cache-Control" header set to "no-cache" to the underlying WebRequest.
 
    .EXAMPLE
 
    Download-WebRequest -Uri http://some.valid.uri/file -OutFile theFile.txt
#>

function Get-WebRequestDownload {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string] $Uri,

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

        [switch] $NoCache = $false
    )

    $originalProgressPreference = $progressPreference
    try {
        $headers = @{}
        if ($NoCache) {
            $headers.Add("Cache-Control", "no-cache")
        }
        $progressPreference = 'silentlyContinue'
        Invoke-WebRequest -Uri $Uri -Headers $headers -OutFile $OutFile -UseBasicParsing
    } finally {
        $progressPreference = $originalProgressPreference
    }
}


<#
.SYNOPSIS
Installs SSIS project
 
.DESCRIPTION
Ensures SSIS catalog and folder exist, and installs SSIS project from specified ISPAC
 
.PARAMETER ProjectName
Name to deploy project under
 
.PARAMETER IspacPath
Path to ISPAC to deploy
 
.PARAMETER CatalogEncryptionKey
Key to use for encrypting catalog, if it must be created
 
.PARAMETER CatalogName
Name of SSIS catalog to contain project
 
.PARAMETER FolderName
Name of SSIS catalog folder to contain project
 
.PARAMETER ConnectionString
Connection string for connecting to SQL Server instance with Integration Services
 
.INPUTS
None. You cannot pipe objects to Install-DosIspac.
 
.OUTPUTS
None.
 
.EXAMPLE
PS> Install-DosIspac -ProjectName 'CatalystLoader' -IspacPath 'SetupContent/SSISLoader2016.ispac'
#>

function Install-DosIspac {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Passing through the -WhatIf and -Confirm to the private functions - this is a supported scenario")]
    [cmdletbinding(SupportsShouldProcess=$true)]
    [OutputType()]
    param(
        [parameter(Mandatory=$true)][string]$ProjectName,
        [parameter(Mandatory=$true)][string]$IspacPath,
        [parameter(Mandatory=$false)][string]$CatalogEncryptionKey,
        [parameter(Mandatory=$false)][string]$CatalogName = 'SSISDB',
        [parameter(Mandatory=$false)][string]$FolderName = 'Catalyst',
        [parameter(Mandatory=$false)][string]$ConnectionString = 'Data Source=localhost;Initial Catalog=EDWAdmin;Integrated Security=True')

    [Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Management.IntegrationServices")
    $integrationServices = Get-IntegrationServices -ConnectionString $ConnectionString

    Write-DosMessage -Level Debug -Message "Testing whether catalog $CatalogName exists"
    if ($integrationServices.Catalogs.Contains($CatalogName)) {
        Write-DosMessage -Level Information -Message "SSIS catalog already exists, skipping"
        $catalog = $integrationServices.Catalogs[$CatalogName]
    }
    else {
        Write-DosMessage -Level Information -Message "SSIS catalog does not exist, creating"

        if ([string]::IsNullOrEmpty($CatalogEncryptionKey)) {
            Write-DosMessage -Level Error -Message "Please provide a catalog encryption key using the CatalogEncryptionKey parameter so that a new catalog can be created." -ErrorAction Stop
        }

        $catalog = New-SsisCatalog -IntegrationServices $integrationServices -CatalogEncryptionKey $CatalogEncryptionKey -CatalogName $CatalogName -WhatIf:$PSBoundParameters.ContainsKey('WhatIf') -Confirm:$PSBoundParameters.ContainsKey('Confirm') 
        Write-DosMessage -Level Debug -Message "SSIS catalog created successfully"
    }

    Write-DosMessage -Level Debug -Message "Testing whether folder $FolderName exists"
    if ($catalog.Folders.Contains($FolderName)) {
        Write-DosMessage -Level Information -Message "SSIS catalog folder already exists"
        $folder = $catalog.Folders[$FolderName]
    }
    else {
        Write-DosMessage -Level Information -Message "SSIS catalog folder does not exist, creating"
        $folder = New-SsisFolder -SsisCatalog $catalog -FolderName $FolderName -WhatIf:$PSBoundParameters.ContainsKey('WhatIf') -Confirm:$PSBoundParameters.ContainsKey('Confirm')
        Write-DosMessage -Level Debug -Message "SSIS catalog folder created successfully"
    }

    Write-DosMessage -Level Information -Message "Deploying SSIS project $ProjectName from $IspacPath"
    New-SsisProject -SsisFolder $folder -ProjectName $ProjectName -IspacPath $IspacPath -WhatIf:$PSBoundParameters.ContainsKey('WhatIf') -Confirm:$PSBoundParameters.ContainsKey('Confirm')
    Write-DosMessage -Level Debug -Message "SSIS project $ProjectName deployed successfully"
}


<#
    .SYNOPSIS
 
    If needed, Installs the correct version of Dot Net Core
 
    .DESCRIPTION
 
    Checks for a specific version of Dot Net Core. If the version is not installed,download and install it.
 
    .PARAMETER version
 
    The string representation of the version. This should match the DisplayVersion for the object as installed in the windows registry.
 
    .PARAMETER downloadUrl
 
    The complete download URL for the exe installer that corresponds to the given version.
 
    .EXAMPLE
 
    Install-DotNetCoreIfNeeded -version "1.2.3.4" -downloadUrl "https://some.web.site/folder/installFill.exe"
#>

function Install-DotNetCoreIfNeeded {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        [string] $version,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        [string] $downloadUrl,

        [string] $filePattern = "*.NET Core*Windows Server Hosting*",
        [switch] $noRuntime
    )

    if (Test-PrerequisiteSameOrNewer $filePattern $version) {
        Write-DosMessage -Level "Information" -Message  ".NET Core Windows Server Hosting Bundle (v$version) or newer installed and meets expectations."
        return 
    }

    try {
        Write-DosMessage -Level "Information" -Message "Windows Server Hosting Bundle version $version not installed...installing version $version"        
        Get-WebRequestDownload -Uri $downloadUrl -OutFile $env:Temp\bundle.exe
        if ($noRuntime.IsPresent) {
            Start-Process $env:Temp\bundle.exe -Wait -ArgumentList  '/quiet /install /norestart OPT_NO_RUNTIME=1 OPT_NO_SHAREDFX=1'
        }
        else {
            Start-Process $env:Temp\bundle.exe -Wait -ArgumentList '/quiet /install /norestart'
        }
        net stop was /y
        net start w3svc
    }
    catch {
        Write-DosMessage -Level "Fatal" -Message "Could not install .NET Windows Server Hosting bundle. Please install the hosting bundle before proceeding. $downloadUrl"
    }
    
    if (Test-PrerequisiteSameOrNewer $filePattern $version) {
        Write-DosMessage -Level "Information" -Message "Windows Server Hosting Bundle version $version or newer installed."        
    }
    else {
        Write-DosMessage -Level "Fatal" -Message "Error executing .NET Windows Server Hosting bundle. Please install the hosting bundle before proceeding. $downloadUrl"
    }

    try {
        Remove-Item $env:Temp\bundle.exe
    }
    catch {
        $e = $_.Exception
        Write-DosMessage -Level "Warning" -Message "Unable to remove temporary download file for server hosting bundle exe." 
        Write-DosMessage -Level "Warning" -Message  $e.Message
    }

}


<#
    .SYNOPSIS
 
    Imports/Installs a module required to run this module/function
 
    .DESCRIPTION
 
    Attempts to load a local module first - if the module isn't available it will attempt to download/install from PSGallery.
 
    .PARAMETER ModuleName
 
    Name of the module to install
 
    .PARAMETER Scope
 
    Scope used to install the module - default is CurrentUser
 
    .EXAMPLE
 
    Install-RequiredModule -ModuleName dbatools
#>

function Install-RequiredModule{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $ModuleName,
        [string] $Scope = "CurrentUser",
        [version] $RequiredVersion
    )

    Write-DosTelemetry -Message "Install-RequiredModule start"
    # Adding PSModule path becuse in some cases the path to user directory was not in the module system path
    $currentUserPSModulePath = "$home\Documents\WindowsPowerShell\Modules"
    $replaceCurrentUserModulePath = Add-ToPSModulePath -Path $currentUserPSModulePath

    #First scenario - required version of the module is already loaded
    $importedModule = Get-ModuleWorkAround -Name $ModuleName

    if($null -ne $importedModule){
        Write-DosMessage -Level "Information" -Message "Module '$ModuleName' already imported."
        
        if ($RequiredVersion) {
            Write-DosMessage -Level "Verbose" -Message "Checking if imported module '$ModuleName' matches required version '$RequiredVersion'."
            if ((Compare-ModuleVersion -ModuleToCompare $importedModule -RequiredVersion $RequiredVersion)) {
                Write-DosMessage -Level "Information" -Message "Confirming '$ModuleName' with version '$RequiredVersion' is loaded into session."
                
                if ($importedModule.Count -gt 1) {
                    Write-DosMessage -Level "Information" -Message "Multiple '$ModuleName' modules loaded in session. This should never happen?"
                    Remove-Module -Name $ModuleName
                    try {
                        Import-Module -Name $ModuleName -RequiredVersion $RequiredVersion -Global -ErrorAction Stop
                    }
                    catch [System.Management.Automation.RuntimeException]{
                        Write-DosMessage -Level "Warning" -Message "'$ModuleName' was previously loaded during this session. If you encounter weird issues with DosInstallUtilities, please restart PowerShell and resolve conflicts with '$ModuleName' imports." 
                    }
                    Write-DosMessage -Level "Information" -Message "Successfully imported '$ModuleName' with version '$RequiredVersion' into session."
                    Write-DosTelemetry -Message "Multiple modules found in session. Removed all and successfully imported '$ModuleName' with version '$RequiredVersion' into session."
                }
                else {
                    Write-DosMessage -Level "Information" -Message "Using '$ModuleName' that is currently loaded in the session."
                    Write-DosTelemetry -Message "Using '$ModuleName' that is currently loaded in the session."
                }

                # Removing user path from ps module to cover cases in it is not included in system path
                if ($replaceCurrentUserModulePath) {
                    Remove-FromPSModulePath -Path $currentUserPSModulePath
                }
                return
            }
            else {
                Write-DosMessage -Level "Information" -Message "Removing '$ModuleName' that does not match required version '$RequiredVersion'."
                
                Remove-Module -Name $ModuleName
            }
        }
        else {
            # Removing user path from ps module to cover cases in it is not included in system path
            if ($replaceCurrentUserModulePath) {
                Remove-FromPSModulePath -Path $currentUserPSModulePath
            }
            return
        }
    }

    Write-DosMessage -Level "Information" -Message "Did not find module '$ModuleName' loaded in session."
    
    # Second scenario is required version of the module is installed on the system
    Write-DosMessage -Level "Information" -Message "Checking if module '$ModuleName' is already installed."
    $installedModule = Get-ModuleWorkAround -Name $ModuleName -ListAvailable
        
    if($null -ne $installedModule){
        Write-DosMessage -Level "Information" -Message "Module '$ModuleName' already installed."
        if ($RequiredVersion) {
            Write-DosMessage -Level "Verbose" -Message "Checking if installed module '$ModuleName' matches required version '$RequiredVersion'."
            if ((Compare-ModuleVersion -ModuleToCompare $installedModule -RequiredVersion $RequiredVersion)) {
                Write-DosMessage -Level "Information" -Message "Importing '$ModuleName' with version '$RequiredVersion'."
                try {
                    Import-Module -Name $ModuleName -RequiredVersion $RequiredVersion -Global -ErrorAction Stop
                }
                catch [System.Management.Automation.RuntimeException]{
                    Write-DosMessage -Level "Warning" -Message "'$ModuleName' was previously loaded during this session. If you encounter weird issues with DosInstallUtilities, please restart PowerShell and resolve conflicts with '$ModuleName' imports." 
                }
                Write-DosTelemetry -Message "'$ModuleName' found on the system and successfully imported with version '$RequiredVersion'"

                if ($replaceCurrentUserModulePath) {
                    Remove-FromPSModulePath -Path $currentUserPSModulePath
                }
                return
            }                        
        }
        else {
            Write-DosMessage -Level "Information" -Message "Module '$ModuleName' is installed on system, attempting to import."
            try {
                Import-Module -Name $ModuleName -Global -ErrorAction Stop
            }
            catch [System.Management.Automation.RuntimeException]{
                Write-DosMessage -Level "Warning" -Message "'$ModuleName' was previously loaded during this session. If you encounter weird issues with DosInstallUtilities, please restart PowerShell and resolve conflicts with '$ModuleName' imports." 
            }
            Write-DosTelemetry -Message "'$ModuleName' found on the system and successfully imported with version"

            if ($replaceCurrentUserModulePath) {
                Remove-FromPSModulePath -Path $currentUserPSModulePath
            }
            return
        }
    }

    Write-DosMessage -Level "Information" -Message "Did not find module '$ModuleName' installed on system."


    #third scenario is module is installed from diu\dependencies folder
    Write-DosMessage -Level "Information" -Message "Seeing if '$ModuleName' is in the DosInstallUtilities dependencies folder."
   
    $dependenciesFolder = "$($MyInvocation.MyCommand.Module.ModuleBase)\dependencies\"

    if(Test-Path -Path $dependenciesFolder){
        #dependencies folder exists, let's look through it.
        
        #the zip file should be named module_version.zip ( e.g. dbatools_1.0.115.zip (where 1.0.115 is the version))
        $foundModuleZip = $false
        $zipFiles = get-childitem -path "$dependenciesFolder" -file -filter '*.zip'
        
        foreach($zipfile in $zipFiles){
            $moduleZipFileWithoutTheZip = $($zipfile.name).replace('.zip','')
            $moduleZipName = $moduleZipFileWithoutTheZip.split("_")[0]
            $moduleZipVersion = $moduleZipFileWithoutTheZip.split("_")[1]
            
            if($moduleZipName -eq $ModuleName){
                #we found a zip file, now let's see if it is the right version.
                if($RequiredVersion){
                    #we are looking for an exact version, so let's check.
                    if($RequiredVersion -eq $moduleZipVersion){
                        #yep, this is the exact version.
                        $foundModuleZip = $true
                    }
                } else {
                    #we don't need an exact version, so we're good.
                    $foundModuleZip = $true
                }
                if($foundModuleZip){
                    #we found the module we were looking for, let's save it off and break out of this foreach loop
                    $moduleZipFile = $zipfile.PSPath
                    break
                }

            }
        }

        if($moduleZipFile){
            Write-DosMessage -Level 'Information' -Message "Found $moduleZipFile in $dependenciesFolder"
            #now, we want to go to the my documents folder
            $finalFolder = [Environment]::GetFolderPath("MyDocuments")
            $finalFolder = "$finalFolder\WindowsPowerShell\Modules\$moduleZipName\$moduleZipVersion"
            if($(Test-Path -Path $finalFolder) -and $(get-childitem $finalFolder -filter "$moduleZipName.psd1")){
                Write-DosMessage -Level 'Information' -Message "$ModuleName already exists in $finalFolder."
            } else {
                Write-DosMessage -Level 'Information' -Message "$ModuleName doesn't exist in $finalFolder. We will place it there."
                if(Test-Path -Path $finalFolder){
                    Write-DosMessage -Level 'Information' -Message "Removing directory $finalFolder"
                    Remove-item -force $finalFolder
                }
                Write-DosMessage -Level 'Information' -Message "Creating directory $finalFolder"
                new-item $finalFolder -itemtype 'directory'
                Write-DosMessage -Level 'Information' -Message "Unzipping $moduleZipFile to $finalFolder"
                Expand-Archive -Path $moduleZipFile -DestinationPath $finalFolder -Force
            }


            $modules = @( Get-ChildItem -Path "$finalFolder\*" -Include "$moduleZipName.psd1" -Recurse -ErrorAction SilentlyContinue )
            if ($modules) { 
                Write-DosMessage -Level 'Information' -Message "Importing $modules"
                Import-Module $modules -Force -Global 
                return
            } else {
                Write-DosMessage -Level 'Fatal' -Message "Unable to import $ModuleName from $finalFolder when we expected to be able to do so."
            }
        } 
    }



    
    
    Write-DosMessage -Level 'Information' -Message "Did not find a zip file for $ModuleName in $dependenciesFolder."


    #fourth scenario is module is installed from PSGallery
    Write-DosMessage -Level "Information" -Message "Attempting to fetch '$ModuleName'."

    $desiredRepo = "PSGallery"
    $isTrusted = Get-RepositoryTrust -RepositoryName $desiredRepo

    if (!($isTrusted)) {
        Write-DosMessage -Level "Information" -Message "'$desiredRepo' is not trusted. Toggling trust to download '$ModuleName'"
        Set-RepositoryTrust -RepositoryName $desiredRepo -Trust
    }

    #Error check here - also assume that PowerShellGet is loaded/available.
    try {
        if ($RequiredVersion) {
            Write-DosMessage -Level "Information" -Message "Module '$ModuleName' version '$RequiredVersion' being downloaded from PSGallery."
            Install-Module $ModuleName -RequiredVersion $RequiredVersion -Scope $scope
            Write-DosMessage -Level "Information" -Message "Successfully downloaded module '$ModuleName' with version '$RequiredVersion' from PSGallery."
            try {
                    Import-Module -Name $ModuleName -RequiredVersion $RequiredVersion -Global -ErrorAction Stop
                }
                catch [System.Management.Automation.RuntimeException]{
                    Write-DosMessage -Level "Warning" -Message "'$ModuleName' was previously loaded during this session. If you encounter weird issues with DosInstallUtilities, please restart PowerShell and resolve conflicts with '$ModuleName' imports." 
                }
            Write-DosMessage -Level "Information" -Message "Successfully imported module '$ModuleName' with version '$RequiredVersion' from PSGallery."
            Write-DosTelemetry -Message "Successfully imported module '$ModuleName' with version '$RequiredVersion' from PSGallery."
        }
        else {
            Write-DosMessage -Level "Information" -Message "Module '$ModuleName' being downloaded from PSGallery."
            Install-Module $ModuleName -Scope $scope
            Write-DosMessage -Level "Information" -Message "Successfully downloaded module '$ModuleName' from PSGallery."
            try {
                Import-Module -Name $ModuleName -Global -ErrorAction Stop
            }
            catch [System.Management.Automation.RuntimeException]{
                Write-DosMessage -Level "Warning" -Message "'$ModuleName' was previously loaded during this session. If you encounter weird issues with DosInstallUtilities, please restart PowerShell and resolve conflicts with '$ModuleName' imports." 
            }
            Write-DosMessage -Level "Information" -Message "Successfully imported module '$ModuleName' from PSGallery."
            Write-DosTelemetry -Message "Successfully imported module '$ModuleName' from PSGallery."
        }
    }
    catch {
        Write-DosMessage -Level "Error" -Message "Error installing or importing '$ModuleName'. Exception: $($_.Exception)"
        if (!($isTrusted)) {
            Write-DosMessage -Level "Information" -Message "Returning '$desiredRepo' to an untrusted state."
            Set-RepositoryTrust -RepositoryName $desiredRepo
        }
    }

    if (!($isTrusted)) {
        Write-DosMessage -Level "Information" -Message "Returning '$desiredRepo' to an untrusted state."
        Set-RepositoryTrust -RepositoryName $desiredRepo
    }

    if ($replaceCurrentUserModulePath) {
        Remove-FromPSModulePath -Path $currentUserPSModulePath
    }

    Write-DosTelemetry -Message "Install-RequiredModule completed - successfully"
}

#Work around for pester issue: https://github.com/pester/Pester/issues/1007
function Get-ModuleWorkAround{
    param(
        [string] $Name,
        [switch] $ListAvailable
    )


    if($ListAvailable.IsPresent){
        return Get-Module -Name $Name -ListAvailable
    }
    else {
        return Get-Module -Name $Name
    }
}

function Get-RepositoryTrust {
    param (
        [string] $RepositoryName
    )

    $repo = Get-PSRepository -Name $RepositoryName
    return $repo.Trusted
}

function Set-RepositoryTrust {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")]
    param (
        [string] $RepositoryName,
        [switch] $Trust
    )

    if ($Trust.IsPresent) {
        Set-PSRepository -Name $RepositoryName -InstallationPolicy Trusted
    }
    else {
        Set-PSRepository -Name $RepositoryName -InstallationPolicy Untrusted
    }
}

function Add-ToPSModulePath {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string] $Path
    )

    if (!($env:PSModulePath.split(";") -contains $Path)){
        Write-DosMessage -Level "Information" -Message "Adding '$Path' to PSModulePath"
        $current = $env:PSModulePath
        [Environment]::SetEnvironmentVariable("PSModulePath",$current + ";" + $Path, "Machine")
        $env:PSModulePath = [System.Environment]::GetEnvironmentVariable("PSModulePath","Machine")
        return $true
    }else{
        Write-DosMessage -Level "Information" -Message "'$Path' is already present in PSModulePath"
        return $false
    }
}

function Remove-FromPSModulePath{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string] $Path
    )
    if ($env:PSModulePath.split(";") -contains $Path){
        $newValue = (($env:PSModulePath).Split(";") | Where-Object { $_ -ne $Path }) -join ";"
        [Environment]::SetEnvironmentVariable("PSModulePath", $newValue, "Machine")
        $env:PSModulePath = [System.Environment]::GetEnvironmentVariable("PSModulePath","Machine")
        Write-DosMessage -Level "Information" -Message "$Path removed from PSModulePath." 
    }else{
        Write-DosMessage -Level "Information" -Message "$Path is not present in $env:PSModulePath"
    }
}

function Compare-ModuleVersion {
    [CmdletBinding()]
    param(
        [object] $ModuleToCompare,
        [version] $RequiredVersion
    )

    $isMatch = $false

    foreach($module in $ModuleToCompare) {
        if ($module.Version.CompareTo($RequiredVersion) -eq 0) {
            Write-DosMessage -Level "Information" -Message "Found Module '$ModuleName' that meets the version requirements."
            $isMatch = $true
            break;
        }
    }

    if (!$isMatch) {
        Write-DosMessage -Level "Information" -Message "No module with version '$RequiredVersion' was found for module '$ModuleName'."
        
    }

    return $isMatch
}


<#
    .SYNOPSIS
    Makes a web request to the $ServiceUrl that is passed in and returns the odata results.
 
    .DESCRIPTION
    Makes a web request to the $ServiceUrl that is passed in and returns the odata results. If there are not results then it will return an empty object.
 
    .PARAMETER ServiceUrl
    [string] (Required) The URL to make a request to
 
    .PARAMETER AccessToken
    [string] (Required) The AccessToken from Identity needed for authentication
 
    .PARAMETER Headers
    [hashtable] (Optional) Headers to send with the request - by default the code will add: @{Accept = "application/json"; Authorization = "Bearer $AccessToken"} but those can be overwritten
    Note: It will only add the Authorization header if AccessToken is set
 
    .PARAMETER ContentType
    [string] (Optional) The ContentType of the Body being sent with the request (if a body is required) - by default the code will set it to "application/json" if it is not set
 
    .PARAMETER Body
    [string] (Optional) The JSON body to send with Patch, Put, and Post requests - will fail if included with Get or Delete requests
 
    .PARAMETER Get
    [switch] Sets GET as the method type
    One of the Get, Patch, Put, Post, or Delete switches MUST be present
 
    .PARAMETER Patch
    [switch] Sets PATCH as the method type
    One of the Get, Patch, Put, Post, or Delete switches MUST be present
 
    .PARAMETER Put
    [switch] Sets PUT as the method type
    One of the Get, Patch, Put, Post, or Delete switches MUST be present
 
    .PARAMETER Post
    [switch] Sets POST as the method type
    One of the Get, Patch, Put, Post, or Delete switches MUST be present
 
    .PARAMETER Delete
    [switch] Sets DELETE as the method type
    One of the Get, Patch, Put, Post, or Delete switches MUST be present
 
    .EXAMPLE
    Invoke-DosOdataRequest -ServiceUrl "https://mymachine.hqcatalyst.local/MetadataService2/v2/DataMarts" -AccessToken $token $Get
    or
    Invoke-DosOdataRequest -ServiceUrl "https://mymachine.hqcatalyst.local/MetadataService2/v2/DataMarts" -AccessToken $token $Post -Body $jsonPayload
#>

function Invoke-DosOdataRequest {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        [ValidateScript( {
                if ($null -eq ($_ -as [System.URI]).AbsoluteURI) {
                    throw """$_"" is not a valid url"
                }
            
                return $true
            })]
        [string] $ServiceUrl,
        
        [string] $AccessToken,

        [hashtable] $Headers = @{},

        [string] $ContentType = "application/json",

        [switch] $UseDefaultCredentials,
        
        # The parameter set will require this parameter only when that same parameter set is on another parameter (Put, Patch, Post)
        # It will fail if it is included when not required (Get, Delete)
        [Parameter(ParameterSetName = "RequestBody")]
        [string] $Body,

        [Parameter(ParameterSetName = "GetRequest")]
        [switch] $Get,

        # Body is a required parameter for Patch
        [Parameter(ParameterSetName = "PatchRequest")]
        [Parameter(ParameterSetName = "RequestBody")]
        [switch] $Patch,

        # Body is a required parameter for Put
        [Parameter(ParameterSetName = "PutRequest")]
        [Parameter(ParameterSetName = "RequestBody")]
        [switch] $Put,

        # Body is a required parameter for Post
        [Parameter(ParameterSetName = "PostRequest")]
        [Parameter(ParameterSetName = "RequestBody")]
        [switch] $Post,

        [Parameter(ParameterSetName = "DeleteRequest")]
        [switch] $Delete

    )

    Write-DosMessage -Level "Debug" -Message "Checking for required headers"
    if (-Not $Headers.ContainsKey("Accept")) {
        Write-DosMessage -Level "Debug" -Message "Adding ""Accept"" header"
        $Headers.Add("Accept", "application/json")
    }
    
    # Only add the header if AccessToken is not null or empty and the Headers doesn't already contain Authorization
    if (-Not $Headers.ContainsKey("Authorization") -and ![String]::IsNullOrEmpty($AccessToken)) {
        Write-DosMessage -Level "Debug" -Message "Adding ""Authorization"" header"
        $Headers.Add("Authorization", "Bearer $AccessToken")
    }

    Write-DosMessage -Level "Debug" -Message "Checking that Content-Type is set"
    if ([String]::IsNullOrEmpty($ContentType)) {
        Write-DosMessage -Level "Debug" -Message "Setting Content-Type to ""application/json"""
        $ContentType = "application/json"
    }

    Write-DosMessage -Level "Debug" -Message "Determining which request method was selected"
    if ($Get.IsPresent) {
        $method = "GET"
    }
    elseif ($Post.IsPresent) {
        $method = "POST"
    }
    elseif ($Put.IsPresent) {
        $method = "PUT"
    }
    elseif ($Patch.IsPresent) {
        $method = "PATCH"
    }
    elseif ($Delete.IsPresent) {
        $method = "DELETE"
    }
    Write-DosMessage -Level "Information" -Message "Method selected is ""$method"""

    $output = @()
    $url = $ServiceUrl

    try {
        Write-DosMessage -Level "Information" -Message "Invoking ""$method"" request to ""$ServiceUrl"""
        
        $requestParameters = @{
            Method          = $method
            URI             = $url
            Headers         = $Headers
            ContentType     = $ContentType
            UseBasicParsing = $true #required until Powershell 6+
        }

        if ($Body) {
            Write-DosMessage -Level "Debug" -Message "Testing if the body is already Json"
            # When we go to Powershell 7 there is a Test-Json method that can take the place of the next several lines of code
            try {
                ConvertFrom-Json $Body -ErrorAction Stop | Out-Null
                $validJson = $true
            }
            catch {
                $validJson = $false
            }

            if (-Not $validJson) {
                Write-DosMessage -Level "Debug" -Message "Attempting to convert the body to Json"
                $Body = $Body | ConvertTo-Json -Depth 100 -Compress
            } 

            Write-DosMessage -Level "Debug" -Message "Request body is ""$Body"""
            $requestParameters.Add("Body", $Body)
        }

        if ($UseDefaultCredentials) {
            $requestParameters.Add("UseDefaultCredentials", $true)
        }

        do {
            $response = Invoke-RestMethod @requestParameters

            if ($response.PSOjbect.Properties.Name -contains "value") { 
                $output += $response.value 
            }
            else {
                $output += $response
            }

            $url = $response.'@odata.nextLink';
        }
        while ($url);
    }
    catch [System.Net.WebException] {
        Write-DosMessage -Level "Warning" -Message "A non 200 response was returned from ""$url"". This may be expected.`nRequest: $url`nStatus Code: $($_.Exception.Response.StatusCode.value__)`nMessage: $($_.Exception.Response.StatusDescription)"
        throw $_ # Rethrow error for downstream catching if desired
    }
    catch {
        Write-DosMessage -Level "Fatal" -Message "An error was encountered while making the web request. Exception: $($_.Exception)"
    }

    return $output
}


function Invoke-DosPingServices {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification="Values should be plural.")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $DiscoveryServiceUrl,
        [string] $AccessToken,
        [Parameter(Mandatory = $true)]
        [hashtable[]] $Services
    )
    begin {
        Write-DosMessage -Level "Information" -Message "PING DISCOVERYSERVICE" -HeaderType H2
        Invoke-DosPingService -ServiceName "DiscoveryService" -Uri "$(Remove-VersionFromLocalPath $DiscoveryServiceUrl)/ping" -Headers @{"Accept" = "application/json" } -UseDefaultCredentials -ErrorLevel Fatal
    }
    process {
        foreach ($Service in $Services) {
            try {
                Write-DosMessage -Level "Information" -Message "PING $($Service.Name.ToUpper())" -HeaderType H2
                $Headers = @{"Accept" = "application/json" }
                if ($Service.RequireAuthToken) {
                    if ($AccessToken) {
                        $Headers.Add("Authorization", "Bearer $AccessToken")
                    }
                    else {
                        Write-DosMessage -Level "Fatal" -Message "$($Service.Name) ping header did not provide the required AccessToken"
                    }
                }
            
                if([string]::IsNullOrEmpty($Service.AbsoluteEndpoint)){
                    $UriRoot = Remove-VersionFromLocalPath (Get-DosServiceUrl -DiscoveryServiceUrl $DiscoveryServiceUrl -ServiceName $Service.Name -ServiceVersion $Service.Version)
                    #removing trailing slashes, if there are any
                    if ($UriRoot.EndsWith("/")){
                        $UriRoot = $UriRoot.substring(0,$UriRoot.Length-1)
                    }
        
                    $Uri = "$($UriRoot)/$($Service.PingEndpoint)"
                } else {
                    $Uri = $Service.AbsoluteEndpoint
                }
                
                Write-DosMessage -Level "Information" -Message "Appended ping endpoint to root url --> $Uri"
                $invokePingServiceParams = @{
                    ServiceName           = $Service.Name
                    Uri                   = $Uri
                    Headers               = $Headers
                    ErrorLevel            = $Service.ErrorLevel
                    UseDefaultCredentials = if ($Service.UseDefaultCredentials) { $true } else { $false }
                }
                if ($Service.RetryScriptBlock) {
                    $invokePingServiceParams.Add("RetryScriptBlock", $Service.RetryScriptBlock)
                }
                Invoke-DosPingService @invokePingServiceParams
            }
            catch {
                Write-DosMessage -Level $Service.ErrorLevel -Message "There was an error pinging the service `"$($Service.Name)`". This installation is configured to return a `"$($Service.ErrorLevel.ToUpper())`" message if this occurs."
                if ($Service.ErrorLevel -eq "Fatal") {
                    Write-DosMessage -Level $Service.ErrorLevel -Message "Please check and fix the service `"$($Service.Name)`" before trying this installation again."
                }
            }
        }    
    }
}

function Invoke-DosPingService {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $ServiceName,
        [Parameter(Mandatory = $true)]
        [string] $Uri,
        [Parameter(Mandatory = $true)]
        [hashtable] $Headers,
        [string] $Method = "Get",
        [switch] $UseDefaultCredentials,
        [ValidateSet("Error", "Fatal", "Warning")]
        [string] $ErrorLevel = "Error",
        [int] $RetryAttempts = 5,
        [int] $RetrySleepSeconds = 3,
        [scriptblock] $RetryScriptBlock
    )
    $counter = 0
    $success = $false
    try {
        Write-DosMessage -Level "Information" -Message "Pinging $($ServiceName)...$($Uri)"
        $webRequestParams = @{
            Method  = $Method
            Uri     = $Uri
            Headers = $Headers
        }
        if ($UseDefaultCredentials) {
            $webRequestParams.Add("UseDefaultCredentials", $UseDefaultCredentials)
        }
        while (!$success -and $counter -lt $RetryAttempts) {
            try {
                $response = Invoke-WebRequest @webRequestParams -UseBasicParsing
                Write-DosMessage -Level "Information" -Message "$($ServiceName) ping successful! $($response.StatusCode) ($($response.StatusDescription))"
                $success = $true
            }
            catch {
                $counter++
                Write-DosMessage -Level "Warning" -Message "There was an error pinging the service `"$($ServiceName)`"."
                if ($RetryScriptBlock) {
                    Invoke-Command -ScriptBlock $RetryScriptBlock
                }
                Write-DosMessage -Level "Information" -Message "Retrying ping - attempt $($counter) of $($RetryAttempts) ..."
                Start-Sleep -Seconds $RetrySleepSeconds
            }
        }
        if (!$success) { throw }
    }
    catch {
        Write-DosMessage -Level $ErrorLevel -Message "There was an error pinging the service `"$($ServiceName)`". This installation is configured to return a `"$($ErrorLevel.ToUpper())`" message if this occurs."
        if ($ErrorLevel -eq "Fatal") {
            Write-DosMessage -Level $ErrorLevel -Message "Please check and fix the service `"$($ServiceName)`" before trying this installation again."
        }
    }
}


<#
    .SYNOPSIS
 
    Reads in json manifest files and invokes the appropriate readiness checks based of the supported check types.
 
    .DESCRIPTION
 
     
 
    .PARAMETER ManifestPath
 
    Accepts an array of valid paths to json manifest files.
 
    .PARAMETER ResourceToCheck
 
    Accepts a string of either "dbServer", or "webServer". The environment that the readiness checks will run against.
 
    .EXAMPLE
 
    Invoke-DosPrerequisiteChecks -ManifestPath @("C:/Exampe/Path/Manifest.json", "C:/Another/Example/Manifest.json") -ResourceToCheck "dbServer" -PathToLogRoot "C:\install"
#>


function Invoke-DosPrerequisiteChecks {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification="Values should be plural.")]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [ValidateScript({
            foreach ($path in $_) {
                if (!(Test-Path $path)) {
                    throw "ManfiestPath $path does not exist. Please enter valid path."
                }
                else {
                    $true
                }
            }
        })]
        [string[]] $ManifestPath,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [ValidateSet("dbServer", "webServer")]
        [string] $ResourceToCheck,
        [ValidateScript({
            foreach ($path in 0) {
                if (!(Test-Path $path)) {
                    throw "PathToLogRoot '$path' does not exist. Please enter valid path."
                }
                else {
                    $true
                }
            }
        })]
        [string] $PathToLogRoot = "C:\install\"
    )

    $fileDate = (Get-Date).tostring("dd-MM-yyyy-hh-mm-ss")
    $logFile =  "InstallReadinessTool_" + $fileDate + ".log"
    $logPath = $PathToLogRoot + $logFile
    try {
        New-Item -ItemType File -Path $PathToLogRoot -Name $logFile -Force -ErrorAction Stop | Out-Null
    }
    catch {
        throw "Error creating file, '$logPath'. Exception: $_"
    }
    Write-Output "Log file generated in '$PathToLogRoot'"

    # Potentially Loop for the manifests provided
    foreach ($manifest in $ManifestPath) {
        # Get-Content json manfiests
        $manifestData = Get-Content -Raw -Path "$manifest" | ConvertFrom-Json

        # Powershell Checktype
        $powershellData = $manifestData.readinessChecks | Where-Object { $_.checkType -eq "powershellVersion" -and $_.resourceToCheck -contains "$ResourceToCheck" }
        $psVersionCheckResult = Invoke-PowershellVersionCheck -Data $powershellData *>&1
        Write-VariableToConsoleAndFile -VariableToParse $psVersionCheckResult -Log $logPath

        # Os Version Checktype
        $allowedOSVersions = @("Server 2012 R2", "Server 2016", "Windows 10")
        $osVersionCheckResult = Invoke-OsVersionCheck -AllowedVersions $allowedOSVersions *>&1
        Write-VariableToConsoleAndFile -VariableToParse $osVersionCheckResult -Log $logPath

        # Dependent Software Checktype
        $dependentSoftwareData = $manifestData.readinessChecks | Where-Object { $_.checkType -eq "dependentSoftware" -and $_.resourceToCheck -contains "$ResourceToCheck" }
        $dependentSoftwareCheckResult = Invoke-DependentSoftwareCheck -Data $dependentSoftwareData *>&1
        Write-VariableToConsoleAndFile -VariableToParse $dependentSoftwareCheckResult -Log $logPath

        # Windows Feature Checktype
        $windowsFeatureData = $manifestData.readinessChecks | Where-Object { $_.checkType -eq "windowsFeature" -and $_.resourceToCheck -contains "$ResourceToCheck" }
        $windowsFeatureCheckResult = Invoke-WindowsFeatureCheck -Data $windowsFeatureData *>&1
        Write-VariableToConsoleAndFile -VariableToParse $windowsFeatureCheckResult -Log $logPath
    }
}

function Write-VariableToConsoleAndFile {
    param (
        [object[]] $VariableToParse,
        [string] $Log
    )
    ForEach ($line in $($VariableToParse -split "`r`n"))
    {
        if ($line -like '*`[+`]*') {
            Write-Host $line -Foregroundcolor Green
            $line | Out-File -FilePath $Log -Append
        } elseif ($line -like '*`[-`]*') {
            Write-Host $line -Foregroundcolor Red 
            $line | Out-File -FilePath $Log  -Append
        } else {
            Write-Host $line 
            $line | Out-File -FilePath $Log -Append
        }
    }
}


<#
    .SYNOPSIS
 
    Start or restart an application pool
 
    .DESCRIPTION
 
    Uses methods provided in the WebAdministration module to make an attempt to start or restart a provided application pool.
 
    .PARAMETER AppPoolName
 
    IIS Application Pool name
 
    .PARAMETER ErrorLevel
 
    Error level logged when unsuccessful in starting or restarting the provided application pool. Defaulted to "Error"
 
    .EXAMPLE
 
    Invoke-DosRecycleAppPool -AppPoolName "x" -ErrorLevel "Fatal"
#>

function Invoke-DosRecycleAppPool {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        [ValidateLength(1, 64)]
        [ValidateScript( {
                if ($_ -match '[^a-zA-Z0-9]') {
                    Write-DosMessage -Level "Fatal" -Message "$_ must only contain alphanumeric values. Please remove special characters."
                }
                else {
                    $true
                }
            })]
        [string] $AppPoolName,
        [ValidateSet("Warning", "Error", "Fatal")]
        [string] $ErrorLevel = "Error"
    )

    Test-ElevatedPermission
    Import-Module WebAdministration -Force

    try {
        $appPoolState = Get-WebAppPoolState -Name $AppPoolName -ErrorAction Stop
        if ($appPoolState.Value -ne 'Started') {
            try {
                Start-WebAppPool -Name $AppPoolName -ErrorAction Stop
                Write-DosMessage -Level "Information" -Message "Started Application Pool: $($AppPoolName)"
            }
            catch {
                Write-DosMessage -Level $ErrorLevel -Message $_.Exception.message
            }
        }
        else {
            try {
                Restart-WebAppPool -Name $AppPoolName -ErrorAction Stop
                Write-DosMessage -Level "Information" -Message "Restarted Application Pool: $($AppPoolName)"
            }
            catch {
                Write-DosMessage -Level $ErrorLevel -Message $_.Exception.message
            }
        }
    }
    catch {
        Write-DosMessage -Level "Fatal" -Message $_.Exception.message
    }
}


<#
.SYNOPSIS
Executes a query
 
.DESCRIPTION
Executes TSQL against a SQL Server
 
.PARAMETER SqlConnection
The sql connection to use to execute the query
 
.PARAMETER ConnectionString
The connection string used to create a new connection.
 
.PARAMETER InstanceName
Instance name for use in creating an AdHoc connection
 
.PARAMETER DatabaseName
Database name for use in creating an AdHoc connection
 
.PARAMETER Credentail
An optional PsCredential object if using sql auth
 
.PARAMETER Query
TSQL to execute
 
.PARAMETER Parameters
A hashtable of parameters to build a parameterized query
 
.PARAMETER AsResult
Return results as DataSet, DataTable, or array or DataRows
 
.PARAMETER NonQuery
Executes the query as ExecuteNonQuery()
This will allow you to run the query and then suppress the results output
 
.PARAMETER CommandTimeout
The execution timeout in seconds (default is 30)
By default, all executions of Invoke-DosSqlQuery have a 30 second timeout. Use this parameter to increase the timeout to any number of seconds.
Or pass in 0 for no timeout.
 
.INPUTS
None.
 
.OUTPUTS
[System.Data.DataSet], [System.Data.DataTable], [System.Data.DataRow[]]
 
.EXAMPLE
PSn Invoke-DosSqlQuery ...
#>

function Invoke-DosSqlQuery {
    [cmdletbinding(SupportsShouldProcess=$true)]
    [OutputType([System.Data.DataSet],[System.Data.DataTable],[System.Data.DataRow[]])]
    param(
        [parameter(Mandatory=$true,ParameterSetName='Connection')][Data.SqlClient.SqlConnection]$SqlConnection,
        [parameter(Mandatory=$true,ParameterSetName='ConnectionString')][string]$ConnectionString,
        [parameter(Mandatory=$true,ParameterSetName='AdHoc')][string]$InstanceName,
        [parameter(Mandatory=$false,ParameterSetName='AdHoc')][string]$DatabaseName,
        [parameter(Mandatory=$false,ParameterSetName='AdHoc')][PSCredential]$Credential,
        [parameter(Mandatory=$true)][string]$Query,
        [parameter(Mandatory=$false)][hashtable]$Parameters,
        [parameter(Mandatory=$false)][ValidateSet("DataSet", "DataTable", "Array")][string]$AsResult='Array',
        [parameter(Mandatory=$false)][switch]$NonQuery,
        [parameter(Mandatory=$false)][int]$CommandTimeout = 30,
        [parameter(Mandatory=$false,ValueFromRemainingArguments=$true)]$Arguments
    )

    <# This function is going to be splatted on by other functions $PSBoundParametes as they share
    common parameter sets for connectivity. Non defined parameters lands in $Arguments which
    dont care about nor do we want to pass that only New-SqlConnection. Remove $Arguments from our
    $PSBoundParameters #>

    $PSBoundParameters.Remove('Arguments') | Out-Null
    $PSBoundParameters.Remove('ErrorAction') | Out-Null

    if(@('AdHoc','ConnectionString').Contains($PSCmdlet.ParameterSetName)){
        $SqlConnection = New-DosSqlConnection @PSBoundParameters -ErrorAction Stop
    }

    $Cmd = New-Object System.Data.SqlClient.SqlCommand($Query,$SqlConnection)
    $Cmd.CommandTimeout = $CommandTimeout
    
    if($null -ne $Parameters){
        foreach($key in $Parameters.Keys){
            $cmd.Parameters.AddWithValue($Key,$Parameters[$Key]) | Out-Null
        }
    }

    if($NonQuery){
        if ($pscmdlet.ShouldProcess($Query, "Executing non SQL query")){
            Invoke-ExecuteNonQuery -cmd $cmd
        }
    }
    else{
        if ($pscmdlet.ShouldProcess($Query, "Executing SQL Query")){
            $results = Invoke-Fill -cmd $cmd -AsResult $AsResult
        }
    }

    if(@('AdHoc','ConnectionString').Contains($PSCmdlet.ParameterSetName)){
        if ($pscmdlet.ShouldProcess($ConnectionString, "Closing SQL connection")){
            Invoke-CloseAndDispose -SqlConnection $SqlConnection
        }
    }

    if($NonQuery -eq $false){
        return $results
    }
}

function Invoke-CloseAndDispose{
    param(
        [Data.SqlClient.SqlConnection]$SqlConnection
    )
    $SqlConnection.Close() | Out-Null
    $SqlConnection.Dispose() | Out-Null
}

function Invoke-ExecuteNonQuery{
    param(
        [System.Data.SqlClient.SqlCommand]$cmd
    )
    $cmd.ExecuteNonQuery() | Out-Null
}

function Invoke-Fill{
    param(
        [System.Data.SqlClient.SqlCommand]$cmd
        ,[string]$AsResult
    )

    $ds=New-Object system.Data.DataSet
    $da=New-Object system.Data.SqlClient.SqlDataAdapter($cmd)
    $da.Fill($ds) | Out-Null
    switch($AsResult){
        'DataSet'{
            $results = $ds
        }
        'DataTable'{
            $results = $ds.Tables[0]
        }
        'Array'{
            $results = @()
            $results += $ds.Tables[0].Rows
        }
    }
    return $results

}


<#
    .SYNOPSIS
 
    Merges hashtables together
 
    .DESCRIPTION
 
    When working with DOS configuration values that come from varying places (some common examples may be a config store, pipeline, or static) it is
    nice to be able to merge these configurations into a single $config hashtable.
 
    Merge-DosHashtable combines all key values pairs creating a single hashtable. This method when used in conjunction with Push-DosConfigType keeps
    a merged "_type_" attribute. See Push-DosConfigType for more information about this.
 
    .OUTPUTS
 
    HASHTABLE A:
     
    Name Value
    ---- -----
    clientName Health Catalyst
    _type_ {clientName}
 
    HASHTABLE B:
     
    Name Value
    ---- -----
    ServiceName Test Service
    _type_ {ServiceName}
 
    OUTPUT:
 
    Name Value
    ---- -----
    clientName Health Catalyst
    ServiceName Test Service
    _type_ {clientName, ServiceName}
 
 
    .EXAMPLE
 
    $storeConfig = @{ clientName = 'Health Catalyst' } | Push-DosConfigType "store"
    $staticConfig = @{ ServiceName = 'Test Service' } | Push-DosConfigType "static"
    $config = $storeConfig, $staticConfig | Merge-DosHashtable
#>

function Merge-DosHashtable {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline)]
        $hash
    )
    $output = @{}
    $subHash = @{}
    foreach ($hash in $input) {
        if ($hash -is [Hashtable]) {
            foreach ($key in $hash.Keys) {
                if ($key -ne "_type_") {
                    $output.$key = $hash.$key
                }
                else {
                    foreach ($subKey in $hash[$key].Keys) {
                        $subHash.$subKey = $hash[$key].$subKey
                    }
                }
            }
        }
    }
    $output.Add("_type_", $subHash)
    return $output
}



<#
    .SYNOPSIS
 
    Creates a new ConfigStore
 
    .DESCRIPTION
 
    Attempts to create a new config store
 
    .PARAMETER configStore
 
    The configstore hashtable that will be created
 
    .OUTPUTS
 
    $configStore - the $configStore hashtable is returned
 
    .EXAMPLE
 
    New-DosConfigStore -configStore @{Type = "File"; Format = "XML"; Path = "$PSScriptRoot\nonexistent\nonexistent.config"}
    New-DosConfigStore -configStore $configStore
 
#>

function New-DosConfigStore {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [hashtable] $configStore
    )
    Write-DosTelemetry -Message "$($MyInvocation.MyCommand.Name) called."

    if($configStore.Type -eq "File" -and $configStore.Format -eq "XML"){
        return New-DosConfigStoreXml -configStore $configStore
    } else {
        Write-DosMessage -Level 'Fatal' -Message "New-DosConfigStore not implemented yet for type: $($configStore.Type), format: $($configStore.Format)"
        return $null
    }

    
}


function New-DosDacPacPublishFile {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")]
    param (
        [Parameter(Mandatory = $true)]
        [string] $publishProfilePath,
        [Parameter(Mandatory = $true)]
        [hashtable] $publishProfileValues
    )
    begin {
        Write-DosMessage -Level "Information" -Message "Creating Dacpac Publish Profile $publishProfilePath"
        [xml]$publishProfileXml = @'
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
</Project>
'@

    }
    process {
        try {
            Add-PublishDacPacFile -publishProfileXml $publishProfileXml -publishProfileValues $publishProfileValues -publishProfilePath $publishProfilePath
            Write-DosMessage -Level "Information" -Message "Saving changes to file $publishProfilePath"
        }
        catch {
            $ErrorMessage = $_.Exception.Message
            Write-DosMessage -Level "Fatal" -Message $ErrorMessage    
        }
    }
}

function Add-PublishDacPacFile {
    param (
        [xml] $publishProfileXml,
        [hashtable] $publishProfileValues,
        [string] $publishProfilePath
    )
    $project = $publishProfileXml.Project            
    $itemGroup = $publishProfileXml.CreateElement("ItemGroup", $project.xmlns)
    foreach ($publishProfileValue in $publishProfileValues.GetEnumerator()) {        
        $sqlCmdVariable = $publishProfileXml.CreateElement("SqlCmdVariable", $project.xmlns)
        $include = $publishProfileXml.CreateAttribute("Include")        
        $include.Value = $publishProfileValue.Name
        $value = $publishProfileXml.CreateElement("Value", $project.xmlns)
        $value.InnerText = $publishProfileValue.Value
        $sqlCmdVariable.AppendChild($value) | Out-Null
        $sqlCmdVariable.Attributes.Append($include) | Out-Null
        $itemGroup.AppendChild($sqlCmdVariable) | Out-Null
        Write-DosMessage -Level "Information" -Message "Added $($publishProfileValue.Name) -> $($publishProfileValue.Value)"
    }
    $project.AppendChild($itemGroup) | Out-Null
    $publishProfileXml.Save($publishProfilePath)
}


<#
.SYNOPSIS
Creates a SQL connection
 
.DESCRIPTION
Creates and returns an ***open*** SQL connection.
 
.PARAMETER InstanceName
The full <ComputerName>/<NamedInstance> instance name of SQL server
 
.PARAMETER DatabaseName
The database name to connect too
 
.PARAMETER Credential
A credential containing the SQL auth user and password
 
.PARAMETER ConnectionString
A SQL connection string to build the connection from
 
.INPUTS
None.
 
.OUTPUTS
Data.SqlClient.SqlConnection
 
.EXAMPLE
PS> New-DosSqlConnection ...
#>

function New-DosSqlConnection {
    [cmdletbinding(DefaultParameterSetName='Default',SupportsShouldProcess=$true)]
    [OutputType([Data.SqlClient.SqlConnection])]
    param(
        [parameter(Mandatory=$true,ParameterSetName='Default')]
        [parameter(Mandatory=$true,ParameterSetName='SqlAuth')]
        [string]$InstanceName,
        [parameter(Mandatory=$false,ParameterSetName='Default')]
        [parameter(Mandatory=$false,ParameterSetName='SqlAuth')]
        [string]$DatabaseName='master',
        [parameter(Mandatory=$true,ParameterSetName='SqlAuth')][PSCredential]$Credential,
        [parameter(Mandatory=$true,ParameterSetName='ConnectionString')][string]$ConnectionString,

        [parameter(Mandatory=$false,ParameterSetName='Default',ValueFromRemainingArguments=$true)]
        [parameter(Mandatory=$false,ParameterSetName='SqlAuth',ValueFromRemainingArguments=$true)]
        [parameter(Mandatory=$false,ParameterSetName='ConnectionString',ValueFromRemainingArguments=$true)]
        $Arguments
    )

    switch($PSCmdlet.ParameterSetName){
        'ConnectionString'{}
        'SqlAuth'{
            $ConnectionString = "Server={0};Database={1};User ID={2};Password={3};Pooling=false" -f $InstanceName, $DatabaseName, $Credential.UserName, $Credential.GetNetworkCredential().Password
        }
        'Default'{
            $ConnectionString = "Server={0};Database={1};Integrated Security=True" -f $InstanceName, $DatabaseName
        }
    }

    try{
        if ($pscmdlet.ShouldProcess($ConnectionString, "Creating SQL connection")){
            $connection = New-Object Data.SqlClient.SqlConnection $ConnectionString
            $connection.Open()
        }
    }
    catch{
        Write-DosMessage -Level 'Fatal' -Message "$_.Exception.Message"
    }

    return $connection
}


<#
.SYNOPSIS
Creates SQL login
 
.DESCRIPTION
Creates an instance level SQL login
 
.PARAMETER InstanceName
Full instance name of SQL server
 
.PARAMETER ConnectionString
Connection string to a SQL instnace
 
.PARAMETER SqlConnection
SQL connection to target SQL Server
 
.PARAMETER LoginName
The full name of the login to create
 
.PARAMETER AuthenticationType
Windows or SQL
 
.PARAMETER Credential
The cred (user/pass) to set for the new login
 
.PARAMETER IfNotExists
A switch to only create if login does not exist
 
.INPUTS
None
 
.OUTPUTS
None
 
.EXAMPLE
PS> New-DosSqlLogin -ConnectionString $connectionString -LoginName 'domain\username' -IfNotExists
 
Replaces New-SqlLogin
#>

function New-DosSqlLogin {
    [cmdletbinding(SupportsShouldProcess=$true)]
    [OutputType()]
    param(
        [parameter(Mandatory=$true,ParameterSetName='Connection')][Data.SqlClient.SqlConnection]$SqlConnection,
        [parameter(Mandatory=$true,ParameterSetName='ConnectionString')][string]$ConnectionString,
        [parameter(Mandatory=$true,ParameterSetName='AdHoc')][string]$InstanceName,
        [parameter(Mandatory=$false,ParameterSetName='AdHoc')][PSCredential]$Credential,
        [parameter(Mandatory=$true)][string]$LoginName,
        [parameter(Mandatory=$false)][ValidateSet("Windows", "SQL")][string]$AuthType='Windows',
        [parameter(Mandatory=$false)][pscredential]$NewLoginCredential,
        [parameter(Mandatory=$false)][switch]$IfNotExists
    )

    if($AuthType -eq 'SQL' -and $null -eq $Credential){
        throw [System.Management.Automation.ParameterBindingException] "Credential must be provided when using an AuthType of SQL"
    }
    elseif($AuthType -eq 'Windows' -and $null -ne $Credential){
        throw [System.Management.Automation.ParameterBindingException] "Credential cannot be provided when using an AuthType of Windows"
    }
    $query = ""
    if($IfNotExists){
        $query += "IF NOT EXISTS (SELECT * FROM [sys].[server_principals] WHERE name = '$LoginName')`n"
    }

    switch($AuthType){
        'Windows'{
            $query += "CREATE LOGIN [$LoginName] FROM WINDOWS WITH DEFAULT_DATABASE=[master]"
        }
        'SQL'{
            $query += "CREATE LOGIN [$LoginName] WITH PASSWORD=N'$($Credential.GetNetworkCredential().Password.Replace("'","''"))', DEFAULT_DATABASE=[master], CHECK_EXPIRATION=ON, CHECK_POLICY=ON"
        }

    }

    $PSBoundParameters.Remove('ErrorAction') | Out-Null
    if($PSCmdlet.ShouldProcess($Query,"Creating login: $LoginName")){
        Invoke-DosSqlQuery @PSBoundParameters -Query $query -NonQuery | Out-Null
    }
}


<#
.SYNOPSIS
Creates SQL database user
 
.DESCRIPTION
Creates an SQL database user in a given database
 
.PARAMETER InstanceName
Full instance name of SQL server
 
.PARAMETER ConnectionString
Connection string to a SQL instnace
 
.PARAMETER SqlConnection
SQL connection to target SQL Server
 
.PARAMETER DatabaseName
Database name in which to create the user
 
.PARAMETER UserName
The full name of the user to create
 
.PARAMETER IfNotExists
A switch to only create if user does not exist
 
.INPUTS
None
 
.OUTPUTS
None
 
.EXAMPLE
PS> New-DosSqlUser -ConnectionString $connectionString -UserName 'domain\username' -IfNotExists
 
Replaces New-SqlUser
#>

function New-DosSqlUser {
    [cmdletbinding(SupportsShouldProcess=$true)]
    [OutputType()]
    param(
        [parameter(Mandatory=$true,ParameterSetName='Connection')][Data.SqlClient.SqlConnection]$SqlConnection,
        [parameter(Mandatory=$true,ParameterSetName='ConnectionString')][string]$ConnectionString,
        [parameter(Mandatory=$true,ParameterSetName='AdHoc')][string]$InstanceName,
        [parameter(Mandatory=$true,ParameterSetName='AdHoc')][string]$DatabaseName,
        [parameter(Mandatory=$false,ParameterSetName='AdHoc')][PSCredential]$Credential,
        [parameter(Mandatory=$true)][string]$UserName,
        [parameter(Mandatory=$false)][switch]$IfNotExists
    )

    $query = ""
    if($IfNotExists){
        $query += "IF NOT EXISTS (SELECT * FROM sys.database_principals WHERE name = N'$UserName')`n"
    }

    $query += "CREATE USER [$UserName] FOR LOGIN [$UserName] WITH DEFAULT_SCHEMA=[dbo]"

    $PSBoundParameters.Remove('ErrorAction') | Out-Null
    if($PSCmdlet.ShouldProcess($Query,"Creating user: $UserName")){
        Invoke-DosSqlQuery @PSBoundParameters -Query $query -NonQuery | Out-Null
    }
}


<#
    .SYNOPSIS
 
    Publishes the target DOS dac file to the specified database with the specified options
 
    .DESCRIPTION
 
    Uses Microsoft.SqlServer.Dac.DacServices to install DAC file to specified server.
 
    .PARAMETER DacPacFilePath
 
    File path to dacpac file to publish to server
 
    .PARAMETER TargetSqlInstance
 
    Sql server connection string supports specifiying non-default sql instance and port if needed
 
    .PARAMETER TargetDb
 
    Target database to publish dac pac to.
 
    .PARAMETER PublishOptionsFilePath
 
    Path to publish options file - Required - See tests/SampleFiles/DefaultDacDeployOptions.xml for an example
 
    .PARAMETER ForceMountPointCreation
 
    Will attempt to create the mount points specified in PublishOptionsFilePath if the folders don't exist. If the specified on an upgrade, a warning will be displayed stating that mount points will be whatever the current DB has set.
 
    .EXAMPLE
 
    Publish-DosDacPac -DacPacFilePath ".\test.dac" -TargetSqlInstance "localhost" -TargetDb "EDWAdmin" -PublishOptionsFilePath ".\test.publish.xml"
 
    Publish-DosDacPac -DacPacFilePath ".\test.dac" -TargetSqlInstance "localhost,1433\MSSQLServer" -TargetDb "EDWAdmin" -PublishOptionsFilePath ".\test.publish.xml"
#>

function Publish-DosDacPac {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [ValidateScript({
            if (!(Test-Path $_)) {
                Write-DosMessage -Level "Error" -Message "DacPacFilePath $_ does not exist. Please enter valid path." -ErrorAction Stop
            }
            else {
                $true
            }
        })]
        [string] $DacPacFilePath,
        [Parameter(Mandatory=$true)]
        [string] $TargetSqlInstance,
        [Parameter(Mandatory=$true)]
        [string] $TargetDb,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [ValidateScript({
            if (!(Test-Path $_)) {
                Write-DosMessage -Level "Error" -Message "PublishOptionsFilePath $_ does not exist. Please enter valid path." -ErrorAction Stop
            }
            else {
                $true
            }
        })]
        [string] $PublishOptionsFilePath,
        [switch] $ForceMountPointCreation,
        [string] $MountPointComputerName = $TargetSqlInstance
    )
    Write-DosMessage -Level "Information" -Message "Publish-DosDacPac start"
    Write-DosMessage -Level "Information" -Message "Publishing '$DacPacFilePath' to '$TargetDb'"
    $dbatoolsVersion = New-Object -TypeName System.Version -ArgumentList "1.0.115"

    Write-DosMessage -Level "Information" -Message "Attempting to install dbatools with a version of '$dbatoolsVersion'"
    Write-DosMessage -Level "Information" -Message "Checking for required modules: dbatools '$dbatoolsVersion'"
    Install-RequiredModule -ModuleName dbatools -RequiredVersion $dbatoolsVersion
    Write-DosMessage -Level "Information" -Message "Successfully installed dbatools."

    $previousDataBase = Get-DosDbaDatabase -SqlInstance $TargetSqlInstance -Database $TargetDb

    $previousRecoveryModel = $null
    [string] $previousDbOwner = $null
    if($null -ne $previousDataBase){
        Write-DosMessage -Level "Information" -Message "Found existing DB $TargetDb on $TargetSqlInstance, will ensure that previous owner and recovery model are preserved across update"
        $previousDbOwner = $previousDataBase.Owner
        $previousRecoveryModel = $previousDataBase.RecoveryModel

        if($ForceMountPointCreation){
            Write-DosMessage -Level "Warning" -Message "Previous database already installed, existing mount points will be used"
        }

    }
    else{
        [xml] $parsedPublishOptions = [xml] (Get-Content $PublishOptionsFilePath -ErrorAction Stop)

        $sqlCmdVariable = $parsedPublishOptions.Project.ItemGroup.SqlCmdVariable
        
        try {
            Write-DosMessage -Level "Information" -Message "Beginning attempt to create database mount points"

            $dataMountPoints = $sqlCmdVariable | Where-Object {$_.Include -like "*Data*MountPoint" -or $_.Include -like "PrimaryMountPoint"}

            if($null -eq $dataMountPoints) {
                Write-DosMessage -Level "Error" -Message "Missing data mount point in $PublishOptionsFilePath"
                return
            }

            foreach($dataMountPoint in $dataMountPoints) {
                Write-DosMessage -Level "Information" -Message "Beginning attempt to create mount point $dataMountPoint"
                Add-MountPoint -Path $dataMountPoint.Value -CreateIfForced $ForceMountPointCreation.IsPresent -MountPointComputerName $MountPointComputerName
            }

            foreach($logMountPoint in $sqlCmdVariable | Where-Object {$_.Include -like "*Log*MountPoint"}) {
                Write-DosMessage -Level "Information" -Message "Beginning attempt to create mount point $logMountPoint"
                Add-MountPoint -Path $logMountPoint.Value -CreateIfForced $ForceMountPointCreation.IsPresent -MountPointComputerName $MountPointComputerName
            }
            
            foreach($indexMountPoint in $sqlCmdVariable | Where-Object {$_.Include -like "*Index*MountPoint"}) {
                Write-DosMessage -Level "Information" -Message "Beginning attempt to create mount point $indexMountPoint"
                Add-MountPoint -Path $indexMountPoint.Value -CreateIfForced $ForceMountPointCreation.IsPresent -MountPointComputerName $MountPointComputerName
            }
            
            Write-DosMessage -Level "Information" -Message "Finished attempt to create database mount points - Success"
        }
        catch {
            Write-DosMessage -Level "Fatal" -Message "Error occured while attempting to confirm '$TargetDb' mount points. Validate the connection capabilites of '$MountPointComputerName'. Exception: $($_.Exception)"
            Write-DosMessage -Level "Information" -Message "Finished attempt to create database mount points - Failure"
        }
        
    }

    try{
        $outputPath = "$(Get-DosBaseInstallerPath)\SetupContent\"
    } catch {
        Write-DosMessage -Level 'Warning' -Message "Unable to find SetupContent folder, Publish-DbaDacPackage will record output to $($env:Temp)"
        $outputPath = $env:TEMP
    }

    Write-DosMessage -Level 'Information' -Message "DacPac Deploy Log Folder: $outputPath"

    try {
        Write-DosMessage -Level Information -Message "Beginning Dacpac deployment"
        
        $dacpacReport = Publish-DbaDacPackage -SqlInstance $TargetSqlInstance -Database $TargetDb -Path $DacPacFilePath -PublishXml $PublishOptionsFilePath -EnableException -GenerateDeploymentScript -OutputPath $outputPath
        Write-DosMessage -Level Information -Message "$dacpacReport"
        
        Write-DosMessage -Level Information -Message "Finished Dacpac deployment"
        $currentDatabase = Get-DosDbaDatabase -SqlInstance $TargetSqlInstance -Database $TargetDb

        if($null -ne $previousDataBase){
            Write-DosMessage -Level Information -Message "Checking that Recovery Model and DbOwner settings are preserved on the database"
            if($currentDatabase.RecoveryModel -ne $previousRecoveryModel){
                Write-DosMessage -Level "Information" -Message "New recovery model $($currentDatabase.RecoveryModel) doesn't match previous recovery model $previousRecoveryModel, reverting to previous recovery model"
                Set-DbaDbRecoveryModel -RecoveryModel $previousRecoveryModel.ToString() -SqlInstance $TargetSqlInstance -Database $TargetDb -Confirm:$false -EnableException
            }
            if($currentDatabase.Owner -ne $previousDbOwner){
                Write-DosMessage -Level "Information" -Message "New DB owner $($currentDatabase.Owner) doesn't match previous owner, reverting to old owner $previousDbOwner"
                Set-DbaDbOwner -SqlInstance $TargetSqlInstance -Database $TargetDb -TargetLogin $previousDbOwner -Confirm:$false -EnableException
            }
        }
    }
    catch {
        Write-DosMessage -Level "Error" -Message "Unable to deploy $DacPacFilePath to $TargetDb on $TargetSqlInstance. Exception: $($_.Exception)"
        Write-DosMessage -Level "Information" "Finished Dacpac deployment - failure"
    }

    Write-DosMessage -Level "Information" -Message "Publish-DosDacPac complete"
    Write-DosMessage -Level "Information" -Message "Publishing '$DacPacFilePath' to '$TargetDb' completed"
}


function Add-MountPoint{
    [CmdletBinding()]
    param(
        [string] $Path,
        [bool] $CreateIfForced,
        [string] $MountPointComputerName
    )
    
    $HostcomputerName = hostname

    $scriptBlock = {
        $CreateIfForced = $args[1]
        $Path = $args[0]
        if($CreateIfForced){
            if(!(Test-Path $Path)){
                try {
                    New-Item -ItemType Directory $Path | Out-Null
                    return "Mount point $Path created"
                }
                catch {
                    return "Error creating mount point $Path. Exception: $($_.Exception)"
                }
                
            }
        }
        else{
            if(!(Test-Path $Path)){
                return "Mount point $Path not found use -ForceMountPointCreation to enable creation of necessary folders"
            }
        }
    }

    Test-ElevatedPermission
    Write-DosMessage -Level "Information" -Message "Validating $Path Mount Point on $MountPointComputerName"
    
    $params = @{
        ScriptBlock = $scriptBlock
        ArgumentList = @($Path, $CreateIfForced)
    }


    $MountPointHostname = $MountPointComputerName.split('.')[0]
    if($MountPointHostname -match "^\d+$"){
        #if the $MountPointHostname is a number (e.g. because it is actually an ip address), just use the full computer name
        $MountPointHostname = $MountPointComputerName
    }

    if($MountPointHostname -eq 'localhost' -or $MountPointHostname -eq '127.0.0.1'){
        #don't add computer name to the parameters
        Write-DosMessage -Level "Debug" -Message "It appears that '$MountPointComputerName' is localhost, so we run the Invoke-Command locally"
    } elseif($MountPointHostname -eq $HostcomputerName) {
        #don't add computer namme to the parameters
        Write-DosMessage -Level "Debug" -Message "It appears that '$HostcomputerName' is the same as '$MountPointComputerName', so we run the Invoke-Command locally"
    } else {
        try{
            $IpAddressMountPointHostname = $(Test-NetConnection -computername $MountPointComputerName -ErrorAction SilentlyContinue -WarningAction SilentlyContinue).RemoteAddress.IPAddressToString
            $IpAddressLocalhost = $(Test-NetConnection -computername $HostcomputerName -ErrorAction SilentlyContinue -WarningAction SilentlyContinue).RemoteAddress.IPAddressToString
            if($IpAddressMountPointHostname -eq $IpAddressLocalhost){
                #don't add computer name to the parameters
                Write-DosMessage -Level "Debug" -Message "It appears that '$IpAddressMountPointHostname' is the same as '$IpAddressLocalhost', so we run the Invoke-Command locally"
            } else {
                Write-DosMessage -Level "Debug" -Message "It appears that '$HostcomputerName' is a different computer than '$MountPointComputerName', so we'll use PS Remoting"
                $params.Add("ComputerName", $MountPointComputerName)
            }
        } catch {
            Write-DosMessage -Level "Debug" -Message "It appears that '$HostcomputerName' is a different computer than '$MountPointComputerName', so we'll use PS Remoting."
            $params.Add("ComputerName", $MountPointComputerName)
        }

    }
    
    Write-DosMessage -Level "Information" -Message "Running invoke-command"
    $addMountResult = Invoke-Command @params

    if ($addMountResult -like "*use -ForceMountPointCreation*" -or $addMountResult -like "Error creating mount point*") {
        Write-DosMessage -Level "Error" -Message $addMountResult
    }
    if ($addMountResult -eq "Mount point $Path created") {
        Write-DosMessage -Level "Information" -Message $addMountResult
    }
}


<#
    .SYNOPSIS
 
    Publishes the target DOS web application with the specified options
 
    .DESCRIPTION
 
    Publishes the target DOS web application with the specified options. Either uses WebDeploy (for older apps), or internal logic for new .Net Core applicaitons.
 
    .PARAMETER WebAppPackagePath
 
    File path to web application zip to publish
 
    .PARAMETER SettingsXmlPath
 
    ONLY USED in WebDeploy Applications - File path to xml settings
 
    .PARAMETER AppPoolName
 
    IIS Application Pool name
 
    .PARAMETER AppPoolCredential
 
    Credential object used to configure the built-in account the IIS Application Pool runs as
     
    .PARAMETER AuthenticationType
 
    ONLY USED in WebDeploy Applications - Windows or Anonymous authentication
 
    .PARAMETER WebDeploy
 
    A toggle for the web application to be deployed via WebDeploy or by other means
 
    .PARAMETER AppName
 
    ONLY USED in NON-Webdeploy applications. Specifies both the application's name AND the folder name where the application will be placed underneath the IIS site's root folder.
 
    .PARAMETER IISWebSite
 
    ONLY USED in NON-WebDeploy applications. Specifies the IIS site to publish the application to. Defaults to "Default Web Site"
     
    .PARAMETER WebDeployParameters
     
    ONLY USED in WebDeploy Applications - Arraylist object containing site settings
     
    .PARAMETER PathsToPreserve
     
    Array of paths to preserve during a deployment, such as logs, relative to the install directory in IIS, so they are not removed during the upgrade of an application. Ignored for new installs.
     
    .EXAMPLE
 
    Publish-DosWebApplication -WebAppPackagePath "testapp.zip" -SettingsXmlPath "testapp.settings.xml" -AppPoolName "x" -AppPoolCredential $creds -AuthenticationType "Windows" -WebDeploy -WebDeployParameters $webDeployParams -PathsToPreserve @("logs")
#>

function Publish-DosWebApplication {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [ValidateScript({
            if (!(Test-Path $_)) {
                Write-DosMessage -Level "Error" -Message "$_ does not exist. Please enter valid path." -ErrorAction Stop
            }
            else {
                $true
            }
        })]
        [string] $WebAppPackagePath,
        [string] $SettingsXmlPath,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [ValidateLength(1,64)]
        [ValidateScript({
            if ($_ -match '[^a-zA-Z0-9]') {
                Write-DosMessage -Level "Error" -Message "$_ must only contain alphanumeric values. Please remove special characters." -ErrorAction Stop
            }
            else {
                $true
            }
        })]
        [string] $AppPoolName,
        [PSCredential] $AppPoolCredential,
        [ValidateSet("Windows", "Anonymous")]
        [string[]] $AuthenticationType,
        [switch] $WebDeploy,
        [string] $AppName,
        [string] $IISWebSite = "Default Web Site",
        [System.Collections.ArrayList] $WebDeployParameters,
        [switch] $NoCredential,
        [string[]] $PathsToPreserve = @()
    )

    Test-ElevatedPermission
    Import-Module WebAdministration -Force
    
    if ($PathsToPreserve.Length -gt 0) {
        for ($i = 0; $i -lt $PathsToPreserve.Length; $i++) {
            $PathsToPreserve[$i] = $PathsToPreserve[$i].Trim("\", "/", " ")
        }
    }

    ###Only allow one source of input for webdeploy args

    try {
        if ($NoCredential.IsPresent) {
            Write-DosMessage -Level "Information" -Message "The NoCredential parameter was provided. Proceeding to application pool validation."
    
            if(!(Test-Path "IIS:\AppPools\$AppPoolName" -PathType Container)){
                New-AppPool -IISAppPoolName $AppPoolName
            }
            else {
                Set-AppPoolSettings -IISAppPoolName $AppPoolName -NoCredential $NoCredential.IsPresent
            }
        }
        else {
            if(!(Test-Path "IIS:\AppPools\$AppPoolName" -PathType Container)){
                if($AppPoolCredential -eq $null){
                    Write-DosMessage -Level "Fatal" -Message "No app pool found named $AppPoolName and no credentials specified. Please specify credentials -AppPoolCredential if you want to create a new application pool"
                    return
                }
                
                New-AppPool -IISAppPoolName $AppPoolName -IdentityCredential $AppPoolCredential
            }
            else {
                $existingAppPool = Get-Item -Path "IIS:\AppPools\$AppPoolName"

                # if app pool exists and credential is passed in / checks if the existing credential is the same / if it differs we fail out
                if ($null -ne $AppPoolCredential) {
                    if ($existingAppPool.processModel.userName -ne $AppPoolCredential.UserName -or $existingAppPool.processModel.password -ne $AppPoolCredential.GetNetworkCredential().Password) {
                        Write-DosMessage -Level "Fatal" -Message "The '$AppPoolName' app pool has an identity configured that differs from the identity credential provided. Halting deployment."
                    }
                }

                Set-AppPoolSettings -IISAppPoolName $AppPoolName -IdentityCredential $AppPoolCredential
                Write-DosMessage -Level "Information" -Message "Application Pool: $AppPoolName already exists. Deploying '$WebAppPackagePath' to $AppPoolName"
            }
        }
        
        $appPool = Get-Item "IIS:\AppPools\$AppPoolName"
        $appPool.Start()
    }
    catch {
        Write-DosMessage -Level "Error" -Message "Error occured getting, creating, or updating a IIS application pool. Exception: $($_.Exception)"
        return
    }

    #Deploy Website to IIS
    if ($WebDeploy.IsPresent){

        if([String]::IsNullOrEmpty($SettingsXmlPath) -and ($null -eq $WebDeployParameters)){
            Write-DosMessage -Level "Error" -Message "Must provide parameters through a settings xml file or webdeployparameters when deploying a web deploy application"
            return
        }

        if($SettingsXmlPath) {
            try {
                [xml] $parsedXml = Get-Content $SettingsXmlPath
            }
            catch {
                Write-DosMessage -Level "Error" -Message "Error occured parsing xml settings file. Exception: $($_.Exception)"
                return
            }
        }

        try {
            Write-DosMessage -Level "Information" -Message "Attempting to retrieve the IIS web application information."
            $siteName,$appNameFromSettings = Get-IisWebAppInfo -SettingsXmlPath $SettingsXmlPath -WebDeployParameters $WebDeployParameters -ParsedXml $parsedXml
            Write-DosMessage -Level "Information" -Message "Successfully retrieved the IIS web application information."
            Write-DosMessage -Level "Information" -Message "Deploying '$appNameFromSettings' through WebDeploy."
        }
        catch {
            Write-DosMessage -Level "Error" -Message "Unable to find node containing IIS Web Application Name values"
            return
        }
        
        Publish-WebDeployWebApp -WebDeployPackageFilePath $WebAppPackagePath -WebDeployParameterFilePath $SettingsXmlPath -WebParameters $WebDeployParameters -AppName $appNameFromSettings -IISWebSite $IISWebSite -AppPoolName $AppPoolName -PathsToPreserve $PathsToPreserve

        Set-ApplicationPool -SiteName $siteName -AppName $appNameFromSettings -AppPoolName $AppPoolName
        Set-AuthenticationType -SiteName $siteName -AppName $appNameFromSettings -AuthenticationType $AuthenticationType

        Write-DosTelemetry -Message "Publish-DosWebApplication called and published using Publish-WebDeployWebApp."
    }
    else {
        if([string]::IsNullOrEmpty($AppName)){
            Write-DosMessage -Level "Fatal" -Message "AppName must be non-null and non empty"
        }

        Publish-DotNetCoreWebApp -WebApplicationPackagePath $WebAppPackagePath -AppName $AppName -IISWebSite $IISWebSite -AppPoolName $AppPoolName -PathsToPreserve $PathsToPreserve
        
        Set-ApplicationPool -SiteName $IISWebSite -AppName $AppName -AppPoolName $AppPoolName

        if (-Not ([string]::IsNullOrEmpty($AuthenticationType))) {
            Install-UrlRewrite
            Set-AuthenticationType -SiteName $IISWebSite -AppName $AppName -AuthenticationType $AuthenticationType
        }

        Write-DosTelemetry -Message "Publish-DosWebApplication called and published using Publish-DotNetCoreWebApp."
    }
}

function Get-IisWebAppInfo {
    param (
        [string] $SettingsXmlPath,
        [System.Collections.ArrayList] $WebDeployParameters,
        [xml] $ParsedXml
    )

    if($SettingsXmlPath){
        # Parameter childnodes can be different
        $iisParameter = $ParsedXml.parameters.ChildNodes | Where-Object { $_.name -eq "IIS Web Application Name" }

        # Parameter attribute names differ (eg. defaultValue and value)
        $iisAppPath = $iisParameter.Attributes | Where-Object { $_ -like "*value" }
        $siteName,$appNameFromSettings = $iisAppPath.'#text'.split('/')
    }else{
        # Parameter attribute names differ (eg. defaultValue and value)
        foreach ($param in $WebDeployParameters)
        {
            if($param.Name -eq "IIS Web Application Name"){
                $iisAppPath = $param.Value
            }
        }
        $siteName,$appNameFromSettings = $iisAppPath.split('/')
    }

    return $siteName,$appNameFromSettings
}

function Set-ApplicationPool {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")]
    param (
        [string] $SiteName,
        [string] $AppName,
        [string] $AppPoolName
    )

    try {
        # Set-Application Pool with specific app
        Push-Location -Path IIS:\Sites\$SiteName\
        Set-ItemProperty -Path $AppName -Name applicationPool -Value $AppPoolName
    }
    catch {
        Write-DosMessage -Level "Error" -Message "Error occured associating application to the app pool. Exception: $($_.Exception)"
        return
    }
    finally {
        Pop-Location
    }
}

function Set-AuthenticationType {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")]
    param (
        [string] $SiteName,
        [string] $AppName,
        [string[]] $AuthenticationType
    )

    try{
        Push-Location -Path IIS:\Sites\$SiteName\$AppName
        # Set-Authentication and transform the web config
        Set-IISAuthentication -AuthenticationType $AuthenticationType -SiteName $SiteName -ApplicationName $AppName
    }
    catch {
        Write-DosMessage -Level "Error" -Message "Error occured altering authentication types. Exception: $($_.Exception)"
        return
    }
    finally {
        Pop-Location
    }
}

function Install-UrlRewrite {
    # Check if URL Rewrite is installed before setting authentication
    $urlRewriteRegistry = "HKLM:\SOFTWARE\Microsoft\IIS Extensions\URL Rewrite"

    if (-Not (Test-Path $urlRewriteRegistry)) {
        Write-DosMessage -Level "Information" -Message "UrlRewrite not installed"

        # Install Web Platform Installer if not present
        if (-Not (Test-Path "$($env:ProgramFiles)\Microsoft\Web Platform Installer")) {
            Write-DosMessage -Level "Information" -Message "Web Platform Installer not found"
            Get-WebRequestDownload "https://go.microsoft.com/fwlink/?LinkId=287166" -OutFile "$PSScriptRoot\Web-Platform-Install.msi"
            Write-DosMessage -Level "Information" -Message "Installing Web Platform Installer"
            Start-Process "$PSScriptRoot\Web-Platform-Install.msi" '/qn' -PassThru | Wait-Process
            Remove-Item "$PSScriptRoot\Web-Platform-Install.msi"
        }

        # Install UrlRewrite using Web Platform Installer
        if (Test-Path "$($env:ProgramFiles)\Microsoft\Web Platform Installer\WebpiCmd.exe") {
            Write-DosMessage -Level "Information" -Message "Installing UrlRewrite"
            Start-Process "$($env:ProgramFiles)\Microsoft\Web Platform Installer\WebpiCmd.exe" "/Install /Products:'UrlRewrite2' /AcceptEULA" -PassThru | Wait-Process

            if (-Not (Test-Path $urlRewriteRegistry)) {
                Write-DosMessage -Level "Warning" -Message "UrlRewrite did not install correctly"
            }
        }
        else {
            Write-DosMessage -Level "Warning" -Message "Unable to install UrlRewrite, WebpiCmd.exe not found at $($env:ProgramFiles)\Microsoft\Web Platform Installer\WebpiCmd.exe"
        }
    }
}


<#
    .SYNOPSIS
 
    Pushes a new "_type_" property to any hashtable, which stores the key name and "type" provided as a parameter
 
    .DESCRIPTION
 
    When working with DOS configuration values that come from varying places (some common examples may be a config store, pipeline, or static) it is
    nice to be able to merge these configurations into a single $config hashtable. However, when we merge hashtables together, we lose the information
    about where they came from or what type of configuration it is.
 
    Push-DosConfigType allows you to provide a "type" parameter which then stores the key names and type into a new property called "_type_". This "_type_"
    property is then used by the Confirm-DosConfiguration method when generating meaningful messages about issues with configurations, due to
    the added information about what type of configuration encountered a problem.
 
    .OUTPUTS
 
    Name Value
    ---- -----
    clientName Health Catalyst
    appName Test App
    clientEnvironment Internal
    appPoolName Test App Pool
    _type_ {clientName, appName, clientEnvironment, appPoolName}
 
    .EXAMPLE
 
    $storeConfig = @{
        clientName = 'Health Catalyst'
        clientEnvironment = 'Internal'
        appName = 'Test App'
        appPoolName = 'Test App Pool'
    } | Push-DosConfigType "store"
 
    $staticConfig = @{
        ServiceName = 'Test Service'
        PathToDatabaseQuery = 'some path'
    } | Push-DosConfigType "static"
#>

function Push-DosConfigType {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true, Mandatory = $true)]
        [Hashtable] $hash,
        [Parameter(Position = 0, Mandatory = $true)]
        [String] $type
    )
    $newHash = @{};
    foreach ($key in $hash.keys) {
        $newHash.Add($key, $type)
    }
    $hash._type_ = $newHash
    return $hash
}



<#
    .SYNOPSIS
    Removes an installation scope/section from the provided config store
     
    .DESCRIPTION
    Removes an installation scope/section from the provided config store
     
    .PARAMETER configSection
    Config scope that the contains the configSetting that will be removed
 
    .PARAMETER ConfigStore
    Accepts a powershell hashtable or dictionary with key value pairs following the example, $configStore = @{Type = "File"; Format = "XML"; Path = "C:\Path\To\File"}.
    Current iteration only supports XML Files.
         
    .EXAMPLE
    Remove-DosConfigSection -configSection "common" -configStore $configStore
     
    .NOTES
    General notes
#>


function Remove-DosConfigSection {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [String] $configSection,
        [Parameter(Mandatory=$true)]
        [hashtable] $configStore
    )
    
    if(!(Confirm-ConfigStore -ConfigStore $configStore)) {
        Write-DosMessage -Level Fatal -Message "configStore is invalid (Type: $($configstore.Type); Format: $($configstore.Format); Path: $($configstore.Path)"
        return
    }

    if(-not($configStore.Format -eq 'XML' -and $configStore.Type -eq 'File')){
        Write-DosMessage -Level Fatal -Message "Remove-DosConfigSection is not implemented for this type of configstore (Type: $($configstore.Type); Format: $($configstore.Format); Path: $($configstore.Path)"
        return
    }
    
    if ($configStore.Type -eq "External") {
        $configObject = Remove-DosConfigSectionAzureTable -configStore $configStore -configSection $configSection
    }
    
    if ($configStore.Type -eq "File") {
        $configObject = Remove-DosConfigSectionXml -configStore $configStore -configSection $configSection
    }
    
    
    if($configObject){
        if($PSCmdlet.ShouldProcess("Delete '$configSection' scope in install config")){
            Write-DosMessage -Level Information -Message "Removing $configSection from $($configStore.Type) configstore."
            Save-DosConfigStore -configStoreObject $configObject -configStore $configStore
        }
    } else {
        Write-DosMessage -Level 'Warning' -Message "No changes made to $($configStore.Type) configstore for the $configSection scope"
    }
}




<#
    .SYNOPSIS
    Removes an installation variable from the provided config scope
     
    .DESCRIPTION
    Removes an installation variable and value from the provided config scope
     
    .PARAMETER configSection
    Config scope that the contains the configSetting that will be removed
     
    .PARAMETER configSetting
    Variable that will be removed
     
    .PARAMETER ConfigStore
    Accepts a powershell hashtable or dictionary with key value pairs following the example, $configStore = @{Type = "File"; Format = "XML"; Path = "C:\Path\To\File"}.
    Current iteration only supports XML Files.
     
    .EXAMPLE
    Remove-DosConfigValue -InstallConfigPath $path -configSection "common" -configSetting "sqlServerAddress"
     
    .NOTES
    General notes
#>


function Remove-DosConfigValue {
    
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [Alias("Scope", "ConfigScope")]
        [String] $configSection,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [Alias("ConfigVariable")]
        [String] $configSetting,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [hashtable] $configStore
    )
    
    if(!(Confirm-ConfigStore -ConfigStore $configStore)) {
        Write-DosMessage -Level "Fatal" -Message "ConfigStore is invalid"
        return
    }

    if ($configStore.Type -eq "External") {
        $configObject = Remove-DosConfigValueAzureTable -configStore $configStore -configSection $configSection -configSetting $configSetting
    }
    
    if ($configStore.Type -eq "File") {
        $configObject = Remove-DosConfigValueXml -configStore $configStore -configSection $configSection -configSetting $configSetting
    }
    
    if($configObject){
        if($PSCmdlet.ShouldProcess("Delete $configSection.$configSetting in install config")){
            Write-DosMessage -Level "Debug" -Message "Removing $configSection.$configSetting from $($configStore.Type) configstore."
            Save-DosConfigStore -configStoreObject $configObject -configStore $configStore
        }
    } else {
        Write-DosMessage -Level "Warning" -Message "No changes made to $($configStore.Type) configstore for $configSection.$configSetting."
    }
}



function Remove-DosWebApplication {
    <#
        .SYNOPSIS
        Remove a DOS web application from IIS
 
        .DESCRIPTION
        Removes a given DOS web application, physical directory, and application pool
        if no other applications are associated with it.
 
        .PARAMETER ApplicationName
        The name of the DOS Web application in IIS.
 
        .PARAMETER IISWebSite
        Specifies the IIS site from which to remove the application. Defaults to "Default Web Site"
 
        .EXAMPLE
        Remove-DosWebApplication -ApplicationName "Atlas4" -IISWebSite "Default Web Site"
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")]
    Param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $ApplicationName,
        [string] $IISWebSite = "Default Web Site"
    )
    Import-Module WebAdministration -Force

    $iisPath = "IIS:\Sites\$($IISWebSite)\$($ApplicationName)"
    $webApp = Get-WebApplication -Name $ApplicationName -Site $IISWebSite

    if ($webApp) {
        $webApplicationFolder = Get-WebFilePath -PSPath $iisPath
        Write-DosMessage -Level "Information" -Message "Removing web application '$iisPath'"
        Remove-WebApplication -Name $ApplicationName -Site $IISWebSite
        Write-DosMessage -Level "Information" -Message "Removing folder '$webApplicationFolder'"
        Remove-Item -Path $webApplicationFolder -Recurse

        # Remove app pool if application count is zero
        $appPoolName = $webApp.applicationPool
        $appCount = (Get-WebConfigurationProperty "/system.applicationHost/sites/site/application[@applicationPool='$appPoolName']" "machine/webroot/apphost" -name path).Count
        if ($appCount -eq 0) {
            Write-DosMessage -Level "Information" -Message "Removing application pool '$appPoolName'"
            Remove-WebAppPool -Name $appPoolName
        }
        else {
            Write-DosMessage -Level "Warning" -Message "Application pool '$appPoolName' was not removed since other applications are currently bound to it."
        }
    }
}



function Remove-IISUrlRewriteRule {
    <#
        .SYNOPSIS
        Removes URL rewrite rule
 
        .DESCRIPTION
        Removes a given URL rewrite rule from IIS.
 
        .PARAMETER RuleName
        The unique name of the rule to remove.
 
        .PARAMETER IISWebSite
        The IIS site from which to remove the application. Defaults to "Default Web Site"
 
        .EXAMPLE
        Remove-IISUrlRewriteRule -RuleName "Atlas4-Atlas-Redirect" -IISWebSite "Default Web Site"
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")]

    Param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $RuleName,
        [string] $IISWebSite = "Default Web Site"
    )
    Import-Module WebAdministration -Force
    $iisPath = "IIS:\Sites\$($IISWebSite)"
    $ruleFilter = "/system.webserver/rewrite/rules/rule[@name='$RuleName']"

    $exists = Get-WebConfigurationProperty -PSPath $iisPath -Filter $ruleFilter -Name *
    if ($exists) {
        Write-DosMessage -Level "Information" -Message "Removing URL Rewrite rule '$RuleName'."
        Clear-WebConfiguration -PSPath $iisPath -Filter $ruleFilter
    }
}



<#
    .SYNOPSIS
    Saves the config store
     
    .DESCRIPTION
    Saves the config store (currently only works for XML file types)
     
    .PARAMETER configStoreObject
    The object that represents the configuration settings that need to be saved.
 
    .EXAMPLE
    Save-DosConfigStore -configStoreObject $installConfigXml -configStore @{Type = "File"; Format = "XML"; Path = "install.config"}
    Save-DosConfigStore -configStoreObject $installConfigXml -configStore $configStore
     
    .NOTES
    General notes
#>

function Save-DosConfigStore {
    
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        $configStoreObject
        ,[Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [hashtable] $configStore
    )

    if($configStore.Type -eq "File" -and $configStore.Format -eq "XML"){
        Save-DosConfigStoreXml -installConfigXml $configStoreObject -pathToInstallConfig $configStore.Path
    }

    if ($configStore.Type -eq "External") {
        Write-DosMessage -Level "Debug" -Message "External configstores have no execution requirements in Save-DosConfigStore"
    }
}


<#
    .SYNOPSIS
    NON PUBLIC - Saves the xml file that represents the install.config
     
    .DESCRIPTION
    Saves the xml file that represents the install.config
     
    .PARAMETER installConfigXml
    The xml object that represents the install.config
     
    .PARAMETER pathToInstallConfig
    Full file path to the install.config file that will be saved
 
     
    .EXAMPLE
    Save-DosConfigStoreXml -installConfigXml $installConfigXml -pathToInstallConfig 'C:\Program Files\Health Catalyst\install.config'
     
    .NOTES
    General notes
    #>

function Save-DosConfigStoreXml {
    
    param (
        [Parameter(Mandatory=$true)]
        [xml]$installConfigXml,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [String]$pathToInstallConfig
    )
    $installConfigXml.Save("$pathToInstallConfig")
    
}


<#
    .SYNOPSIS
    Adds an installation variable to the provided config scope
     
    .DESCRIPTION
    Adds or updates an installation variable and value to the provided config scope
     
    .PARAMETER configSection
    Config scope that the variable and value will be saved to
     
    .PARAMETER configSetting
    Variable that will be saved
     
    .PARAMETER configValue
    Value of the variable to be saved
     
    .PARAMETER KeepExisting
    Will not overwrite the existing value if present
     
    .PARAMETER ConfigStore
    Accepts a powershell hashtable or dictionary with key value pairs following the example, $configStore = @{Type = "File"; Format = "XML"; Path = "C:\Path\To\File"}.
    Current iteration only supports XML Files.
     
    .EXAMPLE
    Set-DosConfigValue -PathToInstallConfig $path -configSection "common" -configSetting "sqlServerAddress" -configValue $dbFQN -configstore $configstore
    Set-DosConfigValue -PathToInstallConfig $path -configSection "common" -configSetting "sqlServerAddress" -configValue $dbFQN -configstore $configstore -KeepExisting
     
    .NOTES
    General notes
    #>


function Set-DosConfigValue {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [Alias("Scope", "ConfigScope")]
        [String] $configSection,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [Alias("ConfigVariable")]
        [String] $configSetting,
        [Parameter(Mandatory=$true)]
        [AllowEmptyString()]
        [String] $configValue,
        [Switch] $KeepExisting,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [hashtable] $configStore
    )
    
    if(!(Confirm-ConfigStore -ConfigStore $configStore)) {
        Write-DosMessage -Level "Fatal" -Message "ConfigStore is invalid"
        return
    }

    $configObject = $null

    if ($configStore.Type -eq "External") {
        $configObject = Set-DosConfigValueAzureTable -configStore $configStore -configSection $configSection -configSetting $configSetting -configValue $configValue
    }

    if ($configStore.Type -eq "File") {
        $configObject = Set-DosConfigValueXml -configStore $configStore -configSection $configSection -configSetting $configSetting -configValue $configValue -KeepExisting:$KeepExisting
    }

    # Instead of the condition being on if something changed, will detect if the object is returned at all form the specific configstore set functions
    if($configObject){
        if($PSCmdlet.ShouldProcess("Save changes to configuration")){
            Save-DosConfigStore -configStoreObject $configObject -configStore $configStore
            Write-DosMessage -Level "Debug" -Message "Successfully added $configSection.$configSetting=$configValue into $($configStore.Type) configstore."
        }
    }
    else {
        Write-DosMessage -Level "Debug" -Message "No changes made to $($configStore.Type) configstore for $configSection.$configSetting=$configValue."
    }
    
}




<#
    .SYNOPSIS
 
    Sets the the global IIS configuration authentication settings to allow each application to override authentication with it's own configuration settings
 
    .DESCRIPTION
 
    For more reading, see: https://docs.microsoft.com/en-us/iis/get-started/planning-for-security/how-to-use-locking-in-iis-configuration and https://docs.microsoft.com/en-us/iis/configuration/system.webserver/security/authentication/
     
    .EXAMPLE
 
    Set-GlobalIISAuthentication
#>


function Set-DosGlobalIISAuthentication {
    [CmdletBinding(SupportsShouldProcess=$true)]
    param()

    BEGIN {
        Add-Assembly -Assemblies "$env:systemroot\system32\inetsrv\Microsoft.Web.Administration.dll"
    }
    PROCESS{

        Write-DosMessage -Level "Verbose" -Message "Fetching configuration sections"
        $manager = new-object Microsoft.Web.Administration.ServerManager
        $config = $manager.GetApplicationHostConfiguration()
        $section = $config.GetSection("system.webServer/security/authentication/windowsAuthentication")
        $section.OverrideMode = "Allow"

        $section = $config.GetSection("system.webServer/security/authentication/anonymousAuthentication")
        $section.OverrideMode = "Allow" 

        if($PSCmdlet.ShouldProcess("Committing IIS global authentication settings")){
            Write-DosMessage -Level "Verbose" -Message "Committing changes"
            $manager.CommitChanges()  
        }
        Write-DosTelemetry -Message "Set-DosGlobalIISAuthentication called."
    }
}


<#
    .SYNOPSIS
 
    Configure application logger with specified set of parameters..
 
    .DESCRIPTION
 
    Configures and creates a Serilog logger, capable of logging to the console, a file, or both.
 
    .PARAMETER LoggingMode
 
    Logger message output. Valid modes include: Console, File and Both.
 
    .PARAMETER MinimumLoggingLevel
 
    The minimum logging level that will be written to the logger (file and console).
 
    .PARAMETER LogFilePath
 
    Path to logging file.
 
    .EXAMPLE
 
    Set-DosMessageConfiguration -LoggingMode "Both" -LogFilePath "C:\Path\To\log.txt"
#>


function Set-DosMessageConfiguration {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="Logging messages will not change system state.")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "", Justification="We need a global variable to avoid weird scope issues when turning on serilog selflog. It will normally be null")]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateSet("File", "Console", "Both")]
        [string] $LoggingMode,
        [Parameter(Mandatory=$true)]
        [ValidateSet("Verbose", "Debug", "Information", "Warning", "Error", "Fatal")]
        [string] $MinimumLoggingLevel,
        [ValidateScript({
            if (!(Test-Path $_)) {
                try {
                    New-Item $_ -Type File -Force -ErrorAction Stop
                }
                catch {
                    Write-DosMessage -Level "Error" -Message "$_"
                }
                $true
            }
            else {
                $true
            }
        })]
        [string] $LogFilePath
    )
    
    # Parameter Validation added because if $LogFilePath isn't provided the [ValidateScript] will not run
    # Test case. If user tries to configure a file logger without providing a logfilepath. Associated unit test in Set-DosMessageConfiguration.tests.ps1 line 51-59
    if (($LoggingMode -eq "File" -or $LoggingMode -eq "Both") -and [string]::IsNullOrEmpty($LogFilePath)) {
        Write-DosMessage -Level "Error" -Message "You cannot configure a file logger without providing a the LogFilePath parameter."
    } 

    try {
        [SerilogBridge.SerilogBridge]::CreateDosLogger($LoggingMode, $MinimumLoggingLevel, $LogFilePath, $global:serilogSelfLogEnabled)
    }
    catch {
        Write-DosMessage -Level "Error" -Message "Error creating logger. Exception: $($_.Exception)"
    }

    Write-DosTelemetry -Message "Set-DosMessageConfiguration called."
}


<#
    .SYNOPSIS
 
    Configures the telemetry logger with a specific telemetry key (current defaults to DOS Install Application Insights key).
 
    .DESCRIPTION
 
    Pass in the application insights key and a optional opt out parameter.
 
    .PARAMETER TelemetryKey
 
    Currently associate the telemetry logger with an application insights key.
 
    .PARAMETER TelemetryOptOut
 
    Switch parameter, if included will opt out of the telemetry logger.
 
    .EXAMPLE
 
    Set-DosTelemetry -TelemetryKey "testkey" -TelemetryOptOut
#>


function Set-DosTelemetry {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="Logging messages will not change system state.")]
    [CmdletBinding()]
    param(
        [ValidateNotNullorEmpty()]
        [string] $TelemetryKey,
        [switch] $TelemetryOptOut
    )
    
    # Converts to the appropriate boolean type used for the C# serilogbridge
    Write-DosMessage -Level "Verbose" -Message "Converting Powershell boolean into primitive type to be used in Serilog C# class"
    $telemetryConfirmation = [System.Management.Automation.LanguagePrimitives]::ConvertTo($TelemetryOptOut.IsPresent,[System.Type]::GetType($TelemetryOptOut.IsPresent.GetType().FullName))

    try {
        Write-DosMessage -Level "Information" -Message "Creating Telemetry Logger using '$TelemetryKey' key."
        [SerilogBridge.TelemetryBridge]::CreateDosTelemetryLogger($TelemetryKey, $telemetryConfirmation)
    }
    catch {
        Write-DosMessage -Level "Error" -Message "Error creating telemetry logger. Exception: $($_.Exception)"
    }

    Write-DosTelemetry -Message "Set-DosTelemetry called."
}


<#
    .SYNOPSIS
 
    Given a certificate thumbprint and an encrypted value, Unprotect-DosInstallerSecret will return a decrypted secret value.
 
    .DESCRIPTION
 
    Pass in a certificate thumbprint (generally stored in a configuration file) and the encrypted secret value, and Unprotect-DosInstallerSecret will return the decrypted value in string format.
 
    .PARAMETER CertificateThumprint
 
    Certificate Thumbprint of the certificate that will be used for decryption.
 
    .PARAMETER EncryptedInstallerSecretValue
 
    Encrypted value that will be unprotected.
 
    .EXAMPLE
 
    $decryptedValue = Unprotect-DosInstallerSecret -CertificateThumprint $certThumbprint -EncryptedInstallerSecretValue $encryptedSecret
#>


function Unprotect-DosInstallerSecret {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, ParameterSetName = "CertificateSecret")]
        [ValidateNotNullorEmpty()]
        [string] $CertificateThumprint,
        [Parameter(Mandatory=$true, ParameterSetName = "CertificateSecret")]
        [ValidateNotNullorEmpty()]
        [string] $EncryptedInstallerSecretValue
    )

    $secret = ''

    if ($PSCmdlet.ParameterSetName -eq "CertificateSecret") {
        try{
            Write-DosMessage -Level "Debug" -Message "Attempting to retrieve encryption certificate using the certificate thumbprint provided."
            $encryptionCertificate = Get-EncryptionCertificate $CertificateThumprint
            Write-DosMessage -Level "Debug" -Message "Successfully retrieved encryption certificate."
        }
        catch {
            Write-DosMessage -Level "Error" -Message "Could not get encryption certificte with thumbprint $CertificateThumprint. Exception: $($_.Exception)"
        }
    
        try {
            Write-DosMessage -Level "Debug" -Message "Using encryption certificate to decrypt the installer secret provided."

            $encryptedValue = $EncryptedInstallerSecretValue
            if ($encryptedValue.StartsWith("!!enc!!:")) {
                $encryptedValue = $encryptedValue.Replace("!!enc!!:", "")
            }

            $secret = Get-DecryptedString -Certificate $encryptionCertificate -EncryptedValue $encryptedValue
            Write-DosMessage -Level "Debug" -Message "Successfully decrypted the installer secret provided."
        }
        catch {
            Write-DosMessage -Level "Error" -Message "Error attempting to decrypt installer secret. Exception: $($_.Exception)."
        }
    }

    return $secret
}

function Get-EncryptionCertificate {
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $CertificateThumprint
    )

    $localCertPath = "Cert:\LocalMachine\My"

    Write-DosMessage -Level "Verbose" -Message "Cleaning certificate thumbprint for any invalid characters."
    $cleanCertificateThumbprint = $CertificateThumprint -replace '[^a-zA-Z0-9]', ''
    try {
        Write-DosMessage -Level "Debug" -Message "Pulling certificate from $localCertPath."
        $certificate = Get-Item "$localCertPath\$cleanCertificateThumbprint" -ErrorAction Stop
        Write-DosMessage -Level "Debug" -Message "Successfully retrieved certificate from $localCertPath."
    }
    catch {
        Write-DosMessage -Level "Error" -Message "Error retrieving certificate from $localCertPath. Confirm that certificate with $cleanCertificateThumbprint exists on the machine. Exception: $($_.Exception)."
    }

    return $certificate
}

function Get-DecryptedString {
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [X509Certificate] $Certificate,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $EncryptedValue
    )
    
    try {
        Write-DosMessage -Level "Debug" -Message "Decrypting value..."
        $clearTextValue = Get-DecryptedValueDotNet -Certificate $Certificate -EncryptedValue $EncryptedValue
        Write-DosMessage -Level "Debug" -Message "Successfully decrypted installer secret."
    }
    catch {
        Write-DosMessage -Level "Error" -Message "Error decrypting value provided. Please verify that encryption certificate is valid. Exception: $($_.Exception)."
    }
    return $clearTextValue
}

function Get-DecryptedValueDotNet {
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [X509Certificate] $Certificate,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $EncryptedValue
    )

    $rsaEncryptionOid = '1.2.840.113549.1.1.1' # we are currently only using RSA.
    #$dsaEncryptionOid = '1.2.840.10040.4.1' # possible future
    #$eccEncryptionOid = '1.2.840.10045.2.1' # possible future

    if ($Certificate.PublicKey.Oid.Value -eq $rsaEncryptionOid) {
        Write-DosMessage -Level "Debug" -Message "CNG certificate detected."
        try {
            Write-DosMessage -Level "Debug" -Message "Pulling RSA private key from certificate with thumbprint provided."
            $privateKey = Get-CNGRSAPrivateKey -Certificate $Certificate
            Write-DosMessage -Level "Debug" -Message "Successfully retrieved RSA private key from certificate."
        }
        catch {
            Write-DosMessage -Level "Error" -Message "Error pulling RSA private key from certificate with thumbprint $($Certificate.Thumbprint). Exception: $($_.Exception)."
        }

        try {
            Write-DosMessage -Level "Debug" -Message "Decrypting value..."
            $clearTextValue = Get-DecryptedValueCNGKey -privateKey $privateKey -EncryptedValue $EncryptedValue
            Write-DosMessage -Level "Debug" -Message "Successfully decrypted value."
        }
        catch {
            Write-DosMessage -Level "Error" -Message "Error decrypting value provided. Please verify that encryption certificate is valid. Exception: $($_.Exception)."
        }
    }
    else {
        Write-DosMessage -Level "Debug" -Message "CSP certificate detected."
        try {
            Write-DosMessage -Level "Debug" -Message "Decrypting value..."
            $clearTextValue = Get-DecryptedValueCSPCertificateKey -Certificate $Certificate -EncryptedValue $EncryptedValue
            Write-DosMessage -Level "Debug" -Message "Successfully decrypted value."
        }
        catch {
            Write-DosMessage -Level "Error" -Message "Error decrypting value provided. Please verify that encryption certificate is valid. Exception: $($_.Exception)."
        }
    }

    return $clearTextValue
}

function Get-CNGRSAPrivateKey {
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [X509Certificate] $Certificate
    )

    return [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate)
}

function Get-DecryptedValueCNGKey {
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [System.Security.Cryptography.RSA] $privateKey,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $EncryptedValue
    )

    return [System.Text.Encoding]::UTF8.GetString($privateKey.Decrypt([System.Convert]::FromBase64String($EncryptedValue), [System.Security.Cryptography.RSAEncryptionPadding]::OaepSHA1))
}

function Get-DecryptedValueCSPCertificateKey {
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [X509Certificate] $Certificate,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $EncryptedValue
    )

    return [System.Text.Encoding]::UTF8.GetString($Certificate.PrivateKey.Decrypt([System.Convert]::FromBase64String($EncryptedValue), $true))
}


<#
    .SYNOPSIS
 
    Adds or updates settings in a .NET appsettings.json file
 
    .DESCRIPTION
 
    Modifies a .NET appsettings.json file.
    If a setting is not found, it is added.
    If a setting is present but its value is different, the setting is modified.
 
    .PARAMETER appSettingsPath
 
    The path to the appsettings.json file.
 
    .PARAMETER appSettingsValues
 
    A hashtable that should mirror an appsettings.json file in structure.
 
    .EXAMPLE
 
    Update-DosAppSettingsJson -appSettingsPath "C:\SomeFolder\appsettings.json" -appSettingsValues $appSettings
#>

function Update-DosAppSettingsJson {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory = $true)]
        [string] $appSettingsPath,
        [Parameter(Mandatory = $true)]
        [hashtable] $appSettingsValues
    )
    Write-DosMessage -Level "Information" -Message "Updating app settings $appSettingsPath"
    $appSettings = (Get-Content $appSettingsPath -Raw) | ConvertFrom-Json
    
    foreach ($appSettingValue in $appSettingsValues.GetEnumerator()) {
        if ($appSettings.psobject.properties.name -notcontains $appSettingValue.Key) {
            $appSettings | Add-Member -Type NoteProperty -Name $appSettingValue.Key -Value $appSettingValue.Value
            Write-DosMessage -Level "Information" -Message "Added $($appSettingValue.Name)"
        }
        else {
            if ($appSettings."$($appSettingValue.Key)" -ne $appSettingValue.Value) {
                $appSettings."$($appSettingValue.Key)" = $appSettingValue.Value
                Write-DosMessage -Level "Information" -Message "Updated $($appSettingValue.Name)"
            }
        }
    }

    If ($PSCmdlet.ShouldProcess($appSettingsPath)) {
        $appSettings | ConvertTo-Json -Depth 100 | Set-Content $appSettingsPath
        Write-DosMessage -Level "Information" -Message "Finished updating $appSettingsPath"
    }
}


<#
    .SYNOPSIS
 
    Formats the given input string by performing a search/replace.
 
    .DESCRIPTION
 
    Formats the given input string by performing a search/replace against each delimited targetpattern and replacing it with the replacement pattern in the same array position
 
    .PARAMETER TargetPatterns
 
    Array of strings representing the target patterns to search for and replace. Combines with delimiter to prevent aliasing. Must be equal in length to the replacement array
 
    .PARAMETER ReplacementPattern
 
    Array of replacement strings. Must be equal in length to the target array
 
    .PARAMETER Content
 
    String representing the content to perform a search/replace on. Must be non-null and not empty
 
    .PARAMETER Delimiter
 
    Delimiter to combine with the target replacement patterns to perform search/replace against. May be an empty string if no delimiter is necessary
 
    .EXAMPLE
 
    $formattedContent = Update-DosConfigContent -TargetPatterns $TargetPatterns -ReplacementPattern $ReplacementPattern -Delimiter $Delimiter -Content $configFileContent
#>

function Update-DosConfigContent{
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [ValidateNotNull()]
        [Array] $TargetPatterns,
        [ValidateNotNull()]
        [Array] $ReplacementPattern,
        [ValidateNotNullOrEmpty()]
        [string] $Content,
        [ValidateNotNull()]
        [string] $Delimiter = ""
    )
    
    if($TargetPatterns.Length -ne $ReplacementPattern.Length){
        Write-DosMessage -Level "Error" -Message "Target patterns and replacement pattern length must be equal"
        return 
    }

    for($i = 0 ; $i -lt $TargetPatterns.Length; $i++){
        Write-DosMessage -Level "Verbose" -Message "Replacing $($TargetPatterns[$i]) with $($ReplacementPattern[$i])"
        $searchValue = "$Delimiter$($TargetPatterns[$i])$Delimiter"
        if($PSCmdlet.ShouldProcess("Modifying content via search and replace")){
            $Content = $Content.Replace($searchValue, $ReplacementPattern[$i])
        }
    }

    return $Content

    Write-DosTelemetry -Message "Update-DosConfigFile called."
}


<#
    .SYNOPSIS
 
    Formats the given input file by performing a search/replace. Writes the updated content back into the same file
 
    .DESCRIPTION
 
    Formats the given input string by performing a search/replace against each delimited targetpattern and replacing it with the replacement pattern in the same array position
 
    .PARAMETER TargetPatterns
 
    Array of strings representing the target patterns to search for and replace. Combines with delimiter to prevent aliasing. Must be equal in length to the replacement array
 
    .PARAMETER ReplacementPattern
 
    Array of replacement strings. Must be equal in length to the target array
 
    .PARAMETER FilePath
 
    Path to the file to update with the search/replace
 
    .PARAMETER Delimiter
 
    Delimiter to combine with the target replacement patterns to perform search/replace against. May be an empty string if no delimiter is necessary
 
    .EXAMPLE
 
    Update-DosConfigFile -TargetPatterns $targets -ReplacementPattern $replacements -Delimiter $delimiter -FilePath $ConfigFilePath
#>

function Update-DosConfigFile{
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [ValidateNotNull()]
        [Array] $TargetPatterns,
        [ValidateNotNull()]
        [Array] $ReplacementPattern,
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            if (!(Test-Path $_)) {
                Write-DosMessage -Level "Error" -Message "FilePath $_ does not exist. Please enter valid path." -ErrorAction Stop
            }
            else {
                $true
            }
        })]
        [string] $FilePath,
        [ValidateNotNull()]
        [string] $Delimiter = ""
    )
    
    Write-DosMessage -Level "Information" -Message "Performing search and replace on $FilePath"

    $configFileContent = Get-Content -Path $FilePath -Raw

    Write-DosMessage -Level "Debug" -Message "Pre-formatted content: $configFileContent"

    $formattedContent = Update-DosConfigContent -TargetPatterns $TargetPatterns -ReplacementPattern $ReplacementPattern -Delimiter $Delimiter -Content $configFileContent

    Write-DosMessage -Level "Debug" -Message "Formatted content: $formattedContent"

    if($PSCmdlet.ShouldProcess("Saving updated content back to file $FilePath")){
        Set-Content -Path $FilePath -Value $formattedContent
    }

}


<#
    .SYNOPSIS
 
    Formats the given input file by performing a search/replace.
 
    .DESCRIPTION
 
    Formats the given input string by performing a search/replace agaist pairs in the specified XML file
 
    .PARAMETER ConfigFilePath
 
    Configuration file to update with a search/replacement of patterns in the ReplacementPatternFilePath
 
    .PARAMETER ReplacementPatternFilePath
 
    File containing the delimiter and search/replace pairs to use to transform the config file
 
    .EXAMPLE
 
    Update-DosConfigFileFromInputFile -ConfigFilePath "C:\inetput\wwwroot\testapp\web.config" -ReplacementPatternFilePath ".\testappupdates.xml"
#>

function Update-DosConfigFileFromInputFile {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            if (!(Test-Path $_)) {
                Write-DosMessage -Level "Error" -Message "ConfigFilePath $_ does not exist. Please enter valid path." -ErrorAction Stop
            }
            else {
                $true
            }
        })]
        [string] $ConfigFilePath,
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            if (!(Test-Path $_)) {
                Write-DosMessage -Level "Error" -Message "ReplacementPatternFilePath $_ does not exist. Please enter valid path." -ErrorAction Stop
            }
            else {
                $true
            }
        })]
        [string] $ReplacementPatternFilePath
    )

    Write-DosMessage -Level "Verbose" -Message "Reading replacements from $ReplacementPatternFilePath"
    try{
        $replacementsXmlFile = [xml] (Get-Content $ReplacementPatternFilePath)
    }
    catch{
        Write-DosMessage -Level "Error" -Message "Unable to load xml in file $ReplacementPatternFilePath"
        return
    }

    if($null -eq $replacementsXmlFile.replacements) {
        Write-DosMessage -Level "Error" -Message "No replacements root found in specified replacement file $ReplacementPatternFilePath"
        return
    }
    if ($null -eq $replacementsXmlFile.replacements.pairs){
        Write-DosMessage -Level "Error" -Message "No replacements pairs found in specified replacement file $ReplacementPatternFilePath"
        return
    }

    $targets = $replacementsXmlFile.replacements.pairs.ChildNodes | ForEach-Object {$_.target}

    $replacements = $replacementsXmlFile.replacements.pairs.ChildNodes | ForEach-Object {$_.replacement}


    $delimiter = ""
    if($null -eq  $replacementsXmlFile.replacements.delimiter){
        Write-DosMessage -Level "Warning" -Message "No delimiter specified in replacement file $ReplacementPatternFilePath, assuming no delimiter"   
    }
    else{
        $delimiter = $replacementsXmlFile.replacements.delimiter
    }

    Write-DosMessage -Level "Debug" -Message "Have $($targets.Count) replacements and the delimiter is $delimiter"

    if($PSCmdlet.ShouldProcess("Updating config file $ConfigFilePath")){
        Update-DosConfigFile -TargetPatterns $targets -ReplacementPattern $replacements -Delimiter $delimiter -FilePath $ConfigFilePath
    }

}


<#
    .SYNOPSIS
 
    Updates the target DOS dac publish file to the specified database mount points
 
    .DESCRIPTION
 
    Take the passed in mount points and update the DOS dac publish xml file
 
    .PARAMETER PublishOptionsFilePath
 
    Path to publish options file - Required - See tests/SampleFiles/DefaultDacDeployOptions.xml for an example
 
    .PARAMETER dataMountPointFolder
 
    Path to the base data mount point folder
 
    .PARAMETER indexMountPointFolder
 
    Path to the base index mount point folder
 
    .PARAMETER logMountPointFolder
 
    Path to the base log mount point folder
 
    .EXAMPLE
 
    Update-DosMountPoint -PublishOptionsFilePath ".\test.publish.xml" -dataMountPointFolder "C:\SQLData" -indexMountPointFolder "C:\SQLData" -logMountPointFolder "C:\SQLData"
 
#>

function Update-DosMountPoint {
    [cmdletbinding(SupportsShouldProcess=$true)]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string]$PublishOptionsFilePath,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string]$dataMountPointFolder,
        [string]$indexMountPointFolder,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string]$logMountPointFolder
    )
    
    [xml] $parsedPublishOptions = [xml] (Get-Content $PublishOptionsFilePath -ErrorAction Stop)
    # Mount Points are stored in the SqlCmdVariables
    $sqlCmdVariables = $parsedPublishOptions.Project.ItemGroup.SqlCmdVariable

    Write-DosMessage -Level "Verbose" -Message "Validating that data mount point folder exists at $dataMountPointFolder"
    if(!(Test-Path "$dataMountPointFolder")){
        try {
            Write-DosMessage -Level "Information" -Message "Data mount point folder does not exist, creating folder at $dataMountPointFolder"
            New-Item -ItemType directory -Path "$dataMountPointFolder"
            Write-DosMessage -Level "Verbose" -Message "Created data mount point folder at $dataMountPointFolder for $PublishOptionsFilePath"
        }
        catch {
            Write-DosMessage -Level "Error" -Message "Could not create data mount point folder at $dataMountPointFolder for $PublishOptionsFilePath. Exception: $($_.Exception)"
        }
    }
    else {
        Write-DosMessage -Level "Information" -Message "Data mount point folder already exists at $dataMountPointFolder"
    }
    foreach ($dataMountPoint in $sqlCmdVariables | Where-Object {$_.Include -like "*Data*MountPoint"}) {
        $dataMountPoint.Value = $dataMountPointFolder
    }
    
    foreach ($dataMountPoint in $sqlCmdVariables | Where-Object {$_.Include -like "PrimaryMountPoint"}) {
        $dataMountPoint.Value = $dataMountPointFolder
    }

    Write-DosMessage -Level "Verbose" -Message "Validating that log mount point folder exists at $logMountPointFolder"
    if(!(Test-Path "$logMountPointFolder")){
        try {
            Write-DosMessage -Level "Information" -Message "Log mount point folder does not exist, creating folder at $logMountPointFolder"
            New-Item -ItemType directory -Path "$logMountPointFolder"
            Write-DosMessage -Level "Verbose" -Message "Created log mount point folder at $logMountPointFolder for $PublishOptionsFilePath"
        }
        catch {
            Write-DosMessage -Level "Error" -Message "Could not create log mount point folder at $logMountPointFolder for $PublishOptionsFilePath. Exception: $($_.Exception)"
        }
    }
    else {
        Write-DosMessage -Level "Information" -Message "Log mount point folder already exists at $logMountPointFolder"
    }
    foreach ($logMountPoint in $sqlCmdVariables | Where-Object {$_.Include -like "*Log*MountPoint"}) {
        $logMountPoint.Value = $logMountPointFolder
    }

    if($indexMountPointFolder){
        Write-DosMessage -Level "Verbose" -Message "Validating that index mount point folder exists at $indexMountPointFolder"
        if(!(Test-Path "$indexMountPointFolder")){
            try {
                Write-DosMessage -Level "Information" -Message "Index mount point folder does not exist, creating folder at $indexMountPointFolder"
                New-Item -ItemType directory -Path "$indexMountPointFolder"
                Write-DosMessage -Level "Verbose" -Message "Created index mount point folder at $indexMountPointFolder for $PublishOptionsFilePath"
            }
            catch {
                Write-DosMessage -Level "Error" -Message "Could not create index mount point folder at $indexMountPointFolder for $PublishOptionsFilePath. Exception: $($_.Exception)"
            }
        }
        else {
            Write-DosMessage -Level "Information" -Message "Index mount point folder already exists at $indexMountPointFolder"
        }
        foreach ($indexMountPoint in $sqlCmdVariables | Where-Object {$_.Include -like "*Index*MountPoint"}) {
            $indexMountPoint.Value = $indexMountPointFolder
        }
    }

    try {
        Write-DosMessage -Level "Information" -Message "Saving publish profile mount point settings"
        if ($pscmdlet.ShouldProcess($PublishOptionsFilePath,"Saving modifications to profile mount point settings")){
            $parsedPublishOptions.Save($PublishOptionsFilePath)
        }
    }
    catch {
        Write-DosMessage -Level "Fatal" -Message "Error occured while attempting to save mount point settings. Error $($_.Exception)"
        Write-DosTelemetry -Message "Finished attempt to update database mount point settings"
    }

}


<#
    .SYNOPSIS
 
    Adds or updates a single app setting in a .NET web config file.
 
    .DESCRIPTION
 
    Adds or updates a single app setting in a .NET web config file.
    The file must have an existing configuration element.
    If the appSettings element does not exist, it is created.
 
    .PARAMETER webConfigPath
 
    The path to the web.config file.
 
    .PARAMETER settingKey
 
    The key of the setting to add or modify
 
    .PARAMETER settingValue
 
    The to which to set the setting.
 
    .EXAMPLE
 
    Update-DosWebConfigAppSetting -webConfigPath "C:\Path\To\web.config" -settingKey "readExample" -settingValue "done"
#>

function Update-DosWebConfigAppSetting {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory = $true)]
        [string] $webConfigPath,
        [Parameter(Mandatory = $true)]
        [string] $settingKey,
        [string] $settingValue
    )

    if (!(Test-Path $webConfigPath)) {
        Write-DosMessage -Level Fatal -Message """$webConfigPath"" is not a valid path. Please specify a valid path to the apps web.config file"
    }

    $webConfigDoc = [xml](Get-Content $webConfigPath)
    if (!$webConfigDoc.configuration) {
        Write-DosMessage -Level Fatal -Message "This web.config file appears to not have a valid ""configuration"" xml node to write to"
    }
    
    if ($null -eq $webConfigDoc.configuration.appSettings) {
        $appSettings = $webConfigDoc.CreateElement("appSettings")
        $webConfigDoc.configuration.AppendChild($appSettings) | Out-Null
    }
    
    $appSettings = $webConfigDoc.configuration.SelectSingleNode('//appSettings')
    $existingSetting = $appSettings.add | Where-Object {$_.key -eq $settingKey}
    
    if ($null -eq $existingSetting) {
        $setting = $webConfigDoc.CreateElement("add")
    
        $keyAttribute = $webConfigDoc.CreateAttribute("key")
        $keyAttribute.Value = $settingKey;
        $setting.Attributes.Append($keyAttribute) | Out-Null
    
        $valueAttribute = $webConfigDoc.CreateAttribute("value")
        $valueAttribute.Value = $settingValue
        $setting.Attributes.Append($valueAttribute) | Out-Null
    
        $appSettings.AppendChild($setting) | Out-Null
        Write-DosMessage -Level "Information" -Message "Added $($settingKey) ($webConfigPath)"
    }
    else {
        $existingSetting.Value = $settingValue
        Write-DosMessage -Level "Information" -Message "Updated $($settingKey) ($webConfigPath)"
    }
    
    if ($PSCmdlet.ShouldProcess($webConfigPath)) {
        $webConfigDoc.Save($webConfigPath)
    }
}



<#
    .SYNOPSIS
 
    Write a logging message with a given severity to either a file, the console, or both.
 
    .DESCRIPTION
 
    Uses current logger configuration to log application messages based on severity.
 
    .PARAMETER Level
 
    Logger message level. Valid levels include: Verbose, Debug, Information, Warning, Error, and Fatal.
 
    .PARAMETER Message
 
    Message to be written in the log.
 
    .PARAMETER HeaderType
 
    Optional Header type that can be used to create dividers in a log file. Valid header types include: H1 and H2.
 
    .EXAMPLE
 
    Write-DosMessage -Level "Information" -Message "***[BEGIN]***" -HeaderType H2
    Write-DosMessage -Level "Information" -Message "Main Header" -HeaderType H1 # typically used at the beginning of a script
    Write-DosMessage -Level "Information" -Message "Step1 Header" -HeaderType H2 # typically used in the middle of script
    Write-DosMessage -Level "Information" -Message "Regular log message1."
    Write-DosMessage -Level "Information" -Message "Regular log message2."
    Write-DosMessage -Level "Information" -Message "Step2 Header" -HeaderType H2 # typically used in the middle of script
    Write-DosMessage -Level "Information" -Message "Regular log message3."
    Write-DosMessage -Level "Information" -Message "Regular log message4."
    Write-DosMessage -Level "Information" -Message "***[END]***" -HeaderType H2
 
    Write-DosMessage -Level "Fatal" -Message "Fatal Error Occured."
#>


function Write-DosMessage {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet("Verbose", "Debug", "Information", "Warning", "Error", "Fatal")]
        [string] $Level,
        [Parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        [string] $Message,
        [Parameter(Mandatory = $false)]
        [ValidateSet("H1", "H2")]
        [string] $HeaderType
    )
    $errorAction = $ErrorActionPreference

    # Used for mocking/testing
    if ($HeaderType) {
        $Width = 60;
        $Margin = 5;
        $Spacer = "-";
        $Message = "$($Spacer*$Margin)$Message$($Spacer*$Margin)";
        if ($Message.Length -gt ($Width - ($Margin * 2))) {
            $Width = $Message.Length + ($Margin * 2);
        }
        switch ($HeaderType) {
            "H1" { $Padding = 2 }
            "H2" { $Padding = 0 }
            default { $Padding = 0 }
        }
            
        LoadSerilog -Level "Information" -Message " "
        if ($Padding) {1..$Padding | ForEach-Object { LoadSerilog -Level $Level -Message ($Spacer * $Width)}}
        LoadSerilog -Level $Level -Message "$($Message)$($Spacer * ($Width - $Message.Length))"
        if ($Padding) {1..$Padding | ForEach-Object { LoadSerilog -Level $Level -Message ($Spacer * $Width)}}
    }
    else {
        LoadSerilog -Level $Level -Message $Message
    }
        
    # Silenty Continue do nothing
    # Default Throws on Fatal
    if ($errorAction -eq "Continue" -and $Level -eq "Fatal") {
        # throw reports the line number here instead of the calling function
        # Write-Error will report the line number in calling function

        # override $errorAction with Stop to create Terminating Error
        Write-Error -Message $Message -ErrorAction Stop
    }
        
    # Stop throws on Error and Fatal Levels
    if ($errorAction -eq "Stop" -and ($Level -eq "Error" -or $Level -eq "Fatal")) {
        # throw reports the line number here instead of the calling function
        # Write-Error will report the line number in calling function
        Write-Error -Message $Message
    }
            
}

function LoadSerilog {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet("Verbose", "Debug", "Information", "Warning", "Error", "Fatal")]
        [string] $Level,
        [Parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        [string] $Message
    )
    [SerilogBridge.SerilogBridge]::WriteDosMessage($Level, $Message)
}


<#
    .SYNOPSIS
 
    Write a telemetry logging message with a severity to application insights.
 
    .DESCRIPTION
 
    Uses Information severity level by default. Requires a message to be passed in as well.
 
    .PARAMETER Message
 
    Message to be written in the telemetry log.
 
    .EXAMPLE
 
    Write-DosTelemetry -Message "Telemetry Message Here."
#>


function Write-DosTelemetry {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $Message
    )

    try {
        # Seperate function call for mocking/testing
        LoadTelemetryBridge -Message $Message
    }
    catch {
        Write-DosMessage -Level "Error" -Message "Error writing telemetry message. Exception: $($_.Exception)"
    }
}

function LoadTelemetryBridge {
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $Message
    )
    [SerilogBridge.TelemetryBridge]::WriteDosTelemetry($Message)
}
# SIG # Begin signature block
# MIIa/gYJKoZIhvcNAQcCoIIa7zCCGusCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDAHceYibkC4/E8
# jxVeeRZxVa3gAGwufyjkTOORD9n2D6CCCqMwggUwMIIEGKADAgECAhAECRgbX9W7
# ZnVTQ7VvlVAIMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV
# BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0xMzEwMjIxMjAwMDBa
# Fw0yODEwMjIxMjAwMDBaMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERpZ2lD
# ZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EwggEiMA0GCSqGSIb3
# DQEBAQUAA4IBDwAwggEKAoIBAQD407Mcfw4Rr2d3B9MLMUkZz9D7RZmxOttE9X/l
# qJ3bMtdx6nadBS63j/qSQ8Cl+YnUNxnXtqrwnIal2CWsDnkoOn7p0WfTxvspJ8fT
# eyOU5JEjlpB3gvmhhCNmElQzUHSxKCa7JGnCwlLyFGeKiUXULaGj6YgsIJWuHEqH
# CN8M9eJNYBi+qsSyrnAxZjNxPqxwoqvOf+l8y5Kh5TsxHM/q8grkV7tKtel05iv+
# bMt+dDk2DZDv5LVOpKnqagqrhPOsZ061xPeM0SAlI+sIZD5SlsHyDxL0xY4PwaLo
# LFH3c7y9hbFig3NBggfkOItqcyDQD2RzPJ6fpjOp/RnfJZPRAgMBAAGjggHNMIIB
# yTASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAK
# BggrBgEFBQcDAzB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9v
# Y3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2NhY2VydHMuZGln
# aWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDCBgQYDVR0fBHow
# eDA6oDigNoY0aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJl
# ZElEUm9vdENBLmNybDA6oDigNoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0Rp
# Z2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDBPBgNVHSAESDBGMDgGCmCGSAGG/WwA
# AgQwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAK
# BghghkgBhv1sAzAdBgNVHQ4EFgQUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHwYDVR0j
# BBgwFoAUReuir/SSy4IxLVGLp6chnfNtyA8wDQYJKoZIhvcNAQELBQADggEBAD7s
# DVoks/Mi0RXILHwlKXaoHV0cLToaxO8wYdd+C2D9wz0PxK+L/e8q3yBVN7Dh9tGS
# dQ9RtG6ljlriXiSBThCk7j9xjmMOE0ut119EefM2FAaK95xGTlz/kLEbBw6RFfu6
# r7VRwo0kriTGxycqoSkoGjpxKAI8LpGjwCUR4pwUR6F6aGivm6dcIFzZcbEMj7uo
# +MUSaJ/PQMtARKUT8OZkDCUIQjKyNookAv4vcn4c10lFluhZHen6dGRrsutmQ9qz
# sIzV6Q3d9gEgzpkxYz0IGhizgZtPxpMQBvwHgfqL2vmCSfdibqFT+hKUGIUukpHq
# aGxEMrJmoecYpJpkUe8wggVrMIIEU6ADAgECAhAMMCpTLsjxo9FR9hag8ePUMA0G
# CSqGSIb3DQEBCwUAMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ
# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERpZ2lDZXJ0
# IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EwHhcNMjAwMzMxMDAwMDAw
# WhcNMjMwNTEwMTIwMDAwWjCBpzELMAkGA1UEBhMCVVMxDTALBgNVBAgTBFV0YWgx
# FzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5MR4wHAYDVQQKExVIZWFsdGggQ2F0YWx5
# c3QsIEluYy4xHjAcBgNVBAMTFUhlYWx0aCBDYXRhbHlzdCwgSW5jLjEwMC4GCSqG
# SIb3DQEJARYhYWRtaW5uaXN0cmF0b3JAaGVhbHRoY2F0YWx5c3QuY29tMIIBIjAN
# BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2fY0HWdxDJezDOsbHp7f9u/lrrD5
# nuZ1mENMgvixlrtC/KXgBRXlcWH7ajIOKljKnWCSAZwlZy4nFGbMagKmMzohXUXg
# xo94u5nCdiBa/kgPazNGpL0AyGgX2VARMbcpm8Gdy+/uH3Kc7L91lcoGZVVBnVIt
# 1oj5iXURqmhL83TrMyYqyj3XOH0So8Y10FVLPSukocMzMqBIRgvn/7EP0iWtOjXx
# +o1wB5Ql+z9G3NCqF6CKE/Pn355XYbbmjF7BPzKoOjocHO6VU2uEflJWq1ZFb0QY
# /tAosyyLYi9kFfO1damtJfRbbsVqavwg2UeQkzhg9CpB6eSsmBXPlFHudQIDAQAB
# o4IBxTCCAcEwHwYDVR0jBBgwFoAUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHQYDVR0O
# BBYEFFjfHOOIre2C4m9NCk8TFJlDwMxUMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUE
# DDAKBggrBgEFBQcDAzB3BgNVHR8EcDBuMDWgM6Axhi9odHRwOi8vY3JsMy5kaWdp
# Y2VydC5jb20vc2hhMi1hc3N1cmVkLWNzLWcxLmNybDA1oDOgMYYvaHR0cDovL2Ny
# bDQuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwTAYDVR0gBEUw
# QzA3BglghkgBhv1sAwEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNl
# cnQuY29tL0NQUzAIBgZngQwBBAEwgYQGCCsGAQUFBwEBBHgwdjAkBggrBgEFBQcw
# AYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tME4GCCsGAQUFBzAChkJodHRwOi8v
# Y2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyQXNzdXJlZElEQ29kZVNp
# Z25pbmdDQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAsBxn
# 9yJAQi+9cJPZpJvOEV6iHaOBGv8898wNJCc4eB5g8WPziEY70GZVeqEdx3z0wS8U
# QQIr19Hkju2NFZjDtzB9z1jAc/9EgqFGoCZbPijv1EYAa2oOVAp1BPbLjqBSdXqu
# 2mzqo14CJ30oNom9ep9F6LGZ5zEoPsMrJejSbJGr4EacrksX8C8qeFklc7FzwiGk
# GX7IQxidrrhOm2fOvGGAAxnvNYAR0FqJK0LiWWPSt5R/j63H/6HQtqD2sLevI3+O
# bRP74TPchDobFmWlSogX9oB63E7fsbDAqecY0cRPQ6tVWK53Ke2sB514nahFjZDa
# mxsa3/acZWL659ly3jGCD7Ewgg+tAgEBMIGGMHIxCzAJBgNVBAYTAlVTMRUwEwYD
# VQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAv
# BgNVBAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EC
# EAwwKlMuyPGj0VH2FqDx49QwDQYJYIZIAWUDBAIBBQCgfDAQBgorBgEEAYI3AgEM
# MQIwADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4w
# DAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQg31TVlP1rtMyglJQX4HrjEdgz
# rrcXHv0L8RG3idjeEsswDQYJKoZIhvcNAQEBBQAEggEAoBwd+ziF9RKSjVYGsTu0
# P78sYXUnx1O25V56l4jb8uf07TAmA2vlI05GelOjH5LmfRq6W06sRJKM8X/9+mg0
# bCq2xt57uAg8SwhF74F1kKULUf+54FE4sf93pKNuFLDF3u8C5HUi3lcOE7wSTNpe
# bYXVLwDhbpGGHB0ZqyHei2nvaUM2aDmOQKDzoAwLkl4VPe/tQyKfg3iDh2zOcIom
# Edl0t1+hDFxl1W7ER6mEph8Mp7Eu2DydQOiGnqzrv6ZowRRRFBz+HPVT4ia05biY
# Z2RF8rhm4DEd5auiBvXSzKIlvCZVGeH2+7+g5ay7JpZJukGJO1yqxhN4dPHEHanH
# /aGCDX0wgg15BgorBgEEAYI3AwMBMYINaTCCDWUGCSqGSIb3DQEHAqCCDVYwgg1S
# AgEDMQ8wDQYJYIZIAWUDBAIBBQAwdwYLKoZIhvcNAQkQAQSgaARmMGQCAQEGCWCG
# SAGG/WwHATAxMA0GCWCGSAFlAwQCAQUABCBD/+3SD9N+t4E8LPs3tDVrq7ubSFfR
# D/kT2oKD98wWzgIQHB96nCPtTAQJOWihu3fjvhgPMjAyMTA4MTEyMjM0NTBaoIIK
# NzCCBP4wggPmoAMCAQICEA1CSuC+Ooj/YEAhzhQA8N0wDQYJKoZIhvcNAQELBQAw
# cjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ
# d3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVk
# IElEIFRpbWVzdGFtcGluZyBDQTAeFw0yMTAxMDEwMDAwMDBaFw0zMTAxMDYwMDAw
# MDBaMEgxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjEgMB4G
# A1UEAxMXRGlnaUNlcnQgVGltZXN0YW1wIDIwMjEwggEiMA0GCSqGSIb3DQEBAQUA
# A4IBDwAwggEKAoIBAQDC5mGEZ8WK9Q0IpEXKY2tR1zoRQr0KdXVNlLQMULUmEP4d
# yG+RawyW5xpcSO9E5b+bYc0VkWJauP9nC5xj/TZqgfop+N0rcIXeAhjzeG28ffnH
# bQk9vmp2h+mKvfiEXR52yeTGdnY6U9HR01o2j8aj4S8bOrdh1nPsTm0zinxdRS1L
# sVDmQTo3VobckyON91Al6GTm3dOPL1e1hyDrDo4s1SPa9E14RuMDgzEpSlwMMYpK
# jIjF9zBa+RSvFV9sQ0kJ/SYjU/aNY+gaq1uxHTDCm2mCtNv8VlS8H6GHq756Wwog
# L0sJyZWnjbL61mOLTqVyHO6fegFz+BnW/g1JhL0BAgMBAAGjggG4MIIBtDAOBgNV
# HQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcD
# CDBBBgNVHSAEOjA4MDYGCWCGSAGG/WwHATApMCcGCCsGAQUFBwIBFhtodHRwOi8v
# d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwHwYDVR0jBBgwFoAU9LbhIB3+Ka7S5GGlsqIl
# ssgXNW4wHQYDVR0OBBYEFDZEho6kurBmvrwoLR1ENt3janq8MHEGA1UdHwRqMGgw
# MqAwoC6GLGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9zaGEyLWFzc3VyZWQtdHMu
# Y3JsMDKgMKAuhixodHRwOi8vY3JsNC5kaWdpY2VydC5jb20vc2hhMi1hc3N1cmVk
# LXRzLmNybDCBhQYIKwYBBQUHAQEEeTB3MCQGCCsGAQUFBzABhhhodHRwOi8vb2Nz
# cC5kaWdpY2VydC5jb20wTwYIKwYBBQUHMAKGQ2h0dHA6Ly9jYWNlcnRzLmRpZ2lj
# ZXJ0LmNvbS9EaWdpQ2VydFNIQTJBc3N1cmVkSURUaW1lc3RhbXBpbmdDQS5jcnQw
# DQYJKoZIhvcNAQELBQADggEBAEgc3LXpmiO85xrnIA6OZ0b9QnJRdAojR6OrktIl
# xHBZvhSg5SeBpU0UFRkHefDRBMOG2Tu9/kQCZk3taaQP9rhwz2Lo9VFKeHk2eie3
# 8+dSn5On7UOee+e03UEiifuHokYDTvz0/rdkd2NfI1Jpg4L6GlPtkMyNoRdzDfTz
# ZTlwS/Oc1np72gy8PTLQG8v1Yfx1CAB2vIEO+MDhXM/EEXLnG2RJ2CKadRVC9S0y
# OIHa9GCiurRS+1zgYSQlT7LfySmoc0NR2r1j1h9bm/cuG08THfdKDXF+l7f0P4Tr
# weOjSaH6zqe/Vs+6WXZhiV9+p7SOZ3j5NpjhyyjaW4emii8wggUxMIIEGaADAgEC
# AhAKoSXW1jIbfkHkBdo2l8IVMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNVBAYTAlVT
# MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
# b20xJDAiBgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0xNjAx
# MDcxMjAwMDBaFw0zMTAxMDcxMjAwMDBaMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNV
# BAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBUaW1lc3RhbXBpbmcgQ0EwggEi
# MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC90DLuS82Pf92puoKZxTlUKFe2
# I0rEDgdFM1EQfdD5fU1ofue2oPSNs4jkl79jIZCYvxO8V9PD4X4I1moUADj3Lh47
# 7sym9jJZ/l9lP+Cb6+NGRwYaVX4LJ37AovWg4N4iPw7/fpX786O6Ij4YrBHk8JkD
# bTuFfAnT7l3ImgtU46gJcWvgzyIQD3XPcXJOCq3fQDpct1HhoXkUxk0kIzBdvOw8
# YGqsLwfM/fDqR9mIUF79Zm5WYScpiYRR5oLnRlD9lCosp+R1PrqYD4R/nzEU1q3V
# 8mTLex4F0IQZchfxFwbvPc3WTe8GQv2iUypPhR3EHTyvz9qsEPXdrKzpVv+TAgMB
# AAGjggHOMIIByjAdBgNVHQ4EFgQU9LbhIB3+Ka7S5GGlsqIlssgXNW4wHwYDVR0j
# BBgwFoAUReuir/SSy4IxLVGLp6chnfNtyA8wEgYDVR0TAQH/BAgwBgEB/wIBADAO
# BgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwgweQYIKwYBBQUHAQEE
# bTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQwYIKwYB
# BQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3Vy
# ZWRJRFJvb3RDQS5jcnQwgYEGA1UdHwR6MHgwOqA4oDaGNGh0dHA6Ly9jcmw0LmRp
# Z2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwOqA4oDaGNGh0
# dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5j
# cmwwUAYDVR0gBEkwRzA4BgpghkgBhv1sAAIEMCowKAYIKwYBBQUHAgEWHGh0dHBz
# Oi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCwYJYIZIAYb9bAcBMA0GCSqGSIb3DQEB
# CwUAA4IBAQBxlRLpUYdWac3v3dp8qmN6s3jPBjdAhO9LhL/KzwMC/cWnww4gQiyv
# d/MrHwwhWiq3BTQdaq6Z+CeiZr8JqmDfdqQ6kw/4stHYfBli6F6CJR7Euhx7LCHi
# 1lssFDVDBGiy23UC4HLHmNY8ZOUfSBAYX4k4YU1iRiSHY4yRUiyvKYnleB/WCxSl
# gNcSR3CzddWThZN+tpJn+1Nhiaj1a5bA9FhpDXzIAbG5KHW3mWOFIoxhynmUfln8
# jA/jb7UBJrZspe6HUSHkWGCbugwtK22ixH67xCUrRwIIfEmuE7bhfEJCKMYYVs9B
# NLZmXbZ0e/VWMyIvIjayS6JKldj1po5SMYIChjCCAoICAQEwgYYwcjELMAkGA1UE
# BhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2lj
# ZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIFRpbWVz
# dGFtcGluZyBDQQIQDUJK4L46iP9gQCHOFADw3TANBglghkgBZQMEAgEFAKCB0TAa
# BgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTIxMDgx
# MTIyMzQ1MFowKwYLKoZIhvcNAQkQAgwxHDAaMBgwFgQU4deCqOGRvu9ryhaRtaq0
# lKYkm/MwLwYJKoZIhvcNAQkEMSIEIHQCR5fuMiK3a52C/AFY2jx0KMjQMdHrjLzi
# XQ0QFBrgMDcGCyqGSIb3DQEJEAIvMSgwJjAkMCIEILMQkAa8CtmDB5FXKeBEA0Fc
# g+MpK2FPJpZMjTVx7PWpMA0GCSqGSIb3DQEBAQUABIIBAKg3pU7jkG+WMhLeEYWD
# RQ230wVBOSDUGDNMD6i8gqx8ufpyZUN4YXq+Z9QqfqZbtnsWpWBhX84/tNA22+iP
# hxfq9gpD1IbBiQBG6lSDkvGH62MjQp6TQt5wUN8ChspZ4XgwwEDSn5//fef0bvvo
# 3zPTo0ip+sxRfEOGi911nfgn/RxG8wYNsvfu6uNBarLwF2n2u1oUx4gNT/8d1Pit
# nC2XYXe+d9DuJen09E8qJ+RsPzOGque+QnAInO8xgpbxq64Q2UvZ+EogIOybCHIP
# +JAwrlWXhSlFhKX63z3ZsPf9UTbqbEoQyeK9J4YAK3VRx7AGgjfqLT16vZOJ9x0V
# rtQ=
# SIG # End signature block