ADOPS.psm1

#region SkipTest

class SkipTest : Attribute {
    [string[]]$TestNames

    SkipTest([string[]]$Name)
    {
        $this.TestNames = $Name
    }
}
#endregion SkipTest

#region GetADOPSHeader

function GetADOPSHeader {
    [CmdletBinding()]
    param (
        [string]$Organization
    )

    $Res = @{}
    
    if (-not [string]::IsNullOrEmpty($Organization)) {
        $HeaderObj = $Script:ADOPSCredentials[$Organization]
        $res.Add('Organization', $Organization)
    }
    else {
        $r = $script:ADOPSCredentials.Keys | Where-Object {$script:ADOPSCredentials[$_].Default -eq $true}
        $HeaderObj = $script:ADOPSCredentials[$r]
        $res.Add('Organization', $r)
    }

    $UserName = $HeaderObj.Credential.UserName
    $Password = $HeaderObj.Credential.GetNetworkCredential().Password

    $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $UserName, $Password)))
    $Header = @{
        Authorization = ("Basic {0}" -f $base64AuthInfo)
    }

    $Res.Add('Header',$Header)

    $Res
}
#endregion GetADOPSHeader

#region InvokeADOPSRestMethod

function InvokeADOPSRestMethod {
    param (
        [Parameter(Mandatory)]
        [URI]$Uri,

        [Parameter()]
        [Microsoft.PowerShell.Commands.WebRequestMethod]$Method,

        [Parameter()]
        [string]$Body,

        [Parameter()]
        [string]$Organization,

        [Parameter()]
        [string]$ContentType = 'application/json',

        [Parameter()]
        [switch]$FullResponse
    )

    if (-not [string]::IsNullOrEmpty($Organization)) {
        $CallHeaders = GetADOPSHeader -Organization $Organization
    }
    else {
        $CallHeaders = GetADOPSHeader
    }

    $InvokeSplat = @{
        'Uri' = $Uri
        'Method' = $Method
        'Headers' = $CallHeaders.Header
        'ContentType' = $ContentType
    }

    if (-not [string]::IsNullOrEmpty($Body)) {
        $InvokeSplat.Add('Body', $Body)
    }

    if ($FullResponse) {
        $InvokeSplat.Add('ResponseHeadersVariable', 'ResponseHeaders')
        $InvokeSplat.Add('StatusCodeVariable', 'ResponseStatusCode')
    }
    
    $Result = Invoke-RestMethod @InvokeSplat

    if ($Result -like "*Azure DevOps Services | Sign In*") {
        throw 'Failed to call Azure DevOps API. Please login before using.'
    }
    elseif ($FullResponse) {
        @{ Content = $Result; Headers = $ResponseHeaders; StatusCode = $ResponseStatusCode }
    }
    else {
        $Result
    }
}
#endregion InvokeADOPSRestMethod

#region Connect-ADOPS

function Connect-ADOPS {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$Username,
        
        [Parameter(Mandatory)]
        [string]$PersonalAccessToken,

        [Parameter(Mandatory)]
        [string]$Organization,

        [Parameter()]
        [switch]$Default
    )
    
    $Credential = [pscredential]::new($Username, (ConvertTo-SecureString -String $PersonalAccessToken -AsPlainText -Force))
    $ShouldBeDefault = $Default.IsPresent

    if ($script:ADOPSCredentials.Count -eq 0) {
        $ShouldBeDefault = $true
        $Script:ADOPSCredentials = @{}
    }
    elseif ($default.IsPresent) {
        $r = $script:ADOPSCredentials.Keys | Where-Object { $ADOPSCredentials[$_].Default -eq $true }
        $ADOPSCredentials[$r].Default = $false
    }

    $OrgData = @{
        Credential = $Credential
        Default    = $ShouldBeDefault
    }
    
    $Script:ADOPSCredentials[$Organization] = $OrgData
    
    $URI = "https://vssps.dev.azure.com/$Organization/_apis/profile/profiles/me?api-version=7.1-preview.3"

    try {
        InvokeADOPSRestMethod -Method Get -Uri $URI
    }
    catch {
        $Script:ADOPSCredentials.Remove($Organization)
        throw $_
    }
}
#endregion Connect-ADOPS

#region Disconnect-ADOPS

function Disconnect-ADOPS {
    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Organization
    )

    # Only require $Organization if several connections
    if ($Script:ADOPSCredentials.Count -eq 0) {
        throw "There are no current connections!"
    } # Allow not specifying organization if there's only one connection
    elseif ($Script:ADOPSCredentials.Count -eq 1 -and [string]::IsNullOrWhiteSpace($Organization)) {
        $Script:ADOPSCredentials = @{}
        # Make sure to exit script after clearing hashtable
        return
    }
    elseif (-not $Script:ADOPSCredentials.ContainsKey($Organization)) {
        throw "No connection made for organization $Organization!"
    }

    # If the connection to be removed is set as default, set another one
    $ChangeDefault = $Script:ADOPSCredentials[$Organization].Default
    
    $Script:ADOPSCredentials.Remove($Organization)

    # If there are any connections left and we removed the default
    if ($Script:ADOPSCredentials.Count -gt 0 -and $ChangeDefault) {
        # Set another one to default
        $Name = ($Script:ADOPSCredentials.GetEnumerator() | Select-Object -First 1).Name
        $Script:ADOPSCredentials[$Name].Default = $true
    }
}
#endregion Disconnect-ADOPS

#region Get-ADOPSConnection

function Get-ADOPSConnection {
    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Organization
    )
    
    if (-not [string]::IsNullOrEmpty($Organization)) {
        $Script:ADOPSCredentials.GetEnumerator() | Where-Object { $_.Key -eq $Organization}
    }
    else {
        $Script:ADOPSCredentials
    }
}
#endregion Get-ADOPSConnection

#region Get-ADOPSElasticPool

function Get-ADOPSElasticPool {
    [CmdletBinding()]
    param (
        [Parameter()]
        [int32]$PoolId,

        [Parameter()]
        [string]$Organization
    )

    if (-not [string]::IsNullOrEmpty($Organization)) {
        $Org = GetADOPSHeader -Organization $Organization
    } else {
        $Org = GetADOPSHeader
        $Organization = $Org['Organization']
    }

    if ($PSBoundParameters.ContainsKey('PoolId')) {
        $Uri = "https://dev.azure.com/$Organization/_apis/distributedtask/elasticpools/$PoolId?api-version=7.1-preview.1"
    } else {
        $Uri = "https://dev.azure.com/$Organization/_apis/distributedtask/elasticpools?api-version=7.1-preview.1"
    }
    
    $Method = 'GET'
    $ElasticPoolInfo = InvokeADOPSRestMethod -Uri $Uri -Method $Method -Organization $Organization -Body $Body
    if ($ElasticPoolInfo.psobject.properties.name -contains 'value') {
        Write-Output $ElasticPoolInfo.value
    } else {
        Write-Output $ElasticPoolInfo
    }
}
#endregion Get-ADOPSElasticPool

#region Get-ADOPSNode

function Get-ADOPSNode {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [int32]$PoolId,

        [Parameter()]
        [string]$Organization
    )

    if (-not [string]::IsNullOrEmpty($Organization)) {
        $Org = GetADOPSHeader -Organization $Organization
    } else {
        $Org = GetADOPSHeader
        $Organization = $Org['Organization']
    }

    $Uri = "https://dev.azure.com/$Organization/_apis/distributedtask/elasticpools/$PoolId/nodes?api-version=7.1-preview.1"

    $Method = 'GET'
    $NodeInfo = InvokeADOPSRestMethod -Uri $Uri -Method $Method -Organization $Organization

    if ($NodeInfo.psobject.properties.name -contains 'value') {
        Write-Output $NodeInfo.value
    } else {
        Write-Output $NodeInfo
    }
}
#endregion Get-ADOPSNode

#region Get-ADOPSPipeline

function Get-ADOPSPipeline {
    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Project,
        
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Organization
    )

    if (-not [string]::IsNullOrEmpty($Organization)) {
        $OrgInfo = GetADOPSHeader -Organization $Organization
    }
    else {
        $OrgInfo = GetADOPSHeader
        $Organization = $OrgInfo['Organization']
    }

    $Uri = "https://dev.azure.com/$Organization/$Project/_apis/pipelines?api-version=7.1-preview.1"
    
    $InvokeSplat = @{
        Method       = 'Get'
        Uri          = $URI
        Organization = $Organization
    }

    $AllPipelines = (InvokeADOPSRestMethod @InvokeSplat).value

    if ($PSBoundParameters.ContainsKey('Name')) {
        $Pipelines = $AllPipelines | Where-Object {$_.name -eq $Name}
        if (-not $Pipelines) {
            throw "The specified PipelineName $Name was not found amongst pipelines: $($AllPipelines.name -join ', ')!" 
        } 
    } else {
        $Pipelines = $AllPipelines
    }

    $return = @()

    foreach ($Pipeline in $Pipelines) {

        $InvokeSplat = @{
            Method       = 'Get'
            Uri          = $Pipeline.url
            Organization = $Organization
        }
    
        $result = InvokeADOPSRestMethod @InvokeSplat

        $return += $result
    }

    return $return
}

#endregion Get-ADOPSPipeline

#region Get-ADOPSPool

function Get-ADOPSPool {
    [CmdletBinding(DefaultParameterSetName = 'All')]
    param (
        [Parameter(Mandatory, ParameterSetName = 'PoolId')]
        [int32]$PoolId,

        [Parameter(Mandatory, ParameterSetName = 'PoolName')]
        [string]$PoolName,

        # Include legacy pools
        [Parameter(ParameterSetName = 'All')]
        [switch]$IncludeLegacy,

        [Parameter()]
        [string]$Organization
    )

    if (-not [string]::IsNullOrEmpty($Organization)) {
        $Org = GetADOPSHeader -Organization $Organization
    }
    else {
        $Org = GetADOPSHeader
        $Organization = $Org['Organization']
    }

    switch ($PSCmdlet.ParameterSetName) {
        'PoolId' { $Uri = "https://dev.azure.com/$Organization/_apis/distributedtask/pools/$PoolId`?api-version=7.1-preview.1" }
        'PoolName' { $uri = "https://dev.azure.com/$Organization/_apis/distributedtask/pools?poolName=$PoolName`&api-version=7.1-preview.1" }
        'All' { $Uri = "https://dev.azure.com/$Organization/_apis/distributedtask/pools?api-version=7.1-preview.1" }
    }
    
    $Method = 'GET'
    $PoolInfo = InvokeADOPSRestMethod -Uri $Uri -Method $Method -Organization $Organization

    if ($PoolInfo.psobject.properties.name -contains 'value') {
        $PoolInfo = $PoolInfo.value
    }
    if ((-not ($IncludeLegacy.IsPresent)) -and $PSCmdlet.ParameterSetName -eq 'All') {
        $PoolInfo = $PoolInfo | Where-Object { $_.IsLegacy -eq $false }
    }
    Write-Output $PoolInfo
}
#endregion Get-ADOPSPool

#region Get-ADOPSProject

function Get-ADOPSProject {
    [CmdletBinding()]
    param (
        [Parameter()]
        [string]$Project,

        [Parameter()]
        [string]$Organization
    )

    if (-not [string]::IsNullOrEmpty($Organization)) {
        $Org = GetADOPSHeader -Organization $Organization
    }
    else {
        $Org = GetADOPSHeader
        $Organization = $Org['Organization']
    }

    $Uri = "https://dev.azure.com/$Organization/_apis/projects?api-version=7.1-preview.4"

    $Method = 'GET'
    $ProjectInfo = (InvokeADOPSRestMethod -Uri $Uri -Method $Method -Organization $Organization).value

    if (-not [string]::IsNullOrWhiteSpace($Project)) {
        $ProjectInfo = $ProjectInfo | Where-Object -Property Name -eq $Project
    }

    Write-Output $ProjectInfo
}
#endregion Get-ADOPSProject

#region Get-ADOPSRepository

function Get-ADOPSRepository {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Project,

        [Parameter()]
        [string]$Repository,

        [Parameter()]
        [string]$Organization
    )

    if (-not [string]::IsNullOrEmpty($Organization)) {
        $OrgInfo = GetADOPSHeader -Organization $Organization
    }
    else {
        $OrgInfo = GetADOPSHeader
        $Organization = $OrgInfo['Organization']
    }

    if ($PSBoundParameters.ContainsKey('Repository')) {
        $Uri = "https://dev.azure.com/$Organization/$Project/_apis/git/repositories/$Repository`?api-version=7.1-preview.1"
    }
    else {
        $Uri = "https://dev.azure.com/$Organization/$Project/_apis/git/repositories?api-version=7.1-preview.1"
    }
    
    $result = InvokeADOPSRestMethod -Uri $Uri -Method Get -Organization $Organization

    if ($result.psobject.properties.name -contains 'value') {
        Write-Output -InputObject $result.value
    }
    else {
        Write-Output -InputObject $result
    }
}
#endregion Get-ADOPSRepository

#region Get-ADOPSServiceConnection

function Get-ADOPSServiceConnection {
    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Project,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Organization
    )
    
    if (-not [string]::IsNullOrEmpty($Organization)) {
        $OrgInfo = GetADOPSHeader -Organization $Organization
    }
    else {
        $OrgInfo = GetADOPSHeader
        $Organization = $OrgInfo['Organization']
    }

    $Uri = "https://dev.azure.com/$Organization/$Project/_apis/serviceendpoint/endpoints?api-version=7.1-preview.4"
    
    $InvokeSplat = @{
        Method       = 'Get'
        Uri          = $URI
        Organization = $Organization
    }

    $AllPipelines = (InvokeADOPSRestMethod @InvokeSplat).value

    if ($PSBoundParameters.ContainsKey('Name')) {
        $Pipelines = $AllPipelines | Where-Object {$_.name -eq $Name}
        if (-not $Pipelines) {
            throw "The specified ServiceConnectionName $Name was not found amongst Connections: $($AllPipelines.name -join ', ')!" 
        } 
    } else {
        $Pipelines = $AllPipelines
    }

    return $Pipelines

}
#endregion Get-ADOPSServiceConnection

#region Get-ADOPSUser

function Get-ADOPSUser {
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        [Parameter(Mandatory, ParameterSetName = 'Name', Position = 0)]
        [string]$Name,

        [Parameter(Mandatory, ParameterSetName = 'Descriptor', Position = 0)]
        [string]$Descriptor,

        [Parameter()]
        [string]$Organization,

        [Parameter(ParameterSetName = 'Default', DontShow)]
        [string]$ContinuationToken
    )

    if (-not [string]::IsNullOrEmpty($Organization)) {
        $Org = GetADOPSHeader -Organization $Organization
    }
    else {
        $Org = GetADOPSHeader
        $Organization = $Org['Organization']
    }

    if ($PSCmdlet.ParameterSetName -eq 'Default') {
        $Uri = "https://vssps.dev.azure.com/$Organization/_apis/graph/users?api-version=6.0-preview.1"
        $Method = 'GET'
        if(-not [string]::IsNullOrEmpty($ContinuationToken)) {
            $Uri += "&continuationToken=$ContinuationToken"
        }
        $Response = (InvokeADOPSRestMethod -FullResponse -Uri $Uri -Method $Method -Organization $Organization)
        $Users = [System.Collections.ArrayList]::new(1000)
        $Users.AddRange($Response.Content.value)
        Write-Verbose "Found $($Response.Content.count) users"
        if($Response.Headers.ContainsKey('X-MS-ContinuationToken')) {
            Write-Verbose "Found continuationToken. Will fetch more users."
            $parameters = [hashtable]$PSBoundParameters
            $parameters.Add('ContinuationToken', $Response.Headers['X-MS-ContinuationToken']?[0])
            $Users.AddRange((Get-ADOPSUser @parameters))
        }   
        Write-Output $Users
    }
    elseif ($PSCmdlet.ParameterSetName -eq 'Name') {
        $Uri = "https://vsaex.dev.azure.com/$Organization/_apis/UserEntitlements?`$filter=name eq '$Name'&`$orderBy=name Ascending&api-version=6.0-preview.3"
        $Method = 'GET'
        $Users = (InvokeADOPSRestMethod -Uri $Uri -Method $Method -Organization $Organization).members.user
        Write-Output $Users
    }
    elseif ($PSCmdlet.ParameterSetName -eq 'Descriptor') {
        $Uri = "https://vssps.dev.azure.com/$Organization/_apis/graph/users/$Descriptor`?api-version=6.0-preview.1"
        $Method = 'GET'
        $User = (InvokeADOPSRestMethod -Uri $Uri -Method $Method -Organization $Organization)
        Write-Output $User
    }
}
#endregion Get-ADOPSUser

#region Get-ADOPSWiki

function Get-ADOPSWiki {
    param (
        [Parameter(Mandatory)]
        [string]$Project,

        [Parameter()]
        [string]$WikiId,

        [Parameter()]
        [string]$Organization
    )
 
    if (-not [string]::IsNullOrEmpty($Organization)) {
        $OrgInfo = GetADOPSHeader -Organization $Organization
    }
    else {
        $OrgInfo = GetADOPSHeader
        $Organization = $OrgInfo['Organization']
    }

    $BaseUri = "https://dev.azure.com/$Organization/$Project/_apis/wiki/wikis"
    
    if ($WikiId) {
        $Uri = "${BaseUri}/${WikiId}?api-version=7.1-preview.2"
    }
    else {
        $Uri = "${BaseUri}?api-version=7.1-preview.2"
    }

    $Method = 'Get'

    $res = InvokeADOPSRestMethod -Uri $URI -Method $Method -Organization $Organization
    
    if ($res.psobject.properties.name -contains 'value') {
        Write-Output -InputObject $res.value
    }
    else {
        Write-Output -InputObject $res
    }
}
#endregion Get-ADOPSWiki

#region Import-ADOPSRepository

function Import-ADOPSRepository {
    [CmdLetBinding(DefaultParameterSetName='RepositoryName')]
    param (
        [Parameter(Mandatory)]
        [string]$GitSource,

        [Parameter(Mandatory, ParameterSetName = 'RepositoryId')]
        [string]$RepositoryId,
        
        [Parameter(Mandatory, ParameterSetName = 'RepositoryName')]
        [string]$RepositoryName,

        [Parameter(Mandatory)]
        [string]$Project,

        [Parameter()]
        [string]$Organization
    )

    if (-not [string]::IsNullOrEmpty($Organization)) {
        $OrgInfo = GetADOPSHeader -Organization $Organization
    }
    else {
        $OrgInfo = GetADOPSHeader
        $Organization = $OrgInfo['Organization']
    }

    switch ($PSCmdlet.ParameterSetName) {
        'RepositoryName' { $RepoIdentifier = $RepositoryName}
        'RepositoryId'   { $RepoIdentifier = $RepositoryId}
        Default {}
    }
    $InvokeSplat = @{
        URI = "https://dev.azure.com/$Organization/$Project/_apis/git/repositories/$RepoIdentifier/importRequests?api-version=7.1-preview.1"
        Method = 'Post'
        Body = "{""parameters"":{""gitSource"":{""url"":""$GitSource""}}}"
        Organization = $Organization
    }

    InvokeADOPSRestMethod @InvokeSplat
}
#endregion Import-ADOPSRepository

#region New-ADOPSElasticpool

function New-ADOPSElasticPool {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$PoolName,
        
        [Parameter(Mandatory)]
        $ElasticPoolObject,

        [Parameter()]
        [string]$ProjectId,

        [Parameter()]
        [switch]$AuthorizeAllPipelines,

        [Parameter()]
        [switch]$AutoProvisionProjectPools,

        [Parameter()]
        [string]$Organization
    )

    if (-not [string]::IsNullOrEmpty($Organization)) {
        $Org = GetADOPSHeader -Organization $Organization
    } else {
        $Org = GetADOPSHeader
        $Organization = $Org['Organization']
    }

    if ($PSBoundParameters.ContainsKey('ProjectId')) {
        $Uri = "https://dev.azure.com/$Organization/_apis/distributedtask/elasticpools?poolName=$PoolName`&authorizeAllPipelines=$AuthorizeAllPipelines`&autoProvisionProjectPools=$AutoProvisionProjectPools&projectId=$ProjectId&api-version=7.1-preview.1"
    } else {
        $Uri = "https://dev.azure.com/$Organization/_apis/distributedtask/elasticpools?poolName=$PoolName`&authorizeAllPipelines=$AuthorizeAllPipelines`&autoProvisionProjectPools=$AutoProvisionProjectPools&api-version=7.1-preview.1"
    }

    if ($ElasticPoolObject.gettype().name -eq 'String') {
        $Body = $ElasticPoolObject
    } else {
        try {
            $Body = $ElasticPoolObject | ConvertTo-Json -Depth 100
        }
        catch {
            throw "Unable to convert the content of the ElasticPoolObject to json."
        }
    }
    
    $Method = 'POST'
    $ElasticPoolInfo = InvokeADOPSRestMethod -Uri $Uri -Method $Method -Organization $Organization -Body $Body
    Write-Output $ElasticPoolInfo
}
#endregion New-ADOPSElasticpool

#region New-ADOPSElasticPoolObject

function New-ADOPSElasticPoolObject {
    [SkipTest('HasOrganizationParameter')]
    [CmdletBinding()]
    param (
        # Service Endpoint Id
        [Parameter(Mandatory)]
        [guid]
        $ServiceEndpointId,

        # Service Endpoint Scope
        [Parameter(Mandatory)]
        [guid]
        $ServiceEndpointScope,

        # Azure Id
        [Parameter(Mandatory)]
        [string]
        $AzureId,

        # Operating System Type
        [Parameter()]
        [ValidateSet('linux', 'windows')]
        [string]
        $OsType = 'linux',

        # MaxCapacity
        [Parameter()]
        [int]
        $MaxCapacity = 1,

        # DesiredIdle
        [Parameter()]
        [int]
        $DesiredIdle = 0,

        # Recycle VM after each use
        [Parameter()]
        [boolean]
        $RecycleAfterEachUse = $false,

        # Desired Size of pool
        [Parameter()]
        [int]
        $DesiredSize = 0,

        # Agent Interactive UI
        [Parameter()]
        [boolean]
        $AgentInteractiveUI = $false,

        # Time before scaling down
        [Parameter()]
        [int]
        $TimeToLiveMinues = 15,

        # maxSavedNodeCount
        [Parameter()]
        [int]
        $MaxSavedNodeCount = 0,

        # Output Type
        [Parameter()]
        [ValidateSet('json','pscustomobject')]
        [string]
        $OutputType = 'pscustomobject'
    )

    if ($DesiredIdle -gt $MaxCapacity) {
        throw "The desired idle count cannot be larger than the max capacity."
    }

    $ElasticPoolObject = [PSCustomObject]@{
        serviceEndpointId = $ServiceEndpointId
        serviceEndpointScope = $ServiceEndpointScope
        azureId = $AzureId
        maxCapacity = $MaxCapacity
        desiredIdle = $DesiredIdle
        recycleAfterEachUse = $RecycleAfterEachUse
        maxSavedNodeCount = $MaxSavedNodeCount
        osType = $OsType
        desiredSize = $DesiredSize
        agentInteractiveUI = $AgentInteractiveUI
        timeToLiveMinutes = $TimeToLiveMinues
    }
    
    if ($OutputType -eq 'json') {
        $ElasticPoolObject = $ElasticPoolObject | ConvertTo-Json -Depth 100
    }

    Write-Output $ElasticPoolObject
}
#endregion New-ADOPSElasticPoolObject

#region New-ADOPSPipeline

function New-ADOPSPipeline {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Project,

        [Parameter(Mandatory)]
        [ValidateScript( { $_ -like '*.yaml' },
        ErrorMessage = "Path must be to a yaml file in your repository like: folder/file.yaml")] 
        [string]$YamlPath,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Repository,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$FolderPath,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Organization
    )

    if (-not [string]::IsNullOrEmpty($Organization)) {
        $OrgInfo = GetADOPSHeader -Organization $Organization
    }
    else {
        $OrgInfo = GetADOPSHeader
        $Organization = $OrgInfo['Organization']
    }

    $Uri = "https://dev.azure.com/$Organization/$Project/_apis/pipelines?api-version=7.1-preview.1"

    try {
        $RepositoryID = (Get-ADOPSRepository -Organization $Organization -Project $Project -Repository $Repository -ErrorAction Stop).id
    }
    catch {
        throw "The specified Repository $Repository was not found."
    }

    $Body = [ordered]@{
        "name" = $Name
        "folder" = "\$FolderPath"
        "configuration" = [ordered]@{
            "type" = "yaml"
            "path" = $YamlPath
            "repository" = [ordered]@{
                "id" = $RepositoryID
                "type" = "azureReposGit"
            }
        }
    }
    $Body = $Body | ConvertTo-Json -Compress

    $InvokeSplat = @{
        Method       = 'Post'
        Uri          = $URI
        Organization = $Organization
        Body         = $Body 
    }

    InvokeADOPSRestMethod @InvokeSplat

}
#endregion New-ADOPSPipeline

#region New-ADOPSProject

function New-ADOPSProject {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Description,
        
        [Parameter(Mandatory)]
        [ValidateSet('Private', 'Public')]
        [string]$Visibility,
        
        [Parameter()]
        [ValidateSet('Git', 'Tfvc')]
        [string]$SourceControlType = 'Git',
        
        # The process type for the project, such as Basic, Agile, Scrum or CMMI
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$ProcessTypeName,
        
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Organization
    )

    if (-not [string]::IsNullOrEmpty($Organization)) {
        $OrgInfo = GetADOPSHeader -Organization $Organization
    }
    else {
        $OrgInfo = GetADOPSHeader
        $Organization = $OrgInfo['Organization']
    }

    # Get organization process templates
    $URI = "https://dev.azure.com/$Organization/_apis/process/processes?api-version=7.1-preview.1"

    $InvokeSplat = @{
        Method       = 'Get'
        Uri          = $URI
        Organization = $Organization
    }

    $ProcessTemplates = (InvokeADOPSRestMethod @InvokeSplat).value

    if ([string]::IsNullOrWhiteSpace($ProcessTypeName)) {
        $ProcessTemplateTypeId = $ProcessTemplates | Where-Object isDefault -eq $true | Select-Object -ExpandProperty id
    }
    else {
        $ProcessTemplateTypeId = $ProcessTemplates | Where-Object name -eq $ProcessTypeName | Select-Object -ExpandProperty id
        if ([string]::IsNullOrWhiteSpace($ProcessTemplateTypeId)) {
            throw "The specified ProcessTypeName was not found amongst options: $($ProcessTemplates.name -join ', ')!"
        }
    }

    # Create project endpoint
    $URI = "https://dev.azure.com/$Organization/_apis/projects?api-version=7.1-preview.4"

    $Body = [ordered]@{
        'name'         = $Name
        'visibility'   = $Visibility
        'capabilities' = [ordered]@{
            'versioncontrol'  = [ordered]@{
                'sourceControlType' = $SourceControlType
            }
            'processTemplate' = [ordered]@{
                'templateTypeId' = $ProcessTemplateTypeId
            }
        }
    }
    if (-not [string]::IsNullOrEmpty($Description)) {
        $Body.Add('description', $Description)
    }
    $Body = $Body | ConvertTo-Json -Compress
    
    $InvokeSplat = @{
        Method       = 'Post'
        Uri          = $URI
        Body         = $Body
        Organization = $Organization
    }

    InvokeADOPSRestMethod @InvokeSplat
}
#endregion New-ADOPSProject

#region New-ADOPSRepository

function New-ADOPSRepository {
    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Project,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Organization
    )

    if (-not [string]::IsNullOrEmpty($Organization)) {
        $Org = GetADOPSHeader -Organization $Organization
    }
    else {
        $Org = GetADOPSHeader
        $Organization = $Org['Organization']
    }

    $ProjectID = (Get-ADOPSProject -Project $Project).id

    $URI = "https://dev.azure.com/$Organization/_apis/git/repositories?api-version=7.1-preview.1"
    $Body = "{""name"":""$Name"",""project"":{""id"":""$ProjectID""}}"

    $InvokeSplat = @{
        Uri           = $URI
        Method        = 'Post'
        Body          = $Body
        Organization  = $Organization
      }
    
      InvokeADOPSRestMethod @InvokeSplat
}
#endregion New-ADOPSRepository

#region New-ADOPSServiceConnection

function New-ADOPSServiceConnection {
    [cmdletbinding()]
    param(
        [Parameter()]
        [string]$Organization,
        
        [Parameter(Mandatory)]
        [string]$TenantId,

        [Parameter(Mandatory)]
        [string]$SubscriptionName,

        [Parameter(Mandatory)]
        [string]$SubscriptionId,

        [Parameter(Mandatory)]
        [string]$Project,

        [Parameter()]
        [string]$ConnectionName,
      
        [Parameter(Mandatory)]
        [pscredential]$ServicePrincipal
    )

    # Set organization
    if (-not [string]::IsNullOrEmpty($Organization)) {
        $Org = GetADOPSHeader -Organization $Organization
    }
    else {
        $Org = GetADOPSHeader
        $Organization = $Org['Organization']
    }

    # Get ProjectId
    $ProjectInfo = Get-ADOPSProject -Organization $Organization -Project $Project

    # Set connection name if not set by parameter
    if (-not $ConnectionName) {
        $ConnectionName = $SubscriptionName -replace " "
    }

    # Create body for the API call
    $Body = [ordered]@{
        data                             = [ordered]@{
            subscriptionId   = $SubscriptionId
            subscriptionName = $SubscriptionName
            environment      = "AzureCloud"
            scopeLevel       = "Subscription"
            creationMode     = "Manual"
        }
        name                             = ($SubscriptionName -replace " ")
        type                             = "AzureRM"
        url                              = "https://management.azure.com/"
        authorization                    = [ordered]@{
            parameters = [ordered]@{
                tenantid            = $TenantId
                serviceprincipalid  = $ServicePrincipal.UserName
                authenticationType  = "spnKey"
                serviceprincipalkey = $ServicePrincipal.GetNetworkCredential().Password
            }
            scheme     = "ServicePrincipal"
        }
        isShared                         = $false
        isReady                          = $true
        serviceEndpointProjectReferences = @(
            [ordered]@{
                projectReference = [ordered]@{
                    id   = $ProjectInfo.Id
                    name = $Project
                }
                name             = $ConnectionName
            }
        )
    } | ConvertTo-Json -Depth 10

    # Run function
    $URI = "https://dev.azure.com/$Organization/$Project/_apis/serviceendpoint/endpoints?api-version=6.0-preview.4"
    $InvokeSplat = @{
        Uri          = $URI
        Method       = "POST"
        Body         = $Body
        Organization = $Organization
    }

    InvokeADOPSRestMethod @InvokeSplat

}
#endregion New-ADOPSServiceConnection

#region New-ADOPSUserStory

function New-ADOPSUserStory {
  [CmdletBinding()]
  param (
    [Parameter(Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string]$Title,

    [Parameter(Mandatory)]
    [string]$ProjectName,

    [Parameter()]
    [string]$Description,

    [Parameter()]
    [string]$Tags,

    [Parameter()]
    [string]$Priority,

    [Parameter()]
    [string]$Organization
  )

  if (-not [string]::IsNullOrEmpty($Organization)) {
    $Org = GetADOPSHeader -Organization $Organization
  }
  else {
    $Org = GetADOPSHeader
    $Organization = $Org['Organization']
  }


  $URI = "https://dev.azure.com/$Organization/$ProjectName/_apis/wit/workitems/`$User Story?api-version=5.1"
  $Method = 'POST'

  $desc = $Description.Replace('"', "'")
  $Body = "[
      {
        `"op`": `"add`",
        `"path`": `"/fields/System.Title`",
        `"value`": `"$($Title)`"
      },
      {
        `"op`": `"add`",
        `"path`": `"/fields/System.Description`",
        `"value`": `"$($desc)`"
      },
      {
        `"op`": `"add`",
        `"path`": `"/fields/System.Tags`",
        `"value`": `"$($Tags)`"
      },
      {
        `"op`": `"add`",
        `"path`": `"/fields/Microsoft.VSTS.Common.Priority`",
        `"value`": `"$($Priority)`"
      },
    ]"

    
  $ContentType= "application/json-patch+json"  
  
  $InvokeSplat = @{
    Uri           = $URI
    ContentType   = $ContentType
    Method        = $Method
    Body          = $Body
    Organization  = $Organization
  }

  InvokeADOPSRestMethod @InvokeSplat
}
#endregion New-ADOPSUserStory

#region New-ADOPSVariableGroup

function New-ADOPSVariableGroup {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$VariableGroupName,

        [Parameter(Mandatory, ParameterSetName = 'VariableSingle')]
        [string]$VariableName,

        [Parameter(Mandatory, ParameterSetName = 'VariableSingle')]
        [string]$VariableValue,

        [Parameter(Mandatory)]
        [string]$Project,

        [Parameter(ParameterSetName = 'VariableSingle')]
        [switch]$IsSecret,

        [Parameter(Mandatory, ParameterSetName = 'VariableHashtable')]
        [ValidateScript(
            {
                $_ | ForEach-Object { $_.Keys -Contains 'Name' -and $_.Keys -Contains 'IsSecret' -and $_.Keys -Contains 'Value' -and $_.Keys.count -eq 3 }
            },
            ErrorMessage = 'The hashtable must contain the following keys: Name, IsSecret, Value')]
        [hashtable[]]$VariableHashtable,

        [Parameter()]
        [string]$Description,

        [Parameter()]
        [string]$Organization
    )

    if ([string]::IsNullOrEmpty($Organization)) {
        $Org = GetADOPSHeader
        $Organization = $Org['Organization']
    }
    else {
        $Org = GetADOPSHeader -Organization $Organization
    }

    $ProjectInfo = Get-ADOPSProject -Organization $Organization -Project $Project

    $URI = "https://dev.azure.com/${Organization}/_apis/distributedtask/variablegroups?api-version=7.1-preview.2"
    $method = 'POST'

    if ($VariableName) {
        $Body = @{
            Name                           = $VariableGroupName
            Description                    = $Description
            Type                           = 'Vsts'
            variableGroupProjectReferences = @(@{
                    Name             = $VariableGroupName
                    Description      = $Description
                    projectReference = @{
                        Id = $ProjectInfo.Id
                    }
                })
            variables                      = @{
                $VariableName = @{
                    isSecret = $IsSecret.IsPresent
                    value    = $VariableValue
                }
            }
        } | ConvertTo-Json -Depth 10
    }
    else {

        $Variables = @{}
        foreach ($Hashtable in $VariableHashtable) {
            $Variables.Add(
                $Hashtable.Name, @{
                    isSecret = $Hashtable.IsSecret
                    value    = $Hashtable.Value
                }
            )
        }

        $Body = @{
            Name                           = $VariableGroupName
            Description                    = $Description
            Type                           = 'Vsts'
            variableGroupProjectReferences = @(@{
                    Name             = $VariableGroupName
                    Description      = $Description
                    projectReference = @{
                        Id = $($ProjectInfo.Id)
                    }
                })
            variables                      = $Variables
        } | ConvertTo-Json -Depth 10
    }

    InvokeADOPSRestMethod -Uri $Uri -Method $Method -Body $Body -Organization $Organization
}
#endregion New-ADOPSVariableGroup

#region New-ADOPSWiki

function New-ADOPSWiki {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$WikiName,
        
        [Parameter(Mandatory)]
        [string]$WikiRepository,

        [Parameter(Mandatory)]
        [string]$Project,

        [Parameter()]
        [string]$WikiRepositoryPath = '/',
        
        [Parameter()]
        [string]$GitBranch = 'main',

        [Parameter()]
        [string]$Organization
    )

    if (-not [string]::IsNullOrEmpty($Organization)) {
        $Org = GetADOPSHeader -Organization $Organization
    }
    else {
        $Org = GetADOPSHeader
        $Organization = $Org['Organization']
    }

    $ProjectId = (Get-ADOPSProject -Project $Project).id
    $RepositoryId = (Get-ADOPSRepository -Project $Project -Repository $WikiRepository).id

    $URI = "https://dev.azure.com/$Organization/_apis/wiki/wikis?api-version=7.1-preview.2"
    
    $Method = 'Post'
    $Body = [ordered]@{
        'type' = 'codeWiki'
        'name' = $WikiName
        'projectId' = $ProjectId
        'repositoryId' = $RepositoryId
        'mappedPath' = $WikiRepositoryPath
        'version' = @{'version' = $GitBranch}
    } 

    $InvokeSplat = @{
        Uri = $URI
        Method = $Method
        Body = $Body | ConvertTo-Json -Compress
    }

    InvokeADOPSRestMethod @InvokeSplat
}
#endregion New-ADOPSWiki

#region Remove-ADOPSRepository

function Remove-ADOPSRepository {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$RepositoryID,

        [Parameter(Mandatory)]
        [string]$Project,

        [Parameter()]
        [string]$Organization
    )

    if (-not [string]::IsNullOrEmpty($Organization)) {
        $OrgInfo = GetADOPSHeader -Organization $Organization
    }
    else {
        $OrgInfo = GetADOPSHeader
        $Organization = $OrgInfo['Organization']
    }

    $Uri = "https://dev.azure.com/$Organization/$Project/_apis/git/repositories/$RepositoryID`?api-version=7.1-preview.1"
    
    $result = InvokeADOPSRestMethod -Uri $Uri -Method Delete -Organization $Organization

    if ($result.psobject.properties.name -contains 'value') {
        Write-Output -InputObject $result.value
    }
    else {
        Write-Output -InputObject $result
    }
}
#endregion Remove-ADOPSRepository

#region Remove-ADOPSVariableGroup

function Remove-ADOPSVariableGroup {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$VariableGroupName,

        [Parameter(Mandatory)]
        [string]$Project,

        [Parameter()]
        [string]$Organization
    )

    if (-not [string]::IsNullOrEmpty($Organization)) {
        $Org = GetADOPSHeader -Organization $Organization
    }
    else {
        $Org = GetADOPSHeader
        $Organization = $Org['Organization']
    }

    $Uri = "https://dev.azure.com/$Organization/$Project/_apis/distributedtask/variablegroups?api-version=7.1-preview.2"
    $VariableGroups = (InvokeADOPSRestMethod -Uri $Uri -Method 'Get' -Organization $Organization).value

    $GroupToRemove = $VariableGroups | Where-Object name -eq $VariableGroupName
    if ($null -eq $GroupToRemove) {
        throw "Could not find group $VariableGroupName! Groups found: $($VariableGroups.name -join ', ')."
    }
    
    $ProjectId = (Get-ADOPSProject -Organization $Organization -Project $Project).id

    $URI = "https://dev.azure.com/$Organization/_apis/distributedtask/variablegroups/$($GroupToRemove.id)?projectIds=$ProjectId&api-version=7.1-preview.2"
    $null = InvokeADOPSRestMethod -Uri $Uri -Method 'Delete' -Organization $Organization
}
#endregion Remove-ADOPSVariableGroup

#region Set-ADOPSElasticPool

function Set-ADOPSElasticPool {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [int]$PoolId,
        
        [Parameter(Mandatory)]
        $ElasticPoolObject,

        [Parameter()]
        [string]$Organization
    )

    if (-not [string]::IsNullOrEmpty($Organization)) {
        $Org = GetADOPSHeader -Organization $Organization
    }
    else {
        $Org = GetADOPSHeader
        $Organization = $Org['Organization']
    }

    $Uri = "https://dev.azure.com/$Organization/_apis/distributedtask/elasticpools/$PoolId`?api-version=7.1-preview.1"

    if ($ElasticPoolObject.GetType().Name -eq 'String') {
        $Body = $ElasticPoolObject
    }
    else {
        try {
            $Body = $ElasticPoolObject | ConvertTo-Json -Depth 100
        }
        catch {
            throw "Unable to convert the content of the ElasticPoolObject to json."
        }
    }
    
    $Method = 'PATCH'
    $ElasticPoolInfo = InvokeADOPSRestMethod -Uri $Uri -Method $Method -Organization $Organization -Body $Body
    Write-Output $ElasticPoolInfo
}
#endregion Set-ADOPSElasticPool

#region Start-ADOPSPipeline

function Start-ADOPSPipeline {
    param (
        [Parameter(Mandatory)]
        [string]$Name,

        [Parameter(Mandatory)]
        [string]$Project,

        [Parameter()]
        [string]$Branch = 'main',

        [Parameter()]
        [string]$Organization
    )

    if (-not [string]::IsNullOrEmpty($Organization)) {
        $Org = GetADOPSHeader -Organization $Organization
    }
    else {
        $Org = GetADOPSHeader
    }

    $AllPipelinesURI = "https://dev.azure.com/$($Org['Organization'])/$Project/_apis/pipelines?api-version=7.1-preview.1"
    $AllPipelines = InvokeADOPSRestMethod -Method Get -Uri $AllPipelinesURI -Organization $Org['Organization']
    $PipelineID = ($AllPipelines.value | Where-Object -Property Name -EQ $Name).id

    if ([string]::IsNullOrEmpty($PipelineID)) {
        throw "No pipeline with name $Name found."
    }

    $URI = "https://dev.azure.com/$($Org['Organization'])/$Project/_apis/pipelines/$PipelineID/runs?api-version=7.1-preview.1"
    $Body = '{"stagesToSkip":[],"resources":{"repositories":{"self":{"refName":"refs/heads/' + $Branch + '"}}},"variables":{}}'
    
    $InvokeSplat = @{
        Method = 'Post' 
        Uri = $URI 
        Body = $Body
        Organization = $Org['Organization']
    }

    InvokeADOPSRestMethod @InvokeSplat
}
#endregion Start-ADOPSPipeline

#region Test-ADOPSYamlFile

function Test-ADOPSYamlFile {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$Project,

        [Parameter(Mandatory)]
        [ValidateScript({
            $_ -match '.*\.y[aA]{0,1}ml$'
        }, ErrorMessage = 'Fileextension must be ".yaml" or ".yml"')]
        [string]$File,

        [Parameter(Mandatory)]
        [int]$PipelineId,

        [Parameter()]
        [string]$Organization
    )

    if (-not [string]::IsNullOrEmpty($Organization)) {
        $Org = GetADOPSHeader -Organization $Organization
    }
    else {
        $Org = GetADOPSHeader
        $Organization = $Org['Organization']
    }

    $Uri = "https://dev.azure.com/$Organization/$Project/_apis/pipelines/$PipelineId/runs?api-version=7.1-preview.1"

    $FileData = Get-Content $File -Raw

    $Body = @{
        previewRun = $true
        templateParameters = @{}
        resources = @{}
        yamlOverride = $FileData
    } | ConvertTo-Json -Depth 10 -Compress
    
    $InvokeSplat = @{
        Uri           = $URI
        Method        = 'Post'
        Body          = $Body
        Organization  = $Organization
      }
    
    try {
        $Result = InvokeADOPSRestMethod @InvokeSplat
        Write-Output "$file validation success."
    } 
    catch [Microsoft.PowerShell.Commands.HttpResponseException] {
        if ($_.ErrorDetails.Message) {
            $r = $_.ErrorDetails.Message | ConvertFrom-Json
            if ($r.typeName -like '*PipelineValidationException*') {
                Write-Warning "Validation failed:`n$($r.message)"
            }
            else {
                throw $_
            }
        }
    }
}
#endregion Test-ADOPSYamlFile