Skip to content
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
@@ -0,0 +1,177 @@
using Calamari.AzureResourceGroup.Bicep;
using Calamari.Common.Plumbing.Variables;
using NuGet.Protocol;
using NUnit.Framework;

namespace Calamari.AzureResourceGroup.Tests.Bicep;

[TestFixture]
public class BicepToArmParameterMapperFixture
{
const string SimpleBicepParametersString = """
[{"Key":"storageAccountName","Value":"teststorageaccount"},{"Key":"location","Value":"Australia South East"},{"Key":"myStuff","Value":"[PLACEHOLDER]"}]
""";

const string SimpleArmTemplate = """
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.40.2.10011",
"templateHash": "14097892204907684939"
}
},
"parameters": {
"storageAccountName": {
"type": "string"
},
"location": {
"type": "string",
"defaultValue": "[resourceGroup().location]"
},
"myStuff": {
"type": "string"
}
},
"resources": [
{
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2021-06-01",
"name": "[parameters('storageAccountName')]",
"location": "[parameters('location')]",
"sku": {
"name": "Standard_LRS"
},
"kind": "StorageV2",
"tags": {
"tagValue": "[parameters('myStuff')]"
},
"properties": {
"accessTier": "Hot"
}
}
],
"outputs": {
"storageAccountId": {
"type": "string",
"value": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]"
}
}
}
""";

CalamariVariables variables;
[SetUp]
public void SetUp()
{
variables = new CalamariVariables();

}


[Test]
public void Map_WithEmptyBicepParameters_ReturnsEmptyString()
{
var bicepParametersString = string.Empty;

var result = BicepToArmParameterMapper.Map(bicepParametersString, SimpleArmTemplate, variables);

Assert.That(result, Is.EqualTo(string.Empty));
}

[Test]
public void Map_EmptyArmParameters_ReturnsEmptyString()
{
var result = BicepToArmParameterMapper.Map(SimpleBicepParametersString, "{\"noParametersHere\":{\"value\":\"told ya\"}}", variables);

Assert.That(result, Is.EqualTo(string.Empty));
}

[Test]
public void Map_WithMatchingParameters_ReturnsParameterString()
{
var expectedParameterString = """
{
"storageAccountName": {
"value": "teststorageaccount"
},
"location": {
"value": "Australia South East"
},
"myStuff": {
"value": "[PLACEHOLDER]"
}
}
""".ReplaceLineEndings();



var result = BicepToArmParameterMapper.Map(SimpleBicepParametersString, SimpleArmTemplate, variables).ReplaceLineEndings();


// Convert so we can ignore Platform specific line endings.
Assert.That(result.ToJson(), Is.EqualTo(expectedParameterString.ToJson()));

}

[Test]
public void Map_WithOctopusVariableValueDefined_IsResolvedInParametersString()
{
variables.Add("Octopus.TestValue", "banana");
var parameterStringInput = SimpleBicepParametersString.Replace("[PLACEHOLDER]", "#{ Octopus.TestValue }");
var expectedParameterString = """
{
"storageAccountName": {
"value": "teststorageaccount"
},
"location": {
"value": "Australia South East"
},
"myStuff": {
"value": "banana"
}
}
""".ReplaceLineEndings();

var result = BicepToArmParameterMapper.Map(parameterStringInput, SimpleArmTemplate, variables).ReplaceLineEndings();

// Convert so we can ignore Platform specific line endings.
Assert.That(result, Is.EqualTo(expectedParameterString));
}

[Test]
public void Map_WithOctopusVariableDefinedWithNonDelimitedJsonObject_IsResolvedInParametersStringWithProperDelimiting()
{
variables.Add("Octopus.TestValue",
"""
{
"SomeKey": "WithAValue",
"AnotherKey": "WithADifferentValue",
"NestedObject": {
"NestedObjectKey": "YetAnotherValue"
}
}
""");
var parameterStringInput = SimpleBicepParametersString.Replace("[PLACEHOLDER]", "#{ Octopus.TestValue }");
var expectedParameterString = """
{
"storageAccountName": {
"value": "teststorageaccount"
},
"location": {
"value": "Australia South East"
},
"myStuff": {
"value": "{\"SomeKey\":\"WithAValue\",\"AnotherKey\":\"WithADifferentValue\",\"NestedObject\":{\"NestedObjectKey\":\"YetAnotherValue\"}}"
}
}
""".ReplaceLineEndings();

var result = BicepToArmParameterMapper.Map(parameterStringInput, SimpleArmTemplate, variables).ReplaceLineEndings();

// Convert so we can ignore Platform specific line endings.
Assert.That(result, Is.EqualTo(expectedParameterString));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@
<None Update="Packages\AzureResourceGroup\Default.aspx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Packages\Bicep\parameters.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Packages\Bicep\azure_website_template.bicep">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Azure.ResourceManager;
using Azure.ResourceManager.Resources;
using Calamari.Azure;
using Calamari.AzureResourceGroup.Bicep;
using Calamari.CloudAccounts;
using Calamari.Testing;
using Calamari.Testing.Azure;
Expand All @@ -17,7 +18,7 @@
namespace Calamari.AzureResourceGroup.Tests
{
[TestFixture]
[Category(TestCategory.CompatibleOS.OnlyWindows)]
[WindowsTest] // NOTE: We should look at having the Azure CLI installed on Linux boxes so that these steps can be tested there, particularly if we're moving cloud to a Ubuntu Default Worker.
class DeployAzureBicepTemplateCommandFixture
{
string clientId;
Expand All @@ -32,6 +33,8 @@ class DeployAzureBicepTemplateCommandFixture
readonly string packagePath = TestEnvironment.GetTestPath("Packages", "Bicep");
SubscriptionResource subscriptionResource;

const string ParameterContent = """[{"Key":"storageAccountName","Value":"#{StorageAccountName}"},{"Key":"location","Value":"#{Location}"},{"Key":"sku","Value":"#{SKU}"}]""";

[OneTimeSetUp]
public async Task Setup()
{
Expand Down Expand Up @@ -128,15 +131,14 @@ await CommandTestBuilder.CreateAsync<DeployAzureBicepTemplateCommand, Program>()
public async Task DeployAzureBicepTemplate_InlineSource()
{
var templateFileContent = File.ReadAllText(Path.Combine(packagePath, "azure_website_template.bicep"));
var paramsFileContent = File.ReadAllText(Path.Combine(packagePath, "parameters.json"));

await CommandTestBuilder.CreateAsync<DeployAzureBicepTemplateCommand, Program>()
.WithArrange(context =>
{
AddDefaults(context);
context.Variables.Add(SpecialVariables.Action.Azure.ResourceGroupDeploymentMode, "Complete");
context.Variables.Add(SpecialVariables.Action.Azure.TemplateSource, "Inline");
AddTemplateFiles(context, templateFileContent, paramsFileContent);
context.WithDataFile(templateFileContent, "template.bicep");
})
.Execute();
}
Expand All @@ -151,18 +153,12 @@ void AddDefaults(CommandTestBuilderContext context)
context.Variables.Add(SpecialVariables.Action.Azure.ResourceGroupName, resourceGroupName);
context.Variables.Add(SpecialVariables.Action.Azure.ResourceGroupLocation, resourceGroupLocation);
context.Variables.Add(SpecialVariables.Action.Azure.ResourceGroupDeploymentMode, "Complete");
context.Variables.Add(SpecialVariables.Action.Azure.TemplateParameters, "parameters.json");
context.Variables.Add(SpecialVariables.Action.Azure.BicepTemplateParameters, ParameterContent);

context.Variables.Add("SKU", "Standard_LRS");
context.Variables.Add("Location", resourceGroupLocation);
//storage accounts can be 24 chars long
context.Variables.Add("StorageAccountName", AzureTestResourceHelpers.RandomName(length: 24));
}

static void AddTemplateFiles(CommandTestBuilderContext context, string template, string parameters)
{
context.WithDataFile(template, "template.bicep");
context.WithDataFile(parameters, "parameters.json");
}
}
}

This file was deleted.

128 changes: 128 additions & 0 deletions source/Calamari.AzureResourceGroup/Bicep/BicepCli.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using System;
using System.IO;
using System.Linq;
using Calamari.Common.Commands;
using Calamari.Common.Features.Processes;
using Calamari.Common.Features.Scripting;
using Calamari.Common.Plumbing;
using Calamari.Common.Plumbing.Logging;

namespace Calamari.AzureResourceGroup.Bicep;

public class BicepCli
{
const string ArmTemplateFileName = "ARMTemplate.json";
readonly ILog log;
readonly ICommandLineRunner commandLineRunner;
readonly string workingDirectory;
string azCliLocation = null!;

public BicepCli(ILog log, ICommandLineRunner commandLineRunner, string workingDirectory)
{
this.log = log;
this.commandLineRunner = commandLineRunner;
this.workingDirectory = workingDirectory;

SetAz();
}

public string BuildArmTemplate(string bicepFilePath)
{
var invocation = new CommandLineInvocation(azCliLocation,
"bicep",
"build",
"--file",
bicepFilePath,
"--outfile",
ArmTemplateFileName)
{
WorkingDirectory = workingDirectory
};

ExecuteCommandLineInvocationAndLogOutput(invocation);

return Path.Combine(workingDirectory, ArmTemplateFileName);
}

void SetAz()
{
var result = CalamariEnvironment.IsRunningOnWindows
? ExecuteRawCommandAndReturnOutput("where", "az.cmd")
: ExecuteRawCommandAndReturnOutput("which", "az");

var infoMessages = result.Output.Messages.Where(m => m.Level == Level.Verbose).Select(m => m.Text).ToArray();
var foundExecutable = infoMessages.FirstOrDefault();
if (string.IsNullOrEmpty(foundExecutable))
throw new CommandException("Could not find az. Make sure az is on the PATH.");

azCliLocation = foundExecutable.Trim();
}

CommandResultWithOutput ExecuteRawCommandAndReturnOutput(string exe, params string[] arguments)
{
var captureCommandOutput = new CaptureCommandOutput();
var invocation = new CommandLineInvocation(exe, arguments)
{
WorkingDirectory = workingDirectory,
OutputAsVerbose = false,
OutputToLog = false,
AdditionalInvocationOutputSink = captureCommandOutput
};

var result = commandLineRunner.Execute(invocation);

return new CommandResultWithOutput(result, captureCommandOutput);
}

CommandResult ExecuteCommandLineInvocationAndLogOutput(CommandLineInvocation invocation)
{
invocation.WorkingDirectory = workingDirectory;
invocation.OutputAsVerbose = false;
invocation.OutputToLog = false;

var captureCommandOutput = new CaptureCommandOutput();
invocation.AdditionalInvocationOutputSink = captureCommandOutput;

LogCommandText(invocation);

var result = commandLineRunner.Execute(invocation);

LogCapturedOutput(result, captureCommandOutput);

return result;
}

void LogCommandText(CommandLineInvocation invocation)
{
log.Verbose(invocation.ToString());
}

void LogCapturedOutput(CommandResult result, CaptureCommandOutput captureCommandOutput)
{
foreach (var message in captureCommandOutput.Messages)
{
if (result.ExitCode == 0)
{
log.Verbose(message.Text);
continue;
}

switch (message.Level)
{
case Level.Verbose:
log.Verbose(message.Text);
break;
case Level.Error:
log.Error(message.Text);
break;
}
}
}
}

class CommandResultWithOutput(CommandResult result, CaptureCommandOutput output)
{
public CommandResult Result { get; } = result;

public CaptureCommandOutput Output { get; } = output;
}
Loading