Scripts/Inject-ArmContent.ps1

<#
    Possible injection instructions in ARM templates or recursively referenced files:
     
    ${ fileToInject.xml }
    ${ FileToInject=file.xml }
    ${ FileToInject=c:\file.xml }
    ${ FileToInject = ".\Parent Directory\file.xml" }
    ${ FileToInject = "c:\Parent Directory\file.xml" }
    ${ FileToInject = ".\Parent Directory\file.xml", EscapeJson, ReplaceSpecialChars }
    ${ FileToInject = '.\Parent Directory\file.json', InjectAsJsonObject }
    ${ FileToInject = '.\Parent Directory\file.json', InjectAsBase64 }
#>


param(
    [string] $Path = $PSScriptRoot
)

function Get-FullyQualifiedChildFilePath {
    param(
        [parameter(mandatory = $true)] [string] $ParentFilePath,
        [parameter(mandatory = $true)] [string] $ChildFilePath
    )

    $parentDirectoryPath = Split-Path $ParentFilePath -Parent
    # Note: in case of a fully qualified (i.e. absolute) child path the Combine-function discards the parent directory path;
    # otherwise the relative child path is combined with the parent directory
    $combinedPath = [System.IO.Path]::Combine($parentDirectoryPath, $ChildFilePath)
    $fullPath = [System.IO.Path]::GetFullPath($combinedPath)
    return $fullPath
}

function InjectFile {
    param(
        [string] $filePath
    )

    Write-Verbose "Checking ARM template file '$filePath' for injection tokens..." 

    $replaceContentDelegate = {
        param($match)

        $completeInjectionInstruction = $match.Groups[1].Value;
        $instructionParts = @($completeInjectionInstruction -split "," | foreach { $_.Trim() } )
                
        $filePart = $instructionParts[0];
        # Regex uses non-capturing group for 'FileToInject' part,
        # afterwards character classes and backreferencing to select the optional single or double quotes
        $fileToInjectPathRegex = [regex] "^(?:FileToInject\s*=\s*)?([`"`']?)(?<File>.*?)\1?$";
        $fileMatch = $fileToInjectPathRegex.Match($filePart)
        if ($fileMatch.Success -ne $True) {
            throw "The file part '$filePart' of the injection instruction could not be parsed correctly"
        }

        $fullPathOfFileToInject = Get-FullyQualifiedChildFilePath -ParentFilePath $filePath -ChildFilePath $fileMatch.Groups["File"]
        if (-not(Test-Path -Path $fullPathOfFileToInject -PathType Leaf)) {
            throw "No file can be found at '$fullPathOfFileToInject'"
        }

        # Inject content recursively first
        InjectFile($fullPathOfFileToInject)

        Write-Verbose "`t Injecting content of '$fullPathOfFileToInject' into '$filePath'" 

        $newString = Get-Content -Path $fullPathOfFileToInject -Raw

        # XML declaration can only appear on the first line of an XML document, so remove when injecting
        $newString = $newString -replace '(<\?xml).+(\?>)(\r)?(\n)?', ""

        # By default: retain double quotes around content-to-inject, if present
        $surroundContentWithDoubleQuotes = $match.Value.StartsWith('"') -and $match.Value.EndsWith('"')

        if ($instructionParts.Length -gt 1) {
            $optionParts = $instructionParts | select -Skip 1

            if ($optionParts.Contains("ReplaceSpecialChars")) {
                Write-Verbose "`t Replacing special characters"

                # Replace newline characters with literal equivalents
                if ([Environment]::OSVersion.VersionString -like "*Windows*") {
                    $newString = $newString -replace "`r`n", "\r\n"
                } else {
                    $newString = $newString -replace "`n", "\n"
                }

                # Replace tabs with spaces
                $newString = $newString -replace "`t", " "

                # Replace " with \"
                $newString = $newString -replace """", "\"""
            }

            if ($optionParts.Contains("EscapeJson")) {
                Write-Verbose "`t JSON-escaping file content"

                # Use regex negative lookbehind to replace double quotes not preceded by a backslash with escaped quotes
                $newString = $newString -replace '(?<!\\)"', '\"'
            }

            if ($optionParts.Contains("InjectAsJsonObject")) {
                try {
                    # Test if content is valid JSON
                    Write-Verbose "Test if valid JSON: $newString"
                    ConvertFrom-Json $newString

                    $surroundContentWithDoubleQuotes = $False
                } catch {
                    Write-Warning "Content to inject into ARM template file '$filePath' cannot be parsed as a JSON object!"
                }
            }

            if ($optionParts.Contains("InjectAsBase64")) {
                Write-Verbose "Read binary file as base64 string"

                $bytes = [System.Text.Encoding]::UTF8.GetBytes($newString)
                $newString = [Convert]::ToBase64String($bytes)
            }
        }

        if ($surroundContentWithDoubleQuotes) {
            Write-Verbose "`t Surrounding content in double quotes"

            $newString = '"' + $newString + '"'
        }

        return $newString;
    }

    $rawContents = Get-Content $filePath -Raw
    $injectionInstructionRegex = [regex] '"?\${(.+)}\$"?';
    $injectionInstructionRegex.Replace($rawContents, $replaceContentDelegate) | Set-Content $filePath -NoNewline -Encoding UTF8
    
    Write-Host "Done checking ARM template file '$filePath' for injection tokens" -ForegroundColor Green 
}


$psScriptFileName = $MyInvocation.MyCommand.Name

$PathIsFound = Test-Path -Path $Path
if ($false -eq $PathIsFound) {
    throw "Passed along path '$Path' doesn't point to valid file path"
}

Write-Verbose "Starting '$psScriptFileName' script on path '$Path'..."

$armTemplates = Get-ChildItem -Path $Path -Recurse -Include *.json
$armTemplates | ForEach-Object { InjectFile($_.FullName) }

Write-Host "Finished script '$psScriptFileName' on path '$Path'" -ForegroundColor Green