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 } |