GhCloudOps.psm1

<#
    .SYNOPSIS
        Converts tokenized bicepparam, HCL, json or psd1 files to expanded files using a token map.

    .DESCRIPTION
        Converts tokenized bicepparam, HCL, json or psd1 files to expanded files using a token map. The token map
        is a hashtable where the key is the token and the value is the replacement value. The token is in the
        format '{{ token }}'. The function reads the input file line by line and replaces the tokens with the
        corresponding values. The expanded content is written to the output file. The function also checks for
        unmatched tokens and unused tokens.

    .PARAMETER InputFile
        The path to the input file.

    .PARAMETER OutputFile
        The path to the output file.

    .PARAMETER TokenMap
        The hashtable containing the token map.

    .EXAMPLE
        $tokenParams = @{
            InputFile = 'C:\input.txt'
            OutputFile = 'C:\output.txt'
            TokenMap = @{
                string = 'string'
                int = 1
                bool = $true
                null = $null
            }
        }

        Convert-Token @tokenParams

        Converts the input file 'C:\input.txt' to the output file 'C:\output.txt' using the token map.
        The token map is a hashtable where '{{ string }}' is replaced with 'string', '{{ int }}' is replaced with 1,
        '{{ bool }}' is replaced with true, '{{ null }}' is replaced with null.
#>

function Convert-Token
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [ValidateScript({ Test-Path -Path $_ })]
        [string]
        $InputFile,

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

        [Parameter(Mandatory)]
        [ValidateScript({ $_.Count -gt 0 })]
        [hashtable]
        $TokenMap
    )

    $item = Get-Item -LiteralPath $InputFile
    if (($item.Extension -eq '.bicepparam') -or ($item.Extension -eq '.psd1'))
    {
        $quotedString = "'{0}'"
    }
    elseif (($item.Extension -eq '.json') -or ($item.Extension -eq '.tfvars'))
    {
        $quotedString = '"{0}"'
    }
    else
    {
        throw 'Unsupported file type: {0}' -f $item.Extension
    }

    $content = Get-Content -Path $InputFile
    $temporaryFile = New-TemporaryFile
    $usedTokens = New-Object -TypeName System.Collections.ArrayList
    foreach ($line in $content)
    {
        foreach ($key in $TokenMap.Keys)
        {
            $token = "{{ $key }}"
            if ($line -match $token)
            {
                $value = $TokenMap[$key]
                if ($null -eq $value)
                {
                    if ($item.Extension -eq '.psd1')
                    {
                        $line = $line.Replace(($quotedString -f $token), '$null')
                    }
                    else
                    {
                        $line = $line.Replace(($quotedString -f $token), ('null'.Replace("'", '')))
                    }
                }
                elseif ($value.GetType() -eq [string])
                {
                    $line = $line.Replace($token, $value)
                }
                elseif ($value.GetType() -eq [int])
                {
                    $line = $line.Replace(($quotedString -f $token), $value)
                }
                elseif ($value.GetType() -eq [bool])
                {
                    if ($item.Extension -eq '.psd1')
                    {
                        $line = $line.Replace(($quotedString -f $token), ('$' + $value.ToString().ToLower()))
                    }
                    else
                    {
                        $line = $line.Replace(($quotedString -f $token), $value.ToString().ToLower())
                    }
                }

                $null = $usedTokens.Add($key)
            }
        }

        Out-File -InputObject $line -FilePath $temporaryFile -Append
    }

    $pattern = '\{\{[^\}]+\}\}'
    $unmatchedTokens = Select-String -Path $temporaryFile -Pattern $pattern -AllMatches
    if ($unmatchedTokens)
    {
        Write-Error -Message 'Unmatched tokens found' -ErrorAction 'Continue'
        throw $unmatchedTokens
    }

    $unusedTokens = New-Object -TypeName System.Collections.ArrayList
    foreach ($key in $TokenMap.Keys)
    {
        if (-not $usedTokens.Contains($key))
        {
            $null = $unusedTokens.Add($key)
        }
    }

    if ($unusedTokens.Count -gt 0)
    {
        Write-Warning -Message ('Unused tokens: {0}' -f ($unusedTokens -join ', '))
    }

    if (Test-Path -Path $OutputFile)
    {
        Clear-Content -Path $OutputFile
    }
    else
    {
        $null = New-Item -Path $OutputFile -ItemType File
    }

    Set-Content -Path $OutputFile -Value (Get-Content -Path $temporaryFile)
    Write-Host -Object ("Converted tokens in '{0}' to '{1}'" -f $InputFile, $OutputFile)
}
<#
    .SYNOPSIS
        Get the version from the latest tag or the tag specified in the parameter.

    .DESCRIPTION
        Get the version from the latest tag or the tag specified in the parameter.

    .PARAMETER Ref
        The tag reference to get the version from.

    .PARAMETER DefaultVersion
        The default version to return if no tag is found. Default is 'v1.0.0-beta'.

    .EXAMPLE
        Get-Version.ps1 -Ref refs/tags/v1.0.0
        Returns 'v1.0.0'.

    .EXAMPLE
        Get-Version.ps1 -Ref refs/heads/main
        Returns 'v1.0.1', if it's the latest git tag.

    .EXAMPLE
        Get-Version.ps1 -Ref refs/heads/main -DefaultVersion 'v0.0.1'
        Returns 'v0.0.1', if no tags are found.
#>

function Get-TagVersion
{
    [OutputType([string])]
    param
    (
        [Parameter(Mandatory)]
        [string]
        $Ref,

        [Parameter()]
        [string]
        $DefaultVersion = 'v1.0.0-beta'
    )

    $latestTagCommitHash = & git rev-list --tags --max-count=1
    if ($null -ne $latestTagCommitHash)
    {
        Write-Host -Object ("The latest tag commit hash is '{0}'." -f $latestTagCommitHash)
        $latestTagVersion = & git describe --tags $latestTagCommitHash
        Write-Host -Object ("The latest tag is '{0}'." -f $latestTagVersion)
    }

    if ($Ref -like 'refs/tags/v*.*.*')
    {
        $tag = $Ref.Split('/')[-1]
        $version = $tag
        Write-Host -Object ("Version '{0}' is being set by ref '{1}'." -f $version, $Ref)
    }
    elseif ($Ref -like 'refs/tags/*')
    {
        throw "The tag '{0}' is not a version tag. Please, use a version tag in the format 'v*.*.*'." -f $Ref
    }
    elseif ($null -ne $latestTagVersion)
    {
        $version = $latestTagVersion
        Write-Host -Object ("Version '{0}' is being set by the latest tag." -f $version , $latestTagVersion)
    }
    else
    {
        $version = $DefaultVersion
        Write-Host -Object ("Version '{0}' is being set by the default value." -f $version, $DefaultVersion)
    }

    return $version
}
<#
    .SYNOPSIS
        Generates a random secure string.

    .DESCRIPTION
        Generates a random secure string of a given length.

    .PARAMETER Length
        The length of the random string to be generated.

    .EXAMPLE
        New-RandomSecret -Length 16

        Creates a random secure string that's 16 characters long.
#>

function New-RandomSecret
{
    [Diagnostics.CodeAnalysis.SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')]
    [OutputType([System.Security.SecureString])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [int]
        $Length
    )

    $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_-+=<>?[]{}|;:,.`~'
    $secretValueParams = @{
        String      = -join (1..$Length | ForEach-Object { $chars[(Get-Random -Minimum 0 -Maximum $chars.Length)] })
        AsPlainText = $true
        Force       = $true
    }

    $secretValue = ConvertTo-SecureString @secretValueParams
    return $secretValue
}
<#
    .SYNOPSIS
        Sets a GitHub Actions environment variable.

    .DESCRIPTION
        Sets a GitHub Actions environment variable by appending the variable to the GITHUB_ENV file.
        The function takes the name and value of the variable as parameters and writes them to the file
        in the format 'NAME=VALUE'.

    .PARAMETER Name
        The name of the environment variable.

    .PARAMETER Value
        The value of the environment variable.

    .PARAMETER IsOutput
        Indicates whether the variable should be set in the GITHUB_OUTPUT file instead of the GITHUB_ENV file.

    .PARAMETER IsSecret
        Indicates whether the variable should be treated as a secret.

    .EXAMPLE
        Set-GhVariable -Name 'MY_VAR' -Value 'my_value'

        Sets the environment variable 'MY_VAR' to 'my_value' in the GITHUB_ENV file.

    .EXAMPLE
        Set-GhVariable -Name 'MY_SECRET_VAR' -Value 'my_secret_value' -IsSecret

        Sets the secret environment variable. The value is masked in the log output.

    .EXAMPLE
        Set-GhVariable -Name 'MY_OUTPUT_VAR' -Value 'output_value' -IsOutput

        Sets the output variable 'MY_OUTPUT_VAR' to 'output_value' in the GITHUB_OUTPUT file.

    .NOTES
        For security reasons, secret variables cannot be accessed from outside of the job that defines them.
#>

function Set-GhVariable
{
    [Diagnostics.CodeAnalysis.SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [string]
        $Name,

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

        [Parameter()]
        [switch]
        $IsOutput,

        [Parameter()]
        [switch]
        $IsSecret
    )

    if ($IsSecret)
    {
        Write-Host -Object ('::add-mask::{0}' -f $Value)
    }

    $setVariableParams = @{
        InputObject = '{0}={1}' -f $Name, $Value
        FilePath    = $IsOutput ? $env:GITHUB_OUTPUT : $env:GITHUB_ENV
        Append      = $true
    }

    Out-File @setVariableParams
}