Skip to content

[DO NOT MERGE] Support deploying Azure resources described with the Bicep language #14104

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Feb 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,23 @@
// limitations under the License.
// ----------------------------------------------------------------------------------

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation;
using System.Net;
using Microsoft.Azure.Commands.Common.Authentication;
using Microsoft.Azure.Commands.Common.Authentication.Abstractions;
using Microsoft.Azure.Commands.ResourceManager.Cmdlets.Components;
using Microsoft.Azure.Commands.ResourceManager.Cmdlets.Utilities;
using Microsoft.Azure.Management.ResourceManager;
using Microsoft.Azure.Management.ResourceManager.Models;
using Microsoft.WindowsAzure.Commands.Utilities.Common;

using Newtonsoft.Json.Linq;

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation;
using System.Net;

namespace Microsoft.Azure.Commands.ResourceManager.Cmdlets.Implementation
{
public abstract class ResourceWithParameterCmdletBase : ResourceManagerCmdletBase
Expand Down Expand Up @@ -115,24 +117,24 @@ protected ResourceWithParameterCmdletBase()
public Hashtable TemplateObject { get; set; }

[Parameter(ParameterSetName = TemplateFileParameterObjectParameterSetName,
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "Local path to the template file.")]
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "Local path to the template file. Supported template file type: json and bicep.")]
[Parameter(ParameterSetName = TemplateFileParameterFileParameterSetName,
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "Local path to the template file.")]
Mandatory = true, ValueFromPipelineByPropertyName = true)]
[Parameter(ParameterSetName = TemplateFileParameterUriParameterSetName,
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "Local path to the template file.")]
Mandatory = true, ValueFromPipelineByPropertyName = true)]
[Parameter(ParameterSetName = ParameterlessTemplateFileParameterSetName,
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "Local path to the template file.")]
Mandatory = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
public string TemplateFile { get; set; }

[Parameter(ParameterSetName = TemplateUriParameterObjectParameterSetName,
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "Uri to the template file.")]
[Parameter(ParameterSetName = TemplateUriParameterFileParameterSetName,
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "Uri to the template file.")]
Mandatory = true, ValueFromPipelineByPropertyName = true)]
[Parameter(ParameterSetName = TemplateUriParameterUriParameterSetName,
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "Uri to the template file.")]
Mandatory = true, ValueFromPipelineByPropertyName = true)]
[Parameter(ParameterSetName = ParameterlessTemplateUriParameterSetName,
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "Uri to the template file.")]
Mandatory = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
public string TemplateUri { get; set; }

Expand Down Expand Up @@ -177,6 +179,9 @@ public ITemplateSpecsClient TemplateSpecsClient

public virtual object GetDynamicParameters()
{
if (BicepUtility.IsBicepFile(TemplateFile))
BuildAndUseBicepTemplate();

if (!this.IsParameterBound(c => c.SkipTemplateParameterPrompt))
{
// Resolve the static parameter names for this cmdlet:
Expand Down Expand Up @@ -428,5 +433,10 @@ protected string[] GetStaticParameterNames()
CmdletInfo cmdletInfo = new CmdletInfo(commandName, this.GetType());
return cmdletInfo.Parameters.Keys.ToArray();
}

protected void BuildAndUseBicepTemplate()
{
TemplateFile = BicepUtility.BuildFile(this.ExecuteScript<Object>, this.ResolvePath(TemplateFile));
}
}
}
18 changes: 18 additions & 0 deletions src/Resources/ResourceManager/Properties/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/Resources/ResourceManager/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -494,4 +494,10 @@ You can help us improve the accuracy of the result by opening an issue here: htt
<data name="InvalidChangeType" xml:space="preserve">
<value>Unrecognized resource change {0}: {1}. Specify one ore more values in the following list and try again: {2}.</value>
</data>
<data name="BicepNotFound" xml:space="preserve">
<value>Cannot find Bicep. Please add Bicep to your PATH or visit https://github.com/Azure/bicep/releases to install Bicep.</value>
</data>
<data name="InvalidBicepFilePathOrUri" xml:space="preserve">
<value>Invalid Bicep file path or URI.</value>
</data>
</root>
83 changes: 83 additions & 0 deletions src/Resources/ResourceManager/Utilities/BicepUtility.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// ----------------------------------------------------------------------------------
//
// Copyright Microsoft Corporation
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ----------------------------------------------------------------------------------

using Microsoft.Azure.Commands.Common.Authentication.Abstractions;
using Microsoft.Azure.Commands.Common.Exceptions;
using Microsoft.WindowsAzure.Commands.Utilities.Common;

using System;
using System.Collections.Generic;
using System.IO;

namespace Microsoft.Azure.Commands.ResourceManager.Cmdlets.Utilities
{
internal static class BicepUtility
{
public static bool IsBicepExecutable { get; private set; } = false;

public static bool IsBicepFile(string templateFilePath)
{
return ".bicep".Equals(Path.GetExtension(templateFilePath), System.StringComparison.OrdinalIgnoreCase);
}

public delegate List<T> ScriptExecutor<T>(string script);

public static bool CheckBicepExecutable<T>(ScriptExecutor<T> executeScript)
{
try
{
executeScript("get-command bicep");
}
catch
{
IsBicepExecutable = false;
return IsBicepExecutable;
}
IsBicepExecutable = true;
return IsBicepExecutable;
}

public static string BuildFile<T>(ScriptExecutor<T> executeScript, string bicepTemplateFilePath)
{
if (!IsBicepExecutable && !CheckBicepExecutable(executeScript))
{
throw new AzPSApplicationException(Properties.Resources.BicepNotFound);
}

string tempPath = Path.Combine(Path.GetTempPath(), Path.GetFileName(bicepTemplateFilePath));

try{
if (Uri.IsWellFormedUriString(bicepTemplateFilePath, UriKind.Absolute))
{
FileUtilities.DataStore.WriteFile(tempPath, GeneralUtilities.DownloadFile(bicepTemplateFilePath));
}
else if (FileUtilities.DataStore.FileExists(bicepTemplateFilePath))
{
File.Copy(bicepTemplateFilePath, tempPath, true);
}
else
{
throw new AzPSArgumentException(Properties.Resources.InvalidBicepFilePathOrUri, "TemplateFile");
}
executeScript($"bicep build '{tempPath}'");
return tempPath.Replace(".bicep", ".json");
}
finally
{
File.Delete(tempPath);
}

}
}
}
1 change: 1 addition & 0 deletions src/Resources/Resources.Test/Resources.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<ItemGroup>
<None Update="Resources\*.json" CopyToOutputDirectory="PreserveNewest" />
<None Update="*.json" CopyToOutputDirectory="PreserveNewest" />
<None Update="*.bicep" CopyToOutputDirectory="PreserveNewest" />
<None Update="ScenarioTests\*.pfx" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

Expand Down
14 changes: 14 additions & 0 deletions src/Resources/Resources.Test/ScenarioTests/DeploymentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,5 +187,19 @@ public void TestNewDeploymentWithQueryString()
{
TestRunner.RunTestScript("Test-NewDeploymentWithQueryString");
}

[Fact]
[Trait(Category.AcceptanceType, Category.LiveOnly)]
public void TestNewDeploymentFromBicepFile()
{
TestRunner.RunTestScript("Test-NewDeploymentFromBicepFile");
}

[Fact]
[Trait(Category.AcceptanceType, Category.LiveOnly)]
public void TestTestDeploymentFromBicepFile()
{
TestRunner.RunTestScript("Test-TestDeploymentFromBicepFile");
}
}
}
67 changes: 66 additions & 1 deletion src/Resources/Resources.Test/ScenarioTests/DeploymentTests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -811,12 +811,77 @@ function Test-NewDeploymentWithQueryString

# Assert
Assert-AreEqual Succeeded $deployment.ProvisioningState

}

finally
{
# Cleanup
Clean-ResourceGroup $rgname
}
}

<#
.SYNOPSIS
Tests deployment via Bicep file.
#>
function Test-NewDeploymentFromBicepFile
{
# Setup
$rgname = Get-ResourceGroupName
$rname = Get-ResourceName
$rglocation = "West US 2"
$expectedTags = @{"key1"="value1"; "key2"="value2";}

try
{
# Test
New-AzResourceGroup -Name $rgname -Location $rglocation

$deployment = New-AzResourceGroupDeployment -Name $rname -ResourceGroupName $rgname -TemplateFile sampleDeploymentBicepFile.bicep -Tag $expectedTags

# Assert
Assert-AreEqual Succeeded $deployment.ProvisioningState
Assert-True { AreHashtableEqual $expectedTags $deployment.Tags }

$subId = (Get-AzContext).Subscription.SubscriptionId
$deploymentId = "/subscriptions/$subId/resourcegroups/$rgname/providers/Microsoft.Resources/deployments/$rname"
$getById = Get-AzResourceGroupDeployment -Id $deploymentId
Assert-AreEqual $getById.DeploymentName $deployment.DeploymentName

[hashtable]$actualTags = $getById.Tags
Assert-True { AreHashtableEqual $expectedTags $getById.Tags }
}
finally
{
# Cleanup
Clean-ResourceGroup $rgname
}
}

<#
.SYNOPSIS
Tests deployment template via bicep file.
#>
function Test-TestDeploymentFromBicepFile
{
# Setup
$rgname = Get-ResourceGroupName
$rname = Get-ResourceName
$location = "West US 2"

# Test
try
{
New-AzResourceGroup -Name $rgname -Location $location

$list = Test-AzResourceGroupDeployment -ResourceGroupName $rgname -TemplateFile sampleDeploymentBicepFile.bicep

# Assert
Assert-AreEqual 0 @($list).Count
}
finally
{
# Cleanup
Clean-ResourceGroup $rgname
}
}
Loading