LibreDevOpsHelpers.AzureDevOps/LibreDevOpsHelpers.AzureDevOps.psm1

Set-StrictMode -Version Latest

function Get-LdoAzureDevOpsPatHeader {
    # Internal. Builds a Basic auth header value from a PAT secure string.
    [CmdletBinding()]
    [OutputType([string])]
    param([Parameter(Mandatory)][securestring]$Pat)

    $plain = [System.Net.NetworkCredential]::new('', $Pat).Password
    if ([string]::IsNullOrWhiteSpace($plain)) {
        throw 'The supplied personal access token is empty.'
    }
    $bytes = [Text.Encoding]::ASCII.GetBytes(":$plain")
    return 'Basic ' + [Convert]::ToBase64String($bytes)
}

function Get-LdoAzureDevOpsToken {
    # Internal. Returns the pipeline access token from the standard environment variables.
    [CmdletBinding()]
    [OutputType([string])]
    param()

    $token = $Env:SYSTEM_ACCESSTOKEN
    if ([string]::IsNullOrWhiteSpace($token)) {
        $token = $Env:SYSTEM_ACCESS_TOKEN
    }
    if ([string]::IsNullOrWhiteSpace($token)) {
        throw 'Pipeline access token not found in SYSTEM_ACCESSTOKEN or SYSTEM_ACCESS_TOKEN.'
    }
    return $token
}

function Get-LdoAzureDevOpsOrgId {
    <#
    .SYNOPSIS
        Returns the Azure DevOps organization id for an organization URL.

    .DESCRIPTION
        Calls the Azure DevOps connectionData REST API using a personal access token and
        returns an object describing the organization, including its instance id.

    .PARAMETER OrganizationUrl
        Organization URL, for example https://dev.azure.com/contoso.

    .PARAMETER Pat
        Personal access token with at least read access, supplied as a secure string.

    .EXAMPLE
        $pat = Read-Host -AsSecureString
        Get-LdoAzureDevOpsOrgId -OrganizationUrl 'https://dev.azure.com/contoso' -Pat $pat

    .OUTPUTS
        PSCustomObject with OrganizationId and OrganizationUrl.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$OrganizationUrl,
        [Parameter(Mandatory)][securestring]$Pat
    )

    $authHeader = Get-LdoAzureDevOpsPatHeader -Pat $Pat
    $uri = "$($OrganizationUrl.TrimEnd('/'))/_apis/connectionData?api-version=7.0-preview.1"

    try {
        $response = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = $authHeader } -Method Get -ErrorAction Stop
    }
    catch {
        Write-LdoLog -Level ERROR -Message "Failed to query connectionData for $OrganizationUrl : $_"
        throw
    }

    if (-not $response -or -not $response.instanceId) {
        throw "Failed to obtain the organization id for $OrganizationUrl."
    }

    [pscustomobject]@{
        OrganizationId = [string]$response.instanceId
        OrganizationUrl = $OrganizationUrl
    }
}

function Invoke-LdoAzureDevOpsTokenReplacement {
    <#
    .SYNOPSIS
        Injects the pipeline access token into Terraform module sources.

    .DESCRIPTION
        Scans *.tf files beneath a path for the placeholder
        git::https://__SYSTEM_ACCESS_TOKEN__@ and replaces it with the live pipeline token
        so that private Azure DevOps git module sources can be cloned during a run. Use
        Invoke-LdoAzureDevOpsTokenReplacementRevert afterwards to restore the placeholder.

    .PARAMETER CodePath
        Folder to scan for *.tf files. Defaults to the current working directory.

    .EXAMPLE
        Invoke-LdoAzureDevOpsTokenReplacement -CodePath ./terraform

    .OUTPUTS
        None
    #>

    [CmdletBinding()]
    [OutputType([void])]
    param(
        [ValidateNotNullOrEmpty()][string]$CodePath = (Get-Location).Path
    )

    if (-not (Test-Path $CodePath)) {
        throw "Path not found: $CodePath"
    }

    $orig = Get-Location
    try {
        Set-Location $CodePath
        Write-LdoLog -Level INFO -Message "Scanning Terraform files beneath '$CodePath' for token replacement."

        $tfFiles = Get-ChildItem -Recurse -Filter '*.tf' -File
        if (-not $tfFiles) {
            Write-LdoLog -Level WARN -Message "No *.tf files located under '$CodePath'; nothing to update."
            return
        }

        $placeholder = 'git::https://__SYSTEM_ACCESS_TOKEN__@'
        $token = Get-LdoAzureDevOpsToken
        $replacement = "git::https://$token@"

        foreach ($file in $tfFiles) {
            try {
                $content = Get-Content -LiteralPath $file.FullName -Raw -ErrorAction Stop
                if ($content -notmatch [regex]::Escape($placeholder)) {
                    Write-LdoLog -Level DEBUG -Message "Skipping '$($file.FullName)'; placeholder absent."
                    continue
                }
                $updated = $content -replace [regex]::Escape($placeholder), $replacement
                Set-Content -LiteralPath $file.FullName -Value $updated -ErrorAction Stop
                Write-LdoLog -Level INFO -Message "Injected token into '$($file.FullName)'."
            }
            catch {
                Write-LdoLog -Level WARN -Message "Failed processing '$($file.FullName)': $_"
            }
        }

        Write-LdoLog -Level SUCCESS -Message 'Terraform token replacement completed.'
    }
    finally {
        Set-Location $orig
    }
}

function Invoke-LdoAzureDevOpsTokenReplacementRevert {
    <#
    .SYNOPSIS
        Restores the token placeholder in Terraform module sources.

    .DESCRIPTION
        Reverses Invoke-LdoAzureDevOpsTokenReplacement by scanning *.tf files beneath a path
        for the live pipeline token in git module sources and replacing it with the
        git::https://__SYSTEM_ACCESS_TOKEN__@ placeholder, so the token is never committed.

    .PARAMETER CodePath
        Folder to scan for *.tf files. Defaults to the current working directory.

    .EXAMPLE
        Invoke-LdoAzureDevOpsTokenReplacementRevert -CodePath ./terraform

    .OUTPUTS
        None
    #>

    [CmdletBinding()]
    [OutputType([void])]
    param(
        [ValidateNotNullOrEmpty()][string]$CodePath = (Get-Location).Path
    )

    if (-not (Test-Path $CodePath)) {
        throw "Path not found: $CodePath"
    }

    $orig = Get-Location
    try {
        Set-Location $CodePath
        Write-LdoLog -Level INFO -Message "Reverting token replacement in *.tf files beneath '$CodePath'."

        $tfFiles = Get-ChildItem -Recurse -Filter '*.tf' -File
        if (-not $tfFiles) {
            Write-LdoLog -Level WARN -Message "No *.tf files located under '$CodePath'; nothing to revert."
            return
        }

        $token = Get-LdoAzureDevOpsToken
        $search = [regex]::Escape("git::https://$token@")
        $replacement = 'git::https://__SYSTEM_ACCESS_TOKEN__@'

        foreach ($file in $tfFiles) {
            try {
                $content = Get-Content -LiteralPath $file.FullName -Raw -ErrorAction Stop
                if ($content -notmatch $search) {
                    Write-LdoLog -Level DEBUG -Message "Skipping '$($file.FullName)'; token not present."
                    continue
                }
                $updated = $content -replace $search, $replacement
                Set-Content -LiteralPath $file.FullName -Value $updated -ErrorAction Stop
                Write-LdoLog -Level INFO -Message "Restored placeholder in '$($file.FullName)'."
            }
            catch {
                Write-LdoLog -Level WARN -Message "Failed processing '$($file.FullName)': $_"
            }
        }

        Write-LdoLog -Level SUCCESS -Message 'Terraform token reversion completed.'
    }
    finally {
        Set-Location $orig
    }
}

Export-ModuleMember -Function `
    Get-LdoAzureDevOpsOrgId, `
    Invoke-LdoAzureDevOpsTokenReplacement, `
    Invoke-LdoAzureDevOpsTokenReplacementRevert