AppHandling/Run-AlValidation.ps1

<#
 .Synopsis
  Run AL Validation
 .Description
  Run AL Validation
 .Parameter containerName
  Name of the validation container. Default is bcserver.
 .Parameter imageName
  If imageName is specified it will be used to build an image, which serves as a cache for faster container generation.
  Only speficy imagename if you are going to create multiple containers from the same artifacts.
 .Parameter credential
  These are the credentials used for the container. If not provided, the Run-AlValidation function will generate a random password and use that.
 .Parameter licenseFile
  License file to use for AL Validation
 .Parameter memoryLimit
  MemoryLimit is default set to 8Gb. This is fine for compiling small and medium size apps, but if your have a number of apps or your apps are large and complex, you might need to assign more memory.
 .Parameter installApps
  Array or comma separated list of 3rd party apps to install before validating apps
 .Parameter previousApps
  Array or comma separated list of previous version of apps to use for AppSourceCop validation and upgrade test
 .Parameter apps
  Array or comma separated list of apps to validate
 .Parameter ValidateVersion
  Full or partial version number. If specified, apps will also be validated against this version.
 .Parameter ValidateCurrent
  Include this switch if you want to also validate against current version of Business Central
 .Parameter ValidateNextMinor
  Include this switch if you want to also validate against next minor version of Business Central. If you include this switch you need to specify a sasToken for insider builds as well.
 .Parameter ValidateNextMajor
  Include this switch if you want to also validate against next major version of Business Central. If you include this switch you need to specify a sasToken for insider builds as well.
 .Parameter failOnError
  Include this switch if you want to fail on the first error instead of returning all errors to the caller
 .Parameter includeWarnings
  Include this switch if you want to include Warnings
 .Parameter sasToken
  Shared Access Service Token for accessing insider artifacts of Business Central. Available on http://aka.ms/collaborate
 .Parameter countries
  Array or comma separated list of country codes to validate against
 .Parameter affixes
  Array or comma separated list of affixes to use for AppSourceCop validation
 .Parameter supportedCountries
  Array or comma separated list of supportedCountries to use for AppSourceCop validation
 .Parameter vsixFile
  Specify a URL or path to a .vsix file in order to override the .vsix file in the image with this.
  Use Get-LatestAlLanguageExtensionUrl to get latest AL Language extension from Marketplace.
  Use Get-AlLanguageExtensionFromArtifacts -artifactUrl (Get-BCArtifactUrl -select NextMajor -sasToken $insiderSasToken) to get latest insider .vsix
 .Parameter useGenericImage
  Specify a private (or special) generic image to use for the Container OS.
 .Parameter DockerPull
  Override function parameter for docker pull
 .Parameter NewBcContainer
  Override function parameter for New-BcContainer
 .Parameter CompileAppInBcContainer
  Override function parameter for Compile-AppInBcContainer
 .Parameter GetBcContainerAppInfo
  Override function parameter for Get-BcContainerAppInfo
 .Parameter PublishBcContainerApp
  Override function parameter for Publish-BcContainerApp
 .Parameter RemoveBcContainer
  Override function parameter for Remove-BcContainer
#>

function Run-AlValidation {
Param(
    [string] $containerName = "bcserver",
    [string] $imageName = "",
    [PSCredential] $credential,
    [string] $licenseFile,
    [string] $memoryLimit,
    $installApps = @(),
    $previousApps = @(),
    [Parameter(Mandatory=$true)]
    $apps,
    [string] $validateVersion = "",
    [switch] $validateCurrent,
    [switch] $validateNextMinor,
    [switch] $validateNextMajor,
    [switch] $failOnError,
    [switch] $includeWarnings,
    [string] $sasToken = "",
    [Parameter(Mandatory=$true)]
    $countries,
    $affixes = @(),
    $supportedCountries = @(),
    [string] $vsixFile,
    [switch] $skipVerification,
    [string] $useGenericImage = (Get-BestGenericImageName),
    [scriptblock] $DockerPull,
    [scriptblock] $NewBcContainer,
    [scriptblock] $CompileAppInBcContainer,
    [scriptblock] $PublishBcContainerApp,
    [scriptblock] $GetBcContainerAppInfo,
    [scriptblock] $RemoveBcContainer
)


function RandomChar([string]$str) {
    $rnd = Get-Random -Maximum $str.length
    [string]$str[$rnd]
}

function GetRandomPassword {
    $cons = 'bcdfghjklmnpqrstvwxz'
    $voc = 'aeiouy'
    $numbers = '0123456789'

    ((RandomChar $cons).ToUpper() + `
     (RandomChar $voc) + `
     (RandomChar $cons) + `
     (RandomChar $voc) + `
     (RandomChar $numbers) + `
     (RandomChar $numbers) + `
     (RandomChar $numbers) + `
     (RandomChar $numbers))
}

function DetermineArtifactsToUse {
    Param(
        [string] $version = "",
        [string] $select = "Current",
        [string] $sasToken = "",
        [string[]] $countries = @("us")
    )

Write-Host -ForegroundColor Yellow @'
  _____ _ _ _ _ __ _
 | __ \ | | (_) | | (_)/ _| | |
 | | | | ___| |_ ___ _ __ _ __ ___ _ _ __ ___ __ _ _ __| |_ _| |_ __ _ ___| |_ ___
 | | | |/ _ \ __/ _ \ '__| '_ ` _ \| | '_ \ / _ \ / _` | '__| __| | _/ _` |/ __| __/ __|
 | |__| | __/ |_ __/ | | | | | | | | | | | __/ | (_| | | | |_| | || (_| | (__| |_\__ \
 |_____/ \___|\__\___|_| |_| |_| |_|_|_| |_|\___| \__,_|_| \__|_|_| \__,_|\___|\__|___/
                                                                                            
'@

    
    $minsto = 'bcartifacts'
    $minsel = $select
    $mintok = $sasToken
    if ($countries) {
        $minver = $null
        $countries | ForEach-Object {
            $url = Get-BCArtifactUrl -version $version -country $_ -select $select -sasToken $sasToken | Select-Object -First 1
            Write-Host "Found $($url.Split('?')[0])"
            if ($url) {
                $ver = [Version]$url.Split('/')[4]
                if ($minver -eq $null -or $ver -lt $minver) {
                    $minver = $ver
                    $minsto = $url.Split('/')[2].Split('.')[0]
                    $minsel = "Latest"
                    $mintok = $url.Split('?')[1]; if ($mintok) { $mintok = "?$mintok" }
                }
            }
        }
        if ($minver -eq $null) {
            throw "Unable to locate artifacts"
        }
        $version = $minver.ToString()
    }
    $artifactUrl = Get-BCArtifactUrl -storageAccount $minsto -version $version -country $countries[0] -select $minsel -sasToken $mintok | Select-Object -First 1
    if (!($artifactUrl)) {
        throw "Unable to locate artifacts"
    }
    Write-Host "Using $($artifactUrl.Split('?')[0])"
    $artifactUrl
}

$validationResult = @()

if ($memoryLimit -eq "") {
    $memoryLimit = "8G"
}

$assignPremiumPlan = $false
$tenant = "default"

if ($installApps                    -is [String]) { $installApps = @($installApps.Split(',').Trim() | Where-Object { $_ }) }
if ($previousApps                   -is [String]) { $previousApps = @($previousApps.Split(',').Trim() | Where-Object { $_ }) }
if ($apps                           -is [String]) { $apps = @($apps.Split(',').Trim()  | Where-Object { $_ }) }
if ($countries                      -is [String]) { $countries = @($countries.Split(',').Trim() | Where-Object { $_ }) }
if ($affixes                        -is [String]) { $affixes = @($affixes.Split(',').Trim() | Where-Object { $_ }) }
if ($supportedCountries             -is [String]) { $supportedCountries = @($supportedCountries.Split(',').Trim() | Where-Object { $_ }) }

$countries = $countries | Where-Object { $_ } | ForEach-Object { getCountryCode -countryCode $_ }
$supportedCountries = $supportedCountries | Where-Object { $_ } | ForEach-Object { getCountryCode -countryCode $_ }

Write-Host -ForegroundColor Yellow @'
  _____ _
 | __ \ | |
 | |__) |_ _ _ __ __ _ _ __ ___ ___| |_ ___ _ __ ___
 | ___/ _` | '__/ _` | '_ ` _ \ / _ \ __/ _ \ '__/ __|
 | | | (_| | | | (_| | | | | | | __/ |_ __/ | \__ \
 |_| \__,_|_| \__,_|_| |_| |_|\___|\__\___|_| |___/
 
'@

Write-Host -NoNewLine -ForegroundColor Yellow "Container name "; Write-Host $containerName
Write-Host -NoNewLine -ForegroundColor Yellow "Image name "; Write-Host $imageName
Write-Host -NoNewLine -ForegroundColor Yellow "Credential ";
if ($credential) {
    Write-Host "Specified"
}
else {
    $password = GetRandomPassword
    Write-Host "admin/$password"
    $credential= (New-Object pscredential 'admin', (ConvertTo-SecureString -String $password -AsPlainText -Force))
}
Write-Host -NoNewLine -ForegroundColor Yellow "License file "; if ($licenseFile) { Write-Host "Specified" } else { "Not specified" }
Write-Host -NoNewLine -ForegroundColor Yellow "MemoryLimit "; Write-Host $memoryLimit
Write-Host -NoNewLine -ForegroundColor Yellow "validateVersion "; Write-Host $validateVersion
Write-Host -NoNewLine -ForegroundColor Yellow "validateCurrent "; Write-Host $validateCurrent
Write-Host -NoNewLine -ForegroundColor Yellow "validateNextMinor "; Write-Host $validateNextMinor
Write-Host -NoNewLine -ForegroundColor Yellow "validateNextMajor "; Write-Host $validateNextMajor
Write-Host -NoNewLine -ForegroundColor Yellow "SasToken "; if ($sasToken) { Write-Host "Specified" } else { Write-Host "Not Specified" }
Write-Host -NoNewLine -ForegroundColor Yellow "countries "; Write-Host ([string]::Join(',',$countries))
Write-Host -NoNewLine -ForegroundColor Yellow "affixes "; Write-Host ([string]::Join(',',$affixes))
Write-Host -NoNewLine -ForegroundColor Yellow "supportedCountries "; Write-Host ([string]::Join(',',$supportedCountries))
Write-Host -NoNewLine -ForegroundColor Yellow "vsixFile "; Write-Host $vsixFile

Write-Host -ForegroundColor Yellow "Install Apps"
if ($installApps) { $installApps | ForEach-Object { Write-Host "- $_" } } else { Write-Host "- None" }
Write-Host -ForegroundColor Yellow "Previous Apps"
if ($previousApps) { $previousApps | ForEach-Object { Write-Host "- $_" } } else { Write-Host "- None" }
Write-Host -ForegroundColor Yellow "Apps"
if ($apps) { $apps | ForEach-Object { Write-Host "- $_" } }  else { Write-Host "- None" }

if ($DockerPull) {
    Write-Host -ForegroundColor Yellow "DockerPull override"; Write-Host $DockerPull.ToString()
}
else {
    $DockerPull = { Param($imageName) docker pull $imageName }
}
if ($NewBcContainer) {
    Write-Host -ForegroundColor Yellow "NewBccontainer override"; Write-Host $NewBcContainer.ToString()
}
else {
    $NewBcContainer = { Param([Hashtable]$parameters) New-BcContainer @parameters; Invoke-ScriptInBcContainer $parameters.ContainerName -scriptblock { $progressPreference = 'SilentlyContinue' } }
}
if ($PublishBcContainerApp) {
    Write-Host -ForegroundColor Yellow "PublishBcContainerApp override"; Write-Host $PublishBcContainerApp.ToString()
}
else {
    $PublishBcContainerApp = { Param([Hashtable]$parameters) Publish-BcContainerApp @parameters }
}
if ($CompileAppInBcContainer) {
    Write-Host -ForegroundColor Yellow "CompileAppInBcContainer override"; Write-Host $CompileAppInBcContainer.ToString()
}
else {
    $CompileAppInBcContainer = { Param([Hashtable]$parameters) Compile-AppInBcContainer @parameters }
}
if ($GetBcContainerAppInfo) {
    Write-Host -ForegroundColor Yellow "GetBcContainerAppInfo override"; Write-Host $GetBcContainerAppInfo.ToString()
}
else {
    $GetBcContainerAppInfo = { Param([Hashtable]$parameters) Get-BcContainerAppInfo @parameters }
}
if ($RemoveBcContainer) {
    Write-Host -ForegroundColor Yellow "RemoveBcContainer override"; Write-Host $RemoveBcContainer.ToString()
}
else {
    $RemoveBcContainer = { Param([Hashtable]$parameters) Remove-BcContainer @parameters }
}

Measure-Command {

Write-Host -ForegroundColor Yellow @'
 
  _____ _ _ _ _ _
 | __ \ | | (_) (_) (_)
 | |__) | _| | |_ _ __ __ _ __ _ ___ _ __ ___ _ __ _ ___ _ _ __ ___ __ _ __ _ ___
 | ___/ | | | | | | '_ \ / _` | / _` |/ _ \ '_ \ / _ \ '__| |/ __| | | '_ ` _ \ / _` |/ _` |/ _ \
 | | | |_| | | | | | | | (_| | | (_| | __/ | | | __/ | | | (__ | | | | | | | (_| | (_| | __/
 |_| \__,_|_|_|_|_| |_|\__, | \__, |\___|_| |_|\___|_| |_|\___| |_|_| |_| |_|\__,_|\__, |\___|
                           __/ | __/ | __/ |
                          |___/ |___/ |___/
 
'@


Write-Host "Pulling $useGenericImage"

Invoke-Command -ScriptBlock $DockerPull -ArgumentList $useGenericImage
} | ForEach-Object { Write-Host -ForegroundColor Yellow "`nPulling generic image took $([int]$_.TotalSeconds) seconds" }

Measure-Command {

0..3 | ForEach-Object {

$artifactUrl = ""
if ($_ -eq 0 -and $validateVersion) {
    $artifactUrl = DetermineArtifactsToUse -version $validateVersion -countries $countries -select Latest
}
elseif ($_ -eq 1 -and $validateCurrent) {
    $artifactUrl = DetermineArtifactsToUse -countries $countries -select Current
}
elseif ($_ -eq 2 -and $validateNextMinor) {
    $artifactUrl = DetermineArtifactsToUse -countries $countries -select NextMinor -sasToken $sasToken
}
elseif ($_ -eq 1 -and $validateNextMajor) {
    $artifactUrl = DetermineArtifactsToUse -countries $countries -select NextMajor -sasToken $sasToken
}

if ($artifactUrl) {

$error = $null
$prevProgressPreference = $progressPreference
$progressPreference = 'SilentlyContinue'

try {

$countries | % {
$validateCountry = $_

Write-Host -ForegroundColor Yellow @'
 
   _____ _ _ _ _
  / ____| | | (_) | | (_)
 | | _ __ ___ __ _| |_ _ _ __ __ _ ___ ___ _ __ | |_ __ _ _ _ __ ___ _ __
 | | | '__/ _ \/ _` | __| | '_ \ / _` | / __/ _ \| '_ \| __/ _` | | '_ \ / _ \ '__|
 | |____| | | __/ (_| | |_| | | | | (_| | | (__ (_) | | | | |_ (_| | | | | | __/ |
  \_____|_| \___|\__,_|\__|_|_| |_|\__, | \___\___/|_| |_|\__\__,_|_|_| |_|\___|_|
                                     __/ |
                                    |___/
 
'@


Measure-Command {

    $artifactSegments = $artifactUrl.Split('?')[0].Split('/')
    $artifactUrl = $artifactUrl.Replace("/$($artifactSegments[4])/$($artifactSegments[5])","/$($artifactSegments[4])/$validateCountry")
    Write-Host -ForegroundColor Yellow "Creating container for country $validateCountry"

    $Parameters = @{
        "accept_eula" = $true
        "containerName" = $containerName
        "imageName" = $imageName
        "artifactUrl" = $artifactUrl
        "useGenericImage" = $useGenericImage
        "Credential" = $credential
        "auth" = 'UserPassword'
        "updateHosts" = $true
        "vsixFile" = $vsixFile
        "licenseFile" = $licenseFile
        "EnableTaskScheduler" = $true
        "AssignPremiumPlan" = $assignPremiumPlan
        "MemoryLimit" = $memoryLimit
    }

    Invoke-Command -ScriptBlock $NewBcContainer -ArgumentList $Parameters

} | ForEach-Object { Write-Host -ForegroundColor Yellow "`nCreating container took $([int]$_.TotalSeconds) seconds" }

if ($installApps) {
Write-Host -ForegroundColor Yellow @'
 
  _____ _ _ _ _
 |_ _| | | | | (_)
   | | _ __ ___| |_ __ _| | |_ _ __ __ _ __ _ _ __ _ __ ___
   | | | '_ \/ __| __/ _` | | | | '_ \ / _` | / _` | '_ \| '_ \/ __|
  _| |_| | | \__ \ |_ (_| | | | | | | | (_| | | (_| | |_) | |_) \__ \
 |_____|_| |_|___/\__\__,_|_|_|_|_| |_|\__, | \__,_| .__/| .__/|___/
                                        __/ | | | | |
                                       |___/ |_| |_|
 
'@

Measure-Command {

    $installApps | ForEach-Object{
        $Parameters = @{
            "containerName" = $containerName
            "tenant" = $tenant
            "credential" = $credential
            "appFile" = $_
            "skipVerification" = $skipVerification
            "sync" = $true
            "install" = $true
        }
        Invoke-Command -ScriptBlock $PublishBcContainerApp -ArgumentList $Parameters
    }

} | ForEach-Object { Write-Host -ForegroundColor Yellow "`nInstalling apps took $([int]$_.TotalSeconds) seconds" }
}

Write-Host -ForegroundColor Yellow @'
  _____ _ _____ _____
 | __ \ (_) /\ / ____| / ____|
 | |__) | _ _ __ _ __ _ _ __ __ _ / \ _ __ _ __| (___ ___ _ _ _ __ ___ ___| | ___ _ __
 | _ / | | | '_ \| '_ \| | '_ \ / _` | / /\ \ | '_ \| '_ \\___ \ / _ \| | | | '__/ __/ _ \ | / _ \| '_ \
 | | \ \ |_| | | | | | | | | | | | (_| | / ____ \| |_) | |_) |___) | (_) | |_| | | | (__ __/ |____ (_) | |_) |
 |_| \_\__,_|_| |_|_| |_|_|_| |_|\__, | /_/ \_\ .__/| .__/_____/ \___/ \__,_|_| \___\___|\_____\___/| .__/
                                   __/ | | | | | | |
                                  |___/ |_| |_| |_|
'@


$validationResult += @(Run-AlCops `
    -containerName $containerName `
    -credential $credential `
    -previousApps $previousApps `
    -apps $apps `
    -affixes $affixes `
    -supportedCountries $supportedCountries `
    -enableAppSourceCop `
    -failOnError:$failOnError `
    -ignoreWarnings:(!$includeWarnings))

if ($previousApps) {
Write-Host -ForegroundColor Yellow @'
 
  _____ _ _ _ _ _____ _
 |_ _| | | | | (_) | __ \ (_) /\
   | | _ __ ___| |_ __ _| | |_ _ __ __ _ | |__) | __ _____ ___ ___ _ _ ___ / \ _ __ _ __ ___
   | | | '_ \/ __| __/ _` | | | | '_ \ / _` | | ___/ '__/ _ \ \ / / |/ _ \| | | / __| / /\ \ | '_ \| '_ \/ __|
  _| |_| | | \__ \ |_ (_| | | | | | | | (_| | | | | | | __/\ V /| | (_) | |_| \__ \ / ____ \| |_) | |_) \__ \
 |_____|_| |_|___/\__\__,_|_|_|_|_| |_|\__, | |_| |_| \___| \_/ |_|\___/ \__,_|___/ /_/ \_\ .__/| .__/|___/
                                        __/ | | | | |
                                       |___/ |_| |_|
 
'@

Measure-Command {
    $previousApps | ForEach-Object{
        $Parameters = @{
            "containerName" = $containerName
            "tenant" = $tenant
            "credential" = $credential
            "appFile" = $_
            "skipVerification" = $skipVerification
            "sync" = $true
            "install" = $true
            "useDevEndpoint" = $false
        }
        Invoke-Command -ScriptBlock $PublishBcContainerApp -ArgumentList $Parameters
    }

} | ForEach-Object { Write-Host -ForegroundColor Yellow "`nInstalling apps took $([int]$_.TotalSeconds) seconds" }
}

Write-Host -ForegroundColor Yellow @'
 
  _____ _ _ _ _ _
 | __ \ | | | (_) | | (_) /\
 | |__) | _| |__ | |_ ___| |__ _ _ __ __ _ / \ _ __ _ __ ___
 | ___/ | | | '_ \| | / __| '_ \| | '_ \ / _` | / /\ \ | '_ \| '_ \/ __|
 | | | |_| | |_) | | \__ \ | | | | | | | (_| | / ____ \| |_) | |_) \__ \
 |_| \__,_|_.__/|_|_|___/_| |_|_|_| |_|\__, | /_/ \_\ .__/| .__/|___/
                                           __/ | | | | |
                                          |___/ |_| |_|
 
'@

Measure-Command {

$installedApps = Invoke-Command -ScriptBlock $GetBcContainerAppInfo -ArgumentList $Parameters

$apps | ForEach-Object {
    
    $tmpFolder = Join-Path $ENV:TEMP ([Guid]::NewGuid().ToString())
    Extract-AppFileToFolder -appFilename $_ -appFolder $tmpFolder -generateAppJson
    $appJsonFile = Join-Path $tmpfolder "app.json"
    $appJson = Get-Content $appJsonFile | ConvertFrom-Json
    Remove-Item $tmpFolder -Recurse -Force

    $installedApp = $false
    if ($installedApps | Where-Object { $_.Name -eq $appJson.Name -and $_.Publisher -eq $appJson.Publisher -and $_.AppId -eq $appJson.Id }) {
        $installedApp = $true
    }

    $Parameters = @{
        "containerName" = $containerName
        "tenant" = $tenant
        "credential" = $credential
        "appFile" = $_
        "skipVerification" = $skipVerification
        "sync" = $true
        "install" = !$installedApp
        "upgrade" = $installedApp
    }

    Invoke-Command -ScriptBlock $PublishBcContainerApp -ArgumentList $Parameters

}

} | ForEach-Object { Write-Host -ForegroundColor Yellow "`nPublishing apps took $([int]$_.TotalSeconds) seconds" }
}

} catch {
    $error = $_
}
finally {
    $progressPreference = $prevProgressPreference
}

Write-Host -ForegroundColor Yellow @'
 
  _____ _ _____ _ _
 | __ \ (_) / ____| | | (_)
 | |__) |___ _ __ ___ _____ ___ _ __ __ _ | | ___ _ __ | |_ __ _ _ _ __ ___ _ __
 | _ // _ \ '_ ` _ \ / _ \ \ / / | '_ \ / _` | | | / _ \| '_ \| __/ _` | | '_ \ / _ \ '__|
 | | \ \ __/ | | | | | (_) \ V /| | | | | (_| | | |____ (_) | | | | |_ (_| | | | | | __/ |
 |_| \_\___|_| |_| |_|\___/ \_/ |_|_| |_|\__, | \_____\___/|_| |_|\__\__,_|_|_| |_|\___|_|
                                           __/ |
                                          |___/
 
'@

Measure-Command {

    $Parameters = @{
        "containerName" = $containerName
    }
    Invoke-Command -ScriptBlock $RemoveBcContainer -ArgumentList $Parameters
   
} | ForEach-Object { Write-Host -ForegroundColor Yellow "`nRemoving container took $([int]$_.TotalSeconds) seconds" }

if ($error) {
    throw $error
}

}

}

} | ForEach-Object { Write-Host -ForegroundColor Yellow "`nAL Validation finished in $([int]$_.TotalSeconds) seconds" }

$validationResult

}
Export-ModuleMember -Function Run-AlValidation