Jobs.psm1

# https://docs.gitlab.com/ee/api/jobs.html
function Get-GitlabJob {
    [Alias('job')]
    [Alias('jobs')]
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]
        $ProjectId = '.',

        [Parameter(ParameterSetName='ByPipeline', Mandatory, ValueFromPipelineByPropertyName)]
        [string]
        $PipelineId,

        [Parameter(ParameterSetName='ByJobId', Mandatory)]
        [Alias('Id')]
        [string]
        $JobId,

        [Parameter()]
        [string]
        [ValidateSet('created', 'pending', 'running', 'failed', 'success', 'canceled', 'skipped', 'manual')]
        $Scope,

        [Parameter()]
        [string]
        $Stage,

        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        [switch]
        $IncludeRetried,

        [Parameter()]
        [switch]
        $IncludeTrace,

        [Parameter()]
        [switch]
        $IncludeVariables,

        [Parameter()]
        [string]
        $SiteUrl
    )

    $Project = Get-GitlabProject $ProjectId
    $ProjectId = $Project.Id

    $GitlabApiArguments = @{
        HttpMethod="GET"
        Query=@{}
        Path = "projects/$ProjectId/jobs"
        SiteUrl = $SiteUrl
    }

    if ($PipelineId) {
        $GitlabApiArguments.Path = "projects/$ProjectId/pipelines/$PipelineId/jobs"
    }
    if ($JobId) {
        $GitlabApiArguments.Path = "projects/$ProjectId/jobs/$JobId"
    }

    if ($Scope) {
        $GitlabApiArguments['Query']['scope'] = $Scope
    }
    if ($IncludeRetried) {
        $GitlabApiArguments['Query']['include_retried'] = $true
    }

    $Jobs = Invoke-GitlabApi @GitlabApiArguments | New-WrapperObject 'Gitlab.Job'

    if ($Stage) {
        $Jobs = $Jobs |
            Where-Object Stage -match $Stage
    }
    if ($Name) {
        $Jobs = $Jobs |
            Where-Object Name -match $Name
    }
    if ($IncludeTrace) {
        $Jobs | ForEach-Object {
            try {
                $Trace = $_ | Get-GitlabJobTrace
            }
            catch {
                $Trace = $Null
            }
            $_ | Add-Member -MemberType 'NoteProperty' -Name 'Trace' -Value $Trace
        }
    }
    if ($IncludeVariables) {
        $Jobs | ForEach-Object {
            $Data = Invoke-GitlabGraphQL @"
                {
                    project(fullPath: "$($Project.PathWithNamespace)") {
                        job(id: "gid://gitlab/Ci::Build/$($_.JobId)") {
                            manualVariables {
                                nodes {
                                  key, value
                                }
                            }
                        }
                    }
                }
"@

            $Variables = [PSCustomObject]@{}
            $Data.project.job.manualVariables.nodes | ForEach-Object {
                $Variables | Add-Member -MemberType 'NoteProperty' -Name $($_.key) -Value $_.value
            }
            $_ | Add-Member -MemberType 'NoteProperty' -Name 'Variables' -Value $Variables
        }
    }

    $Jobs
}


function Get-GitlabJobTrace {
    [Alias('trace')]
    [CmdletBinding()]
    param (

        [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$true)]
        [string]
        $ProjectId = '.',

        [Parameter(Position=0, Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [string]
        $JobId,

        [Parameter(Mandatory=$false)]
        [string]
        $SiteUrl,

        [switch]
        [Parameter(Mandatory=$false)]
        $WhatIf
    )

    $Project = Get-GitlabProject $ProjectId
    $ProjectId = $Project.Id

    $GitlabApiArguments = @{
        HttpMethod = "GET"
        Query      = @{}
        Path       = "projects/$ProjectId/jobs/$JobId/trace"
        SiteUrl    = $SiteUrl
    }

    Invoke-GitlabApi @GitlabApiArguments -WhatIf:$WhatIf
}

function Start-GitlabJob {
    [Alias('Play-GitlabJob')]
    [Alias('play')]
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory, Position=0, ValueFromPipelineByPropertyName)]
        [string]
        $JobId,

        [Parameter(ValueFromPipelineByPropertyName)]
        [string]
        $ProjectId = '.',

        [Parameter()]
        [Alias('vars')]
        $Variables,

        [Parameter()]
        [string]
        $SiteUrl
    )

    $Project = Get-GitlabProject -ProjectId $ProjectId

    $GitlabApiArguments = @{
        HttpMethod = "POST"
        Path       = "projects/$($Project.Id)/jobs/$JobId/play"
        SiteUrl    = $SiteUrl
        Body       = @{
            job_variables_attributes = @{}
        }
    }

    if ($Variables) {
        $ReformattedVariables = $Variables.GetEnumerator() | ForEach-Object {
            @{
                key           = $_.Name
                value         = $_.Value
            }
        }
        $GitlabApiArguments.Body.job_variables_attributes = @($ReformattedVariables)
    }

    if ($PSCmdlet.ShouldProcess("start job $($Project.PathWithNamespace)", "$($GitlabApiArguments | ConvertTo-Json)")) {
        try {
            # https://docs.gitlab.com/ee/api/jobs.html#run-a-job
            Invoke-GitlabApi @GitlabApiArguments | New-WrapperObject "Gitlab.Job"
        }
        catch {
            Write-Verbose $_
            if ($_.ErrorDetails.Message -match 'Unplayable Job') {
                # https://docs.gitlab.com/ee/api/jobs.html#retry-a-job
                $GitlabApiArguments.Path = $GitlabApiArguments.Path -replace '/play', '/retry'
                Invoke-GitlabApi @GitlabApiArguments | New-WrapperObject "Gitlab.Job"
            }
        }
    }
}

function Test-GitlabPipelineDefinition {

    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter()]
        [string]
        $ProjectId = '.',

        [Parameter()]
        [string]
        $Content,

        [Parameter()]
        [string]
        $Select,

        [Parameter()]
        [string]
        $SiteUrl
    )

    $Project = Get-GitlabProject $ProjectId
    $ProjectId = $Project.Id

    $Params = @{
        Path    = "projects/$ProjectId/ci/lint"
        Body    = @{}
        Query   = @{}
        SiteUrl = $SiteUrl
    }

    if ($Content) {
        if (Test-Path $Content) {
            $Content = Get-Content -Raw -Path $Content
        }
        # https://docs.gitlab.com/ee/api/lint.html#validate-the-ci-yaml-configuration
        $Params.HttpMethod                = 'POST'
        $Params.Body.content              = $Content
        $Params.Query.include_merged_yaml = 'true'
    }
    else {
        # https://docs.gitlab.com/ee/api/lint.html#validate-a-projects-ci-configuration
        $Params.HttpMethod = 'GET'
    }

    if ($PSCmdlet.ShouldProcess("$($Project.PathWithNamespace)", "Validate CI definition ($($Params | ConvertTo-Json))")) {
        Invoke-GitlabApi @Params |
            New-WrapperObject 'Gitlab.PipelineDefinition' |
            Get-FilteredObject $Select
    }
}

function Get-GitlabPipelineDefinition {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false)]
        [string]
        $ProjectId = '.',

        [Parameter(Mandatory=$false)]
        [Alias("Branch")]
        [string]
        $Ref,

        [Parameter(Mandatory=$false)]
        [string]
        $SiteUrl,

        [switch]
        [Parameter(Mandatory=$false)]
        $WhatIf
    )

    Get-GitlabRepositoryYmlFileContent -ProjectId $ProjectId -FilePath '.gitlab-ci.yml' -Ref $Ref -SiteUrl $SiteUrl
}

<#
.SYNOPSIS
Produces a section that can be collapsed in the Gitlab CI output

.PARAMETER HeaderText
Name of the section

.PARAMETER Collapsed
Whether or not the section is pre-collapsed. Not currently supported. Has no affect

.EXAMPLE
Start-GitlabJobLogSection "Doing the thing"
try {
    #the things
}
finally {
    Stop-GitlabJobLogSection
}

.NOTES
for reference: https://docs.gitlab.com/ce/ci/jobs/index.html#custom-collapsible-sections
#>

function Start-GitlabJobLogSection {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0)]
        [string]
        $HeaderText,

        [Parameter()]
        [switch]
        $Collapsed
    )

    $Timestamp = Get-EpochTimestamp
    $CollapsedHeader = ''
    if ($Collapsed) {
        $CollapsedHeader = '[collapsed=true]'
    }

    # use timestamp as the section name (since we are hiding that in our API)
    $SectionId = "$([System.Guid]::NewGuid().ToString("N"))"
    Write-Host "`e[$($global:GitlabConsoleColors.White)m`e[0Ksection_start:$($Timestamp):$($SectionId)$($CollapsedHeader)`r`e[0K$HeaderText"
    $global:GitlabJobLogSections.Push($SectionId)
}

<#
.SYNOPSIS
Closes out a previously declared collapsible section in Gitlab CI output

.DESCRIPTION
Long description

.EXAMPLE
Start-GitlabJobLogSection "Doing the thing"
try {
    #the things
}
finally {
    Stop-GitlabJobLogSection
}

.NOTES
for reference: https://docs.gitlab.com/ce/ci/jobs/index.html#custom-collapsible-sections
#>

function Stop-GitlabJobLogSection {
    [CmdletBinding()]
    param(
    )

    if ($global:GitlabJobLogSections.Count -eq 0) {
        # explicitly do nothing
        # most likely case is if stop is called more than start
        return
    }
    $PreviousId = $global:GitlabJobLogSections.Pop()
    $Timestamp = Get-EpochTimestamp
    Write-Host "section_end:$($Timestamp):$PreviousId`r`e[0K"
}

function Get-EpochTimestamp {
    [int] $(New-TimeSpan -Start $(Get-Date "01/01/1970") -End $(Get-Date)).TotalSeconds
}

function Write-GitlabJobTrace {
    [CmdletBinding()]
    param (
        [Parameter()]
        [string]
        $Text,

        [Parameter()]
        [ValidateSet('Black', 'Red', 'Green', 'Orange', 'Blue', 'Purple', 'Cyan', 'LightGray', 'DarkGray', 'LightRed', 'LightGreen', 'Yellow', 'LightBlue', 'LightPurple', 'LightCyan', 'White')]
        [string]
        $Color
    )

    if ($Color) {
        $Text = "`e[$($global:GitlabConsoleColors[$Color])m$Text"
    }

    Write-Host $Text
}