Public/utils.ps1

function Get-RKRandomCharacters($length, $characters) { 
      [CmdletBinding()]
      $random = 1..$length | ForEach-Object { Get-Random -Maximum $characters.length } 
      $private:ofs = "" 
      return [String]$characters[$random]
}

function Confirm-RKCurrentDirectoryInGitRepository {
      <#
    .SYNOPSIS
          Git Directory Checker
    .DESCRIPTION
          Validates whether or not the current directory exists within a git repo.
    .OUTPUTS
    .NOTES
          Version: 1.0
          Author: Liam Dunphy
          Creation Date: 05/05/2021
          Purpose/Change: Initial function development
 
    .EXAMPLE
          Confirm-RKCurrentDirectoryInGitRepository
    #>

    
      $res = git rev-parse --is-inside-work-tree

      if ($null -eq $res) {
            return $false
      }
      else {
            return $true
      }
}


Function Get-RKGitDeltaFiles {
      <#
  .SYNOPSIS
        Gets the name and status of files which have changed in the given commit.
  .DESCRIPTION
        Generates an object documenting files that have been added, modified, or deleted in a given commit.
  .PARAMETER CommitId
        The git commit id we are looking at to identify changes. This can either be a literal id, or use the relative HEAD value. In DevOps we can leverage the system variable $(Build.SourceVersion) within our YAML pipelines.
  .PARAMETER LongKeyNames
        Indicates whether the default git status code should be converted into its more descriptive version e.g. "A" becomes "Added".
  .PARAMETER Filter
        Simple wildcard filtering to limit the scope of considered files. E.g. if you only want to see files in FolderA, you could write "*FolderA/*".
  .PARAMETER Pattern
        Advanced regex pattern filtering to limit the scope of considered files.
  .PARAMETER SimpleMatch
        Switch condition to indicate that simple filtering should be used.
  .PARAMETER RegexMatch
        Switch cndition to indicate that regex pattern matching should be used.
  .OUTPUTS
  .NOTES
        Version: 1.0
        Author: Liam Dunphy
        Creation Date: 05/05/2021
        Purpose/Change: Initial function development
 
  .EXAMPLE
        Get-RKGitDeltaFiles -CommitId "HEAD" -SimpleMatch -Filter "*FolderA*"
  .EXAMPLE
        Get-RKGitDeltaFiles -CommitId "1e49aef" -LongKeyNames $true -RegexMatch -Pattern '.*[0-9]{2}.*(.sql){1}'
  #>

      [CmdletBinding(DefaultParameterSetName = 'Simple')]

      param (
            [OutputType([Object[]])]

            [string]
            $CommitId = "HEAD",

            [bool]
            $LongKeyNames = $true,

            [Parameter(ParameterSetName = 'Simple')]
            [string]
            $Filter = "*",

            [Parameter(ParameterSetName = 'Regex', Mandatory = $true)]
            [string]
            $Pattern,

            [Parameter(ParameterSetName = 'Simple', Mandatory = $true)]
            [switch]
            $SimpleMatch,

            [Parameter(ParameterSetName = 'Regex', Mandatory = $true)]
            [switch]
            $RegexMatch
      )

      [Object[]] $diffFiles = @()

      if ($RegexMatch) {
            $diffFiles = (git diff-tree --no-commit-id --name-status -r $CommitId) | Where-Object { $_ -match $Pattern } | ConvertFrom-String -Delimiter '\t' -PropertyNames Status, Name
      }
      else {
            $diffFiles = (git diff-tree --no-commit-id --name-status -r $CommitId) | Where-Object { $_ -like $Filter } | ConvertFrom-String -Delimiter '\t' -PropertyNames Status, Name
      }

      if ($LongKeyNames -and ($diffFiles.Count -ne 0)) {
            $diffFiles | ForEach-Object {
                  $_.Status = switch ($_.Status) {
                        "A" { "Added"; Continue }
                        "D" { "Deleted"; Continue }
                        "M" { "Modified"; Continue }
                  }
            }
      }

      Write-Output $diffFiles -NoEnumerate
}

Function Get-RKPublicIp {
      return Invoke-RestMethod http://ipinfo.io/json | Select-Object -exp ip
}


Function New-RKGitManifestFile {
      <#
  .SYNOPSIS
        Manifest File Generator.
  .DESCRIPTION
        This script generates a manifest file documenting files that have been added, modified, or deleted based on properties retrieved using git diff-tree.
   
        We can optionally specify to copy added or modified files over to a bin folder, and have this incorporated in the manifest file.
  .PARAMETER DiffFiles
        Object with Status and Name columns. You can generate this object using Get-RKGitDeltaFiles and pass it down the pipeline.
  .PARAMETER OutputFolder
        The output folder for the manifest and copied files relative to the script root. It is recommended that this should follow the form of bin/... so that if the file is run locally it is ignored by .gitignore settings.
  .PARAMETER CleanOutputFolder
        Switch command indicating if the output folder should be cleaned before writing files to it.
  .PARAMETER ManifestOnly
        Switch command indicating if only the manifest file itself should be generated (i.e. source files are not copied to the output folder)
  .PARAMETER OrderByFile
        Switch command indicating that the manifest file should be ordered by file name.
  .PARAMETER OrderByStatus
        Switch command indicating that the manifest file should be ordered by status.
  .OUTPUTS
        A JSON manifest file is generated and placed in the outputFolder path, along with copied files if specified.
  .NOTES
        Version: 1.0
        Author: Liam Dunphy
        Creation Date: 30/04/2021
        Purpose/Change: Initial script development
  .EXAMPLE
        New-RKGitManifestFile -DiffFiles $files -OutputFolder "bin" -ManifestOnly
   
  .EXAMPLE
        New-RKGitManifestFile -DiffFiles $files -OutputFolder "bin" -ManifestOnly -CleanOutputFolder
  #>

      [CmdletBinding(DefaultParameterSetName = "File")]

      param(
            [Parameter(Mandatory = $true)]
            [object[]]$DiffFiles,

            [string]$OutputFolder = "bin",

            [switch]$CleanOutputFolder,

            [switch]$ManifestOnly,

            [Parameter(ParameterSetName = "File", Mandatory = $true)]
            [switch]$OrderByFile,

            [Parameter(ParameterSetName = "Status", Mandatory = $true)]
            [switch]$OrderByStatus
      )

      $rootFolder = Get-RKGitRepositoryRoot

      Write-Verbose "Git Repo Root: $rootFolder"

      $outputFolderAbsoluteFilePath = Join-Path $rootFolder -ChildPath $OutputFolder

      Write-Verbose "Absolute Output Path: $outputFolderAbsoluteFilePath"

      if ($DiffFiles.Count -ne 0) {
            if ($CleanOutputFolder.IsPresent) {
                  Write-Verbose "Cleaning Output Directory (If Exists)..."

                  Remove-Item -Recurse -Force $outputFolderAbsoluteFilePath -ErrorAction SilentlyContinue | Out-Null
                
                  Write-Verbose "Cleaned Output Directory."
            }
            Write-Verbose "Force Creating Output Directory..."

            mkdir -Path $outputFolderAbsoluteFilePath -Force | Out-Null

            Write-Verbose "Created Output Directory."

            $manifestDictionary = @()
            $metadataDictionary = @{}
            $counter = 1

            Write-Verbose "Looping Through ($($DiffFiles.Count)) Delta Files"

            $DiffFiles | ForEach-Object {
                  Write-Verbose "Loop $counter/$($DiffFiles.Count)"
                  $targetFile = @()
                  $fileProperties = @{}

                  Write-Verbose "Attempting To Resolve Source File Name..."

                  $sourceAbsoluteFilePath = (Resolve-RKTheoreticalPath -FileName (Join-Path $rootFolder -ChildPath $_.Name))
                  $sourceRelativeFilePath = [System.IO.Path]::GetRelativePath($rootFolder, $sourceAbsoluteFilePath)
                
                  Write-Verbose "Source File Name: $sourceAbsoluteFilePath"
                  Write-Verbose "Source File Name: $sourceRelativeFilePath"

                  if (!($ManifestOnly) -and (Test-Path $sourceAbsoluteFilePath)) {
                        Write-Verbose "Generating Output Directory"

                        $targetAbsoluteFolderPath = (Join-Path $outputFolderAbsoluteFilePath -ChildPath (Split-Path $_.Name -Parent)) 
                        New-Item -Type dir $targetAbsoluteFolderPath -Force

                        Write-Verbose "Generating Output File..."

                        $targetFile = Copy-Item $sourceAbsoluteFilePath -Destination $targetAbsoluteFolderPath -Force -PassThru -ErrorAction SilentlyContinue

                        if ($targetFile -ne "") {
                              Write-Verbose "Output File Name: $($targetFile.Name)"
                              Write-Verbose "Generating File Property..."

                              $targetAbsoluteFilePath = (Resolve-RKTheoreticalPath -FileName $targetFile.FullName)
                              $targetRelativeFilePath = [System.IO.Path]::GetRelativePath($rootFolder, $targetAbsoluteFilePath)

                              $fileProperties += @{ Status = $_.Status; SourceAbsoluteFilePath = $sourceAbsoluteFilePath; SourceRelativeFilePath = $sourceRelativeFilePath; TargetAbsoluteFilePath = $targetAbsoluteFilePath; TargetRelativeFilePath = $targetRelativeFilePath }
                            
                              Write-Verbose "Generated File Property:`n$($fileProperties | ConvertTo-Json)"
                        }
                        else {
                              Write-Error "Failed To Generate Output File."
                        }
                  }
                  else {
                        Write-Verbose "Generating File Property..."

                        $fileProperties += @{ Status = $_.Status; SourceAbsoluteFilePath = $sourceAbsoluteFilePath; SourceRelativeFilePath = $sourceRelativeFilePath }

                        Write-Verbose "Generated File Property:`n$($fileProperties | ConvertTo-Json)"
                  }

                  Write-Verbose "Updating Manifest HashTable..."

                  $manifestDictionary += $fileProperties
                  Write-Verbose "Manifest Updated."

                  $counter++
            }

            $metadataDictionary = Add-ValueToHashTable -HashTable $metadataDictionary -Key "created_timestamp" -Value (Get-Date -Format o -AsUTC) -AsJsonObject

            $jsonDoc = [pscustomobject]@{
                  Files    = $manifestDictionary
                  Metadata = $metadataDictionary
            }

            $manifestFileName = "_manifest$(New-RKTimestampGuid).json"

            $manifestAbsoluteFilePath = (Resolve-RKTheoreticalPath -FileName $outputFolderAbsoluteFilePath/$manifestFileName)

            $jsonDoc | ConvertTo-Json -Depth 10 | Out-File $manifestAbsoluteFilePath -Force

            Write-Verbose "Manifest written to $manifestAbsoluteFilePath."

            return $manifestAbsoluteFilePath
      }
}

# Following function based off code here: https://stackoverflow.com/a/57503237.
#
# Code has been amended to swap back to a script-block and verify errors based off LASTEXITCODE
# as PS automatic variable does not pickup success/fail when the code is executed via ScriptBlock.

function Retry-RKAzCliScript() {
      <#
  .SYNOPSIS
        Execute Az Cli Script up to a specified number of exponentially backed off retries.
  .DESCRIPTION
        This script allows us to execute an az cli script with embeded retry and error handling when passed as a script block.
 
        This can be useful in scenarios where there is a propogation delay between two commands e.g. adding a firewall rule to a storage account, and then writing to the account.
  .PARAMETER ScriptBlockToCall
        The az cli script expressed within a Powershell ScriptBlock (curly braces) e.g. { az account show }
  .PARAMETER MaxAttempts
        The number of times the command is retried if invocation leads to an error.
 
        Time between attempts is calculated as follows: (2^n)-1.
  .EXAMPLE
        Retry-RKAzCliScript -ScriptBlock { az account show }
   
  .EXAMPLE
        Retry-RKAzCliScript -ScriptBlock { az storage fs directory create @params } -MaxAttempts 5
  #>

      param(
          [Parameter(Mandatory = $true)][scriptblock]$ScriptBlockToCall,
          [Parameter(Mandatory = $false)][int]$MaxAttempts = 3
      )
  
      $attempts = 1    
      $ErrorActionPreferenceToRestore = $ErrorActionPreference
      $ErrorActionPreference = "Stop"
  
      do {
          try {
              (& $scriptBlockToCall);
              if ($LASTEXITCODE -ne 0) {
                  throw;
              }
              else {
                  break;
              }
          }
          catch [Exception] {
              Write-Host $_.Exception.Message
          }
  
          $attempts++
          if ($attempts -le $maxAttempts) {
              $retryDelaySeconds = [math]::Pow(2, $attempts)
              $retryDelaySeconds = $retryDelaySeconds - 1
              Write-Host("Action failed. Waiting " + $retryDelaySeconds + " seconds before attempt " + $attempts + " of " + $maxAttempts + ".")
              Start-Sleep $retryDelaySeconds 
          }
          else {
              $ErrorActionPreference = $ErrorActionPreferenceToRestore
              Write-Error $_.Exception.Message
          }
      } while ($attempts -le $maxAttempts)
      $ErrorActionPreference = $ErrorActionPreferenceToRestore
  }