Runners.psm1

# https://docs.gitlab.com/ee/api/runners.html

function Get-GitlabRunner {
    [CmdletBinding(DefaultParameterSetName='ListAll')]
    param (
        [Parameter(Mandatory, Position=0, ParameterSetName='RunnerId')]
        [string]
        $RunnerId,

        [Parameter(ParameterSetName='ListAll')]
        [ValidateSet('instance_type', 'group_type', 'project_type')]
        [string]
        $Type,

        [Parameter(ParameterSetName='ListAll')]
        [ValidateSet('active', 'paused', 'online', 'offline')]
        [string]
        $Status,

        [Parameter(ParameterSetName='ListAll')]
        [string []]
        $Tags,

        [switch]
        [Parameter()]
        $FetchDetails,

        [Parameter()]
        [uint]
        $MaxPages,

        [switch]
        [Parameter()]
        $All,

        [Parameter()]
        [string]
        $SiteUrl
    )

    $Params = @{
        HttpMethod = 'GET'
        Query      = @{}
        MaxPages   = Get-GitlabMaxPages -MaxPages:$MaxPages -All:$All
        SiteUrl    = $SiteUrl
    }

    switch ($PSCmdlet.ParameterSetName) {
        # https://docs.gitlab.com/ee/api/runners.html#get-runners-details
        RunnerId { 
            $Params.Path = "runners/$RunnerId"
        }
        # https://docs.gitlab.com/ee/api/runners.html#list-all-runners
        ListAll {
            $Params.Path = 'runners/all'
            $Params.Query.type = $Type
            $Params.Query.status = $Status
            $Params.Query.tag_list = $Tags -join ','
        }
        Default { throw "Unsupported parameter combination" }
    }

    $Runners = Invoke-GitlabApi @Params | New-WrapperObject 'Gitlab.Runner'
    if ($FetchDetails) {
        $RunnerCount = $Runners.Count
        $i = 0
        $Runners | ForEach-Object {
            $PercentComplete = $($i++ / $RunnerCount * 100)
            Write-Progress "Fetching runner details ($i of $RunnerCount)" -PercentComplete $PercentComplete
            Get-GitlabRunner -RunnerId $_.Id -SiteUrl $SiteUrl
        }
    }
    $Runners
}

function Get-GitlabRunnerJob {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, Position=0, ValueFromPipelineByPropertyName)]
        [string]
        $RunnerId,

        [Parameter()]
        [uint]
        $MaxPages,

        [Parameter()]
        [switch]
        $All,

        [Parameter()]
        [string]
        $SiteUrl
    )

    # https://docs.gitlab.com/ee/api/runners.html#list-runners-jobs
    $Params = @{
        HttpMethod = 'GET'
        Path       = "runners/$RunnerId/jobs"
        MaxPages   = Get-GitlabMaxPages -MaxPages:$MaxPages -All:$All
        SiteUrl    = $SiteUrl
    }

    Invoke-GitlabApi @Params | New-WrapperObject 'Gitlab.RunnerJob'
}

function Update-GitlabRunner {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory, Position=0, ValueFromPipelineByPropertyName)]
        [string]
        $RunnerId,

        [Parameter()]
        [string]
        $Id,

        [Parameter()]
        [string]
        $Description,

        [Parameter()]
        [object]
        [ValidateSet($null, 'true', 'false')]
        $Active,

        [Parameter()]
        [string]
        $Tags,

        [Parameter()]
        [object]
        [ValidateSet($null, 'true', 'false')]
        $RunUntaggedJobs,

        [Parameter()]
        [object]
        [ValidateSet($null, 'true', 'false')]
        $Locked,

        [Parameter()]
        [string]
        [ValidateSet('not_protected', 'ref_protected')]
        $AccessLevel,

        [Parameter()]
        [uint]
        $MaximumTimeoutSeconds,

        [Parameter()]
        [string]
        $SiteUrl
    )

    $Params = @{
        HttpMethod = 'PUT'
        Path       = "runners/$RunnerId"
        Query      = @{}
        SiteUrl    = $SiteUrl
    }
    if ($Description) {
        $Params.Query.description = $Description
    }
    if ($Tags) {
        $Params.Query.tag_list = $Tags
    }
    if ($AccessLevel) {
        $Params.Query.access_level = $Tags
    }
    if ($MaximumTimeoutSeconds) {
        if ($MaximumTimeoutSeconds -lt 600) {
            throw "maximum_timeout must be >= 600"
        }
        if ($MaximumTimeoutSeconds -gt [int]::MaxValue) {
            throw "maximum_timeout must be <= $([int]::MaxValue)"
        }
        $Params.Query.maximum_timeout = $MaximumTimeoutSeconds
    }

    if ($Active) {
        $Params.Query.active = $Active
    }
    if ($RunUntaggedJobs) {
        $Params.Query.run_untagged = $RunUntaggedJobs
    }
    if ($Locked) {
        $Params.Query.locked = $Locked
    }

    if ($PSCmdlet.ShouldProcess("$($Params.Path)", "update ($($Params.Query | ConvertTo-Json))")) {
        # https://docs.gitlab.com/ee/api/runners.html#update-runners-details
        Invoke-GitlabApi @Params | New-WrapperObject 'Gitlab.Runner'
    }
}
function Suspend-GitlabRunner {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory, Position=0, ValueFromPipelineByPropertyName)]
        [string]
        $RunnerId,

        [Parameter()]
        [string]
        $SiteUrl
    )

    Update-GitlabRunner $RunnerId -Active $false -SiteUrl $SiteUrl -WhatIf:$WhatIfPreference
}

function Resume-GitlabRunner {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory, Position=0, ValueFromPipelineByPropertyName)]
        [string]
        $RunnerId,

        [Parameter()]
        [string]
        $SiteUrl
    )

    Update-GitlabRunner $RunnerId -Active $true -SiteUrl $SiteUrl -WhatIf:$WhatIfPreference
}

function Remove-GitlabRunner {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory, Position=0, ValueFromPipelineByPropertyName)]
        [Alias('Id')]
        [string]
        $RunnerId,

        [Parameter()]
        [string]
        $SiteUrl
    )

    $Runner = Get-GitlabRunner -RunnerId $RunnerId

    if ($PSCmdlet.ShouldProcess("runner $($Runner.Id) [$($Runner.Status)] ($($Runner.Summary))", "delete")) {
        # https://docs.gitlab.com/ee/api/runners.html#delete-a-runner-by-id
        if (Invoke-GitlabApi DELETE "runners/$($Runner.Id)") {
            Write-Host "Runner $($Runner.Id) deleted"
        }
    }
}

function Get-GitlabRunnerStats {
    [CmdletBinding(DefaultParameterSetName='ByTags')]
    param (
        [Parameter(ParameterSetName='ById', Mandatory, Position=0, ValueFromPipelineByPropertyName)]
        [string]
        $RunnerId,

        [Parameter(ParameterSetName='ByTags', Mandatory, Position=0, ValueFromPipelineByPropertyName)]
        [Alias('Tag')]
        [string[]]
        $RunnerTag,

        [Parameter()]
        [datetime]
        $Before,

        [Parameter()]
        [datetime]
        $After,

        [Parameter()]
        [int]
        $JobLimit = 100,

        [Parameter()]
        [string]
        $SiteUrl
    )

    if ($PSCmdlet.ParameterSetName -eq 'ById') {
        $RunnerIds = @($RunnerId) | ForEach-Object { "gid://gitlab/Ci::Runner/$($_)" }
    } elseif ($PSCmdlet.ParameterSetName -eq 'ByTags') {
        $TagList = $RunnerTag -join ','
        $GetRunners = @{
            Query = @"
            {
                runners(tagList: `"$TagList`") {
                    nodes {
                        id
                    }
                }
            }
"@

            SiteUrl = $SiteUrl
        }
        $Runners = Invoke-GitlabGraphQL @GetRunners
        $RunnerIds = $Runners.Runners.nodes.id
    }

    $Data = @()
    foreach ($RunnerId in $RunnerIds) {
        $GetJobs = @{
            Query = @"
        {
            runner(id: `"$RunnerId`") {
                id
                runnerType
                jobs(first: $JobLimit) {
                    nodes {
                        project {
                            webUrl
                        }
                        id
                        status
                        queuedDuration
                        startedAt
                    }
                }
            }
        }
"@

            SiteUrl = $SiteUrl
        }
        $Jobs = Invoke-GitlabGraphQL @GetJobs

        $Data += [pscustomobject]@{
            Runner = $RunnerId
            Jobs   = $Jobs.Runner.jobs.nodes
        }
    }

    $JobCountByStatus = [ordered]@{}
    $Data.Jobs | Group-Object -Property status -NoElement | Sort-Object -Descending Count | ForEach-Object {
        $JobCountByStatus[$_.Name.ToLower()] = $_.Count
    }
    $QueuedJobs = $Data.Jobs | Where-Object { $_.queuedDuration -ne $null -and $_.queuedDuration -gt 0 }
    $LongestQueuedJobs = $QueuedJobs | Sort-Object -Property queuedDuration -Descending | Select-Object -First 5 | ForEach-Object {
      $_ | Add-Member -MemberType NoteProperty -Name 'Uri' -Value "$($_.project.webUrl)/-/jobs/$(($_.id -split '/')[-1])" -PassThru
    }

    [PSCustomObject]@{
        JobCount          = $Data.Jobs.Count
        JobCountByStatus  = $JobCountByStatus
        JobQueuedStats    = $QueuedJobs.queuedDuration | Measure-Object -AllStats | Select-Object Count, Average, Sum, StandardDeviation
        LongestQueuedJobs = $LongestQueuedJobs | Select-Object @{l='QueuedDuration'; e={$_.queuedDuration}}, Uri
    } | New-WrapperObject 'Gitlab.RunnerStats' -PreserveCasing
}