Skip to content

Commit bbe1989

Browse files
feat: Bicep support for InjectFile (#357)
* added arcus.scripting.bicep project with the script `Inject-BicepContent.ps1` * fix linux tests * fix linux tests * Update docs/preview/03-Features/powershell/bicep.md Co-authored-by: Stijn Moreels <9039753+stijnmoreels@users.noreply.github.com> * updated docs and changed logging levels Co-authored-by: Pim Simons <pim.simons@codit.eu> Co-authored-by: Stijn Moreels <9039753+stijnmoreels@users.noreply.github.com>
1 parent 4bed620 commit bbe1989

22 files changed

Lines changed: 449 additions & 6 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
label: 'Templates'

docs/preview/03-Features/powershell/arm.md renamed to docs/preview/03-Features/powershell/templates/arm.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
---
1+
---
22
title: "ARM Templates"
33
layout: default
44
---
@@ -72,5 +72,5 @@ ${ FileToInject = ".\Parent Directory\file.xml", EscapeJson, ReplaceSpecialChars
7272
${ FileToInject = '.\Parent Directory\file.json', InjectAsJsonObject }
7373
```
7474

75-
### Recommendations
75+
### 🥇 Recommendations
7676
Always inject the content in your ARM template as soon as possible, preferably during release build that creates the artifact
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
---
2+
title: "Bicep Templates"
3+
layout: default
4+
---
5+
6+
# Bicep
7+
8+
## Installation
9+
10+
To have access to the following features, you have to import the module:
11+
12+
```powershell
13+
PS> Install-Module -Name Arcus.Scripting.Bicep
14+
```
15+
16+
## Injecting content into a Bicep template
17+
18+
In certain scenarios, you have to embed content into a Bicep template to deploy it.
19+
20+
However, the downside of it is that it's buried inside the template and tooling around it might be less ideal - An example of this is OpenAPI specifications you'd want to deploy.
21+
22+
By using this script, you can inject external files inside your Bicep template.
23+
24+
| Parameter | Mandatory | Description |
25+
| --------- | --------- | ----------------------------------------------------------------------------------------------- |
26+
| `Path` | no | The file path to the Bicep template to inject the external files into (default: `$PSScriptRoot`) |
27+
28+
### Usage
29+
Annotating content to inject:
30+
31+
``` bicep
32+
resource api 'Microsoft.ApiManagement/service/apis@2019-01-01' = {
33+
name: '${ApiManagement.Name}/${ApiManagement.Api.Name}'
34+
properties: {
35+
subscriptionRequired: true
36+
path: 'demo'
37+
value: '${ FileToInject='.\\..\\openapi\\api-sample.json', InjectAsBicepObject}$'
38+
format: 'swagger-json'
39+
}
40+
dependsOn: []
41+
}
42+
43+
```
44+
45+
Injecting the content:
46+
47+
```powershell
48+
PS> Inject-BicepContent -Path deploy\bicep-template.json
49+
```
50+
51+
### Injection Instructions
52+
53+
It is possible to supply injection instructions in the injection annotation, this allows you to add specific functionality to the injection. These are the available injection instructions:
54+
55+
| Injection Instruction | Description |
56+
| ---------------------- | ---------------------------------------------------------------------------------------- |
57+
| `ReplaceSpecialChars` | Replace newline characters with literal equivalents, tabs with spaces and `'` with `\'` |
58+
| `InjectAsBicepObject` | Makes sure the content is injected without surrounding single quotes |
59+
60+
Usage of multiple injection instructions is supported as well, for example if you need both the `ReplaceSpecialChars` and `InjectAsBicepObject` functionality.
61+
The reference to the file to inject can either be a path relative to the 'parent' file or an absolute path.
62+
63+
Some examples are:
64+
```powershell
65+
${ FileToInject = ".\Parent Directory\file.xml" }$
66+
${ FileToInject = "c:\Parent Directory\file.xml" }$
67+
${ FileToInject = ".\Parent Directory\file.xml", ReplaceSpecialChars, InjectAsBicepObject }$
68+
${ FileToInject = '.\Parent Directory\file.json', InjectAsBicepObject }$
69+
```
70+
71+
### 🥇 Recommendations
72+
Always inject the content in your Bicep template as soon as possible, preferably during release build that creates the artifact.

src/Arcus.Scripting.ARM/Scripts/Inject-ArmContent.ps1

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ function InjectFile {
7272
$optionParts = $instructionParts | select -Skip 1
7373

7474
if ($optionParts.Contains("ReplaceSpecialChars")) {
75-
Write-Host "`t Replacing special characters"
75+
Write-Verbose "`t Replacing special characters"
7676

7777
# Replace newline characters with literal equivalents
7878
if ([Environment]::OSVersion.VersionString -like "*Windows*") {
@@ -89,7 +89,7 @@ function InjectFile {
8989
}
9090

9191
if ($optionParts.Contains("EscapeJson")) {
92-
Write-Host "`t JSON-escaping file content"
92+
Write-Verbose "`t JSON-escaping file content"
9393

9494
# Use regex negative lookbehind to replace double quotes not preceded by a backslash with escaped quotes
9595
$newString = $newString -replace '(?<!\\)"', '\"'
@@ -110,7 +110,7 @@ function InjectFile {
110110
}
111111

112112
if ($surroundContentWithDoubleQuotes) {
113-
Write-Host "`t Surrounding content in double quotes"
113+
Write-Verbose "`t Surrounding content in double quotes"
114114

115115
$newString = '"' + $newString + '"'
116116
}
7.39 KB
Binary file not shown.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<#
2+
.Synopsis
3+
Inject external files inside your Bicep template
4+
5+
.Description
6+
In certain scenarios, you have to embed content into an Bicep template to deploy it.
7+
However, the downside of it is that it's buried inside the template and tooling around it might be less ideal - An example of this is OpenAPI specifications you'd want to deploy.
8+
By using this command, you can inject external files inside your Bicep template.
9+
Recommendation: Always inject the content in your Bicep template as soon as possible, preferably during release build that creates the artifact
10+
11+
.Parameter Path
12+
The file path to the Bicep template to inject the external files into.
13+
#>
14+
function Inject-BicepContent {
15+
param (
16+
[string] $Path = $PSScriptRoot
17+
)
18+
. $PSScriptRoot\Scripts\Inject-BicepContent.ps1 -Path $Path
19+
}
20+
21+
Export-ModuleMember -Function Inject-BicepContent
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
2+
<PropertyGroup>
3+
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
4+
<SchemaVersion>2.0</SchemaVersion>
5+
<ProjectGuid>{e256737a-7319-420a-b21a-3986af8ef049}</ProjectGuid>
6+
<OutputType>Exe</OutputType>
7+
<RootNamespace>Arcus.Scripting.Bicep</RootNamespace>
8+
<AssemblyName>Arcus.Scripting.Bicep</AssemblyName>
9+
<Name>Arcus.Scripting.Bicep</Name>
10+
</PropertyGroup>
11+
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
12+
<DebugSymbols>true</DebugSymbols>
13+
<DebugType>full</DebugType>
14+
<Optimize>false</Optimize>
15+
<OutputPath>bin\Debug\</OutputPath>
16+
<DefineConstants>DEBUG;TRACE</DefineConstants>
17+
<ErrorReport>prompt</ErrorReport>
18+
<WarningLevel>4</WarningLevel>
19+
</PropertyGroup>
20+
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
21+
<DebugType>pdbonly</DebugType>
22+
<Optimize>true</Optimize>
23+
<OutputPath>bin\Release\</OutputPath>
24+
<DefineConstants>TRACE</DefineConstants>
25+
<ErrorReport>prompt</ErrorReport>
26+
<WarningLevel>4</WarningLevel>
27+
</PropertyGroup>
28+
<ItemGroup>
29+
<Folder Include="Scripts\" />
30+
</ItemGroup>
31+
<ItemGroup>
32+
<Compile Include="Arcus.Scripting.Bicep.psd1" />
33+
<Compile Include="Arcus.Scripting.Bicep.psm1" />
34+
<Compile Include="Scripts\Inject-BicepContent.ps1" />
35+
</ItemGroup>
36+
<ItemGroup>
37+
<Folder Include="Scripts\" />
38+
</ItemGroup>
39+
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
40+
<Target Name="Build" />
41+
<Import Project="$(MSBuildExtensionsPath)\PowerShell Tools for Visual Studio\PowerShellTools.targets" Condition="Exists('$(MSBuildExtensionsPath)\PowerShell Tools for Visual Studio\PowerShellTools.targets')" />
42+
</Project>
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<#
2+
Possible injection instructions in Bicep templates or recursively referenced files:
3+
4+
${ fileToInject.xml }$
5+
${ FileToInject=file.xml }$
6+
${ FileToInject=c:\file.xml }$
7+
${ FileToInject = ".\Parent Directory\file.xml" }$
8+
${ FileToInject = "c:\Parent Directory\file.xml" }$
9+
${ FileToInject = ".\Parent Directory\file.xml", ReplaceSpecialChars }$
10+
${ FileToInject = '.\Parent Directory\file.bicep', InjectAsBicepObject }$
11+
12+
#>
13+
14+
param (
15+
[string] $Path = $PSScriptRoot
16+
)
17+
18+
function Get-FullyQualifiedChildFilePath {
19+
param(
20+
[parameter(mandatory=$true)] [string] $ParentFilePath,
21+
[parameter(mandatory=$true)] [string] $ChildFilePath
22+
)
23+
24+
$parentDirectoryPath = Split-Path $ParentFilePath -Parent
25+
# Note: in case of a fully qualified (i.e. absolute) child path the Combine-function discards the parent directory path;
26+
# otherwise the relative child path is combined with the parent directory
27+
$combinedPath = [System.IO.Path]::Combine($parentDirectoryPath, $ChildFilePath)
28+
$fullPath = [System.IO.Path]::GetFullPath($combinedPath)
29+
return $fullPath
30+
}
31+
32+
function InjectFile {
33+
param(
34+
[string] $filePath
35+
)
36+
37+
Write-Verbose "Checking Bicep template file '$filePath' for injection tokens..."
38+
39+
$replaceContentDelegate = {
40+
param($match)
41+
42+
$completeInjectionInstruction = $match.Groups[1].Value;
43+
$instructionParts = @($completeInjectionInstruction -split "," | foreach { $_.Trim() } )
44+
45+
$filePart = $instructionParts[0];
46+
# Regex uses non-capturing group for 'FileToInject' part,
47+
# afterwards character classes and backreferencing to select the optional single or double quotes
48+
$fileToInjectPathRegex = [regex] "^(?:FileToInject\s*=\s*)?([`"`']?)(?<File>.*?)\1?$";
49+
$fileMatch = $fileToInjectPathRegex.Match($filePart)
50+
if ($fileMatch.Success -ne $True){
51+
throw "The file part '$filePart' of the injection instruction could not be parsed correctly"
52+
}
53+
54+
$fullPathOfFileToInject = Get-FullyQualifiedChildFilePath -ParentFilePath $filePath -ChildFilePath $fileMatch.Groups["File"]
55+
if (-not(Test-Path -Path $fullPathOfFileToInject -PathType Leaf)) {
56+
throw "No file can be found at '$fullPathOfFileToInject'"
57+
}
58+
59+
# Inject content recursively first
60+
InjectFile($fullPathOfFileToInject)
61+
62+
Write-Verbose "`t Injecting content of '$fullPathOfFileToInject' into '$filePath'"
63+
64+
$newString = Get-Content -Path $fullPathOfFileToInject -Raw
65+
66+
# XML declaration can only appear on the first line of an XML document, so remove when injecting
67+
$newString = $newString -replace '(<\?xml).+(\?>)(\r)?(\n)?', ""
68+
69+
# By default: retain single quotes around content-to-inject, if present
70+
$surroundContentWithSingleQuotes = $match.Value.StartsWith('''') -and $match.Value.EndsWith('''')
71+
72+
if ($instructionParts.Length -gt 1) {
73+
$optionParts = $instructionParts | select -Skip 1
74+
75+
if ($optionParts.Contains("ReplaceSpecialChars")) {
76+
Write-Verbose "`t Replacing special characters"
77+
78+
# Replace newline characters with literal equivalents
79+
if ([Environment]::OSVersion.VersionString -like "*Windows*") {
80+
$newString = $newString -replace "`r`n", "\r\n"
81+
} else {
82+
$newString = $newString -replace "`n", "\n"
83+
}
84+
85+
# Replace tabs with spaces
86+
$newString = $newString -replace "`t", " "
87+
88+
# Replace ' with \'
89+
$newString = $newString -replace "'", "\'"
90+
}
91+
92+
if ($optionParts.Contains("InjectAsBicepObject")) {
93+
$surroundContentWithSingleQuotes = $False
94+
}
95+
}
96+
97+
if ($surroundContentWithSingleQuotes) {
98+
Write-Verbose "`t Surrounding content in double quotes"
99+
100+
$newString = '''' + $newString + ''''
101+
}
102+
103+
return $newString;
104+
}
105+
106+
$rawContents = Get-Content $filePath -Raw
107+
$injectionInstructionRegex = [regex] '"?\${(.+)}\$"?';
108+
$injectionInstructionRegex.Replace($rawContents, $replaceContentDelegate) | Set-Content $filePath -NoNewline -Encoding UTF8
109+
110+
Write-Host "Done checking Bicep template file '$filePath' for injection tokens" -ForegroundColor Green
111+
}
112+
113+
114+
$psScriptFileName = $MyInvocation.MyCommand.Name
115+
116+
$PathIsFound = Test-Path -Path $Path
117+
if ($false -eq $PathIsFound) {
118+
throw "Passed along path '$Path' doesn't point to valid file path"
119+
}
120+
121+
Write-Verbose "Starting '$psScriptFileName' script on path '$Path'..."
122+
123+
$bicepTemplates = Get-ChildItem -Path $Path -Recurse -Include *.bicep
124+
$bicepTemplates | ForEach-Object { InjectFile($_.FullName) }
125+
126+
Write-Host "Finished script '$psScriptFileName' on path '$Path'" -ForegroundColor Green

0 commit comments

Comments
 (0)