Skip to content

Commit 16e4c12

Browse files
authored
Support deploying Azure resources described with the Bicep language (#14104)
* Added support for Azure resources deployment in Bicep language * fix relative path issue * remove bicep support on templateUri * refine codes Co-authored-by: Beisi Zhou <[email protected]>
1 parent fe0e48d commit 16e4c12

24 files changed

+5299
-32
lines changed

src/Resources/ResourceManager/Implementation/CmdletBase/ResourceWithParameterCmdletBase.cs

+23-13
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,23 @@
1212
// limitations under the License.
1313
// ----------------------------------------------------------------------------------
1414

15-
using System;
16-
using System.Collections;
17-
using System.Collections.Generic;
18-
using System.Linq;
19-
using System.Management.Automation;
20-
using System.Net;
2115
using Microsoft.Azure.Commands.Common.Authentication;
2216
using Microsoft.Azure.Commands.Common.Authentication.Abstractions;
2317
using Microsoft.Azure.Commands.ResourceManager.Cmdlets.Components;
2418
using Microsoft.Azure.Commands.ResourceManager.Cmdlets.Utilities;
2519
using Microsoft.Azure.Management.ResourceManager;
2620
using Microsoft.Azure.Management.ResourceManager.Models;
2721
using Microsoft.WindowsAzure.Commands.Utilities.Common;
22+
2823
using Newtonsoft.Json.Linq;
2924

25+
using System;
26+
using System.Collections;
27+
using System.Collections.Generic;
28+
using System.Linq;
29+
using System.Management.Automation;
30+
using System.Net;
31+
3032
namespace Microsoft.Azure.Commands.ResourceManager.Cmdlets.Implementation
3133
{
3234
public abstract class ResourceWithParameterCmdletBase : ResourceManagerCmdletBase
@@ -115,24 +117,24 @@ protected ResourceWithParameterCmdletBase()
115117
public Hashtable TemplateObject { get; set; }
116118

117119
[Parameter(ParameterSetName = TemplateFileParameterObjectParameterSetName,
118-
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "Local path to the template file.")]
120+
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "Local path to the template file. Supported template file type: json and bicep.")]
119121
[Parameter(ParameterSetName = TemplateFileParameterFileParameterSetName,
120-
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "Local path to the template file.")]
122+
Mandatory = true, ValueFromPipelineByPropertyName = true)]
121123
[Parameter(ParameterSetName = TemplateFileParameterUriParameterSetName,
122-
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "Local path to the template file.")]
124+
Mandatory = true, ValueFromPipelineByPropertyName = true)]
123125
[Parameter(ParameterSetName = ParameterlessTemplateFileParameterSetName,
124-
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "Local path to the template file.")]
126+
Mandatory = true, ValueFromPipelineByPropertyName = true)]
125127
[ValidateNotNullOrEmpty]
126128
public string TemplateFile { get; set; }
127129

128130
[Parameter(ParameterSetName = TemplateUriParameterObjectParameterSetName,
129131
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "Uri to the template file.")]
130132
[Parameter(ParameterSetName = TemplateUriParameterFileParameterSetName,
131-
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "Uri to the template file.")]
133+
Mandatory = true, ValueFromPipelineByPropertyName = true)]
132134
[Parameter(ParameterSetName = TemplateUriParameterUriParameterSetName,
133-
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "Uri to the template file.")]
135+
Mandatory = true, ValueFromPipelineByPropertyName = true)]
134136
[Parameter(ParameterSetName = ParameterlessTemplateUriParameterSetName,
135-
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "Uri to the template file.")]
137+
Mandatory = true, ValueFromPipelineByPropertyName = true)]
136138
[ValidateNotNullOrEmpty]
137139
public string TemplateUri { get; set; }
138140

@@ -177,6 +179,9 @@ public ITemplateSpecsClient TemplateSpecsClient
177179

178180
public virtual object GetDynamicParameters()
179181
{
182+
if (BicepUtility.IsBicepFile(TemplateFile))
183+
BuildAndUseBicepTemplate();
184+
180185
if (!this.IsParameterBound(c => c.SkipTemplateParameterPrompt))
181186
{
182187
// Resolve the static parameter names for this cmdlet:
@@ -428,5 +433,10 @@ protected string[] GetStaticParameterNames()
428433
CmdletInfo cmdletInfo = new CmdletInfo(commandName, this.GetType());
429434
return cmdletInfo.Parameters.Keys.ToArray();
430435
}
436+
437+
protected void BuildAndUseBicepTemplate()
438+
{
439+
TemplateFile = BicepUtility.BuildFile(this.ExecuteScript<Object>, this.ResolvePath(TemplateFile));
440+
}
431441
}
432442
}

src/Resources/ResourceManager/Properties/Resources.Designer.cs

+18
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Resources/ResourceManager/Properties/Resources.resx

+6
Original file line numberDiff line numberDiff line change
@@ -494,4 +494,10 @@ You can help us improve the accuracy of the result by opening an issue here: htt
494494
<data name="InvalidChangeType" xml:space="preserve">
495495
<value>Unrecognized resource change {0}: {1}. Specify one ore more values in the following list and try again: {2}.</value>
496496
</data>
497+
<data name="BicepNotFound" xml:space="preserve">
498+
<value>Cannot find Bicep. Please add Bicep to your PATH or visit https://github.com/Azure/bicep/releases to install Bicep.</value>
499+
</data>
500+
<data name="InvalidBicepFilePathOrUri" xml:space="preserve">
501+
<value>Invalid Bicep file path or URI.</value>
502+
</data>
497503
</root>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// ----------------------------------------------------------------------------------
2+
//
3+
// Copyright Microsoft Corporation
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
// ----------------------------------------------------------------------------------
14+
15+
using Microsoft.Azure.Commands.Common.Authentication.Abstractions;
16+
using Microsoft.Azure.Commands.Common.Exceptions;
17+
using Microsoft.WindowsAzure.Commands.Utilities.Common;
18+
19+
using System;
20+
using System.Collections.Generic;
21+
using System.IO;
22+
23+
namespace Microsoft.Azure.Commands.ResourceManager.Cmdlets.Utilities
24+
{
25+
internal static class BicepUtility
26+
{
27+
public static bool IsBicepExecutable { get; private set; } = false;
28+
29+
public static bool IsBicepFile(string templateFilePath)
30+
{
31+
return ".bicep".Equals(Path.GetExtension(templateFilePath), System.StringComparison.OrdinalIgnoreCase);
32+
}
33+
34+
public delegate List<T> ScriptExecutor<T>(string script);
35+
36+
public static bool CheckBicepExecutable<T>(ScriptExecutor<T> executeScript)
37+
{
38+
try
39+
{
40+
executeScript("get-command bicep");
41+
}
42+
catch
43+
{
44+
IsBicepExecutable = false;
45+
return IsBicepExecutable;
46+
}
47+
IsBicepExecutable = true;
48+
return IsBicepExecutable;
49+
}
50+
51+
public static string BuildFile<T>(ScriptExecutor<T> executeScript, string bicepTemplateFilePath)
52+
{
53+
if (!IsBicepExecutable && !CheckBicepExecutable(executeScript))
54+
{
55+
throw new AzPSApplicationException(Properties.Resources.BicepNotFound);
56+
}
57+
58+
string tempPath = Path.Combine(Path.GetTempPath(), Path.GetFileName(bicepTemplateFilePath));
59+
60+
try{
61+
if (Uri.IsWellFormedUriString(bicepTemplateFilePath, UriKind.Absolute))
62+
{
63+
FileUtilities.DataStore.WriteFile(tempPath, GeneralUtilities.DownloadFile(bicepTemplateFilePath));
64+
}
65+
else if (FileUtilities.DataStore.FileExists(bicepTemplateFilePath))
66+
{
67+
File.Copy(bicepTemplateFilePath, tempPath, true);
68+
}
69+
else
70+
{
71+
throw new AzPSArgumentException(Properties.Resources.InvalidBicepFilePathOrUri, "TemplateFile");
72+
}
73+
executeScript($"bicep build '{tempPath}'");
74+
return tempPath.Replace(".bicep", ".json");
75+
}
76+
finally
77+
{
78+
File.Delete(tempPath);
79+
}
80+
81+
}
82+
}
83+
}

src/Resources/Resources.Test/Resources.Test.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
<ItemGroup>
2828
<None Update="Resources\*.json" CopyToOutputDirectory="PreserveNewest" />
2929
<None Update="*.json" CopyToOutputDirectory="PreserveNewest" />
30+
<None Update="*.bicep" CopyToOutputDirectory="PreserveNewest" />
3031
<None Update="ScenarioTests\*.pfx" CopyToOutputDirectory="PreserveNewest" />
3132
</ItemGroup>
3233

src/Resources/Resources.Test/ScenarioTests/DeploymentTests.cs

+14
Original file line numberDiff line numberDiff line change
@@ -187,5 +187,19 @@ public void TestNewDeploymentWithQueryString()
187187
{
188188
TestRunner.RunTestScript("Test-NewDeploymentWithQueryString");
189189
}
190+
191+
[Fact]
192+
[Trait(Category.AcceptanceType, Category.LiveOnly)]
193+
public void TestNewDeploymentFromBicepFile()
194+
{
195+
TestRunner.RunTestScript("Test-NewDeploymentFromBicepFile");
196+
}
197+
198+
[Fact]
199+
[Trait(Category.AcceptanceType, Category.LiveOnly)]
200+
public void TestTestDeploymentFromBicepFile()
201+
{
202+
TestRunner.RunTestScript("Test-TestDeploymentFromBicepFile");
203+
}
190204
}
191205
}

src/Resources/Resources.Test/ScenarioTests/DeploymentTests.ps1

+66-1
Original file line numberDiff line numberDiff line change
@@ -811,12 +811,77 @@ function Test-NewDeploymentWithQueryString
811811

812812
# Assert
813813
Assert-AreEqual Succeeded $deployment.ProvisioningState
814-
815814
}
816815

817816
finally
818817
{
819818
# Cleanup
820819
Clean-ResourceGroup $rgname
821820
}
821+
}
822+
823+
<#
824+
.SYNOPSIS
825+
Tests deployment via Bicep file.
826+
#>
827+
function Test-NewDeploymentFromBicepFile
828+
{
829+
# Setup
830+
$rgname = Get-ResourceGroupName
831+
$rname = Get-ResourceName
832+
$rglocation = "West US 2"
833+
$expectedTags = @{"key1"="value1"; "key2"="value2";}
834+
835+
try
836+
{
837+
# Test
838+
New-AzResourceGroup -Name $rgname -Location $rglocation
839+
840+
$deployment = New-AzResourceGroupDeployment -Name $rname -ResourceGroupName $rgname -TemplateFile sampleDeploymentBicepFile.bicep -Tag $expectedTags
841+
842+
# Assert
843+
Assert-AreEqual Succeeded $deployment.ProvisioningState
844+
Assert-True { AreHashtableEqual $expectedTags $deployment.Tags }
845+
846+
$subId = (Get-AzContext).Subscription.SubscriptionId
847+
$deploymentId = "/subscriptions/$subId/resourcegroups/$rgname/providers/Microsoft.Resources/deployments/$rname"
848+
$getById = Get-AzResourceGroupDeployment -Id $deploymentId
849+
Assert-AreEqual $getById.DeploymentName $deployment.DeploymentName
850+
851+
[hashtable]$actualTags = $getById.Tags
852+
Assert-True { AreHashtableEqual $expectedTags $getById.Tags }
853+
}
854+
finally
855+
{
856+
# Cleanup
857+
Clean-ResourceGroup $rgname
858+
}
859+
}
860+
861+
<#
862+
.SYNOPSIS
863+
Tests deployment template via bicep file.
864+
#>
865+
function Test-TestDeploymentFromBicepFile
866+
{
867+
# Setup
868+
$rgname = Get-ResourceGroupName
869+
$rname = Get-ResourceName
870+
$location = "West US 2"
871+
872+
# Test
873+
try
874+
{
875+
New-AzResourceGroup -Name $rgname -Location $location
876+
877+
$list = Test-AzResourceGroupDeployment -ResourceGroupName $rgname -TemplateFile sampleDeploymentBicepFile.bicep
878+
879+
# Assert
880+
Assert-AreEqual 0 @($list).Count
881+
}
882+
finally
883+
{
884+
# Cleanup
885+
Clean-ResourceGroup $rgname
886+
}
822887
}

0 commit comments

Comments
 (0)