Patch/Install-Patch.ps1

<#
.SYNOPSIS
    Installs patch to BC database
.DESCRIPTION
    Imports fob file and data about patch and included objects' versions to BC database specified in the settings of BC instance provided as a parameter.
.EXAMPLE
    Install-Patch -instanceName n721 -path 'C:\N.7.2.1_1_00000003.zip'
    Install patch from file
.EXAMPLE
    Install-Patch -instanceName n721 -path "C:\7.2 BC_1_00000001.zip"
    Run script by its full path.
.EXAMPLE
    "n721", "prod" | Install-Patch -path "C:\7.2 BC_1_00000001.zip"
    Install patch to multiple instances
.EXAMPLE
    Get-ChildItem c:\temp\* -Include "*.zip" | Install-Patch -instanceName n721
    Install all patches from folder
.EXAMPLE
    Get-ChildItem -Path "c:\patches\*" | `
        % { if ($_.Name -match "(?:0+)([0-9]+)" -and $matches[1] -in 9..31) { $_ } } | `
            Install-Patch -instanceName prod
    Install patches 9 through 31 from folder
.NOTES
    Instances from destenation parameter must be configured to accept NTLM auth and have API endpoint enabled.
    Must be run as administrator because it requires to tun Sync-NavTenant cmdlet.
#>

function Install-Patch {
    [CmdletBinding()]
    param(
        # Destination BC instance where objects should be imported to.
        [Parameter(Mandatory = $True, ValueFromPipeline)]
        [string]$instanceName,
        # Path to patch archive.
        [Parameter(Mandatory = $true, ValueFromPipeline)]
        [string]$path,
        # When this switch is present all instances set to the same database will be restarted after last patch is imported.
        [Parameter(Mandatory = $false)]
        [switch]$restart,
        # When this switch is present current patch will be checked with the one being imported and if they are match, then current will be deleted and reimported.
        [Parameter(Mandatory = $false)]
        [switch]$allowReinstall,
        # Skip the 45s delay between Sync and Patch Data update request. Usefull when installing small patches in a batch.
        [Parameter(Mandatory = $false)]
        [switch]$noSleep
    )
    process {
        #Requires -RunAsAdministrator
        $ErrorActionPreference = "Stop"
        Import-NavModule -Service -Development

        $portDest = Get-NAVServerConfiguration $instanceName -KeyName ODataServicesPort
        $bcServerDest = "localhost"

        $temparchive = "$Env:TEMP\$([System.IO.Path]::GetFileNameWithoutExtension($path))\"
        Write-Verbose $temparchive
        Expand-Archive $path -DestinationPath $temparchive -Force
        $tempFob = Get-ChildItem $temparchive -Include "*.fob" -Recurse
        $tempJson = Get-ChildItem $temparchive -Include "*.json" -Recurse

        Write-Host "Installing patch $path"
        $destBaseUrl = "http://$bcServerDest`:$portDest/$instanceName/api/v1.0/companies"
        $company = (Invoke-RestMethod -Uri ($destBaseUrl) -UseDefaultCredentials -ErrorAction Stop).value[0].id
        $serverInfo = (Invoke-RestMethod -Uri ("$destBaseUrl($company)/tfsInfos") -UseDefaultCredentials).value[0]
        $destUrl = "$destBaseUrl($company)/tfsPatches"
        # Validity checks
        $importData = Get-Content $tempJson | ConvertFrom-Json
        $current = (Invoke-RestMethod -Uri "$destUrl`?`$filter=Level eq $($importData.Level)&`$top=1&`$orderby=Number desc" -UseDefaultCredentials -Method Get -ContentType "application/json").value

        Write-Verbose ($current | Format-List | Out-String)
        if ([int]$current.Number -ne 0) {
            if ([int]$current.Number + 1 -ne [int]$importData.Number) {
                if ($allowReinstall) {
                    if ([int]$current.Number -ne [int]$importData.Number) {
                        throw "Patch being imported has number $($importData.Number) and latest installed has number $($current.Number). With -reinstall parameter it is only allowed to install current patch or next one. Import cancelled."
                    }
                    Invoke-WebRequest -Uri "$destUrl($($importData.ID))" -UseDefaultCredentials -Method Delete -ContentType "application/json" | Out-Null
                }
                else {
                    if ([int]$current.Number -eq [int]$importData.Number) {
                        throw "Patch $($importData.Number) being imported is already installed if you want to reinstall patch then use parameter -reinstall. Import cancelled."
                    }
                    else {
                        throw "Patch being imported has number $($importData.Number) and latest installed has number $($current.Number). Install missing patches first. Import cancelled."
                    }
                }
            }
        }

        # Import objects
        Write-Host "Destenation ($($serverInfo.SQLServerName)) ($($serverInfo.SQLDatabaseName)): $destUrl" -ForegroundColor Green
        Import-NAVApplicationObject -Path $tempFob -DatabaseName $serverInfo.SQLDatabaseName -DatabaseServer $serverInfo.SQLServerName -ImportAction Overwrite -SynchronizeSchemaChanges No -SuppressBuildSearchIndex -Confirm:$false -ErrorAction Stop
        Sync-NAVTenant -ServerInstance $instanceName -Mode Sync -Force -ErrorAction SilentlyContinue -ErrorVariable syncError

        # Update version data
        if (-not $noSleep) { Start-Sleep -Seconds 45 } # Wait to avoid (503) Server Unavailable

        Invoke-WebRequest -Uri $destUrl -UseDefaultCredentials -Method Post -ContentType "application/json" -InFile $tempJson | Out-Null

        if ($syncError) {
            Write-Error "Patch ($([System.IO.Path]::GetFileName($path))) installed but schema was not synced due to error:
                    $syncError"

        }
        else {
            Write-Host "Patch ($([System.IO.Path]::GetFileName($path))) installed successfully." -ForegroundColor Cyan
        }
    }
    end {
        if ($restart) {
            $current = Get-NAVApplication $instanceName
            Get-NAVServerInstance | Where-Object -Property State -EQ -Value Running | % {
                $app = Get-NAVApplication $_.ServerInstance
                if ($app.'Database server' -eq $current.'Database server' -and $app.'Database name' -eq $current.'Database name') { $_ }
            } | % {
                Write-Host "Restarting instance $($_.ServerInstance)" -ForegroundColor Cyan
                Restart-NAVServerInstance $_.ServerInstance -ErrorAction Continue | Out-Null
            }
        }
    }
}