PSSemaphore.psm1

function Connect-Semaphore
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [ValidatePattern("^(https?://[\w\.-]+)")]
        [String]$Url,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSCredential]$Credential
    )


    try
    {
        # Add a trailing slash if it's not there:
        if($Url[-1] -ne '/')
        {
            $Url += '/'
        }
        $APIBaseEndPoint = $Url + 'api'

        # Set a script scoped variable containing the host URL (and any other required data), to be used by all calls within the module:
        $Script:Config = [PSCustomObject]@{
            url = $APIBaseEndPoint
        }
    }
    catch
    {
        throw $_
    }


    Write-Verbose -Message "Logging into $Url as $($Credential.UserName)"
    try
    {
        # Construct the body of the request for logging in:
        $Body = @{
            'auth'     = $Credential.UserName
            'password' = $Credential.GetNetworkCredential().Password
        } | ConvertTo-Json -Compress

        # Make the call to login, storing the session in a script scoped variable to be used by all calls within the module:
        Invoke-RestMethod -Uri "$($Script:Config.url)/auth/login" -Method Post -Body $Body -ContentType 'application/json' -SessionVariable Script:Session | Out-Null
    }
    catch
    {
        throw $_
    }
}
function Disable-SemaphoreUserToken
{
    [CmdletBinding(SupportsShouldProcess)]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'Does not alter system state.')]
    param (
        [Parameter(Mandatory = $true)]
        [String]
        $TokenId
    )

    begin
    {
        Write-Verbose -Message "Calling function $($MyInvocation.MyCommand)"
        if(!$Script:Session)
        {
            throw "Please run Connect-Semaphore first"
        }
    }
    process
    {
        # Encode the token:
        $TokenId = [System.Web.HttpUtility]::UrlEncode($TokenId)

        try
        {
            Invoke-RestMethod -Uri "$($Script:Config.url)/user/tokens/$TokenId" -Method Delete -ContentType 'application/json' -WebSession $Script:Session
        }
        catch
        {
            throw $_
        }
    }
    end
    {
    }
}
function Get-SemaphoreProject
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [string]
        $Name
    )

    begin
    {
        Write-Verbose -Message "Calling function $($MyInvocation.MyCommand)"
        if(!$Script:Session)
        {
            throw "Please run Connect-Semaphore first"
        }
    }
    process
    {
        Write-Verbose -Message "Getting projects"
        try
        {
            $Data = Invoke-RestMethod -Uri "$($Script:Config.url)/projects" -Method Get -ContentType 'application/json' -WebSession $Script:Session
            if($Name)
            {
                $Data = $Data | Where-Object { $_.name -eq $Name }
            }
            $Data
        }
        catch
        {
            throw $_
        }
    }
    end
    {
    }
}
function Get-SemaphoreProjectEnvironment
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $ProjectId,

        [Parameter(Mandatory = $false)]
        [string]
        $Name
    )

    begin
    {
        Write-Verbose -Message "Calling function $($MyInvocation.MyCommand)"
        if(!$Script:Session)
        {
            throw "Please run Connect-Semaphore first"
        }
    }
    process
    {
        Write-Verbose -Message "Getting environment(s) for project $ProjectId"
        try
        {
            $Data = Invoke-RestMethod -Uri "$($Script:Config.url)/project/$ProjectId/environment" -Method Get -ContentType 'application/json' -WebSession $Script:Session
            if($Name)
            {
                $Data = $Data | Where-Object { $_.name -eq $Name }
            }
            $Data
        }
        catch
        {
            throw $_
        }
    }
    end
    {
    }
}
function Get-SemaphoreProjectInventory
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $ProjectId,

        [Parameter(Mandatory = $false)]
        [String]
        $Name
    )

    begin
    {
        Write-Verbose -Message "Calling function $($MyInvocation.MyCommand)"
        if(!$Script:Session)
        {
            throw "Please run Connect-Semaphore first"
        }
    }
    process
    {
        try
        {
            $Data = Invoke-RestMethod -Uri "$($Script:Config.url)/project/$ProjectId/inventory" -Method Get -ContentType 'application/json' -WebSession $Script:Session
            if($Name)
            {
                $Data = $Data | Where-Object { $_.name -eq $Name }
            }
            $Data
        }
        catch
        {
            throw $_
        }
    }
    end
    {
    }
}
function Get-SemaphoreProjectKey
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $ProjectId,

        [Parameter(Mandatory = $false)]
        [String]
        $Name
    )

    begin
    {
        Write-Verbose -Message "Calling function $($MyInvocation.MyCommand)"
        if(!$Script:Session)
        {
            throw "Please run Connect-Semaphore first"
        }
    }
    process
    {
        try
        {
            $Data = Invoke-RestMethod -Uri "$($Script:Config.url)/project/$ProjectId/keys" -Method Get -ContentType 'application/json' -WebSession $Script:Session
            if($Name)
            {
                $Data = $Data | Where-Object { $_.name -eq $Name }
            }
            $Data
        }
        catch
        {
            throw $_
        }
    }
    end
    {
    }
}
function Get-SemaphoreProjectRepository
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $ProjectId,

        [Parameter(Mandatory = $false)]
        [String]
        $Name
    )

    begin
    {
        Write-Verbose -Message "Calling function $($MyInvocation.MyCommand)"
        if(!$Script:Session)
        {
            throw "Please run Connect-Semaphore first"
        }
    }
    process
    {
        try
        {
            $Data = Invoke-RestMethod -Uri "$($Script:Config.url)/project/$ProjectId/repositories" -Method Get -ContentType 'application/json' -WebSession $Script:Session
            if($Name)
            {
                $Data = $Data | Where-Object { $_.name -eq $Name }
            }
            $Data
        }
        catch
        {
            throw $_
        }
    }
    end
    {
    }
}
function Get-SemaphoreProjectTask
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $ProjectId,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, [int]::MaxValue)]
        $Id,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, [int]::MaxValue)]
        $TemplateId
    )

    begin
    {
        Write-Verbose -Message "Calling function $($MyInvocation.MyCommand)"
        if(!$Script:Session)
        {
            throw "Please run Connect-Semaphore first"
        }
    }
    process
    {
        try
        {
            $Data = Invoke-RestMethod -Uri "$($Script:Config.url)/project/$ProjectId/tasks/$Id" -Method Get -ContentType 'application/json' -WebSession $Script:Session
            # E.g. if we only want the tasks for a specific template (note this will only apply if we are getting all tasks for a project)
            if($TemplateId)
            {
                $Data = $Data | Where-Object { $_.template_id -eq $TemplateId }
            }
            $Data
        }
        catch
        {
            throw $_
        }
    }
    end
    {
    }
}
function Get-SemaphoreProjectTaskOutput
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $ProjectId,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $Id,

        [Parameter(Mandatory = $false)]
        [ValidateSet('json', 'text')]
        [string]
        $ParseType = 'json'
    )

    begin
    {
        Write-Verbose -Message "Calling function $($MyInvocation.MyCommand)"
        if(!$Script:Session)
        {
            throw "Please run Connect-Semaphore first"
        }
    }
    process
    {
        try
        {
            $Data = Invoke-RestMethod -Uri "$($Script:Config.url)/project/$ProjectId/tasks/$Id/output" -Method Get -ContentType 'application/json' -WebSession $Script:Session
            if(!$Data)
            {
                return $Null
            }

            # Write all data to the verbose stream so we can see it if we want to:
            Write-Verbose -Message $($Global:Data | Out-String)
        }
        catch
        {
            throw $_
        }

        if($ParseType -eq 'json')
        {
            # The output, when ansible.cfg is set to return JSON is as followed:
            <#
                task_id task time output
                ------- ---- ---- ------
                    25 17/10/2023 13:47:53 Task 25 added to queue
                    25 17/10/2023 13:47:58 Started: 25
                    25 17/10/2023 13:47:58 Run TaskRunner with template: Test Install Via Choco on TESTHOST
                    25 17/10/2023 13:47:58 Preparing: 25
                    25 17/10/2023 13:47:58 Updating Repository https://github.com/temp/ansibleplaybooks.git
                    25 17/10/2023 13:47:58 From https://github.com/temp/ansibleplaybooks
                    25 17/10/2023 13:47:58 * branch main -> FETCH_HEAD
                    25 17/10/2023 13:47:58 Already up to date.
                    25 17/10/2023 13:47:58 No collections/requirements.yml file found. Skip galaxy install process.
                    25 17/10/2023 13:47:58 No roles/requirements.yml file found. Skip galaxy install process.
                    25 17/10/2023 13:48:45 {
                    25 17/10/2023 13:48:45 "custom_stats": {},
                    25 17/10/2023 13:48:45 "global_custom_stats": {},
                    25 17/10/2023 13:48:45 "plays": [
                    25 17/10/2023 13:48:45 {
                            ..................................... SNIP .....................................
                    25 17/10/2023 13:48:45 }
                    25 17/10/2023 13:48:45 ],
                    25 17/10/2023 13:48:45 "stats": {
                    25 17/10/2023 13:48:45 "testhost.domain.com": {
                    25 17/10/2023 13:48:45 "changed": 1,
                    25 17/10/2023 13:48:45 "failures": 0,
                    25 17/10/2023 13:48:45 "ignored": 0,
                    25 17/10/2023 13:48:45 "ok": 6,
                    25 17/10/2023 13:48:45 "rescued": 0,
                    25 17/10/2023 13:48:45 "skipped": 0,
                    25 17/10/2023 13:48:45 "unreachable": 0
                    25 17/10/2023 13:48:45 }
                    25 17/10/2023 13:48:45 }
                    25 17/10/2023 13:48:45 }
            #>


            #Region Find Start and End of JSON
            try
            {
                # Find the array number where .output equals exactly "{" as this is the start of the JSON data:
                $JSONStart = $Data.Output.IndexOf('{')
                # If -1 then we have no result data yet.
                if($JSONStart -eq -1)
                {
                    return $Null
                }

                # Not sure why but LastIndexOf('}') returns an array. Let's use IndexOf('}') instead. This works as the JSON is returned 'pretty' with indentation so the final line is just }.

                $JSONEnd = $Data.Output.IndexOf('}')

                # If this function is called at exactly the right (wrong) time, it can be possible that the { is found but the } is not. This is because the task data is one line per record
                # and presumably behind the scenes, data is still being written to disk and thus we end up with a partial result.

                # To cater to this, if we've found '{' but '}' isn't a value above 0, use a small retry loop making additional queries for the task data.

                $RetryCount = 0
                while(($JSONEnd -lt 0) -and $RetryCount -lt 20)
                {
                    $JSONEnd = $Data.Output.IndexOf('}')
                    $Data = Invoke-RestMethod -Uri "$($Script:Config.url)/project/$ProjectId/tasks/$Id/output" -Method Get -ContentType 'application/json' -WebSession $Script:Session -Verbose:$False
                    $RetryCount++
                    Start-Sleep -Seconds 2
                }

                if($JSONEnd -eq -1)
                {
                    throw "Unable to find end of JSON data."
                }

                # We are assuming these are integers here...
                if($JSONStart -and $JSONEnd)
                {
                    # Add all items in the array between the start and end to a new array:
                    $Global:JSON = $Data.Output[$JSONStart..$JSONEnd]
                }
                else
                {
                    return $Null
                }
            }
            catch
            {
                throw $_
            }
            #EndRegion

            #Region Convert to a PowerShell object and hope it doesn't break:
            try
            {
                $Converted = $JSON | ConvertFrom-Json -ErrorAction Stop
            }
            catch
            {
                throw $_
            }
            #EndRegion

            #Region Manipulate data to make it more useful:
            # Unfortunately, .stats is converted to singular PSCustomObject (count = 1!) with the host names as NoteProperties and their value is another PSCustomObject
            # with multiple properties (failures, changed, ok ,etc). This means you can't iterate over it to use with Where/Select statements. So, convert it into a useful array of objects:
            return $Converted.stats | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name | ForEach-Object { [pscustomobject]@{'Name' = $_; 'Results' = $Converted.stats.$_ } }
            #EndRegion
        }
        elseif($ParseType -eq 'text')
        {
            return $Data
        }
        else
        {
        }
    }
    end
    {
    }
}
function Get-SemaphoreProjectTemplate
{
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $ProjectId,

        [Parameter(Mandatory = $true, ParameterSetName = "Id")]
        [ValidateRange(1, [int]::MaxValue)]
        $Id,

        [Parameter(Mandatory = $true, ParameterSetName = "Name")]
        [String]
        $Name
    )

    begin
    {
        Write-Verbose -Message "Calling function $($MyInvocation.MyCommand)"
        if(!$Script:Session)
        {
            throw "Please run Connect-Semaphore first"
        }
    }
    process
    {
        try
        {
            $Data = Invoke-RestMethod -Uri "$($Script:Config.url)/project/$ProjectId/templates/$Id" -Method Get -ContentType 'application/json' -WebSession $Script:Session
            if($Name)
            {
                $Data = $Data | Where-Object { $_.name -eq $Name }
            }
            $Data
        }
        catch
        {
            throw $_
        }
    }
    end
    {
    }
}
function Get-SemaphoreUserToken
{
    [CmdletBinding(SupportsShouldProcess)]
    param (
    )

    begin
    {
        Write-Verbose -Message "Calling function $($MyInvocation.MyCommand)"
        if(!$Script:Session)
        {
            throw "Please run Connect-Semaphore first"
        }
    }
    process
    {
        try
        {
            Invoke-RestMethod -Uri "$($Script:Config.url)/user/tokens" -Method Get -ContentType 'application/json' -WebSession $Script:Session
        }
        catch
        {
            throw $_
        }
    }
    end
    {
    }
}
function New-SemaphoreProject
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [String]$Name,

        [Parameter(Mandatory = $false)]
        [Switch]$Alert,

        [Parameter(Mandatory = $false)]
        [String]$TelegramChatId,

        [Parameter(Mandatory = $false)]
        [ValidateRange(0, [int]::MaxValue)]
        [Int]$MaxParallelTasks
    )

    begin
    {
        Write-Verbose -Message "Calling function $($MyInvocation.MyCommand)"
        if(!$Script:Session)
        {
            throw "Please run Connect-Semaphore first"
        }
    }
    process
    {
        #Region Check If Exists
        # Check if already exists by name. Whilst permitted in Semaphore, it's impossible to tell them apart when using them in Task Templates.
        $CheckIfExists = Get-SemaphoreProject -Name $Name
        if($CheckIfExists)
        {
            throw "An project with the name $Name already exists in project $ProjectId. Please use a different name."
        }
        #EndRegion

        #Region Construct body and send the request
        try
        {
            $Body = @{
                name = $Name
            }

            if($Alert)
            {
                $Body.Add("alert", $true)
            }

            if($TelegramChatId)
            {
                $Body.Add("telegram_chat_id", $TelegramChatId)
            }

            if($MaxParallelTasks)
            {
                $Body.Add("max_parallel_tasks", $MaxParallelTasks)
            }

            $Body = $Body | ConvertTo-Json -Compress
            Invoke-RestMethod -Uri "$($Script:Config.url)/projects" -Method Post -Body $Body -ContentType 'application/json' -WebSession $Script:Session
        }
        catch
        {
            throw $_
        }
        #EndRegion
    }
    end
    {
    }
}
function New-SemaphoreProjectEnvironment
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $ProjectId,

        [Parameter(Mandatory = $true)]
        [String]$Name
    )

    begin
    {
        Write-Verbose -Message "Calling function $($MyInvocation.MyCommand)"
        if(!$Script:Session)
        {
            throw "Please run Connect-Semaphore first"
        }
    }
    process
    {
        #Region Check If Exists
        # Check if already exists by name. Whilst permitted in Semaphore, it's impossible to tell them apart when using them in Task Templates.
        $CheckIfExists = Get-SemaphoreProjectEnvironment -ProjectId $ProjectId -Name $Name
        if($CheckIfExists)
        {
            throw "An environment with the name $Name already exists in project $ProjectId. Please use a different name."
        }
        #EndRegion

        #Region Construct body and send the request
        try
        {
            $Body = @{
                json       = "{}"
                name       = $Name
                project_id = $ProjectId
            } | ConvertTo-Json -Compress
            Invoke-RestMethod -Uri "$($Script:Config.url)/project/$ProjectId/environment" -Method Post -Body $Body -ContentType 'application/json' -WebSession $Script:Session | Out-Null
            # Return the created object:
            Get-SemaphoreProjectEnvironment -ProjectId $ProjectId -Name $Name
        }
        catch
        {
            throw $_
        }
        #EndRegion
    }
    end
    {
    }
}
function New-SemaphoreProjectInventory
{
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $ProjectId,

        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $KeyId,

        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ParameterSetName = 'Static')]
        [string[]]$Hostnames,

        [Parameter(Mandatory = $false, ParameterSetName = 'Static')]
        [Switch]$WinRMConnection,

        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [string]$InventoryFile
    )

    begin
    {
        Write-Verbose -Message "Calling function $($MyInvocation.MyCommand)"
        if(!$Script:Session)
        {
            throw "Please run Connect-Semaphore first"
        }
    }
    process
    {
        #Region Check If Exists
        # Check if already exists by name. Whilst permitted in Semaphore, it's impossible to tell them apart when using them in Task Templates.
        $CheckIfExists = Get-SemaphoreProjectInventory -ProjectId $ProjectId -Name $Name
        if($CheckIfExists)
        {
            throw "An inventory with the name $Name already exists in project $ProjectId. Please use a different name."
        }
        #EndRegion

        #Region Construct body and send the request
        try
        {
            $Body = @{
                "name"          = $Name.ToLower()
                "project_id"    = $ProjectId
                "ssh_key_id"    = $KeyId
                "become_key_id" = $KeyId
            }

            if($Hostnames)
            {
                $InventoryData = "[$Name]`n" + ($Hostnames -join "`n")
                if($WinRMConnection)
                {
                    $InventoryData += "`n`n"
                    $InventoryData += "[$($Name):vars]`n"
                    $InventoryData += "ansible_connection=winrm`n"
                    $InventoryData += "ansible_winrm_transport=ntlm`n"
                    $InventoryData += "ansible_winrm_server_cert_validation=ignore`n"
                }

                $Body.Add("inventory", $InventoryData)
                $Body.Add("type", "static")
            }
            elseif($InventoryFile)
            {
                $Body.Add("inventory", $InventoryFile)
                $Body.Add("type", "file")
            }

            $Body = $Body | ConvertTo-Json -Compress

            if($PSCmdlet.ShouldProcess("Project $ProjectId", "Create inventory $Name"))
            {
                Invoke-RestMethod -Uri "$($Script:Config.url)/project/$ProjectId/inventory" -Method Post -Body $Body -ContentType 'application/json' -WebSession $Script:Session
                # Return the created object| EDIT: No need because it actually returns the object...
                #Get-SemaphoreProjectInventory -ProjectId $ProjectId -Name $Name
            }
        }
        catch
        {
            throw $_
        }
        #EndRegion
    }
    end
    {
    }
}
function New-SemaphoreProjectKey
{
    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'EmptyCredentials')]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $ProjectId,

        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ParameterSetName = 'Credentials')]
        [ValidateSet('UserNamePassword', 'SSHKey', 'Empty')]
        [String]
        $Type,

        [Parameter(Mandatory = $true, ParameterSetName = 'Credentials')]
        [pscredential]$Credential
    )

    begin
    {
        Write-Verbose -Message "Calling function $($MyInvocation.MyCommand)"
        if(!$Script:Session)
        {
            throw "Please run Connect-Semaphore first"
        }
    }
    process
    {
        #Region Check If Exists
        # Check if already exists by name. Whilst permitted in Semaphore, it's impossible to tell them apart when using them in Task Templates.
        $CheckIfExists = Get-SemaphoreProjectKey -ProjectId $ProjectId -Name $Name
        if($CheckIfExists)
        {
            throw "A key with the name $Name already exists in project $ProjectId. Please use a different name."
        }
        #EndRegion

        #Region Construct body and send the request
        try
        {
            $Body = @{
                "name"       = $Name
                "project_id" = $ProjectId
            }

            # If the parameter set is Credentials:
            if($PSCmdlet.ParameterSetName -eq 'Credentials')
            {
                # Append the appropriate key type and credentials:
                if($Type -eq 'UserNamePassword')
                {
                    $Body.Add("type", "login_password")
                    $Body.Add("login_password", @{
                            "login"    = $Credential.UserName
                            "password" = $Credential.GetNetworkCredential().Password
                        })
                }
                elseif($Type -eq 'SSHKey')
                {
                    $Body.Add("type", "ssh")
                    $Body.Add("ssh", @{
                            "private_key" = $Credential.GetNetworkCredential().Password
                            "login"       = $Credential.UserName
                        })
                }
            }
            else
            {
                # If the default parameter set is EmptyCredentials, so set the type to none:
                $Body.Add("type", "none")
            }

            $Body = $Body | ConvertTo-Json -Compress

            if($PSCmdlet.ShouldProcess("Project $ProjectId", "Create key $Name"))
            {
                Invoke-RestMethod -Uri "$($Script:Config.url)/project/$ProjectId/keys" -Method Post -Body $Body -ContentType 'application/json' -WebSession $Script:Session | Out-Null
                # Return the created object:
                Get-SemaphoreProjectKey -ProjectId $ProjectId -Name $Name
            }
            else
            {
                Write-Verbose -Message "Would create key $Name in project $ProjectId"
            }
        }
        catch
        {
            throw $_
        }
        #EndRegion
    }
    end
    {
    }
}
function New-SemaphoreProjectRepository
{
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $ProjectId,

        [Parameter(Mandatory = $true)]
        [String]$Name,

        [Parameter(Mandatory = $true)]
        [String]$Url,

        [Parameter(Mandatory = $true)]
        [String]$Branch,

        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [Int]$KeyId
    )

    begin
    {
        Write-Verbose -Message "Calling function $($MyInvocation.MyCommand)"
        if(!$Script:Session)
        {
            throw "Please run Connect-Semaphore first"
        }
    }
    process
    {
        #Region Check If Exists
        # Check if already exists by name. Whilst permitted in Semaphore, it's impossible to tell them apart when using them in Task Templates.
        $CheckIfExists = Get-SemaphoreProjectRepository -ProjectId $ProjectId -Name $Name
        if($CheckIfExists)
        {
            throw "A repository with the name $Name already exists in project $ProjectId. Please use a different name."
        }
        #EndRegion

        #Region Construct body and send the request
        try
        {
            $Body = [Ordered]@{
                name       = $Name
                git_url    = $Url
                git_branch = $Branch
                ssh_key_id = $KeyId
                project_id = $ProjectId
            } | ConvertTo-Json -Compress
            Invoke-RestMethod -Uri "$($Script:Config.url)/project/$ProjectId/repositories" -Method Post -Body $Body -ContentType 'application/json' -WebSession $Script:Session | Out-Null
            # Return the created object:
            Get-SemaphoreProjectRepository -ProjectId $ProjectId -Name $Name
        }
        catch
        {
            throw $_
        }
        #EndRegion
    }
    end
    {
    }
}
function New-SemaphoreProjectTemplate
{
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $ProjectId,

        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $InventoryId,

        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $RepositoryId,

        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $EnvironmentId,

        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $KeyId,

        [Parameter(Mandatory = $true)]
        [String]$Playbook,

        [Parameter(Mandatory = $true)]
        [String]$Name,

        [Parameter(Mandatory = $false)]
        [String]$Description = 'Inventory created by New-SemaphoreProjectTemplate'
    )

    begin
    {
        Write-Verbose -Message "Calling function $($MyInvocation.MyCommand)"
        if(!$Script:Session)
        {
            throw "Please run Connect-Semaphore first"
        }
    }
    process
    {
        <#
            {
                "project_id": 1,
                "inventory_id": 1,
                "repository_id": 1,
                "environment_id": 1,
                "view_id": 1,
                "name": "Test",
                "playbook": "test.yml",
                "arguments": "[]",
                "description": "Hello, World!",
                "": false,
                "limit": "",
                "suppress_success_alerts": true,
                "survey_vars": [
                    {
                    "name": "string",
                    "title": "string",
                    "description": "string",
                    "type": "String => \"\", Integer => \"int\"",
                    "required": true
                    }
                ]
            }
        #>




        #Region Construct body and send the request
        try
        {
            $Body = @{
                'type'                        = ''
                'name'                        = $Name
                'description'                 = $Description
                'playbook'                    = $Playbook
                'inventory_id'                = $InventoryId
                'repository_id'               = $RepositoryId
                'environment_id'              = $EnvironmentId
                'vault_key_id'                = $KeyId
                'project_id'                  = $ProjectId
                'suppress_success_alerts'     = $SuppressSuccessAlerts
                'allow_override_args_in_task' = $AllowOverrideArgsInTask
            } | ConvertTo-Json
            Invoke-RestMethod -Uri "$($Script:Config.url)/project/$ProjectId/templates" -Method Post -Body $Body -ContentType 'application/json' -WebSession $Script:Session | Out-Null
            # Return the created object:
            Get-SemaphoreProjectTemplate -ProjectId $ProjectId -Name $Name
        }
        catch
        {
            throw $_
        }
        #EndRegion
    }
    end
    {
    }
}
function New-SemaphoreUserToken
{
    [CmdletBinding(SupportsShouldProcess)]
    param (
    )

    begin
    {
        Write-Verbose -Message "Calling function $($MyInvocation.MyCommand)"
        if(!$Script:Session)
        {
            throw "Please run Connect-Semaphore first"
        }
    }
    process
    {
        #Region Send the request
        try
        {
            Invoke-RestMethod -Uri "$($Script:Config.url)/user/tokens" -Method Post -ContentType 'application/json' -WebSession $Script:Session
        }
        catch
        {
            throw $_
        }
        #EndRegion
    }
    end
    {
    }
}
function Remove-SemaphoreProject
{
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $Id
    )

    begin
    {
        Write-Verbose -Message "Calling function $($MyInvocation.MyCommand)"
        if(!$Script:Session)
        {
            throw "Please run Connect-Semaphore first"
        }
    }
    process
    {
        #Region Send the request to remove
        try
        {
            if($PSCmdlet.ShouldProcess("Project $ProjectId", "Remove $Id"))
            {
                Invoke-RestMethod -Uri "$($Script:Config.url)/project/$Id" -Method Delete -WebSession $Script:Session | Out-Null
            }
        }
        catch
        {
            throw $_
        }
        #EndRegion
    }
    end
    {
    }
}
function Remove-SemaphoreProjectEnvironment
{
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $ProjectId,

        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $Id
    )

    begin
    {
        Write-Verbose -Message "Calling function $($MyInvocation.MyCommand)"
        if(!$Script:Session)
        {
            throw "Please run Connect-Semaphore first"
        }
    }
    process
    {
        #Region Send the request to remove
        try
        {
            if($PSCmdlet.ShouldProcess("Project $ProjectId", "Remove $Id"))
            {
                Invoke-RestMethod -Uri "$($Script:Config.url)/project/$ProjectId/environment/$Id" -Method Delete -WebSession $Script:Session | Out-Null
            }
        }
        catch
        {
            throw $_
        }
        #EndRegion
    }
    end
    {
    }
}
function Remove-SemaphoreProjectInventory
{
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $ProjectId,

        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $Id
    )

    begin
    {
        Write-Verbose -Message "Calling function $($MyInvocation.MyCommand)"
        if(!$Script:Session)
        {
            throw "Please run Connect-Semaphore first"
        }
    }
    process
    {
        #Region Send the request to remove
        try
        {
            if($PSCmdlet.ShouldProcess("Project $ProjectId", "Remove $Id"))
            {
                Invoke-RestMethod -Uri "$($Script:Config.url)/project/$ProjectId/inventory/$Id" -Method Delete -WebSession $Script:Session | Out-Null
            }
        }
        catch
        {
            throw $_
        }
        #EndRegion
    }
    end
    {
    }
}
function Remove-SemaphoreProjectKey
{
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $ProjectId,

        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $Id
    )

    begin
    {
        Write-Verbose -Message "Calling function $($MyInvocation.MyCommand)"
        if(!$Script:Session)
        {
            throw "Please run Connect-Semaphore first"
        }
    }
    process
    {
        #Region Send the request to remove
        try
        {
            if($PSCmdlet.ShouldProcess("Project $ProjectId", "Remove $Id"))
            {
                Invoke-RestMethod -Uri "$($Script:Config.url)/project/$ProjectId/keys/$Id" -Method Delete -WebSession $Script:Session | Out-Null
            }
        }
        catch
        {
            throw $_
        }
        #EndRegion
    }
    end
    {
    }
}
function Remove-SemaphoreProjectRepository
{
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $ProjectId,

        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $Id
    )

    begin
    {
        Write-Verbose -Message "Calling function $($MyInvocation.MyCommand)"
        if(!$Script:Session)
        {
            throw "Please run Connect-Semaphore first"
        }
    }
    process
    {
        #Region Send the request to remove
        try
        {
            if($PSCmdlet.ShouldProcess("Project $ProjectId", "Remove $Id"))
            {
                Invoke-RestMethod -Uri "$($Script:Config.url)/project/$ProjectId/repositories/$Id" -Method Delete -WebSession $Script:Session | Out-Null
            }
        }
        catch
        {
            throw $_
        }
        #EndRegion
    }
    end
    {
    }
}
function Remove-SemaphoreProjectTemplate
{
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $ProjectId,

        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $Id
    )

    begin
    {
        Write-Verbose -Message "Calling function $($MyInvocation.MyCommand)"
        if(!$Script:Session)
        {
            throw "Please run Connect-Semaphore first"
        }
    }
    process
    {
        #Region Send the request to remove
        try
        {
            if($PSCmdlet.ShouldProcess("Project $ProjectId", "Remove $Id"))
            {
                Invoke-RestMethod -Uri "$($Script:Config.url)/project/$ProjectId/templates/$Id" -Method Delete -WebSession $Script:Session | Out-Null
            }
        }
        catch
        {
            throw $_
        }
        #EndRegion
    }
    end
    {
    }
}
function Start-SemaphoreProjectTask
{
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $ProjectId,

        [Parameter(Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $TemplateId,

        [Parameter(Mandatory = $false)]
        [String]$CLIArguments,

        [Parameter(Mandatory = $false)]
        [Switch]
        $Wait
    )

    begin
    {
        Write-Verbose -Message "Calling function $($MyInvocation.MyCommand)"
        if(!$Script:Session)
        {
            throw "Please run Connect-Semaphore first"
        }
    }
    process
    {
        $Body = @{
            "template_id" = $TemplateId
            "environment" = "{}"
            "project_id"  = $ProjectId
        }

        if($CLIArguments)
        {
            $Body.Add("cli_arguments", $CLIArguments)
        }

        try
        {
            $Data = Invoke-RestMethod -Uri "$($Script:Config.url)/project/$ProjectId/tasks" -Method Post -Body $Body -ContentType 'application/json' -WebSession $Script:Session
            if(!$Data)
            {
                return $Null
            }
        }
        catch
        {
            throw $_
        }


        if(!$Wait)
        {
            return $Data
        }
        else
        {
            # Start a loop that calls Get-SemaphoreProjectTask with the Id returned from the previous call. If the status property is running or success
            # break out of the loop and return the task object. Attempt the loop for a maximum of 50 attempts with 5 seconds wait between each attempt.
            $AttemptCount = 0
            $MaxAttempts = 50
            $WaitTime = 5
            $TaskId = $Data.id
            do
            {
                $AttemptCount++
                Write-Verbose -Message "Attempt $AttemptCount of $MaxAttempts"
                Write-Progress -Activity "Waiting for task to complete" -Status "Attempt $AttemptCount of $MaxAttempts" -PercentComplete (($AttemptCount / $MaxAttempts) * 100)

                try
                {
                    $Task = Get-SemaphoreProjectTask -ProjectId $ProjectId -TaskId $TaskId
                    if($Task.status -eq "running")
                    {
                        Write-Verbose -Message "Task is running"
                        Start-Sleep -Seconds $WaitTime
                    }
                    elseif($Task.status -eq "waiting")
                    {
                        Write-Verbose -Message "Task is waiting"
                        Start-Sleep -Seconds $WaitTime
                    }
                    else
                    {
                        Write-Verbose -Message "Task status is: $($Task.status)"
                        break
                    }
                }
                catch
                {
                    throw $_
                }
            }
            until($AttemptCount -eq $MaxAttempts)
        }

        return $Task
    }
    end
    {
    }
}