PSPortainer.psm1

#Region '.\prefix.ps1' 0
# The content of this file will be prepended to the top of the psm1 module file. This is useful for custom module setup is needed on import.
#EndRegion '.\prefix.ps1' 2
#Region '.\Classes\PortainerContainerProcess.class.ps1' 0
class PortainerContainerProcess
{
    [string]$UserID
    [int]$ProcessID
    [int]$ParentProcessID
    [int]$C
    [string]$STIME
    [string]$Terminal
    [timespan]$TIME
    [string]$Command

    PortainerContainerProcess ()
    {
        $this.UserID = ''
        $this.ProcessID = 0
        $this.ParentProcessID = 0
        $this.C = 0
        $this.STime = ''
        $this.Terminal = ''
        $this.Time = New-TimeSpan
        $this.Command = ''
    }

    PortainerContainerProcess ([string[]]$Object)
    {
        $this.UserID = $Object[0]
        $this.ProcessID = $Object[1]
        $this.ParentProcessID = $Object[2]
        $this.C = $Object[3]
        $this.STime = $Object[4]
        $this.Terminal = $Object[5]
        $this.Time = [timespan]::Parse($Object[6])
        $this.Command = $Object[7]
    }

    [string] ToString ()
    {
        return "ProcessID: $($this.ProcessID)"
    }
}
#EndRegion '.\Classes\PortainerContainerProcess.class.ps1' 41
#Region '.\Classes\PortainerSession.class.ps1' 0
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'JWT token retreived in plain text')]
class PortainerSession
{
    [string]$BaseUri
    [string]$AuthMethod
    [securestring]$AccessToken
    [pscredential]$Credential
    [securestring]$JWT
    [string]$APIUri
    [string]$InstanceID
    [string]$PortainerVersion
    [string]$DefaultDockerEndpoint
    [string]$SessionID

    PortainerSession ([string]$BaseUri, [securestring]$AccessToken)
    {
        Write-Debug -Message 'PortainerSession.Class; Running constructor accesstoken'
        $this.SessionID = (New-Guid).Guid
        $this.BaseUri = $BaseUri
        $this.APIUri = "$BaseUri/api"
        $this.AuthMethod = 'AccessToken'
        $this.AccessToken = $AccessToken
        $this.GetStatus()
        $this.ResolveDockerEndpoint()
        Write-Verbose -Message "Connected to portainer instance at $($this.BaseUri) with AccessToken"
    }

    PortainerSession ([string]$BaseUri, [pscredential]$Credential)
    {
        Write-Debug -Message 'PortainerSession.Class; Running constructor credential'
        $this.SessionID = (New-Guid).Guid
        $this.BaseUri = $BaseUri
        $this.APIUri = "$BaseUri/api"
        $this.AuthMethod = 'Credential'
        $this.Credential = $Credential
        $this.AuthenticateCredential()
        $this.GetStatus()
        $this.ResolveDockerEndpoint()
        Write-Verbose -Message "Connected to portainer instance at $($this.BaseUri) with Credentials"
    }

    hidden ResolveDockerEndpoint ()
    {
        [array]$AllEndpoints = InvokePortainerRestMethod -Method Get -RelativePath '/endpoints' -PortainerSession $this
        if ($AllEndpoints.Count -eq 1)
        {
            $this.DefaultDockerEndpoint = $AllEndpoints[0].Name
        }
    }

    hidden AuthenticateCredential()
    {
        $JWTResponse = InvokePortainerRestMethod -NoAuth -Method:'Post' -PortainerSession $this -RelativePath '/auth' -Body @{password = $this.Credential.GetNetworkCredential().Password; username = $this.Credential.Username }
        $this.JWT = ConvertTo-SecureString -String $JWTResponse.JWT -AsPlainText -Force
        Remove-Variable -Name JWTResponse
    }

    hidden GetStatus()
    {
        $Status = InvokePortainerRestMethod -NoAuth -Method:'Get' -PortainerSession $this -RelativePath '/status'
        $this.PortainerVersion = $Status.Version
        $this.InstanceID = $Status.InstanceID
        Remove-Variable -Name Status
    }
}
#EndRegion '.\Classes\PortainerSession.class.ps1' 66
#Region '.\private\Assert-FolderExist.ps1' 0
function Assert-FolderExist
{
    <#
    .SYNOPSIS
        Verify and create folder
    .DESCRIPTION
        Verifies that a folder path exists, if not it will create it
    .PARAMETER Path
        Defines the path to be validated
    .EXAMPLE
        'C:\Temp' | Assert-FolderExist
 
        This will verify that the path exists and if it does not the folder will be created
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [string]
        $Path
    )

    process
    {
        $exists = Test-Path -Path $Path -PathType Container
        if (!$exists)
        {
            $null = New-Item -Path $Path -ItemType Directory
        }
    }
}
#EndRegion '.\private\Assert-FolderExist.ps1' 31
#Region '.\private\enums.ps1' 0

enum ContainerCondition
{
    notrunning
    nextexit
    removed
}
#EndRegion '.\private\enums.ps1' 8
#Region '.\private\GetNonNullOrEmptyFromList.ps1' 0
<#PSScriptInfo
{
  "VERSION": "1.0.0",
  "GUID": "4a356917-cc37-4ba0-b54b-82ead703ac2a",
  "FILENAME": "GetNonNullOrEmptyFromList.ps1",
  "AUTHOR": "Hannes Palmquist",
  "CREATEDDATE": "2022-10-26",
  "COMPANYNAME": [],
  "COPYRIGHT": "(c) 2022, Hannes Palmquist, All Rights Reserved"
}
PSScriptInfo#>

function GetNonNullOrEmptyFromList
{
    <#
    .DESCRIPTION
        Selects a non null, non empty value from the provided array, if more than one is found it coniders the lowest value in the array with priority.
    .PARAMETER Array
        String array containing the values to evaluate, null and empty strings are allowed as these will not be considered for selection, efectievly filtering them out.
    .PARAMETER AskIfNoneIsFound
        Defines if the script should try and ask the user for a value instead of failing.
    .PARAMETER PropertyName
        If the parameter AskIfNoneIsFound is selected this string is used to ask the user for a value
    .EXAMPLE
        GetNonNullOrEmptyFromList -Array @('test1',$null,'','test2')
 
        The example above would return test1 as it is not null or empty and by beeing defined earlier in the array than test2.
    #>


    [CmdletBinding()] # Enabled advanced function support
    [OutputType([string])]
    param(
        [Parameter(Mandatory)][AllowNull()][AllowEmptyString()][string[]]$Array,
        [Parameter(Mandatory, ParameterSetName = 'Ask')][switch]$AskIfNoneIsFound,
        [Parameter(Mandatory, ParameterSetName = 'Ask')][string]$PropertyName
    )

    $Selected = $null

    # Reverse array to process the first item as highest prio
    [array]::Reverse($Array)
    foreach ($item in $Array)
    {
        if (-not ([string]::IsNullOrEmpty($Item)))
        {
            $Selected = $item
        }
    }

    # Ask user if no cadidate can be selected
    if (-not $Selected -and $AskIfNoneIsFound)
    {
        $Selected = Read-Host -Prompt "Please enter a value for $PropertyName"
    }

    # Verify that user input is not empty before returning
    if ([string]::IsNullOrEmpty($Selected))
    {
        Write-Error -Message 'GetNonNullOrEmptyFromList; Unable to find candidate' -ErrorAction Stop
    }
    else
    {
        return $Selected
    }
}



#EndRegion '.\private\GetNonNullOrEmptyFromList.ps1' 68
#Region '.\private\Invoke-GarbageCollect.ps1' 0
function Invoke-GarbageCollect
{
    <#
    .SYNOPSIS
        Calls system.gc collect method. Purpose is mainly for readability.
    .DESCRIPTION
        Calls system.gc collect method. Purpose is mainly for readability.
    .EXAMPLE
        Invoke-GarbageCollect
    #>

    [system.gc]::Collect()
}
#EndRegion '.\private\Invoke-GarbageCollect.ps1' 13
#Region '.\private\InvokePortainerRestMethod.ps1' 0
<#PSScriptInfo
{
  "VERSION": "1.0.0",
  "GUID": "0cf86f27-137b-43ee-a194-ec0207d85ce5",
  "FILENAME": "InvokePortainerRestMethod.ps1",
  "AUTHOR": "Hannes Palmquist",
  "CREATEDDATE": "2022-10-23",
  "COMPANYNAME": [],
  "COPYRIGHT": "(c) 2022, Hannes Palmquist, All Rights Reserved"
}
PSScriptInfo#>

function InvokePortainerRestMethod
{
    <#
    .DESCRIPTION
        Function that is responsible for making the rest api web call
    .PARAMETER NoAuth
        Specifies that the REST API call do not need authentication
    .PARAMETER Method
        Defines the method to use when calling the REST API, valid values GET,POST etc.
    .PARAMETER PortainerSession
        A PortainerSession object to use for the call.
    .PARAMETER RelativePath
        The REST API path relative to the base URL
    .PARAMETER Body
        Defines body attributes for the REST API call
    .PARAMETER Headers
        Defines header attributes for the REST API call
    .EXAMPLE
        InvokePortainerRestMethod
    #>

    [CmdletBinding()] # Enabled advanced function support
    param(
        [switch]$NoAuth,
        [string]$Method,
        [portainersession]$PortainerSession,
        [string]$RelativePath,
        [hashtable]$Body = @{},
        [hashtable]$Headers = @{}
    )

    if (-not $PortainerSession)
    {
        Write-Debug -Message 'InvokePortainerRestMethod; No PortainerSession passed as parameter'
        if ($script:PortainerSession)
        {
            Write-Debug -Message 'InvokePortainerRestMethod; PortainerSession found in script scope'
            $PortainerSession = $script:PortainerSession
        }
        else
        {
            Write-Error -Message 'No Portainer Session established, please call Connect-Portainer'
        }
    }

    $InvokeRestMethodSplat = @{
        Method = $Method
        Uri    = "$($PortainerSession.ApiUri)$($RelativePath)"
    }

    if (-not $NoAuth)
    {
        switch ($PortainerSession.AuthMethod)
        {
            'Credential'
            {
                $InvokeRestMethodSplat.Authentication = 'Bearer'
                $InvokeRestMethodSplat.Token = $PortainerSession.JWT
            }
            'AccessToken'
            {
                $Headers.'X-API-Key' = (ConvertFrom-SecureString -SecureString $PortainerSession.AccessToken -AsPlainText)
            }
        }
    }

    if ($Headers.Keys.Count -gt 0)
    {
        $InvokeRestMethodSplat.Headers = $Headers
    }
    if ($InvokeRestMethodSplat.Method -eq 'Get')
    {
        if ($Body.Keys.Count -gt 0 )
        {
            $InvokeRestMethodSplat.Body = $Body
        }
    }
    elseif ($InvokeRestMethodSplat.Method -eq 'Post')
    {
        # Might need to be changed, some post requests require formdata
        $InvokeRestMethodSplat.Body = $Body | ConvertTo-Json -Compress
        $InvokeRestMethodSplat.ContentType = 'application/json'
    }


    Write-Debug -Message "InvokePortainerRestMethod; Calling Invoke-RestMethod with settings`r`n$($InvokeRestMethodSplat | ConvertTo-Json)"
    Invoke-RestMethod @InvokeRestMethodSplat -Verbose:$false | ForEach-Object { $_ }
}


#endregion
#EndRegion '.\private\InvokePortainerRestMethod.ps1' 102
#Region '.\private\pslog.ps1' 0
function pslog
{
    <#
    .SYNOPSIS
        This is simple logging function that automatically log to file. Logging to console is maintained.
    .DESCRIPTION
        This is simple logging function that automatically log to file. Logging to console is maintained.
    .PARAMETER Severity
        Defines the type of log, valid vales are, Success,Info,Warning,Error,Verbose,Debug
    .PARAMETER Message
        Defines the message for the log entry
    .PARAMETER Source
        Defines a source, this is useful to separate log entries in categories for different stages of a process or for each function, defaults to default
    .PARAMETER Throw
        Specifies that when using severity error pslog will throw. This is useful in catch statements so that the terminating error is propagated upwards in the stack.
    .PARAMETER LogDirectoryOverride
        Defines a hardcoded log directory to write the log file to. This defaults to %appdatalocal%\<modulename\logs.
    .PARAMETER DoNotLogToConsole
        Specifies that logs should only be written to the log file and not to the console.
    .EXAMPLE
        pslog Verbose 'Successfully wrote to logfile'
        Explanation of the function or its result. You can include multiple examples with additional .EXAMPLE lines
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Sole purpose of function is logging, including console')]
    [cmdletbinding()]
    param(
        [parameter(Position = 0)]
        [ValidateSet('Success', 'Info', 'Warning', 'Error', 'Verbose', 'Debug')]
        [Alias('Type')]
        [string]
        $Severity,

        [parameter(Mandatory, Position = 1)]
        [string]
        $Message,

        [parameter(position = 2)]
        [string]
        $source = 'default',

        [parameter(Position = 3)]
        [switch]
        $Throw,

        [parameter(Position = 4)]
        [string]
        $LogDirectoryOverride,

        [parameter(Position = 5)]
        [switch]
        $DoNotLogToConsole
    )

    begin
    {
        if (-not $LogDirectoryOverride)
        {
            $localappdatapath = [Environment]::GetFolderPath('localapplicationdata') # ie C:\Users\<username>\AppData\Local
            $modulename = $MyInvocation.MyCommand.Module
            $logdir = "$localappdatapath\$modulename\logs"
        }
        else
        {
            $logdir = $LogDirectoryOverride
        }
        $logdir | Assert-FolderExist -Verbose:$VerbosePreference
        $timestamp = (Get-Date)
        $logfilename = ('{0}.log' -f $timestamp.ToString('yyy-MM-dd'))
        $timestampstring = $timestamp.ToString('yyyy-MM-ddThh:mm:ss.ffffzzz')
    }

    process
    {
        switch ($Severity)
        {
            'Success'
            {
                "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                if (-not $DoNotLogToConsole)
                {
                    Write-Host -Object "SUCCESS: $timestampstring`t$source`t$message" -ForegroundColor Green
                }
            }
            'Info'
            {
                "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                if (-not $DoNotLogToConsole)
                {
                    Write-Information -MessageData "$timestampstring`t$source`t$message"
                }
            }
            'Warning'
            {
                "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                if (-not $DoNotLogToConsole)
                {
                    Write-Warning -Message "$timestampstring`t$source`t$message"
                }
            }
            'Error'
            {
                "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                if (-not $DoNotLogToConsole)
                {
                    Write-Error -Message "$timestampstring`t$source`t$message"
                }
                if ($throw)
                {
                    throw
                }
            }
            'Verbose'
            {
                if ($VerbosePreference -ne 'SilentlyContinue')
                {
                    "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                }
                if (-not $DoNotLogToConsole)
                {
                    Write-Verbose -Message "$timestampstring`t$source`t$message"
                }
            }
            'Debug'
            {
                if ($DebugPreference -ne 'SilentlyContinue')
                {
                    "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                }
                if (-not $DoNotLogToConsole)
                {
                    Write-Debug -Message "$timestampstring`t$source`t$message"
                }
            }
        }
    }
}
#EndRegion '.\private\pslog.ps1' 137
#Region '.\private\ResolveEndpointID.ps1' 0
<#PSScriptInfo
{
  "VERSION": "1.0.0",
  "GUID": "bc240721-1c04-45de-bc7b-3945f6220f0b",
  "FILENAME": "ResolveEndpointID.ps1",
  "AUTHOR": "Hannes Palmquist",
  "CREATEDDATE": "2022-10-30",
  "COMPANYNAME": "GetPS",
  "COPYRIGHT": "(c) 2022, Hannes Palmquist, All Rights Reserved"
}
PSScriptInfo#>

function ResolveEndpointID
{
    <#
    .DESCRIPTION
        Determines the source of endpoint and returns the endpoint ID
    .PARAMETER Endpoint
        Defines the portainer endpoint to use when retreiving containers. If not specified the portainer sessions default docker endpoint value is used.
 
        Use Get-PSession to see what endpoint is selected
 
        Use Select-PEndpoint to change the default docker endpoint in the portainer session.
 
        -Endpoint 'local'
    .PARAMETER Session
        Optionally define a portainer session object to use. This is useful when you are connected to more than one portainer instance.
 
        -Session $Session
    .EXAMPLE
        ResolveEndpointID
    #>


    [CmdletBinding()]
    param(
        [Parameter()][AllowNull()][AllowEmptyString()][string]$Endpoint,
        [Parameter()][PortainerSession]$Session = $null
    )

    $EndpointName = GetNonNullOrEmptyFromList -Array @($Endpoint, $Session.DefaultDockerEndpoint) -AskIfNoneIsFound -PropertyName 'EndpointName'
    Write-Debug "ResolveEndpointID; Endpoint $EndpointName select"

    $EndpointId = Get-PEndpoint -SearchString $EndpointName | Select-Object -ExpandProperty Id

    if ($EndpointId)
    {
        return $EndpointID
    }
    else
    {
        throw 'No endpoint found'
    }
}
#EndRegion '.\private\ResolveEndpointID.ps1' 53
#Region '.\private\Write-PSProgress.ps1' 0
function Write-PSProgress
{
    <#
    .SYNOPSIS
        Wrapper for PSProgress
    .DESCRIPTION
        This function will automatically calculate items/sec, eta, time remaining
        as well as set the update frequency in case the there are a lot of items processing fast.
    .PARAMETER Activity
        Defines the activity name for the progressbar
    .PARAMETER Id
        Defines a unique ID for this progressbar, this is used when nesting progressbars
    .PARAMETER Target
        Defines a arbitrary text for the currently processed item
    .PARAMETER ParentId
        Defines the ID of a parent progress bar
    .PARAMETER Completed
        Explicitly tells powershell to set the progress bar as completed removing
        it from view. In some cases the progress bar will linger if this is not done.
    .PARAMETER Counter
        The currently processed items counter
    .PARAMETER Total
        The total number of items to process
    .PARAMETER StartTime
        Sets the start datetime for the progressbar, this is required to calculate items/sec, eta and time remaining
    .PARAMETER DisableDynamicUpdateFrquency
        Disables the dynamic update frequency function and every item will update the status of the progressbar
    .PARAMETER NoTimeStats
        Disables calculation of items/sec, eta and time remaining
    .EXAMPLE
        1..10000 | foreach-object -begin {$StartTime = Get-Date} -process {
            Write-PSProgress -Activity 'Looping' -Target $PSItem -Counter $PSItem -Total 10000 -StartTime $StartTime
        }
        Explanation of the function or its result. You can include multiple examples with additional .EXAMPLE lines
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Standard')]
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Completed')]
        [string]
        $Activity,

        [Parameter(Position = 1, ParameterSetName = 'Standard')]
        [Parameter(Position = 1, ParameterSetName = 'Completed')]
        [ValidateRange(0, 2147483647)]
        [int]
        $Id,

        [Parameter(Position = 2, ParameterSetName = 'Standard')]
        [string]
        $Target,

        [Parameter(Position = 3, ParameterSetName = 'Standard')]
        [Parameter(Position = 3, ParameterSetName = 'Completed')]
        [ValidateRange(-1, 2147483647)]
        [int]
        $ParentId,

        [Parameter(Position = 4, ParameterSetname = 'Completed')]
        [switch]
        $Completed,

        [Parameter(Mandatory = $true, Position = 5, ParameterSetName = 'Standard')]
        [long]
        $Counter,

        [Parameter(Mandatory = $true, Position = 6, ParameterSetName = 'Standard')]
        [long]
        $Total,

        [Parameter(Position = 7, ParameterSetName = 'Standard')]
        [datetime]
        $StartTime,

        [Parameter(Position = 8, ParameterSetName = 'Standard')]
        [switch]
        $DisableDynamicUpdateFrquency,

        [Parameter(Position = 9, ParameterSetName = 'Standard')]
        [switch]
        $NoTimeStats
    )

    # Define current timestamp
    $TimeStamp = (Get-Date)

    # Define a dynamic variable name for the global starttime variable
    $StartTimeVariableName = ('ProgressStartTime_{0}' -f $Activity.Replace(' ', ''))

    # Manage global start time variable
    if ($PSBoundParameters.ContainsKey('Completed') -and (Get-Variable -Name $StartTimeVariableName -Scope Global -ErrorAction SilentlyContinue))
    {
        # Remove the global starttime variable if the Completed switch parameter is users
        try
        {
            Remove-Variable -Name $StartTimeVariableName -ErrorAction Stop -Scope Global
        }
        catch
        {
            throw $_
        }
    }
    elseif (-not (Get-Variable -Name $StartTimeVariableName -Scope Global -ErrorAction SilentlyContinue))
    {
        # Global variable do not exist, create global variable
        if ($null -eq $StartTime)
        {
            # No start time defined with parameter, use current timestamp as starttime
            Set-Variable -Name $StartTimeVariableName -Value $TimeStamp -Scope Global
            $StartTime = $TimeStamp
        }
        else
        {
            # Start time defined with parameter, use that value as starttime
            Set-Variable -Name $StartTimeVariableName -Value $StartTime -Scope Global
        }
    }
    else
    {
        # Global start time variable is defined, collect and use it
        $StartTime = Get-Variable -Name $StartTimeVariableName -Scope Global -ErrorAction Stop -ValueOnly
    }

    # Define frequency threshold
    $Frequency = [Math]::Ceiling($Total / 100)
    switch ($PSCmdlet.ParameterSetName)
    {
        'Standard'
        {
            # Only update progress is any of the following is true
            # - DynamicUpdateFrequency is disabled
            # - Counter matches a mod of defined frequecy
            # - Counter is 0
            # - Counter is equal to Total (completed)
            if (($DisableDynamicUpdateFrquency) -or ($Counter % $Frequency -eq 0) -or ($Counter -eq 1) -or ($Counter -eq $Total))
            {

                # Calculations for both timestats and without
                $Percent = [Math]::Round(($Counter / $Total * 100), 0)

                # Define count progress string status
                $CountProgress = ('{0}/{1}' -f $Counter, $Total)

                # If percent would turn out to be more than 100 due to incorrect total assignment revert back to 100% to avoid that write-progress throws
                if ($Percent -gt 100)
                {
                    $Percent = 100
                }

                # Define write-progress splat hash
                $WriteProgressSplat = @{
                    Activity         = $Activity
                    PercentComplete  = $Percent
                    CurrentOperation = $Target
                }

                # Add ID if specified
                if ($Id)
                {
                    $WriteProgressSplat.Id = $Id
                }

                # Add ParentID if specified
                if ($ParentId)
                {
                    $WriteProgressSplat.ParentId = $ParentId
                }

                # Calculations for either timestats and without
                if ($NoTimeStats)
                {
                    $WriteProgressSplat.Status = ('{0} - {1}%' -f $CountProgress, $Percent)
                }
                else
                {
                    # Total seconds elapsed since start
                    $TotalSeconds = ($TimeStamp - $StartTime).TotalSeconds

                    # Calculate items per sec processed (IpS)
                    $ItemsPerSecond = ([Math]::Round(($Counter / $TotalSeconds), 2))

                    # Calculate seconds spent per processed item (for ETA)
                    $SecondsPerItem = if ($Counter -eq 0)
                    {
                        0
                    }
                    else
                    {
 ($TotalSeconds / $Counter)
                    }

                    # Calculate seconds remainging
                    $SecondsRemaing = ($Total - $Counter) * $SecondsPerItem
                    $WriteProgressSplat.SecondsRemaining = $SecondsRemaing

                    # Calculate ETA
                    $ETA = $(($Timestamp).AddSeconds($SecondsRemaing).ToShortTimeString())

                    # Add findings to write-progress splat hash
                    $WriteProgressSplat.Status = ('{0} - {1}% - ETA: {2} - IpS {3}' -f $CountProgress, $Percent, $ETA, $ItemsPerSecond)
                }

                # Call writeprogress
                Write-Progress @WriteProgressSplat
            }
        }
        'Completed'
        {
            Write-Progress -Activity $Activity -Id $Id -Completed
        }
    }
}
#EndRegion '.\private\Write-PSProgress.ps1' 214
#Region '.\public\Container\Get-PContainer.ps1' 0
function Get-PContainer
{
    <#
    .DESCRIPTION
        Test Retreives docker containers from Portainer
    .PARAMETER Endpoint
        Defines the portainer endpoint to use when retreiving containers. If not specified the portainer sessions default docker endpoint value is used.
 
        Use Get-PSession to see what endpoint is selected
 
        Use Select-PEndpoint to change the default docker endpoint in the portainer session.
 
        -Endpoint 'local'
    .PARAMETER Id
        Defines the id of the container to retreive.
 
        -Id '<Id>'
    .PARAMETER Session
        Optionally define a portainer session object to use. This is useful when you are connected to more than one portainer instance.
 
        -Session $Session
    .EXAMPLE
        Get-PContainer
 
        Retreives all containers from the endpoint configured on the portainer session default docker endpoint setting.
    .EXAMPLE
        Get-PContainer -Id "<id>"
 
        Retreives a single container object with the specified Id
    .EXAMPLE
        Get-PContainer -Endpoint 'prod'
 
        Retreives all containers on the prod endpoint
    .EXAMPLE
        Get-PContainer -Session $Session
 
        Retreives all containers on the portainer instance defined
    #>


    [CmdletBinding(DefaultParameterSetName = 'list')]
    param(
        [Parameter()][string]$Endpoint,
        [Parameter(ParameterSetName = 'id', ValueFromPipeline)][object[]]$Id,
        [Parameter()][PortainerSession]$Session = $null
    )

    BEGIN
    {
        $Session = Get-PSession -Session:$Session
        $EndpointID = ResolveEndpointID -Endpoint:$Endpoint -Session:$Session

        if ($PSCmdlet.ParameterSetName -eq 'list')
        {
            [array]$Id = InvokePortainerRestMethod -Method Get -RelativePath "/endpoints/$EndpointId/docker/containers/json" -PortainerSession:$Session -Body @{all = $true } | Select-Object -ExpandProperty Id
        }
    }

    PROCESS
    {
        $Id | ForEach-Object {
            if ($PSItem.PSObject.TypeNames -contains 'PortainerContainer')
            {
                $ContainerID = $PSItem.Id
            }
            elseif ($PSItem.GetType().Name -eq 'string')
            {
                $ContainerID = $PSItem
            }
            else
            {
                Write-Error -Message 'Cannot determine input object type' -ErrorAction Stop
            }

            InvokePortainerRestMethod -Method Get -RelativePath "/endpoints/$EndpointId/docker/containers/$ContainerID/json" -PortainerSession:$Session | ForEach-Object { $PSItem.PSobject.TypeNames.Insert(0, 'PortainerContainer'); $_ }
        }
    }
}
#EndRegion '.\public\Container\Get-PContainer.ps1' 78
#Region '.\public\Container\Get-PContainerProcess.ps1' 0
function Get-PContainerProcess
{
    <#
    .DESCRIPTION
        Get processes running inside the container
    .PARAMETER Endpoint
        Defines the portainer endpoint to use when retreiving containers. If not specified the portainer sessions default docker endpoint value is used.
 
        Use Get-PSession to see what endpoint is selected
 
        Use Select-PEndpoint to change the default docker endpoint in the portainer session.
 
        -Endpoint 'local'
    .PARAMETER Id
        Defines the id of the container to retreive.
 
        -Id '<Id>'
    .PARAMETER Session
        Optionally define a portainer session object to use. This is useful when you are connected to more than one portainer instance.
 
        -Session $Session
    .EXAMPLE
        Get-PContainer -Id '<id>' | Get-PContainerProcess
 
        Retreives the running processes in the specified container
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Session', Justification = 'False positive')]
    [CmdletBinding()] # Enabled advanced function support
    param(
        [Parameter()][string]$Endpoint,
        [Parameter(ValueFromPipeline)][object[]]$Id,
        [Parameter()][PortainerSession]$Session = $null
    )

    BEGIN
    {
        $Session = Get-PSession -Session:$Session
        $EndpointID = ResolveEndpointID -Endpoint:$Endpoint -Session:$Session
    }

    PROCESS
    {
        $Id | ForEach-Object {
            if ($PSItem.PSObject.TypeNames -contains 'PortainerContainer')
            {
                $ContainerID = $PSItem.Id
            }
            elseif ($PSItem.GetType().Name -eq 'string')
            {
                $ContainerID = $PSItem
            }
            else
            {
                Write-Error -Message 'Cannot determine input object type' -ErrorAction Stop
            }

            InvokePortainerRestMethod -Method Get -RelativePath "/endpoints/$EndpointId/docker/containers/$ContainerID/top" -PortainerSession:$Session | Select-Object -expand processes | ForEach-Object { [PortainerContainerProcess]::New($PSItem) }
        }
    }
}
#EndRegion '.\public\Container\Get-PContainerProcess.ps1' 61
#Region '.\public\Container\Get-PContainerStatistic.ps1' 0
function Get-PContainerStatistic
{
    <#
    .DESCRIPTION
        Retreives container statistics
    .PARAMETER Endpoint
        Defines the portainer endpoint to use when retreiving containers. If not specified the portainer sessions default docker endpoint value is used.
 
        Use Get-PSession to see what endpoint is selected
 
        Use Select-PEndpoint to change the default docker endpoint in the portainer session.
 
        -Endpoint 'local'
    .PARAMETER Id
        Defines the id of the container to retreive.
 
        -Id '<Id>'
    .PARAMETER Session
        Optionally define a portainer session object to use. This is useful when you are connected to more than one portainer instance.
 
        -Session $Session
    .EXAMPLE
        Get-PContainer -Id '<id>' | Get-PContainerStatistic
 
    #>


    [CmdletBinding()] # Enabled advanced function support
    param(
        [Parameter()][string]$Endpoint,
        [Parameter(ValueFromPipeline)][object[]]$Id,
        [Parameter()][PortainerSession]$Session = $null
    )

    BEGIN
    {
        $Session = Get-PSession -Session:$Session
        $EndpointID = ResolveEndpointID -Endpoint:$Endpoint -Session:$Session
    }

    PROCESS
    {
        $Id | ForEach-Object {
            if ($PSItem.PSObject.TypeNames -contains 'PortainerContainer')
            {
                $ContainerID = $PSItem.Id
            }
            elseif ($PSItem.GetType().Name -eq 'string')
            {
                $ContainerID = $PSItem
            }
            else
            {
                Write-Error -Message 'Cannot determine input object type' -ErrorAction Stop
            }

            InvokePortainerRestMethod -Method Get -RelativePath "/endpoints/$EndpointId/docker/containers/$ContainerID/stats" -PortainerSession:$Session -Body @{'stream' = $false; 'one-shot' = $true }
        }
    }
}
#endregion
#EndRegion '.\public\Container\Get-PContainerStatistic.ps1' 61
#Region '.\public\Container\New-PContainer.ps1' 0
function New-PContainer
{
    <#
    .DESCRIPTION
        Create a new container in portainer
    .PARAMETER Endpoint
        Defines the portainer endpoint to use when retreiving containers. If not specified the portainer sessions default docker endpoint value is used.
 
        Use Get-PSession to see what endpoint is selected
 
        Use Select-PEndpoint to change the default docker endpoint in the portainer session.
 
        -Endpoint 'local'
    .PARAMETER Name
        Assign the specified name to the container.
    .PARAMETER Platform
        Platform in the format os[/arch[/variant]] used for image lookup.
 
        When specified, the daemon checks if the requested image is present
        in the local image cache with the given OS and Architecture, and
        otherwise returns a 404 status.
 
        If the option is not set, the host's native OS and Architecture are
        used to look up the image in the image cache. However, if no platform
        is passed and the given image does exist in the local image cache,
        but its OS or architecture does not match, the container is created
        with the available image, and a warning is added to the Warnings
        field in the response, for example;
    .PARAMETER Hostname
        The hostname to use for the container, as a valid RFC 1123 hostname.
    .PARAMETER DomainName
        The domain name to use for the container.
    .PARAMETER User
        The user that commands are run as inside the container.
    .PARAMETER AttachStdin
        Whether to attach to stdin.
    .PARAMETER AttachStdout
        Whether to attach to stdout.
    .PARAMETER AttachStderr
        Whether to attach to stderr.
    .PARAMETER ExposedPorts
        Defines exposed ports for the container. Accepts a string array where
        each string should be in the following form @("<port>/<tcp|udp|sctp>","<port>/<tcp|udp|sctp>")
    .PARAMETER Tty
        Attach standard streams to a TTY, including stdin if it is not closed.
    .PARAMETER OpenStdin
        Open stdin for the container
    .PARAMETER StdinOnce
        Close stdin after one attached client disconnects
    .PARAMETER Env
        A list of environment variables specified as a string array in the
        following form @("<name>=<value>","<name>=<value>"). A variable
        without = is removed from the environment, rather than to have an empty value.
    .PARAMETER Cmd
        Command to run specified as a string or an array of strings.
    .PARAMETER HealthCheck
        Defines a hashtable with the configuration items of healthcheck object. Use the
        function New-PContainerHealthCheckObject to generate the hashtable.
    .PARAMETER ArgsEscaped
        Command is already escaped (Windows only)
    .PARAMETER Image
        The name (or reference) of the image to use when creating the container, or which was used when the container was created.
    .PARAMETER Volumes
        An object mapping mount point paths inside the container to empty objects. Accepts a string array in the form @('/volumes/data','/volumes/config')
    .PARAMETER WorkingDir
        The working directory for commands to run in.
    .PARAMETER Entrypoint
        The entry point for the container as a string or an array of strings.
 
        If the array consists of exactly one empty string ([""]) then the entry point
        is reset to system default (i.e., the entry point used by docker when there
        is no ENTRYPOINT instruction in the Dockerfile).
    .PARAMETER NetworkDisabled
        Disable networking for the container.
    .PARAMETER MacAddress
        MAC address of the container.
    .PARAMETER OnBuild
        ONBUILD metadata that were defined in the image's Dockerfile.
    .PARAMETER Labels
        Hashtable of labels. @{<label>=<value>;<label>=<value>}
    .PARAMETER StopSignal
        Signal to stop a container as a string or unsigned integer.
    .PARAMETER StopTimeout
        Timeout to stop a container in seconds. Default is 10.
    .PARAMETER Shell
        Shell for when RUN, CMD, and ENTRYPOINT uses a shell.
    .PARAMETER HostConfig
        A container's resources (cgroups config, ulimits, etc). Accepts a hashtable.
        Use the function New-PContainerHostConfig to generate the hashtable.
    .PARAMETER NetworkConfig
        NetworkingConfig represents the container's networking configuration for
        each of its interfaces. It is used for the networking configs specified
        in the docker create and docker network connect commands. Use the
        function New-PContainerNetworkConfig to generate the hashtable.
    .PARAMETER Session
        Optionally define a portainer session object to use. This is useful when you are connected to more than one portainer instance.
 
        -Session $Session
    .EXAMPLE
        New-PContainer
        Description of example
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'False positive')]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter()]
        [string]
        $Endpoint,

        [Parameter(Mandatory)]
        [string]
        [ValidatePattern('/?[a-zA-Z0-9][a-zA-Z0-9_.-]+')]
        $Name,

        [Parameter()]
        [string]
        $Platform = '',

        [Parameter()]
        [string]
        $Hostname,

        [Parameter()]
        [string]
        $DomainName,

        [Parameter()]
        [string]
        $User,

        [Parameter()]
        [boolean]
        $AttachStdin = $false,

        [Parameter()]
        [boolean]
        $AttachStdout = $true,

        [Parameter()]
        [boolean]
        $AttachStderr = $true,

        [Parameter()]
        [string[]]
        $ExposedPorts,

        [Parameter()]
        [boolean]
        $Tty = $false,

        [Parameter()]
        [boolean]
        $OpenStdin = $false,

        [Parameter()]
        [boolean]
        $StdinOnce = $false,

        [Parameter()]
        [string[]]
        $Env,

        [Parameter()]
        [string[]]
        $Cmd,

        [Parameter()]
        [hashtable]
        $HealthCheck,

        [Parameter()]
        [boolean]
        $ArgsEscaped = $false,

        [Parameter()]
        [string]
        $Image,

        [Parameter()]
        [string[]]
        $Volumes,

        [Parameter()]
        [string]
        $WorkingDir,

        [Parameter()]
        [string[]]
        $Entrypoint,

        [Parameter()]
        [boolean]
        $NetworkDisabled = $false,

        [Parameter()]
        [string]
        $MacAddress,

        [Parameter()]
        [string[]]
        $OnBuild,

        [Parameter()]
        [hashtable]
        $Labels,

        [Parameter()]
        [string]
        $StopSignal,

        [Parameter()]
        [int]
        $StopTimeout,

        [Parameter()]
        [string[]]
        $Shell,

        [Parameter()]
        [hashtable]
        $HostConfig,

        [Parameter()]
        [hashtable]
        $NetworkConfig,

        [Parameter()]
        [PortainerSession]
        $Session = $null
    )

    $Session = Get-PSession -Session:$Session
    $EndpointID = ResolveEndpointID -Endpoint:$Endpoint -Session:$Session

    $Configuration = @{}

    $AsIsIfValueProperties = @('Hostname', 'Domainname', 'User', 'ArgsEscaped', 'Image', 'WorkingDir', 'MacAddress', 'StopSignal', 'StopTimeout', 'Labels', 'HostConfig', 'NetworkConfig', 'HealthCheck')
    $AsIsAlwaysProperties = @('AttachStdin', 'AttachStdout', 'AttachStderr', 'NetworkDisabled')
    $ValueArrayProperties = @('Tty', 'OpenStdin', 'StdinOnce', 'Volumes')
    $StringArrayProperties = @('Env', 'Cmd', 'Entrypoint', 'OnBuild', 'Shell')

    foreach ($Property in $AsIsIfValueProperties)
    {
        if (Get-Variable -Name $Property)
        {
            $Configuration.$Property = Get-Variable -Name $Property
        }
    }

    foreach ($Property in $AsIsAlwaysProperties)
    {
        $Configuration.$Property = Get-Variable -Name $Property
    }

    foreach ($Property in $ValueArrayProperties)
    {
        if (Get-Variable -Name $Property)
        {
            $Configuration.$Property = @{}
            foreach ($Item in (Get-Variable -Name $Property))
            {
                $Configuration.$Property.$Item = @{}
            }
        }
    }

    foreach ($Property in $StringArrayProperties)
    {
        $Configuration.$Property = [string[]](Get-Variable -Name $Property)
    }

    if ($PSCmdlet.ShouldProcess($Name, 'Create container'))
    {
        try
        {
            InvokePortainerRestMethod -Method POST -RelativePath "/endpoints/$EndpointId/docker/containers/create?name=$Name&platform=$Platform" -Body $Configuration -PortainerSession:$Session
        }
        catch
        {
            Write-Error -Message "Could not create container, bad parameter: $_"
        }
    }
}
#EndRegion '.\public\Container\New-PContainer.ps1' 284
#Region '.\public\Container\Rename-PContainer.ps1' 0
function Rename-PContainer
{
    <#
    .DESCRIPTION
        Rename a container in portainer
    .PARAMETER Endpoint
        Defines the portainer endpoint to use when retreiving containers. If not specified the portainer sessions default docker endpoint value is used.
 
        Use Get-PSession to see what endpoint is selected
 
        Use Select-PEndpoint to change the default docker endpoint in the portainer session.
 
        -Endpoint 'local'
    .PARAMETER Id
        Defines the id of the container to retreive.
 
        -Id '<Id>'
    .PARAMETER Session
        Optionally define a portainer session object to use. This is useful when you are connected to more than one portainer instance.
 
        -Session $Session
    .PARAMETER NewName
        Defines the new name of the container
    .EXAMPLE
        Resize-PContainerTTY
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'NewName', Justification = 'False positive')]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter()][string]$Endpoint,
        [Parameter(ValueFromPipeline)][object[]]$Id,
        [Parameter()][PortainerSession]$Session = $null,
        [Parameter()][string]$NewName
    )

    BEGIN
    {
        $Session = Get-PSession -Session:$Session
        $EndpointID = ResolveEndpointID -Endpoint:$Endpoint -Session:$Session
    }

    PROCESS
    {
        $Id | ForEach-Object {
            if ($PSItem.PSObject.TypeNames -contains 'PortainerContainer')
            {
                $ContainerID = $PSItem.Id
            }
            elseif ($PSItem.GetType().Name -eq 'string')
            {
                $ContainerID = $PSItem
            }
            else
            {
                Write-Error -Message 'Cannot determine input object type' -ErrorAction Stop
            }

            if ($PSCmdlet.ShouldProcess($ContainerID, 'Rename'))
            {
                try
                {
                    InvokePortainerRestMethod -Method POST -RelativePath "/endpoints/$EndpointId/docker/containers/$ContainerID/rename" -PortainerSession:$Session -Body @{ name = $NewName }
                }
                catch
                {
                    if ($_.Exception.Message -like '*404*')
                    {
                        Write-Error -Message "No container with id <$ContainerID> could be found"
                    }
                    elseif ($_.Exception.Message -like '*409*')
                    {
                        Write-Error -Message "Name $NewName is already in use"
                    }
                    else
                    {
                        Write-Error -Message "Failed to rename container with id <$ContainerID> with error: $_"
                    }
                }
            }
        }

    }
}
#EndRegion '.\public\Container\Rename-PContainer.ps1' 84
#Region '.\public\Container\Resize-PContainerTTY.ps1' 0
function Resize-PContainerTTY
{
    <#
    .DESCRIPTION
        Resizes the TTY for a container
    .PARAMETER Endpoint
        Defines the portainer endpoint to use when retreiving containers. If not specified the portainer sessions default docker endpoint value is used.
 
        Use Get-PSession to see what endpoint is selected
 
        Use Select-PEndpoint to change the default docker endpoint in the portainer session.
 
        -Endpoint 'local'
    .PARAMETER Id
        Defines the id of the container to retreive.
 
        -Id '<Id>'
    .PARAMETER Session
        Optionally define a portainer session object to use. This is useful when you are connected to more than one portainer instance.
 
        -Session $Session
    .PARAMETER Height
        Defines the height of the TTY session
    .PARAMETER Width
        Defines the width of the TTY session
    .EXAMPLE
        Resize-PContainerTTY
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Height', Justification = 'False positive')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Width', Justification = 'False positive')]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter()][string]$Endpoint,
        [Parameter(ValueFromPipeline)][object[]]$Id,
        [Parameter()][PortainerSession]$Session = $null,
        [Parameter()][int]$Height,
        [Parameter()][int]$Width
    )

    BEGIN
    {
        $Session = Get-PSession -Session:$Session
        $EndpointID = ResolveEndpointID -Endpoint:$Endpoint -Session:$Session
    }

    PROCESS
    {
        $Id | ForEach-Object {
            if ($PSItem.PSObject.TypeNames -contains 'PortainerContainer')
            {
                $ContainerID = $PSItem.Id
            }
            elseif ($PSItem.GetType().Name -eq 'string')
            {
                $ContainerID = $PSItem
            }
            else
            {
                Write-Error -Message 'Cannot determine input object type' -ErrorAction Stop
            }

            if ($PSCmdlet.ShouldProcess($ContainerID, 'Resize TTY'))
            {
                try
                {
                    InvokePortainerRestMethod -Method POST -RelativePath "/endpoints/$EndpointId/docker/containers/$ContainerID/resize" -PortainerSession:$Session -Body @{h = $Height; w = $Width }
                }
                catch
                {
                    if ($_.Exception.Message -like '*404*')
                    {
                        Write-Error -Message "No container with id <$ContainerID> could be found"
                    }
                    else
                    {
                        Write-Error -Message "Failed to resize container TTY with id <$ContainerID> with error: $_"
                    }
                }
            }
        }

    }
}
#EndRegion '.\public\Container\Resize-PContainerTTY.ps1' 84
#Region '.\public\Container\Restart-PContainer.ps1' 0
function Restart-PContainer
{
    <#
    .DESCRIPTION
        Restart container in portainer
    .PARAMETER Endpoint
        Defines the portainer endpoint to use when retreiving containers. If not specified the portainer sessions default docker endpoint value is used.
 
        Use Get-PSession to see what endpoint is selected
 
        Use Select-PEndpoint to change the default docker endpoint in the portainer session.
 
        -Endpoint 'local'
    .PARAMETER Id
        Defines the id of the container to retreive.
 
        -Id '<Id>'
    .PARAMETER Session
        Optionally define a portainer session object to use. This is useful when you are connected to more than one portainer instance.
 
        -Session $Session
    .EXAMPLE
        Restart-PContainer
        Description of example
    #>


    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter()][string]$Endpoint,
        [Parameter(ValueFromPipeline)][object[]]$Id,
        [Parameter()][PortainerSession]$Session = $null
    )

    BEGIN
    {
        $Session = Get-PSession -Session:$Session
        $EndpointID = ResolveEndpointID -Endpoint:$Endpoint -Session:$Session
    }

    PROCESS
    {
        $Id | ForEach-Object {
            if ($PSItem.PSObject.TypeNames -contains 'PortainerContainer')
            {
                $ContainerID = $PSItem.Id
            }
            elseif ($PSItem.GetType().Name -eq 'string')
            {
                $ContainerID = $PSItem
            }
            else
            {
                Write-Error -Message 'Cannot determine input object type' -ErrorAction Stop
            }

            if ($PSCmdlet.ShouldProcess($ContainerID, 'Restart'))
            {
                try
                {
                    InvokePortainerRestMethod -Method POST -RelativePath "/endpoints/$EndpointId/docker/containers/$ContainerID/restart" -PortainerSession:$Session
                }
                catch
                {
                    if ($_.Exception.Message -like '*404*')
                    {
                        Write-Error -Message "No container with id <$ContainerID> could be found"
                    }
                    else
                    {
                        Write-Error -Message "Failed to restart container with id <$ContainerID> with error: $_"
                    }
                }
            }
        }
    }
}
#endregion
#EndRegion '.\public\Container\Restart-PContainer.ps1' 78
#Region '.\public\Container\Resume-PContainer.ps1' 0
function Resume-PContainer
{
    <#
    .DESCRIPTION
        Resumes a container in portainer
    .PARAMETER Endpoint
        Defines the portainer endpoint to use when retreiving containers. If not specified the portainer sessions default docker endpoint value is used.
 
        Use Get-PSession to see what endpoint is selected
 
        Use Select-PEndpoint to change the default docker endpoint in the portainer session.
 
        -Endpoint 'local'
    .PARAMETER Id
        Defines the id of the container to retreive.
 
        -Id '<Id>'
    .PARAMETER Session
        Optionally define a portainer session object to use. This is useful when you are connected to more than one portainer instance.
 
        -Session $Session
    .EXAMPLE
        Restart-PContainer
        Description of example
    #>


    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter()][string]$Endpoint,
        [Parameter(ValueFromPipeline)][object[]]$Id,
        [Parameter()][PortainerSession]$Session = $null
    )

    BEGIN
    {
        $Session = Get-PSession -Session:$Session
        $EndpointID = ResolveEndpointID -Endpoint:$Endpoint -Session:$Session
    }

    PROCESS
    {
        $Id | ForEach-Object {
            if ($PSItem.PSObject.TypeNames -contains 'PortainerContainer')
            {
                $ContainerID = $PSItem.Id
            }
            elseif ($PSItem.GetType().Name -eq 'string')
            {
                $ContainerID = $PSItem
            }
            else
            {
                Write-Error -Message 'Cannot determine input object type' -ErrorAction Stop
            }

            if ($PSCmdlet.ShouldProcess($ContainerID, 'Resume'))
            {
                try
                {
                    InvokePortainerRestMethod -Method POST -RelativePath "/endpoints/$EndpointId/docker/containers/$ContainerID/unpause" -PortainerSession:$Session
                }
                catch
                {
                    if ($_.Exception.Message -like '*404*')
                    {
                        Write-Error -Message "No container with id <$ContainerID> could be found"
                    }
                    else
                    {
                        Write-Error -Message "Failed to resume container with id <$ContainerID> with error: $_"
                    }
                }
            }
        }
    }
}
#EndRegion '.\public\Container\Resume-PContainer.ps1' 77
#Region '.\public\Container\Start-PContainer.ps1' 0
function Start-PContainer
{
    <#
    .DESCRIPTION
        Starts a container in portainer
    .PARAMETER Endpoint
        Defines the portainer endpoint to use when retreiving containers. If not specified the portainer sessions default docker endpoint value is used.
 
        Use Get-PSession to see what endpoint is selected
 
        Use Select-PEndpoint to change the default docker endpoint in the portainer session.
 
        -Endpoint 'local'
    .PARAMETER Id
        Defines the id of the container to retreive.
 
        -Id '<Id>'
    .PARAMETER Session
        Optionally define a portainer session object to use. This is useful when you are connected to more than one portainer instance.
 
        -Session $Session
    .EXAMPLE
        Start-PContainer
        Description of example
    #>


    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter()][string]$Endpoint,
        [Parameter(ValueFromPipeline)][object[]]$Id,
        [Parameter()][PortainerSession]$Session = $null
    )

    BEGIN
    {
        $Session = Get-PSession -Session:$Session
        $EndpointID = ResolveEndpointID -Endpoint:$Endpoint -Session:$Session
    }

    PROCESS
    {
        $Id | ForEach-Object {
            if ($PSItem.PSObject.TypeNames -contains 'PortainerContainer')
            {
                $ContainerID = $PSItem.Id
            }
            elseif ($PSItem.GetType().Name -eq 'string')
            {
                $ContainerID = $PSItem
            }
            else
            {
                Write-Error -Message 'Cannot determine input object type' -ErrorAction Stop
            }

            if ($PSCmdlet.ShouldProcess($ContainerID, 'Start'))
            {
                try
                {
                    InvokePortainerRestMethod -Method POST -RelativePath "/endpoints/$EndpointId/docker/containers/$ContainerID/start" -PortainerSession:$Session
                }
                catch
                {
                    if ($_.Exception.Message -like '*304 (Not Modified)*')
                    {
                        Write-Warning -Message "Container <$ContainerID> is already started"
                    }
                    elseif ($_.Exception.Message -like '*404*')
                    {
                        Write-Error -Message "No container with id <$ContainerID> could be found"
                    }
                    else
                    {
                        Write-Error -Message "Failed to start container with id <$ContainerID> with error: $_"
                    }
                }
            }
        }
    }
}
#endregion
#EndRegion '.\public\Container\Start-PContainer.ps1' 82
#Region '.\public\Container\Stop-PContainer.ps1' 0
function Stop-PContainer
{
    <#
    .DESCRIPTION
        Stop a container in portainer
    .PARAMETER Endpoint
        Defines the portainer endpoint to use when retreiving containers. If not specified the portainer sessions default docker endpoint value is used.
 
        Use Get-PSession to see what endpoint is selected
 
        Use Select-PEndpoint to change the default docker endpoint in the portainer session.
 
        -Endpoint 'local'
    .PARAMETER Id
        Defines the id of the container to retreive.
 
        -Id '<Id>'
    .PARAMETER Session
        Optionally define a portainer session object to use. This is useful when you are connected to more than one portainer instance.
 
        -Session $Session
    .PARAMETER Kill
        Defines that the container should be stopped with the kill command rather than a graceful shutdown
    .EXAMPLE
        Stop-PContainer
        Description of example
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Kill', Justification = 'False positive')]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter()][string]$Endpoint,
        [Parameter(ValueFromPipeline)][object[]]$Id,
        [Parameter()][PortainerSession]$Session = $null,
        [Parameter()][switch]$Kill
    )

    BEGIN
    {
        $Session = Get-PSession -Session:$Session
        $EndpointID = ResolveEndpointID -Endpoint:$Endpoint -Session:$Session
    }

    PROCESS
    {
        $Id | ForEach-Object {
            if ($PSItem.PSObject.TypeNames -contains 'PortainerContainer')
            {
                $ContainerID = $PSItem.Id
            }
            elseif ($PSItem.GetType().Name -eq 'string')
            {
                $ContainerID = $PSItem
            }
            else
            {
                Write-Error -Message 'Cannot determine input object type' -ErrorAction Stop
            }

            if ($PSCmdlet.ShouldProcess($ContainerID, 'Stop'))
            {
                if ($Kill)
                {
                    try
                    {
                        InvokePortainerRestMethod -Method POST -RelativePath "/endpoints/$EndpointId/docker/containers/$ContainerID/kill" -PortainerSession:$Session -Body @{signal = 'SIGKILL' }
                    }
                    catch
                    {
                        if ($_.Exception.Message -like '*409*')
                        {
                            Write-Warning -Message "Container <$ContainerID> is already stopped"
                        }
                        elseif ($_.Exception.Message -like '*404*')
                        {
                            Write-Error -Message "No container with id <$ContainerID> could be found"
                        }
                        else
                        {
                            Write-Error -Message "Failed to stop container with id <$ContainerID> with error: $_"
                        }
                    }

                }
                else
                {
                    try
                    {
                        InvokePortainerRestMethod -Method POST -RelativePath "/endpoints/$EndpointId/docker/containers/$ContainerID/stop" -PortainerSession:$Session
                    }
                    catch
                    {
                        if ($_.Exception.Message -like '*304 (Not Modified)*')
                        {
                            Write-Warning -Message "Container <$ContainerID> is already stopped"
                        }
                        elseif ($_.Exception.Message -like '*404*')
                        {
                            Write-Error -Message "No container with id <$ContainerID> could be found"
                        }
                        else
                        {
                            Write-Error -Message "Failed to stop container with id <$ContainerID> with error: $_"
                        }
                    }
                }
            }
        }
    }
}
#endregion
#EndRegion '.\public\Container\Stop-PContainer.ps1' 111
#Region '.\public\Container\Suspend-PContainer.ps1' 0
function Suspend-PContainer
{
    <#
    .DESCRIPTION
        Pauses a container in portainer
    .PARAMETER Endpoint
        Defines the portainer endpoint to use when retreiving containers. If not specified the portainer sessions default docker endpoint value is used.
 
        Use Get-PSession to see what endpoint is selected
 
        Use Select-PEndpoint to change the default docker endpoint in the portainer session.
 
        -Endpoint 'local'
    .PARAMETER Id
        Defines the id of the container to retreive.
 
        -Id '<Id>'
    .PARAMETER Session
        Optionally define a portainer session object to use. This is useful when you are connected to more than one portainer instance.
 
        -Session $Session
    .EXAMPLE
        Restart-PContainer
        Description of example
    #>


    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter()][string]$Endpoint,
        [Parameter(ValueFromPipeline)][object[]]$Id,
        [Parameter()][PortainerSession]$Session = $null
    )

    BEGIN
    {
        $Session = Get-PSession -Session:$Session
        $EndpointID = ResolveEndpointID -Endpoint:$Endpoint -Session:$Session
    }

    PROCESS
    {
        $Id | ForEach-Object {
            if ($PSItem.PSObject.TypeNames -contains 'PortainerContainer')
            {
                $ContainerID = $PSItem.Id
            }
            elseif ($PSItem.GetType().Name -eq 'string')
            {
                $ContainerID = $PSItem
            }
            else
            {
                Write-Error -Message 'Cannot determine input object type' -ErrorAction Stop
            }

            if ($PSCmdlet.ShouldProcess($ContainerID, 'Suspend'))
            {
                try
                {
                    InvokePortainerRestMethod -Method POST -RelativePath "/endpoints/$EndpointId/docker/containers/$ContainerID/pause" -PortainerSession:$Session
                }
                catch
                {
                    if ($_.Exception.Message -like '*404*')
                    {
                        Write-Error -Message "No container with id <$ContainerID> could be found"
                    }
                    else
                    {
                        Write-Error -Message "Failed to suspend container with id <$ContainerID> with error: $_"
                    }
                }
            }
        }
    }
}
#EndRegion '.\public\Container\Suspend-PContainer.ps1' 77
#Region '.\public\Container\Wait-PContainer.ps1' 0
function Wait-PContainer
{
    <#
    .DESCRIPTION
        Wait for container to stop
    .PARAMETER Endpoint
        Defines the portainer endpoint to use when retreiving containers. If not specified the portainer sessions default docker endpoint value is used.
 
        Use Get-PSession to see what endpoint is selected
 
        Use Select-PEndpoint to change the default docker endpoint in the portainer session.
 
        -Endpoint 'local'
    .PARAMETER Id
        Defines the id of the container to retreive.
 
        -Id '<Id>'
    .PARAMETER Session
        Optionally define a portainer session object to use. This is useful when you are connected to more than one portainer instance.
 
        -Session $Session
    .PARAMETER Condition
        Defines the condition that we are waiting for, valid values are notrunning, nextexit, removed
    .EXAMPLE
        Restart-PContainer
        Description of example
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Condition', Justification = 'False positive')]
    [CmdletBinding()]
    param(
        [Parameter()][string]$Endpoint,
        [Parameter(ValueFromPipeline)][object[]]$Id,
        [Parameter()][PortainerSession]$Session = $null,
        [Parameter()][ContainerCondition]$Condition = 'notrunning'
    )

    BEGIN
    {
        $Session = Get-PSession -Session:$Session
        $EndpointID = ResolveEndpointID -Endpoint:$Endpoint -Session:$Session
    }

    PROCESS
    {
        $Id | ForEach-Object {
            if ($PSItem.PSObject.TypeNames -contains 'PortainerContainer')
            {
                $ContainerID = $PSItem.Id
            }
            elseif ($PSItem.GetType().Name -eq 'string')
            {
                $ContainerID = $PSItem
            }
            else
            {
                Write-Error -Message 'Cannot determine input object type' -ErrorAction Stop
            }

            try
            {
                InvokePortainerRestMethod -Method POST -RelativePath "/endpoints/$EndpointId/docker/containers/$ContainerID/wait" -PortainerSession:$Session -body @{condition = $Condition }
            }
            catch
            {
                if ($_.Exception.Message -like '*404*')
                {
                    Write-Error -Message "No container with id <$ContainerID> could be found"
                }
                elseif ($_.Exception.Message -like '*400*')
                {
                    Write-Error -Message 'Bad parameter'
                }
                else
                {
                    Write-Error -Message "Failed to initiate wait for container with id <$ContainerID> with error: $_"
                }
            }
        }
    }
}
#EndRegion '.\public\Container\Wait-PContainer.ps1' 81
#Region '.\public\Endpoint\Get-PEndpoint.ps1' 0
function Get-PEndpoint
{
    <#
    .DESCRIPTION
        Retreives available endpoints
    .PARAMETER SearchString
        Defines a searchstring to use for filtering endpoints
    .PARAMETER Id
        Defines the Id of the endpoint to retreive.
    .PARAMETER Session
        Optionally define a portainer session object to use. This is useful when you are connected to more than one portainer instance.
 
        -Session $Session
    .EXAMPLE
        Get-PEndpoint -SearchString 'local'
 
        Retreives all endpoints containing the word local
    #>


    [CmdletBinding(DefaultParameterSetName = 'List')]
    param(
        [Parameter(ParameterSetName = 'Search')][string]$SearchString,
        [Parameter(ParameterSetname = 'Id')][int]$Id,
        [Parameter()][PortainerSession]$Session = $null

        # Does not work for some reason, regardless of input to the API parameter name, all endpoints are returned...
        #[Parameter(ParameterSetName = 'Name')][string]$Name
    )

    switch ($PSCmdlet.ParameterSetName)
    {
        'List'
        {
            InvokePortainerRestMethod -Method Get -RelativePath '/endpoints' -PortainerSession:$Session
        }
        'Search'
        {
            InvokePortainerRestMethod -Method Get -RelativePath '/endpoints' -Body @{search = $SearchString } -PortainerSession:$Session
        }
        'Id'
        {
            InvokePortainerRestMethod -Method Get -RelativePath "/endpoints/$Id" -PortainerSession:$Session
        }
        <#
        'Name'
        {
            InvokePortainerRestMethod -Method Get -RelativePath '/endpoints' -Body @{name = $Name } -PortainerSession:$Session
        }
        #>

    }

}
#EndRegion '.\public\Endpoint\Get-PEndpoint.ps1' 53
#Region '.\public\Endpoint\Select-PEndpoint.ps1' 0
function Select-PEndpoint
{
    <#
    .DESCRIPTION
        Configures the default endpoint to use
    .PARAMETER Endpoint
        Defines the endpoint name to select
    .PARAMETER Session
        Optionally define a portainer session object to use. This is useful when you are connected to more than one portainer instance.
 
        -Session $Session
    .EXAMPLE
        Select-PEndpoint -Endpoint 'prod'
 
        Set the default endpoint to use
    #>


    [CmdletBinding()]
    param(
        [Parameter()][string]$Endpoint,
        [Parameter()][PortainerSession]$Session = $null
    )

    if ($Session)
    {
        $Session.DefaultDockerEndpoint = $Endpoint
    }
    elseif ($script:PortainerSession)
    {
        $script:PortainerSession.DefaultDockerEndpoint = $Endpoint
    }
    else
    {
        Write-Warning 'No session found'
    }

}
#EndRegion '.\public\Endpoint\Select-PEndpoint.ps1' 38
#Region '.\public\Portainer\Connect-Portainer.ps1' 0
function Connect-Portainer
{
    <#
    .DESCRIPTION
        Connect to a Portainer instance
    .PARAMETER BaseURL
        Defines the base URL to the portainer instance
 
        -BaseURL 'https://portainer.contoso.com'
    .PARAMETER AccessToken
        Connects to portainer using a access token. This AccessToken can be generated from the Portainer Web GUI.
 
        -AccessToken 'ptr_ABoR54bB1NUc4aNY0F2PhppP1tVDu2Husr3vEbPUsw5'
    .PARAMETER Credential
        Connect to portainer using username and password. Parameter accepts a PSCredentials object
 
        -Credential (Get-Credential)
    .PARAMETER PassThru
        This parameter will cause the function to return a PortainerSession object that can be stored in a variable and referensed with the -Session parameter on most cmdlets.
 
        -PassThru
    .EXAMPLE
        Connect-Portainer -BaseURL 'https://portainer.contoso.com' -AccessToken 'ptr_ABoR54bB1NUc4aNY0F2PhppP1tVDu2Husr3vEbPUsw5='
 
        Connect using access token
    .EXAMPLE
        Connect-Portainer -BaseURL 'https://portainer.contoso.com' -Credentials (Get-Credential)
 
        Connect using username and password
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'AccessToken')]

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$BaseURL,
        [Parameter(ParameterSetName = 'AccessToken')][string]$AccessToken,
        [Parameter(ParameterSetName = 'Credentials')][pscredential]$Credential,
        [switch]$PassThru
    )

    switch ($PSCmdlet.ParameterSetName)
    {
        'AccessToken'
        {
            $AccessTokenSS = ConvertTo-SecureString -String $AccessToken -AsPlainText -Force
            Remove-Variable -Name AccessToken
            $script:PortainerSession = [PortainerSession]::New($BaseURL, $AccessTokenSS)
        }
        'Credentials'
        {
            $script:PortainerSession = [PortainerSession]::New($BaseURL, $Credential)
        }
    }

    if ($Passthru)
    {
        return $script:PortainerSession
    }
}
#EndRegion '.\public\Portainer\Connect-Portainer.ps1' 60
#Region '.\public\Portainer\Disconnect-Portainer.ps1' 0
function Disconnect-Portainer
{
    <#
    .DESCRIPTION
        Disconnect and cleanup session configuration
    .PARAMETER Session
        Defines a PortainerSession object that will be disconnected and cleaned up.
    .EXAMPLE
        Disconnect-Portainer
 
        Disconnect from the default portainer session
    .EXAMPLE
        Disconnect-Portainer -Session $Session
 
        Disconnect the specified session
    #>


    [CmdletBinding()]
    param(
        [Parameter()][PortainerSession]$Session = $null
    )

    InvokePortainerRestMethod -Method Post -RelativePath '/auth/logout' -PortainerSession:$Session

    # Remove PortainerSession variable
    if ($Session)
    {
        if ($script:PortainerSession.SessionID -eq $Session.SessionID)
        {
            Remove-Variable PortainerSession -Scope Script
        }
    }
    else
    {
        Remove-Variable PortainerSession -Scope Script
    }

}
#EndRegion '.\public\Portainer\Disconnect-Portainer.ps1' 39
#Region '.\public\Portainer\Get-PSession.ps1' 0
function Get-PSession
{
    <#
    .DESCRIPTION
        Displays the Portainer Session object.
    .PARAMETER Session
        Optionally define a portainer session object to use. This is useful when you are connected to more than one portainer instance.
 
        -Session $Session
    .EXAMPLE
        Get-PSession
 
        Returns the PortainerSession, if none is specified, it tries to retreive the default
    #>


    [CmdletBinding()]
    param(
        [Parameter()][PortainerSession]$Session = $null
    )

    if ($Session)
    {
        Write-Debug -Message 'Get-PSession; PortainerSession was passed as parameter'
        return $Session
    }
    elseif ($script:PortainerSession)
    {
        Write-Debug -Message 'Get-PSession; PortainerSession found in script scope'
        return $script:PortainerSession
    }
    else
    {
        Write-Error -Message 'No Portainer Session established, please call Connect-Portainer'
    }
}
#endregion
#EndRegion '.\public\Portainer\Get-PSession.ps1' 37
#Region '.\public\Portainer\Get-PSettingsPublic.ps1' 0
function Get-PSettingsPublic
{
    <#
    .DESCRIPTION
        Retreives Portainer Public Settings
    .PARAMETER Session
        Optionally define a portainer session object to use. This is useful when you are connected to more than one portainer instance.
 
        -Session $Session
    .EXAMPLE
        Get-PSettingsPublic
 
        Retreives Portainer Public Settings
    #>


    [CmdletBinding()]
    param(
        [Parameter()][PortainerSession]$Session = $null
    )

    InvokePortainerRestMethod -NoAuth -Method Get -RelativePath '/settings/public' -PortainerSession:$Session

}
#endregion
#EndRegion '.\public\Portainer\Get-PSettingsPublic.ps1' 25
#Region '.\public\Portainer\Get-PStatus.ps1' 0
function Get-PStatus
{
    <#
    .DESCRIPTION
        Get public status for portainer instance
    .PARAMETER Session
        Optionally define a portainer session object to use. This is useful when you are connected to more than one portainer instance.
 
        -Session $Session
    .EXAMPLE
        Get-PStatus
 
        Get public status for portainer instance
    #>

    [CmdletBinding()]
    param(
        [Parameter()][PortainerSession]$Session = $null
    )

    InvokePortainerRestMethod -NoAuth -Method Get -RelativePath '/status' -PortainerSession:$Session

}
#EndRegion '.\public\Portainer\Get-PStatus.ps1' 23
#Region '.\suffix.ps1' 0
# The content of this file will be appended to the top of the psm1 module file. This is useful for custom procesedures after all module functions are loaded.

#
# Argument completers
#

$AC_Endpoints = [scriptblock] {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'Parameter is automatically provided by powershell when argument completer is invoked regardless of weather it is used or not')]
    [OutputType([System.Management.Automation.CompletionResult])]
    param(
        [string] $CommandName,
        [string] $ParameterName,
        [string] $WordToComplete,
        [System.Management.Automation.Language.CommandAst] $CommandAst,
        [System.Collections.IDictionary] $FakeBoundParameters
    )

    $CompletionResults = [System.Collections.Generic.List[System.Management.Automation.CompletionResult]]::new()

    Get-PEndpoint | ForEach-Object {
        $CompletionResults.Add(
            [System.Management.Automation.CompletionResult]::New(($_.Name))
        )
    }

    return $CompletionResults
}
Register-ArgumentCompleter -CommandName 'Get-PContainer' -Parameter 'Endpoint' -ScriptBlock $AC_Endpoints
Register-ArgumentCompleter -CommandName 'Select-PEndpoint' -Parameter 'Endpoint' -ScriptBlock $AC_Endpoints
#EndRegion '.\suffix.ps1' 30

# SIG # Begin signature block
# MIIbmAYJKoZIhvcNAQcCoIIbiTCCG4UCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUbtOsKmqElVsoNk9YhUUDlao+
# TEegghYPMIIDBDCCAeygAwIBAgIQFqvAHPTHupdKm+zXSmZcFTANBgkqhkiG9w0B
# AQUFADAaMRgwFgYDVQQDDA9IYW5uZXNQYWxtcXVpc3QwHhcNMjExMTA2MjEzODU5
# WhcNMjYxMTA2MjE0ODU5WjAaMRgwFgYDVQQDDA9IYW5uZXNQYWxtcXVpc3QwggEi
# MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6WNXwWLjX4EAPDxg9U54O83Sr
# Vdf41bEPJz8V0wDQm4MJnnBGZTCqPBENXsfSbIPhHwad+tcsd3nc1ZNuQEJTV3W1
# pXTOtNK+yV0r51VSJvFYYo3qXFyvWdi9n8Z8/yydoWy1vXkx7CsbmIuniODGEzIE
# C1z0QYgrjyNmXw1CewOjX+oW2cgjiJqjuxsS/sLnmqAOMOeV3047GnEEKTyderWR
# L/U+ohEu2WcMao1UCKEtUmPo5rncRyHjY7wzwevby1I8oLgHWm8NlH9K2Kp43tx9
# +Mcpn64hTYW+jd7VCFmrEIDfZEWdY5EBfJZDwnol0vXvFPl+7E+WaCtKe6zNAgMB
# AAGjRjBEMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNV
# HQ4EFgQU0+0dMijn0qYkRTkT8lp0KErfo+swDQYJKoZIhvcNAQEFBQADggEBACoT
# mkcM6C7Atq/ZzwVOH0A1qd5t1KsnAX393xLk73ssnmOW/c4P2fcoeEmQ8Idk6FJf
# v+D6MkZIRb7PefqTCVhgi2ZZynNCWE2zqt5VxEZIPiG1Amke/OhEI8fxL/01HWZj
# kGZD3cquXMA3SGS86Y7tX2S31/rAG812ew57ghfOL3EUPfBwCrSKV679ncNL0Td9
# AKiK2pUraM32/UppF7kEZlSTPtPwCPSp+xihf6ZRJM95gB8bSa+K/1wd8CD9f6Ng
# XkyiSesaUjMhfh+hhg8otqB1ZqvsqsNtsibKo1IAHWWNDxa3sv8g4rErTQFARx4p
# 3pksn4fYdZXhQzOHMtIwggWNMIIEdaADAgECAhAOmxiO+dAt5+/bUOIIQBhaMA0G
# CSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ
# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0RpZ2lDZXJ0
# IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBaFw0zMTExMDkyMzU5
# NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNV
# BAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQg
# Um9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL/mkHNo3rvk
# XUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/zG6Q4FutWxpdt
# HauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZanMylNEQRBAu
# 34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7sWxq868nPzaw0
# QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL2pNe3I6PgNq2
# kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfbBHMqbpEBfCFM
# 1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3JFxGj2T3wWmI
# dph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3cAORFJYm2mkQZ
# K37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqxYxhElRp2Yn72
# gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0viastkF13nqs
# X40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aLT8LWRV+dIPyh
# HsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIBNjAPBgNVHRMBAf8E
# BTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwPTzAfBgNVHSMEGDAW
# gBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMCAYYweQYIKwYBBQUH
# AQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQwYI
# KwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFz
# c3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0aHR0cDovL2NybDMu
# ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDARBgNVHSAE
# CjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0NcVec4X6CjdBs9thbX
# 979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnovLbc47/T/gLn4offy
# ct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65ZyoUi0mcudT6cGAxN3
# J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFWjuyk1T3osdz9HNj0
# d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPFmCLBsln1VWvPJ6ts
# ds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9ztwGpn1eqXijiuZQw
# ggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqGSIb3DQEBCwUAMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1
# c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwggIiMA0GCSqG
# SIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXHJQPE8pE3qZdRodbS
# g9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMfUBMLJnOWbfhXqAJ9
# /UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w1lbU5ygt69OxtXXn
# HwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRktFLydkf3YYMZ3V+0
# VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYbqMFkdECnwHLFuk4f
# sbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUmcJgmf6AaRyBD40Nj
# gHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP65x9abJTyUpURK1h0
# QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzKQtwYSH8UNM/STKvv
# mz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo80VgvCONWPfcYd6T
# /jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjBJgj5FBASA31fI7tk
# 42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXcheMBK9Rp6103a50g5r
# mQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E
# FgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5n
# P+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMHcG
# CCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu
# Y29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln
# aUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8v
# Y3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAgBgNV
# HSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIB
# AH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd4ksp+3CKDaopafxp
# wc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiCqBa9qVbPFXONASIl
# zpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl/Yy8ZCaHbJK9nXzQ
# cAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeCRK6ZJxurJB4mwbfe
# Kuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYTgAnEtp/Nh4cku0+j
# Sbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/a6fxZsNBzU+2QJsh
# IUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37xJV77QpfMzmHQXh6
# OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmLNriT1ObyF5lZynDw
# N7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0YgkPCr2B2RP+v6TR
# 81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJRyvmfxqkhQ/8mJb2
# VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIGwDCCBKigAwIBAgIQ
# DE1pckuU+jwqSj0pB4A9WjANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEX
# MBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0
# ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTIyMDkyMTAw
# MDAwMFoXDTMzMTEyMTIzNTk1OVowRjELMAkGA1UEBhMCVVMxETAPBgNVBAoTCERp
# Z2lDZXJ0MSQwIgYDVQQDExtEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMiAtIDIwggIi
# MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDP7KUmOsap8mu7jcENmtuh6BSF
# dDMaJqzQHFUeHjZtvJJVDGH0nQl3PRWWCC9rZKT9BoMW15GSOBwxApb7crGXOlWv
# M+xhiummKNuQY1y9iVPgOi2Mh0KuJqTku3h4uXoW4VbGwLpkU7sqFudQSLuIaQyI
# xvG+4C99O7HKU41Agx7ny3JJKB5MgB6FVueF7fJhvKo6B332q27lZt3iXPUv7Y3U
# TZWEaOOAy2p50dIQkUYp6z4m8rSMzUy5Zsi7qlA4DeWMlF0ZWr/1e0BubxaompyV
# R4aFeT4MXmaMGgokvpyq0py2909ueMQoP6McD1AGN7oI2TWmtR7aeFgdOej4TJEQ
# ln5N4d3CraV++C0bH+wrRhijGfY59/XBT3EuiQMRoku7mL/6T+R7Nu8GRORV/zbq
# 5Xwx5/PCUsTmFntafqUlc9vAapkhLWPlWfVNL5AfJ7fSqxTlOGaHUQhr+1NDOdBk
# +lbP4PQK5hRtZHi7mP2Uw3Mh8y/CLiDXgazT8QfU4b3ZXUtuMZQpi+ZBpGWUwFjl
# 5S4pkKa3YWT62SBsGFFguqaBDwklU/G/O+mrBw5qBzliGcnWhX8T2Y15z2LF7OF7
# ucxnEweawXjtxojIsG4yeccLWYONxu71LHx7jstkifGxxLjnU15fVdJ9GSlZA076
# XepFcxyEftfO4tQ6dwIDAQABo4IBizCCAYcwDgYDVR0PAQH/BAQDAgeAMAwGA1Ud
# EwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwIAYDVR0gBBkwFzAIBgZn
# gQwBBAIwCwYJYIZIAYb9bAcBMB8GA1UdIwQYMBaAFLoW2W1NhS9zKXaaL3WMaiCP
# nshvMB0GA1UdDgQWBBRiit7QYfyPMRTtlwvNPSqUFN9SnDBaBgNVHR8EUzBRME+g
# TaBLhklodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRS
# U0E0MDk2U0hBMjU2VGltZVN0YW1waW5nQ0EuY3JsMIGQBggrBgEFBQcBAQSBgzCB
# gDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFgGCCsGAQUF
# BzAChkxodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVk
# RzRSU0E0MDk2U0hBMjU2VGltZVN0YW1waW5nQ0EuY3J0MA0GCSqGSIb3DQEBCwUA
# A4ICAQBVqioa80bzeFc3MPx140/WhSPx/PmVOZsl5vdyipjDd9Rk/BX7NsJJUSx4
# iGNVCUY5APxp1MqbKfujP8DJAJsTHbCYidx48s18hc1Tna9i4mFmoxQqRYdKmEIr
# UPwbtZ4IMAn65C3XCYl5+QnmiM59G7hqopvBU2AJ6KO4ndetHxy47JhB8PYOgPvk
# /9+dEKfrALpfSo8aOlK06r8JSRU1NlmaD1TSsht/fl4JrXZUinRtytIFZyt26/+Y
# siaVOBmIRBTlClmia+ciPkQh0j8cwJvtfEiy2JIMkU88ZpSvXQJT657inuTTH4YB
# ZJwAwuladHUNPeF5iL8cAZfJGSOA1zZaX5YWsWMMxkZAO85dNdRZPkOaGK7DycvD
# +5sTX2q1x+DzBcNZ3ydiK95ByVO5/zQQZ/YmMph7/lxClIGUgp2sCovGSxVK05iQ
# RWAzgOAj3vgDpPZFR+XOuANCR+hBNnF3rf2i6Jd0Ti7aHh2MWsgemtXC8MYiqE+b
# vdgcmlHEL5r2X6cnl7qWLoVXwGDneFZ/au/ClZpLEQLIgpzJGgV8unG1TnqZbPTo
# ntRamMifv427GFxD9dAq6OJi7ngE273R+1sKqHB+8JeEeOMIA11HLGOoJTiXAdI/
# Otrl5fbmm9x+LMz/F0xNAKLY1gEOuIvu5uByVYksJxlh9ncBjDGCBPMwggTvAgEB
# MC4wGjEYMBYGA1UEAwwPSGFubmVzUGFsbXF1aXN0AhAWq8Ac9Me6l0qb7NdKZlwV
# MAkGBSsOAwIaBQCgeDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3
# DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEV
# MCMGCSqGSIb3DQEJBDEWBBTfJghFGP67fmGAbIiViVUsccgzLzANBgkqhkiG9w0B
# AQEFAASCAQBO+lcvfAJStkYo7kgjYs7i1wCoxkteLLsduP9hWxwXG+Gbchg08ltY
# qvrd4SM6svhHuR5KMpttoJREtZqEA02TZzlDgcl20NzeyUUsW9+K4EjZ0t+moZPS
# rnCIHOqCmtX6E8WX9UGYGZvJC569vgAVqd0sjzHzIVBCdWc3lMOR0wZNoihujTLz
# lPpQ4KqOMTLMxkejyb2Ej2U3aJnMa/WJGsj/mTFc1/qvyfER44KKy2jrl+yGOqeC
# VtlWkdqQkG2jsuTNNPgZT8pA97UzhTAC2xOZTrrgXpc5SKxxkVwBZREpkDE/Kq7a
# HIlBqSibLErZ5CIcJ+OGEyLi2YY1+hsEoYIDIDCCAxwGCSqGSIb3DQEJBjGCAw0w
# ggMJAgEBMHcwYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMu
# MTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hBMjU2IFRp
# bWVTdGFtcGluZyBDQQIQDE1pckuU+jwqSj0pB4A9WjANBglghkgBZQMEAgEFAKBp
# MBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIyMTIw
# NzAwMTUyMVowLwYJKoZIhvcNAQkEMSIEIMXAu9bL5iNvLTV+jJEvwDwt7Axt896W
# pbhuhnfd0pLBMA0GCSqGSIb3DQEBAQUABIICAMTqGMlJD69MdtHaRbmVZWBHuN4/
# SuHJv7LMeJeKSDsGU1UtIlgt+X23aLjihGgzBMGAHmSKm4gwKGrgxlyXjsGKYNt1
# BpM4A34veHq8nAzzP0aybbYZcab3282dUbPs1kzu12lil+Gfqvv3wqC0uN9N3Pgq
# y6C1rwVdbK/rJslRT7UvbfMnRNSqHlB/Mka/W2126jZTXkrLQpBnIGaLjBfdasMJ
# 4P+lZSaehFJj6qNbsekJCPjlirS28kXZGcilpyZqbCl+fhhQ+uXhYpDggHCZD/15
# mWEIPmtMqUuZ6XLi+KQA/f02OQCYjRdzzr3OnR4+zwK/i3T2963G80gg0PWcg9bz
# 08cFWDO/sw3qh/2kCqRHdF67cBp/vlSvpkOqRswNCW1Qq/Uaq8F98Y6HHaC//Yub
# 6DyjmrYb5Zb5OZ4S9fhgADEyRz91JzpIShYZkQGZGIVFCYq7VLzDcK4R7Rt4zLZm
# IR+R0lHHEQ3DszkF/BpGuupfR2fXOQcD2PpWdi7SIycX/JGp5ouIVS6je3aGRCuZ
# JIztvwy2B0KWxhw1HufV+LndquR5qFkcIgj3ANklhlM91Jw7raWCID6wXtrqa6nZ
# K7U+tDo8Fl0jBwDXol5V7uEqLv4rHchvsHZwFYrV5qZUEXlM7zc/U0iY1NuIoTsI
# 1UqqJ7VaicY+bou1
# SIG # End signature block