Tririga-Manage-Rest.psm1

#!/usr/bin/env pwsh
#
# PowerShell commands to manage TRIRIGA instances using the Admin REST API
#

$script:ModuleRoot = $PSScriptRoot
$script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\Tririga-Manage-Rest.psd1").ModuleVersion

function GetConfiguration() {
    if (!$TririgaEnvironments) {
        throw "Module is not configured. Visit https://github.com/nithinphilips/tririga-manage-ps1/blob/main/README.rst#configuration for instructions."
    }
    $TririgaEnvironments
}

# From https://stackoverflow.com/a/76555554/260740
function GetAllCookiesFromWebRequestSession
{
   <#
         .SYNOPSIS
         Get all cookies stored in the WebRequestSession variable from any Invoke-RestMethod and/or Invoke-WebRequest request
 
         .DESCRIPTION
         Get all cookies stored in the WebRequestSession variable from any Invoke-RestMethod and/or Invoke-WebRequest request
         The WebRequestSession stores useful info and it has something that some my know as CookieJar or http.cookiejar.
 
         .PARAMETER WebRequestSession
         Specifies a variable where Invoke-RestMethod and/or Invoke-WebRequest saves values.
         Must be a valid [Microsoft.PowerShell.Commands.WebRequestSession] object!
 
         .EXAMPLE
         PS C:\> $null = Invoke-WebRequest -UseBasicParsing -Uri 'http://jhochwald.com' -Method Get -SessionVariable WebSession -ErrorAction SilentlyContinue
         PS C:\> $WebSession | Get-AllCookiesFromWebRequestSession
 
         Get all cookies stored in the $WebSession variable from the request above.
         This page doesn't use or set any cookies, but the (awesome) CloudFlare service does.
 
           .EXAMPLE
         $null = Invoke-RestMethod -UseBasicParsing -Uri 'https://jsonplaceholder.typicode.com/todos/1' -Method Get -SessionVariable RestSession -ErrorAction SilentlyContinue
         $RestSession | Get-AllCookiesFromWebRequestSession
 
         Get all cookies stored in the $RestSession variable from the request above.
         Please do not abuse the free API service above!
 
         .NOTES
         I used something I had stolen from Chrissy LeMaire's TechNet Gallery entry a (very) long time ago.
         But I needed something more generic, independent from the URL! This can become handy, to find any cookie from a 3rd party site or another host.
 
         .LINK
         https://docs.python.org/3/library/http.cookiejar.html
 
         .LINK
         https://en.wikipedia.org/wiki/HTTP_cookie
 
         .LINK
         https://gallery.technet.microsoft.com/scriptcenter/Getting-Cookies-using-3c373c7e
 
         .LINK
         Invoke-RestMethod
 
         .LINK
         Invoke-WebRequest
   #>


   [CmdletBinding(ConfirmImpact = 'None')]
   param
   (
      [Parameter(Mandatory,
         ValueFromPipeline,
         ValueFromPipelineByPropertyName,
         Position = 0,
         HelpMessage = 'Specifies a variable where Invoke-RestMethod and/or Invoke-WebRequest saves values.')]
      [ValidateNotNull()]
      [Alias('Session', 'InputObject')]
      [Microsoft.PowerShell.Commands.WebRequestSession]
      $WebRequestSession
   )

   begin
   {
      # Do the housekeeping
      $CookieInfoObject = $null
   }

   process
   {
      try
      {
         # I know, this look very crappy, but it just work fine!
         [pscustomobject]$CookieInfoObject = ((($WebRequestSession).Cookies).GetType().InvokeMember('m_domainTable', [Reflection.BindingFlags]::NonPublic -bor [Reflection.BindingFlags]::GetField -bor [Reflection.BindingFlags]::Instance, $null, (($WebRequestSession).Cookies), @()))
      }
      catch
      {
         #region ErrorHandler
         # get error record
         [Management.Automation.ErrorRecord]$e = $_

         # retrieve information about runtime error
         $info = [PSCustomObject]@{
            Exception = $e.Exception.Message
            Reason    = $e.CategoryInfo.Reason
            Target    = $e.CategoryInfo.TargetName
            Script    = $e.InvocationInfo.ScriptName
            Line      = $e.InvocationInfo.ScriptLineNumber
            Column    = $e.InvocationInfo.OffsetInLine
         }

         # output information. Post-process collected info, and log info (optional)
         $info | Out-String | Write-Verbose

         $paramWriteError = @{
            Message      = $e.Exception.Message
            ErrorAction  = 'Stop'
            Exception    = $e.Exception
            TargetObject = $e.CategoryInfo.TargetName
         }
         Write-Error @paramWriteError

         # Only here to catch a global ErrorAction overwrite
         exit 1
         #endregion ErrorHandler
      }
   }

   end
   {
      # Dump the Cookies to the Console
      ((($CookieInfoObject).Values).Values)
   }
}

function ConvertPSObjectToHashtable
{
    param (
        [Parameter(ValueFromPipeline)]
        $InputObject
    )

    process
    {
        if ($null -eq $InputObject) { return $null }

        if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string])
        {
            $collection = @(
                foreach ($object in $InputObject) { ConvertPSObjectToHashtable $object }
            )

            Write-Output -NoEnumerate $collection
        }
        elseif ($InputObject -is [psobject])
        {
            $hash = @{}

            foreach ($property in $InputObject.PSObject.Properties)
            {
                $hash[$property.Name] = (ConvertPSObjectToHashtable $property.Value).PSObject.BaseObject
            }

            $hash
        }
        else
        {
            $InputObject
        }
    }
}

function HasPager() {
    $oldPreference = $ErrorActionPreference;
    $ErrorActionPreference = "stop";
    try {
        if(Get-Command "bat"){
            return $true
        }
    }
    catch {
        return $false
    }
    finally {
        $ErrorActionPreference=$oldPreference;
    }
}

function CallTririgaApiRaw() {
    param(
        [Parameter(Mandatory)]
        [string]$serverUrlBase,
        [string]$apiMethod = "GET",
        [Parameter(Mandatory)]
        [string]$apiPath,
        $apiBody,
        [Parameter(Mandatory)]
        [string]$username,
        [Parameter(Mandatory)]
        [string]$password,

        [boolean]$useProxy = $false,
        [string]$proxyUrl = "http://localhost:8080"
    )

    if ($input) {
        $apiBody = $input
    }

    if ($useProxy) {
        $proxyProps = @{
            Proxy = $proxyUrl
            ProxyUseDefaultCredentials = $true
        }
        [System.Net.WebRequest]::DefaultWebProxy = New-Object System.Net.WebProxy($proxyUri, $true)
        [System.Net.WebRequest]::DefaultWebProxy.BypassProxyOnLocal = $false
    } else {
        $proxyProps = @{}
    }

    $logonUrl = "$serverUrlBase/p/websignon/signon"
    $apiUrl = "$serverUrlBase$apiPath"

    $loginData = @{
        "userName"=$username;
        "password"=$password;
        "normal"="false";
        "api"="true";
    }

    $tririgaSession = $null

    if($tririgaSessionTable) {
        Write-Verbose "Session Table found"
        $tririgaSession = $tririgaSessionTable[$serverUrlBase]
    } else {
        Write-Verbose "Initialize Session table"
        $sessionTable = @{}
        # This is now failing to persist!
        New-Variable -Name tririgaSessionTable -Value $sessionTable -Scope Script -Force -WhatIf:$false -Confirm:$false
    }

    # TODO: The session might be stale, we need a way to check and invalidate $tririgaSession
    if(!$tririgaSession) {
        # https://thedavecarroll.com/powershell/how-i-implement-module-variables/
        $tririgaSession = [Microsoft.PowerShell.Commands.WebRequestSession]::new()
        $tririgaSessionTable[$serverUrlBase] = $tririgaSession

        #$loginResponseHeaders = @{}

        Write-Verbose "No active sessions found. Send Login request to $logonUrl"
        $loginResponse = Invoke-RestMethod -Method POST `
                -WebSession $tririgaSession `
                @proxyProps `
                -ContentType application/json `
                -Body ($loginData | ConvertTo-Json) `
                -Uri $logonUrl

        Write-Verbose $loginResponse

        # -ResponseHeadersVariable loginResponseHeaders `
        # ResponseHeadersVariable param is not supported in PS 5:
        #Write-Verbose ($loginResponseHeaders | ConvertTo-Json)

        # Ignore cookie Secure and HttpOnly attributes
        # GetAllCookies is not supported in .net framework 4.0 (used by PS 5). It's only in .NET 6+
        #ForEach($cookie in $session.Cookies.GetAllCookies()) {

        if($PSVersionTable.PSVersion.Major -gt 5) {
            Write-Verbose "Modify Cookies"
            ForEach($cookie in $tririgaSession.Cookies.GetAllCookies()) {
                $cookie.Secure = $false
                $cookie.HttpOnly = $false
                Write-Verbose $cookie
            }
        } else {
            $cookies = GetAllCookiesFromWebRequestSession $tririgaSession
            foreach ($cookie in $cookies)
            {
                $cookie.Secure = $false
                $cookie.HttpOnly = $false
                Write-Verbose $cookie
            }
            Write-Verbose "Modify Cookies (PS v5 method)"
        }
    } else {
        Write-Verbose "Active session found. Not logging in again"
    }

    if ($VerbosePreference) {
        Write-Verbose "Send $apiMethod request to $apiUrl"
        if($apiBody) {
            Write-Verbose " with data: $($apiBody | ConvertTo-Json)"
        }
    }

    try {
        $response = Invoke-RestMethod -Method $apiMethod -WebSession $tririgaSession -ContentType application/json -Uri $apiUrl @proxyProps -Body $apiBody
        return $response
    } catch {
        if ($_.Exception.Response.StatusCode.value__ -eq 401) {
            Write-Error "Got HTTP 401 (unauthorized) error. Your session for $serverUrlBase may have expired. It has been reset. Please Try again."
            $tririgaSessionTable[$serverUrlBase] = $null
        }
    }
}

function CallTririgaApi() {
    param(
        [Parameter(Mandatory)]
        [string]$environment,
        [string]$instance = $null,
        [string]$apiMethod = "GET",
        $apiBody,
        [Parameter(Mandatory)]
        [string]$apiPath,
        # When no $instance is set, runs on any one instance
        [switch]$onlyOnAnyOneInstance = $false,
        [switch]$noTag = $false
    )

    if ($input) {
        $apiBody = $input
    }

    $tririgaEnvironment = (GetConfiguration)[$environment]

    if (!$tririgaEnvironment) {
        Write-Error "The environment `"$environment`" was not found."
        Write-Error "Possible values are: $(((GetConfiguration).keys) -join ', ')"
        return $null
    }

    if ($tririgaEnvironment) {
        ForEach($inst in $tririgaEnvironment.Servers.keys) {
            Write-Verbose "CallTririgaApiRaw: On $environment.$inst."

            # If $instance is set, run only on the given instance
            if($instance -and !($instance -eq $inst)) {
                Write-Verbose "Wanted: $instance, Got: $inst. Skip to next"
                continue
            }

            $tririgaInstance = $tririgaEnvironment["Servers"][$inst]
            $hostUrl = $tririgaInstance["ApiUrl"]
            if (!$hostUrl){
                $hostUrl = $tririgaInstance["Url"]
            }

            if (!$hostUrl) {
                Write-Error "Neither 'ApiUrl' nor 'Url' not set correctly for $environment.$inst"
                return $null
            }

            if (!$tririgaEnvironment.username) {
                Write-Error "Username property is not set for $environment environment"
                return $null
            }

            if (!$tririgaEnvironment.password) {
                Write-Error "Password property is not set for $environment environment"
                return $null
            }

            $result = CallTririgaApiRaw -serverUrlBase $hostUrl -apiMethod $apiMethod -apiPath $apiPath -apiBody $apiBody -username $tririgaEnvironment.username -password $tririgaEnvironment.password


            # TODO: Only do this of the result is PSObject
            if (!$noTag) {
                $result = $result | Add-Member -PassThru environment $environment | Add-Member -PassThru instance $inst
            }

            # Yield Result
            $result

            if (!$instance -and $onlyOnAnyOneInstance) {
                Write-Verbose "OnlyAnyOneInstance is set. Stop after the first one."
                break
            }
        }
    } else {
        return $null
    }
}

#
#
# Public Methods
#
#

<#
.SYNOPSIS
Gets TRIRIGA build number
.DESCRIPTION
Gets TRIRIGA build number
 
Uses the /api/v1/admin/buildNumber method
#>

function Get-BuildNumber() {
    [CmdletBinding()]
    param(
        # The TRIRIGA environment to use.
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [Alias("Env", "E")]
        [string]$environment,
        # The TRIRIGA instance within the environment to use.
        # If omitted, command will act on all instances.
        [Alias("Inst", "I")]
        [Parameter(Position=1)]
        [string]$instance
    )

    $apiCall = @{
        Environment = $environment
        Instance = $instance
        ApiMethod = "GET"
        ApiPath = "/api/v1/admin/buildNumber"
    }

    CallTririgaApi @apiCall
}

function ResolveLogCategoryDescriptions() {
    [CmdletBinding()]
    param(
        [string]$environment,
        [string]$instance,
        [string[]]$category
    )

    # Lookup category name from the given description
    $lookupApiCall = @{
        Environment = $environment
        Instance = $instance
        ApiMethod = "GET"
        ApiPath = "/api/v1/admin/platformLogging/list"
        OnlyOnAnyOneInstance = $true
    }

    $loggingCategories = CallTririgaApi @lookupApiCall

    #$loggingCategories

    $categoryIds = @()

    foreach($categoryItem in $category) {
        Write-Verbose "Search for $categoryItem"
        $categoryIds += ($loggingCategories | Where-Object -Property description -Eq $categoryItem)
    }

    return $categoryIds
}


<#
.SYNOPSIS
Gets basic information about a TRIRIGA instance
.DESCRIPTION
Gets basic information about a TRIRIGA instance
 
Uses the /api/v1/admin/summary method
.EXAMPLE
PS> Get-Summary Local -Raw | Select-Object databaseConnection
databaseConnection
------------------
jdbc:oracle:thin:@localhost:1521/XEPDB1
#>

function Get-Summary() {
    [CmdletBinding()]
    param(
        # The TRIRIGA environment to use.
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [Alias("Env", "E")]
        [string]$environment,
        # The TRIRIGA instance within the environment to use.
        # If omitted, command will act on the first instance.
        [Alias("Inst", "I")]
        [string]$instance
    )

    $apiCall = @{
        Environment = $environment
        Instance = $instance
        ApiMethod = "GET"
        ApiPath = "/api/v1/admin/summary"
        OnlyOnAnyOneInstance = $true
    }

    CallTririgaApi @apiCall
}

<#
.SYNOPSIS
Gets TRIRIGA Agents configuration
.DESCRIPTION
Gets TRIRIGA Agents configuration
 
Uses the /api/v1/admin/agent/status method
#>

function Get-Agent() {
    [CmdletBinding()]
    param(
        # The TRIRIGA environment to use.
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [Alias("Env", "E")]
        [string]$environment,
        # The TRIRIGA instance within the environment to use.
        # If omitted, command will act on the first instance.
        [Alias("Inst", "I")]
        [string]$instance,
        # If provided, only lists the given agent.
        [Parameter(Position=1)]
        [string]$agent,
        # If set, only list agents that are running
        [switch]$running,
        # If set, only list agents that are not running
        [switch]$notRunning
    )

    $agentArg = ""
    if($agent) {
        $agentArg = "?agent=$agent"
    }

    $apiCall = @{
        Environment = $environment
        Instance = $instance
        ApiMethod = "GET"
        ApiPath = "/api/v1/admin/agent/status$agentArg"
        OnlyOnAnyOneInstance = $true
        NoTag = $true
    }

    $result = CallTririgaApi @apiCall

    foreach ($property in $result.PSObject.Properties)
    {
        if($running -and $property.Value.Status -ne "Running") { continue }
        if($notRunning -and $property.Value.Status -ne "Not Running") { continue }

        New-Object PSObject -Property @{
            "ID" = $property.Value.startupId;
            "Agent" = $property.Value.agent;
            "Hostname" = $property.Value.hostname;
            "Status" = $property.Value.status;
        };
    }
}

<#
.SYNOPSIS
Gets the configured host(s) for the given agent
.DESCRIPTION
Gets the configured host(s) for the given agent
 
Uses the /api/v1/admin/agent/status method
.EXAMPLE
PS> Get-AgentHost -Environment LOCAL -Agent WFAgent | ForEach-Object { Write-Host "WFAgent is configured on $_" }
WFAgent is configured on localhost
WFAgent is configured on somewhere
.EXAMPLE
PS> Get-AgentHost -Environment LOCAL -Agent WFAgent | ForEach-Object { Write-Host "WFAgent is running on $_" }
WFAgent is running on localhost
#>

function Get-AgentHost() {
    [CmdletBinding()]
    param(
        # The TRIRIGA environment to use.
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [Alias("Env", "E")]
        [string]$environment,
        # The TRIRIGA instance within the environment to use.
        # If omitted, command will act on the first instance.
        [Alias("Inst", "I")]
        [string]$instance,
        # Set the type of agent to query. Run 'Get-Agent' to see a list of all possible agent types.
        [Parameter(Mandatory, Position=1)]
        [string]$agent,
        # If set, only list the host when the agent is running
        [switch]$running,
        # If set, only list the host when the aget is not running
        [switch]$notRunning
    )

    $agentCall = @{
        Environment = $environment
        Instance = $instance
        Agent = $agent
        Running = $running
        NotRunning = $notRunning
    }

    Get-Agent @agentCall | ForEach-Object { $_.Hostname }
}


<#
.SYNOPSIS
Gets a list of users who can access the TRIRIGA Admin Console
.DESCRIPTION
Gets a list of users with access to the TRIRIGA Admin Console
 
Not all listed users have active access. They are in the Admin group an can be granted access.
 
Uses /api/v1/admin/users/list method
.EXAMPLE
PS> Get-TririgaAdminUser LOCAL | Where-Object fullaccess -eq True
userId fullaccess username fullName
------ ---------- -------- --------
221931 True system System System
#>

function Get-AdminUser() {
    [CmdletBinding()]
    param(
        # The TRIRIGA environment to use.
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [Alias("Env", "E")]
        [string]$environment,
        # The TRIRIGA instance within the environment to use.
        # If omitted, command will act on the first instance.
        [Alias("Inst", "I")]
        [string]$instance
    )

    $apiCall = @{
        Environment = $environment
        Instance = $instance
        ApiMethod = "GET"
        ApiPath = "/api/v1/admin/users/list"
        OnlyOnAnyOneInstance = $true
    }

    CallTririgaApi @apiCall `
    | ForEach-Object { $_ | Add-Member -PassThru "fullaccess" ( `
        $_.adminSummary -and $_.adminUsers -and $_.agents -and $_.buildNumber -and `
        $_.caches -and $_.databaseInfo -and $_.databaseQueryTool -and $_.dataConnect -and `
        $_.errorLogs -and $_.javaInfo -and $_.licenses -and $_.maintenanceSchedule -and `
        $_.metadataAnalysis -and $_.performanceMonitor -and $_.platformLogging -and `
        $_.schedulerInfo -and $_.systemInfo -and $_.threadSettings -and $_.usersLoggedIn `
        -and $_.workflowAgentInfo -and $_.workflowEvents -and $_.workflowExecuting `
        -and $_.mustGatherTool ) } `
    | ForEach-Object { New-Object PSObject -Property @{ "userId" = $_.userId; "fullName" = $_.fullName; "username" = $_.username; "fullaccess" = $_.fullaccess; }; }

}

<#
.SYNOPSIS
Gets a list of currently logged in users
.DESCRIPTION
Gets a list of currently logged in users
.EXAMPLE
PS> Get-TririgaActiveUser LOCAL | ft
#>

function Get-ActiveUser() {
    [CmdletBinding()]
    param(
        # The TRIRIGA environment to use.
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [Alias("Env", "E")]
        [string]$environment,
        # The TRIRIGA instance within the environment to use.
        # If omitted, command will act on all instances.
        [Alias("Inst", "I")]
        [Parameter(Position=1)]
        [string]$instance,
        # If set, the response object is returned as-is. Otherwise it is printed as a table.
        [switch]$raw,
        # If set, and not $raw, only print unique users no matter how many sessions they have
        [switch]$unique
    )

    $apiCall = @{
        Environment = $environment
        Instance = $instance
        ApiMethod = "GET"
        ApiPath = "/api/v1/admin/activeUsers/list"
    }

    $resultHash = (CallTririgaApi @apiCall).Replace('eMail', 'email1') | ConvertFrom-Json

    # If no -Instance value, then $resultHash is an array of array. Otherwise it is an array of objects.
    # Unroll array of arrays into an array of objects.
    if(!$instance) {
        $newResultHash = @()
        $resultHash | ForEach-Object { $newResultHash += $_ }
        $resultHash = $newResultHash
    }

    $now = Get-Date # -AsUTC Only in PS 7

    foreach($item in $resultHash)
    {
        # Only PS 7+
        #$item["lastTouch"] = (Get-Date -UnixTimeSeconds (([long]$item["lastTouchDateTime"]) / 1000) -AsUTC)

        $unixTime = (([long]$item.lastTouchDateTime) / 1000)
        $item | Add-Member lastTouch ((([System.DateTimeOffset]::FromUnixTimeSeconds($unixTime)).DateTime).ToString("s"))
        $item | Add-Member lastTouchDuration ("{0:dd}d:{0:hh}h:{0:mm}m:{0:ss}s" -f (New-TimeSpan -Start $item.lastTouch -End $now))
    }


    if($raw) {
        $resultHash
    } else {
        if ($unique) {
            $resultHash | Sort-Object -Property userAccount -Unique | Select-Object userAccount,fullName,email,lastTouchDuration
        } else {
            $resultHash | Select-Object userAccount,fullName,email,lastTouchDuration
        }
    }

}

<#
.SYNOPSIS
Stops a TRIRIGA agent
.DESCRIPTION
Stops a TRIRIGA agent
 
Uses the /api/v1/admin/agent/stop method
#>

function Stop-Agent() {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The TRIRIGA environment to use.
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [Alias("Env", "E")]
        [string]$environment,
        # The TRIRIGA instance within the environment to use.
        # If omitted, command will act on the first instance.
        [Alias("Inst", "I")]
        [string]$instance,
        # The Agent to stop. All instances of the agent are stopped.
        [Parameter(Mandatory, Position=1)]
        [string]$agent
    )

    $statusApiCall = @{
        Environment = $environment
        Instance = $instance
        ApiMethod = "GET"
        ApiPath = "/api/v1/admin/agent/status?agent=$agent"
        OnlyOnAnyOneInstance = $true
        NoTag = $true
    }

    $agentInfo = CallTririgaApi @statusApiCall | ConvertPSObjectToHashtable

    ForEach($agent in $agentInfo.Keys) {
        $thisAgent = $agentInfo[$agent]

        $agentHost = $thisAgent.hostname
        $agentId = $agent

        # When the configuration is <ANY>, we can't tell what server is actually running the agent
        # Since it's usually seen with Vagrant, guess localhost
        if($agentHost -eq "<ANY>"){
            $agentHost = "localhost"
        }

        if($PSCmdlet.ShouldProcess("$($thisAgent.agent) [$agent] on $agentHost", "Stop")){
            $stopApiCall = @{
                Environment = $environment
                Instance = $instance
                ApiMethod = "POST"
                ApiPath = "/api/v1/admin/agent/stop?agent=$agent&startOnHost=$agentHost&runningOnHost=$agentHost&startupId=$agentId"
                OnlyOnAnyOneInstance = $true
            }

            $result = CallTririgaApi @stopApiCall

            # Some values are in single-item arrays. Flatten it and replace the values
            $result `
            | Add-Member -PassThru "agent" ($result.agent -Join ",") -Force `
            | Add-Member -PassThru "agentname" $agent -Force `
            | Add-Member -PassThru "hostname" ($result.hostname -Join ",") -Force `
            | Add-Member -PassThru "status" ($result.status -Join ",") -Force
        }


    }
}

<#
.SYNOPSIS
Starts a TRIRIGA agent
.DESCRIPTION
Starts a TRIRIGA agent
 
Uses the /api/v1/admin/agent/start method
#>

function Start-Agent() {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The TRIRIGA environment to use.
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [Alias("Env", "E")]
        [string]$environment,
        # The TRIRIGA instance within the environment to use.
        # If omitted, command will act on the first instance.
        [Alias("Inst", "I")]
        [string]$instance,
        # The Agent to start. All instances of the agent are started.
        [Parameter(Mandatory, Position=1)]
        [string]$agent
    )

    $statusApiCall = @{
        Environment = $environment
        Instance = $instance
        ApiMethod = "GET"
        ApiPath = "/api/v1/admin/agent/status?agent=$agent"
        OnlyOnAnyOneInstance = $true
        NoTag = $true
    }

    $agentInfo = CallTririgaApi @statusApiCall | ConvertPSObjectToHashtable

    ForEach($agent in $agentInfo.Keys) {
        $thisAgent = $agentInfo[$agent]

        $agentHost = $thisAgent.hostname
        $agentId = $agent

        if($agentHost -eq "<ANY>"){
            $agentHost = "localhost"
        }

        if($PSCmdlet.ShouldProcess("$($thisAgent.agent) [$agent] on $agentHost", "Start")){
            $startApiCall = @{
                Environment = $environment
                Instance = $instance
                ApiMethod = "POST"
                ApiPath = "/api/v1/admin/agent/start?agent=$agent&hostname=$agentHost&startupId=$agentId"
                OnlyOnAnyOneInstance = $true
            }

            $result = CallTririgaApi @startApiCall

            # Some values are in single-item arrays. Flatten it and replace the values
            $result `
            | Add-Member -PassThru "agent" ($result.agent -Join ",") -Force `
            | Add-Member -PassThru "agentname" $agent -Force `
            | Add-Member -PassThru "hostname" ($result.hostname -Join ",") -Force `
            | Add-Member -PassThru "status" ($result.status -Join ",") -Force
        }

    }
}

<#
.SYNOPSIS
Updates workflow instance recording setting
.DESCRIPTION
Updates workflow instance recording setting
 
Uses the /api/v1/admin/workflowAgentInfo/workflowInstance/update method
#>

function Set-WorkflowInstance() {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The TRIRIGA environment to use.
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [Alias("Env", "E")]
        [string]$environment,
        # The TRIRIGA instance within the environment to use.
        # If omitted, command will act on all instances.
        [Alias("Inst", "I")]
        [Parameter()]
        [string]$instance,
        # The value to set.
        [ValidateSet("ALWAYS", "ERRORS_ONLY", "PER_WORKFLOW_ALWAYS", "PER_WORKFLOW_PRODUCTION", "DATA_LOAD")]
        [Parameter(Mandatory, Position=1)]
        [string]$value
    )


    $apiCall = @{
        Environment = $environment
        Instance = $instance
        ApiMethod = "POST"
        ApiPath = "/api/v1/admin/workflowAgentInfo/workflowInstance/update?instanceName=$value"
    }

    $instanceLabel = "[All]"
    if($instance) { $instanceLabel = $instance }

    if($PSCmdlet.ShouldProcess("$environment.$instanceLabel")){
        CallTririgaApi @apiCall
    }
}

<#
.SYNOPSIS
Sets the workflow instance recording setting to ALWAYS
.DESCRIPTION
Sets the workflow instance recording setting to ALWAYS
 
Uses the /api/v1/admin/workflowAgentInfo/workflowInstance/update method
#>

function Enable-WorkflowInstance() {
    [CmdletBinding()]
    param(
        # The TRIRIGA environment to use.
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [Alias("Env", "E")]
        [string]$environment,
        # The TRIRIGA instance within the environment to use.
        # If omitted, command will act on all instances.
        [Alias("Inst", "I")]
        [Parameter()]
        [string]$instance
    )

    Set-WorkflowInstance -Environment $environment -Value "ALWAYS" -Instance $instance
}

<#
.SYNOPSIS
Sets the workflow instance recording setting to ERRORS_ONLY
.DESCRIPTION
Sets the workflow instance recording setting to ERRORS_ONLY
 
Uses the /api/v1/admin/workflowAgentInfo/workflowInstance/update method
#>

function Disable-WorkflowInstance() {
    [CmdletBinding()]
    param(
        # The TRIRIGA environment to use.
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [Alias("Env", "E")]
        [string]$environment,
        # The TRIRIGA instance within the environment to use.
        # If omitted, command will act on all instances.
        [Alias("Inst", "I")]
        [Parameter()]
        [string]$instance
    )

    Set-WorkflowInstance -Environment $environment -Value "ERRORS_ONLY" -Instance $instance
}

<#
.SYNOPSIS
Write a message to TRIRIGA Log file
.DESCRIPTION
Write a message to TRIRIGA Log file
 
Uses the /api/v1/admin/platformLogging/write method
#>

function Write-LogMessage() {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The TRIRIGA environment to use.
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [Alias("Env", "E")]
        [string]$environment,
        # The TRIRIGA instance within the environment to use.
        # If omitted, command will act on all instances.
        [Alias("Inst", "I")]
        [Parameter()]
        [string]$instance,
        # The value to set.
        [Parameter(Mandatory, ValueFromPipeline, Position=1)]
        [string]$message
    )

    $messageEscaped = [System.Net.WebUtility]::UrlEncode($message)

    $apiCall = @{
        Environment = $environment
        Instance = $instance
        ApiMethod = "POST"
        ApiPath = "/api/v1/admin/platformLogging/write?message=$messageEscaped"
    }

    $instanceLabel = "[All]"
    if($instance) { $instanceLabel = $instance }

    if($PSCmdlet.ShouldProcess("$environment.$instanceLabel", "Write To Log")) {
        CallTririgaApi @apiCall
    }
}

<#
.SYNOPSIS
Reload logging categories from disk
.DESCRIPTION
Reload logging categories from disk
 
Uses the /api/v1/admin/platformLogging/reload method
#>

function Sync-PlatformLogging() {
    [Alias("Reload-PlatformLogging")]
    [CmdletBinding()]
    param(
        # The TRIRIGA environment to use.
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [Alias("Env", "E")]
        [string]$environment,
        # The TRIRIGA instance within the environment to use.
        # If omitted, command will act on all instances.
        [Alias("Inst", "I")]
        [Parameter()]
        [string]$instance
    )

    $apiCall = @{
        Environment = $environment
        Instance = $instance
        ApiMethod = "GET"
        ApiPath = "/api/v1/admin/platformLogging/reload"
    }

    $instanceLabel = "[All]"
    if($instance) { $instanceLabel = $instance }

    if($PSCmdlet.ShouldProcess("$environment/$instanceLabel", "Sync Log Categories")) {
        CallTririgaApi @apiCall
    }
}

<#
.SYNOPSIS
Reset duplicate categories
.DESCRIPTION
Reset duplicate categories
 
Uses the /api/v1/admin/platformLogging/resetDuplicates method
#>

function Reset-PlatformLoggingDuplicates() {
    [CmdletBinding()]
    param(
        # The TRIRIGA environment to use.
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [Alias("Env", "E")]
        [string]$environment,
        # The TRIRIGA instance within the environment to use.
        # If omitted, command will act on all instances.
        [Alias("Inst", "I")]
        [Parameter()]
        [string]$instance
    )

    $apiCall = @{
        Environment = $environment
        Instance = $instance
        ApiMethod = "GET"
        ApiPath = "/api/v1/admin/platformLogging/resetDuplicates"
    }

    $instanceLabel = "[All]"
    if($instance) { $instanceLabel = $instance }

    if($PSCmdlet.ShouldProcess("$environment/$instanceLabel", "Reset Log Duplicates")) {
        CallTririgaApi @apiCall
    }
}

<#
.SYNOPSIS
Gets information about TRIRIGA platform Logging
.DESCRIPTION
Enables TRIRIGA platform Logging for the given categories
 
Uses the /api/v1/admin/platformLogging/list method
#>

function Get-PlatformLogging() {
    [CmdletBinding()]
    param(
        # The TRIRIGA environment to use.
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [Alias("Env", "E")]
        [string]$environment,
        # The TRIRIGA instance within the environment to use.
        # If omitted, command will act on all instances.
        [Alias("Inst", "I")]
        [Parameter()]
        [string]$instance,
        # If set, only shows categories that have logging currently enabled
        [switch]$enabled,
        # The maximum level to show. Set to higher number (like 99) to see all levels.
        [int]$level = 1
    )

    $apiCall = @{
        Environment = $environment
        Instance = $instance
        ApiMethod = "GET"
        ApiPath = "/api/v1/admin/platformLogging/list"
    }

    $instanceLabel = "[All]"
    if($instance) { $instanceLabel = $instance }

    if ($enabled) {
        CallTririgaApi @apiCall | Where-Object -Property checkedStatus -eq "CHECKED" | Where-Object -Property level -le $level
    } else {
        CallTririgaApi @apiCall | Where-Object -Property level -le $level
    }
}

<#
.SYNOPSIS
Sets a logging level for the given TRIRIGA platform Logging categories
.DESCRIPTION
Sets a logging level for the given TRIRIGA platform Logging categories
 
WARNING: We have not been able to make this API call work.
 
Uses the /api/v1/admin/platformLogging/debug/manual method
#>

function Set-PlatformLogging() {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The TRIRIGA environment to use.
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [Alias("Env", "E")]
        [string]$environment,
        # The TRIRIGA instance within the environment to use.
        # If omitted, command will act on all instances.
        [Alias("Inst", "I")]
        [Parameter()]
        [string]$instance,
        # One or more categories to enable
        [Parameter(Mandatory, Position=1)]
        [ValidateNotNullOrEmpty()]
        [string[]]$category,
        # The log level to set for the category
        [string]$level="TRACE"
    )

    #TODO: THis has no effect. Not sure what the correct values to pass are
    $categoryObjects = ResolveLogCategoryDescriptions -Environment $environment -Instance $instance -Category $category

    foreach($categoryObject in $categoryObjects) {

        $categoryName = $categoryObject.Name.split(".")[-1]
        $categoryNameEscaped = [System.Net.WebUtility]::UrlEncode($categoryName)

        $apiCall  = @{
            Environment = $environment
            Instance = $instance
            ApiMethod = "POST"
            ApiPath = "/api/v1/admin/platformLogging/debug/manual?categoryPackage=$categoryNameEscaped&level=$level"
        }

        $instanceLabel = "[All]"
        if($instance) { $instanceLabel = $instance }

        if($PSCmdlet.ShouldProcess("$environment.$instanceLabel")) {
            CallTririgaApi @apiCall | Add-Member -PassThru category $categoryObject.Description
        }
    }
}

<#
.SYNOPSIS
Enables TRIRIGA platform Logging for the given categories
.DESCRIPTION
Enables TRIRIGA platform Logging for the given categories
 
1. To see available log categories, run:
        Get-TririgaPlatformLogging <ENV> -Level 1 | Select-Object description
   Increase level to see sub categories
2. The -Category argument is the "description" of Get-TririgaPlatformLogging output .
   If you are looking in the TRIRIGA Admin Console, it is the name of the category that you see there.
3. Multiple categories can be given. See examples.
4. If the description matches multiple categories, all matches will be enabled.
 
Uses the /api/v1/admin/platformLogging/enable method
.EXAMPLE
PS> Enable-PlatformLogging LOCAL "SQL", "Workflow Logging", "Data Integrator (DataImport) Agent"
#>

function Enable-PlatformLogging() {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The TRIRIGA environment to use.
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [Alias("Env", "E")]
        [string]$environment,
        # The TRIRIGA instance within the environment to use.
        # If omitted, command will act on all instances.
        [Alias("Inst", "I")]
        [Parameter()]
        [string]$instance,
        # One or more categories to enable
        [Parameter(Mandatory, Position=1)]
        [ValidateNotNullOrEmpty()]
        [string[]]$category
    )

    $categoryObjects = ResolveLogCategoryDescriptions -Environment $environment -Instance $instance -Category $category

    foreach($categoryObject in $categoryObjects) {

        $categoryName = $categoryObject.Name.split(".")[-1]
        $categoryNameEscaped = [System.Net.WebUtility]::UrlEncode($categoryName)

        $apiCall  = @{
            Environment = $environment
            Instance = $instance
            ApiMethod = "POST"
            ApiPath = "/api/v1/admin/platformLogging/enable?category=$categoryNameEscaped"
        }

        $instanceLabel = "[All]"
        if($instance) { $instanceLabel = $instance }

        if($PSCmdlet.ShouldProcess("$environment.$instanceLabel/$($categoryObject.Description)", "Enable Logging")) {
            CallTririgaApi @apiCall | Add-Member -PassThru category $categoryObject.Description
        }
    }
}

<#
.SYNOPSIS
Disables TRIRIGA platform Logging for the given categories
.DESCRIPTION
Disables TRIRIGA platform Logging for the given categories
 
If no category is given, all currently enabled categories will be disabled.
 
1. To see available log categories, run:
        Get-TririgaPlatformLogging <ENV> -Level 1 | Select-Object description
   Increase level to see sub categories
2. The -Category argument is the "description" of Get-TririgaPlatformLogging output .
   If you are looking in the TRIRIGA Admin Console, it is the name of the category that you see there.
3. Multiple categories can be given. See examples.
4. If the description matches multiple categories, all matches will be enabled.
 
Uses the /api/v1/admin/platformLogging/enable method
.EXAMPLE
PS> Enable-PlatformLogging LOCAL "SQL", "Workflow Logging", "Data Integrator (DataImport) Agent"
#>

function Disable-PlatformLogging() {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The TRIRIGA environment to use.
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [Alias("Env", "E")]
        [string]$environment,
        # The TRIRIGA instance within the environment to use.
        # If omitted, command will act on all instances.
        [Alias("Inst", "I")]
        [Parameter()]
        [string]$instance,
        # One or more categories to enable
        [Parameter(Position=1)]
        [string[]]$category
    )

     $categoryObjects = @()

    if(!$category) {
        Write-Verbose "No categories specified. All enabled categories will be disabled."
        $categoryObjects = (Get-PlatformLogging -Environment $environment -Instance $instance -Enabled -Level 99 )
    } else {
         $categoryObjects = ResolveLogCategoryDescriptions -Environment $environment -Instance $instance -Category $category
    }

    foreach($categoryObject in $categoryObjects) {

        $categoryName = $categoryObject.Name.split(".")[-1]
        $categoryNameEscaped = [System.Net.WebUtility]::UrlEncode($categoryName)

        $apiCall  = @{
            Environment = $environment
            Instance = $instance
            ApiMethod = "POST"
            ApiPath = "/api/v1/admin/platformLogging/disable?category=$categoryNameEscaped"
        }

        $instanceLabel = "[All]"
        if($instance) { $instanceLabel = $instance }

        if($PSCmdlet.ShouldProcess("$environment/$instanceLabel/$($categoryObject.Description)", "Disable Logging")) {
            CallTririgaApi @apiCall | Add-Member -PassThru category $categoryObject.Description
        }
    }
}