DSCResources/DSC_PostgreSqlInstall/DSC_PostgreSqlInstall.psm1

$script:ParentModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent
$script:modulesFolderPath = Join-Path -Path $script:ParentModulePath -ChildPath 'Modules'

$script:CommonHelperModulePath = Join-Path -Path $script:modulesFolderPath -ChildPath 'DscResource.Common'
Import-Module $script:CommonHelperModulePath -ErrorAction Stop

$script:localizedData = Get-LocalizedData -DefaultUICulture en-US

<#
    .SYNOPSIS
        Returns the current state of the PostgreSql install.
 
    .PARAMETER Ensure
        Specify if PostgreSQL should be absent or present.
 
    .PARAMETER Version
        The version of PostgreSQL that is going to be install or uninstalled.
 
    .PARAMETER InstallerPath
        The full path to the EDB PostgreSql installer.
#>

function Get-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateSet('Present', 'Absent')]
        [System.String]
        $Ensure,

        [Parameter(Mandatory = $true)]
        [ValidateSet('9', '10', '11', '12', '13')]
        [System.String]
        $Version,

        [Parameter(Mandatory = $true)]
        [System.String]
        $InstallerPath
    )

    Write-Verbose -Message ($script:localizedData.SearchingRegistry -f $Version)
    $registryKeys = Get-ChildItem -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' | Where-Object -FilterScript {($($_.Name -Split '\\')[-1]) -eq "PostgreSQL $Version"}

    if ($null -eq $registryKeys)
    {
        Write-Verbose -Message ($script:localizedData.NoVersionFound)

        $getResults =  @{
            Ensure           = 'Absent'
            InstallerPath    = $InstallerPath
            Version          = $Version
            InstallDirectory = $null
            ServiceName      = $null
            ServiceAccount   = $null
            DataDirectory    = $null
            ServerPort       = $null
            Features         = $null
        }
    }
    else
    {
        $prefixRegistry = $registryKeys.GetValue('InstallLocation')

        # Search Services for PostgreSQL so we can get the status of the service.
        Write-Verbose -Message ($script:localizedData.CheckingForService)
        $services = Get-ChildItem -Path 'HKLM:\SYSTEM\CurrentControlSet\Services'
        foreach ($service in $services)
        {
            $value = Get-ItemProperty -Path $service.PSPath -Name ImagePath -ErrorAction SilentlyContinue

            if ($value.ImagePath -like "*$prefixRegistry*")
            {
                $result = $service
            }
        }

        if ($result)
        {
            $serviceDisplayName = ($result.GetValue('DisplayName') -split ' - ')[0]
            $serviceLogon = $result.GetValue('ObjectName')
            $serviceDataDir = (($result.GetValue('ImagePath') -split ' -D')[1] -split ' -w')[0].Trim().Replace('"','')
        }

        # Open config to check port.
        Write-Verbose -Message ($script:localizedData.CheckingConfig)
        $conf = Get-Content -Path "$serviceDataDir\postgresql.conf"
        foreach ($line in $conf)
        {
            if ($line -like 'port =*')
            {
                $confPort = $line.Substring(7,8).Trim()
            }
        }

        # Check licenses that are in the install dir to see what features are installed.
        Write-Verbose -Message ($script:localizedData.CheckingFeatures)
        $files = Get-ChildItem -Path $prefixRegistry -Name '*license*'

        $installedFeatures = @()
        if ($files -contains 'commandlinetools_3rd_party_licenses.txt')
        {
            $installedFeatures += 'commandlinetools'
        }
        if ($files -contains 'pgAdmin_license.txt')
        {
            $installedFeatures += 'pgAdmin'
        }
        if ($files -contains 'server_license.txt')
        {
            $installedFeatures += 'server'
        }
        if ($files -contains 'StackBuilder_3rd_party_licenses.txt')
        {
            $installedFeatures += 'stackbuilder'
        }

        Write-Verbose -Message ($script:localizedData.FoundKeysForVersion -f $Version)

        $getResults = @{
            Ensure           = 'Present'
            Version          = $Version
            InstallerPath    = $InstallerPath
            InstallDirectory = $prefixRegistry
            ServiceName      = $serviceDisplayName
            ServiceAccount   = $serviceLogon
            DataDirectory    = $serviceDataDir
            ServerPort       = $confPort
            Features         = $installedFeatures
        }
    }

    return $getResults
}

<#
    .SYNOPSIS
        Installs or Uninstalls PostgreSQL.
 
    .PARAMETER Ensure
        Specify if PostgreSQL should be absent or present.
 
    .PARAMETER Version
        The version of PostgreSQL that is going to be installed or uninstalled.
 
    .PARAMETER InstallerPath
       The full path to the EDB PostgreSql installer.
 
    .PARAMETER ServiceName
        The name of the Windows service that PostgreSql will run under.
 
    .PARAMETER InstallationDirectory
        The folder path that PostgreSql should be installed to.
 
    .PARAMETER ServerPort
        The port that PostgreSql will listen on for incoming connections.
 
    .PARAMETER DataDirectory
        The path for all the data from this PostgreSql install.
 
    .PARAMETER ServiceAccount
        The account that will be used to run the service.
 
    .PARAMETER SuperAccount
        The account that will be the super account in PostgreSQL.
 
    .PARAMETER Features
        The PostgreSql features to install.
 
    .PARAMETER OptionFile
        The file that has options for the install.
#>

function Set-TargetResource
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateSet('Present', 'Absent')]
        [System.String]
        $Ensure,

        [Parameter(Mandatory = $true)]
        [ValidateSet('9', '10', '11', '12', '13')]
        [System.String]
        $Version,

        [Parameter(Mandatory = $true)]
        [System.String]
        $InstallerPath,

        [Parameter()]
        [ValidateScript( {$_ -notmatch '\s'})]
        [System.String]
        $ServiceName,

        [Parameter()]
        [System.String]
        $InstallDirectory,

        [Parameter()]
        [System.UInt16]
        $ServerPort,

        [Parameter()]
        [System.String]
        $DataDirectory,

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $ServiceAccount,

        [Parameter()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $SuperAccount,

        [Parameter()]
        [System.String[]]
        $Features,

        [Parameter()]
        [System.String]
        $OptionFile
    )

    if ($Ensure -eq 'Present')
    {
        if (-not (Test-Path -Path $InstallerPath))
        {
            throw ($script:localizedData.PathIsMissing -f $InstallerPath)
        }

        $arguments = @(
            "--unattendedmodeui none"
            "--mode unattended"
        )

        $argumentParameters = @('servicename', 'InstallDirectory', 'DataDirectory', 'serverport', 'features', 'optionfile')

        foreach ($arg in $argumentParameters)
        {
            if (-not [string]::IsNullOrEmpty($PSBoundParameters[$arg]))
            {
                if ($arg -eq 'ServiceName')
                {
                    $arguments += '--servicename "{0}"' -f $ServiceName
                    Write-Verbose -Message ($script:localizedData.ParameterSetTo -f $arg, $ServiceName)
                }
                elseif ($arg -eq 'features')
                {
                    $featuresToString = ($PSBoundParameters[$arg] -join ',').ToLower()
                    $finalFeatureString = $featuresToString.Replace('pgadmin', 'pgAdmin')
                    $arguments += '--enable-components "{0}"' -f $finalFeatureString
                    Write-Verbose -Message ($script:localizedData.ParameterSetTo -f 'enable-components', $finalFeatureString)
                }
                elseif ($arg -eq 'DataDirectory')
                {
                    $arguments += '--datadir "{0}"' -f $($PSBoundParameters[$arg])
                    Write-Verbose -Message ($script:localizedData.ParameterSetTo -f 'datadir', $($PSBoundParameters[$arg]))
                }
                elseif ($arg -eq 'InstallDirectory')
                {
                    $arguments += '--prefix "{0}"' -f $($PSBoundParameters[$arg])
                    Write-Verbose -Message ($script:localizedData.ParameterSetTo -f 'prefix', $($PSBoundParameters[$arg]))
                }
                else
                {
                    $arguments += '--{0} "{1}"' -f $arg, $($PSBoundParameters[$arg])
                    Write-Verbose -Message ($script:localizedData.ParameterSetTo -f $arg, $($PSBoundParameters[$arg]))
                }
            }
        }

        $builtinAccounts = @('NT AUTHORITY\NetworkService', 'NT AUTHORITY\System', 'NT AUTHORITY\Local Service')
        if (-not ($null -eq $ServiceAccount))
        {
            $arguments += '--serviceaccount "{0}"' -f $($ServiceAccount.UserName)
            Write-Verbose -Message ($script:localizedData.ParameterSetTo -f 'serviceaccount', $($ServiceAccount.UserName))

            if (-not ($ServiceAccount.UserName -in $builtinAccounts))
            {
                $arguments += '--servicepassword "{0}"' -f $($ServiceAccount.GetNetworkCredential().Password)
            }
        }

        if (-not ($null -eq $SuperAccount))
        {
            $arguments += '--superaccount "{0}"' -f $($SuperAccount.UserName)
            Write-Verbose -Message ($script:localizedData.ParameterSetTo -f 'SuperAccount', $($SuperAccount.UserName))

            $arguments += '--superpassword "{0}"' -f $($SuperAccount.GetNetworkCredential().Password)
        }

        $displayArguments = $arguments.Clone()
        $i = 0
        foreach ($arg in $displayArguments)
        {
            if ($arg -match '--superpassword' -or $arg -match '--servicepassword')
            {
                $displayArguments[$i] = $arg.Split(' ')[0] + ' *******'
            }
            $i++
        }

        Write-Verbose -Message ($script:localizedData.StartingInstall)
        Write-Verbose -Message ($script:localizedData.InstallString -f $InstallerPath, $($displayArguments -join ' '))
        $process = Start-Process $InstallerPath -ArgumentList ($arguments -join ' ') -Wait -PassThru -NoNewWindow
        $exitCode = $process.ExitCode

        if ($exitCode -eq 0 -or $exitCode -eq 1641 -or $exitCode -eq 3010)
        {
            Write-Verbose -Message ($script:localizedData.PostgreSqlSuccess -f 'installed', $exitCode)
        }
        else
        {
            $logPath = "$env:TEMP\instabuilder_installer.log"
            throw ($script:localizedData.PostgreSqlFailed -f 'install', $exitCode, $logPath)
        }
    }
    else
    {
        Write-Verbose -Message ($script:localizedData.SearchingRegistry -f $Version)
        $uninstallRegistry = Get-ChildItem -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' | Where-Object -FilterScript {($($_.Name -Split '\\')[-1]) -eq "PostgreSQL $Version"}
        $uninstallString = $uninstallRegistry.GetValue('UninstallString')

        Write-Verbose -Message ($script:localizedData.PosgreSqlUninstall)
        Write-Verbose -Message ($script:localizedData.UninstallString -f $uninstallString, '--mode unattended')
        $process = Start-Process -FilePath $uninstallString -ArgumentList '--mode unattended' -Wait
        $exitCode = $process.ExitCode

        if ($exitCode -eq 0 -or $null -eq $exitCode)
        {
            Write-Verbose -Message ($script:localizedData.PostgreSqlSuccess -f 'uninstalled', $exitCode)
        }
        else
        {
            $logPath = "$env:TEMP\instabuilder_installer.log"
            throw  ($script:localizedData.PostgreSqlFailed -f 'uninstall', $exitCode, $logPath)
        }
    }
}

<#
    .SYNOPSIS
        Checks if the current status of PostgreSQL is the same as what is to be configured.
 
    .PARAMETER Ensure
        Specify if PostgreSQL should be absent or present.
 
    .PARAMETER Version
        The version of PostgreSQL that is going to be installed or uninstalled.
 
    .PARAMETER InstallerPath
       The full path to the EDB PostgreSql installer.
 
    .PARAMETER ServiceName
        The name of the Windows service that PostgreSql will run under.
 
    .PARAMETER InstallDirectory
        The folder path that PostgreSql should be installed to.
 
    .PARAMETER ServerPort
        The server port that PostgreSql will listen on for incoming connections.
 
    .PARAMETER DataDirectory
        The path for all the data from this PostgreSql install.
 
    .PARAMETER ServiceAccount
        The account that will be used to run the service.
 
    .PARAMETER SuperAccount
        The account that will be the super account in PostgreSQL.
 
    .PARAMETER Features
        The PostgreSql features to install.
 
    .PARAMETER OptionFile
        The file that has options for the install.
#>

function Test-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateSet('Present', 'Absent')]
        [System.String]
        $Ensure,

        [Parameter(Mandatory = $true)]
        [ValidateSet('9', '10', '11', '12', '13')]
        [System.String]
        $Version,

        [Parameter(Mandatory = $true)]
        [System.String]
        $InstallerPath,

        [Parameter()]
        [System.String]
        $ServiceName,

        [Parameter()]
        [System.String]
        $InstallDirectory,

        [Parameter()]
        [System.UInt16]
        $ServerPort,

        [Parameter()]
        [System.String]
        $DataDirectory,

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $ServiceAccount,

        [Parameter()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $SuperAccount,

        [Parameter()]
        [System.String[]]
        $Features,

        [Parameter()]
        [System.String]
        $OptionFile
    )

    $getTargetResourceParameters = @{
        Ensure        = $Ensure
        Version       = $Version
        InstallerPath = $InstallerPath
    }

    $getTargetResourceResults = Get-TargetResource @getTargetResourceParameters
    $result = $true

    if ($Ensure -eq 'Present')
    {
        if ($getTargetResourceResults.Ensure -eq 'Absent')
        {
            Write-Verbose -Message ($script:localizedData.MismatchSetting -f 'Ensure', $Ensure, $getTargetResourceResults.Ensure, 'false')
            $result = $false
        }
        else
        {
            if ($getTargetResourceResults.Version -ne $Version)
            {
                Write-Warning -Message ($script:localizedData.MismatchWarning -f 'Version', $Version, $getTargetResourceResults.Version)
            }
            if ($getTargetResourceResults.ServiceName -ne $ServiceName -and $null -ne $ServiceName)
            {
                Write-Warning -Message ($script:localizedData.MismatchWarning -f 'ServiceName', $ServiceName, $getTargetResourceResults.ServiceName)
            }
            if ($getTargetResourceResults.InstallDirectory -ne $InstallDirectory -and $null -ne $InstallDirectory)
            {
                Write-Warning -Message ($script:localizedData.MismatchWarning -f 'InstallDirectory', $InstallDirectory, $getTargetResourceResults.InstallDirectory)
            }
            if ($getTargetResourceResults.ServerPort -ne $ServerPort -and $null -ne $ServerPort)
            {
                Write-Warning -Message ($script:localizedData.MismatchWarning -f 'ServerPort', $ServerPort, $getTargetResourceResults.ServerPort)
            }
            if ($getTargetResourceResults.DataDirectory -ne $DataDirectory -and $null -ne $DataDirectory)
            {
            Write-Warning -Message ($script:localizedData.MismatchWarning -f 'DataDirectory', $DataDirectory, $getTargetResourceResults.DataDirectory)
            }
            if ($getTargetResourceResults.ServiceAccount -ne $ServiceAccount.UserName -and $null -ne $ServiceAccount)
            {
                Write-Warning -Message ($script:localizedData.MismatchWarning -f 'ServiceAccount', $ServiceAccount.UserName, $getTargetResourceResults.ServiceAccount)
            }
            if ($null -ne $getTargetResourceResults.Features)
            {
                $featureArray = $getTargetResourceResults.Features -Split ','
                foreach ($feature in $Features)
                {
                    if ($featureArray -notcontains $feature)
                    {
                        Write-Warning -Message ($script:localizedData.MissingFeature -f $feature)
                    }
                }

                foreach ($feature in $featureArray)
                {
                    if ($Features -notcontains $feature)
                    {
                        Write-Warning -Message ($script:localizedData.ExtraFeature -f $feature)
                    }
                }

            }
        }
    }
    else
    {
        if ($getTargetResourceResults.Ensure -eq 'Present')
        {
            Write-Verbose -Message ($script:localizedData.MismatchSetting -f 'Ensure', $Ensure, $getTargetResourceResults.Ensure, 'false')
            $result = $false
        }
    }

    return $result
}

Export-ModuleMember -Function *-TargetResource