diff --git a/source/Calamari.AzureResourceGroup.Tests/Bicep/BicepToArmParameterMapperFixture.cs b/source/Calamari.AzureResourceGroup.Tests/Bicep/BicepToArmParameterMapperFixture.cs
new file mode 100644
index 0000000000..a7f79f9e4e
--- /dev/null
+++ b/source/Calamari.AzureResourceGroup.Tests/Bicep/BicepToArmParameterMapperFixture.cs
@@ -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));
+ }
+}
diff --git a/source/Calamari.AzureResourceGroup.Tests/Calamari.AzureResourceGroup.Tests.csproj b/source/Calamari.AzureResourceGroup.Tests/Calamari.AzureResourceGroup.Tests.csproj
index ce4b9d11ea..55a49cc53c 100644
--- a/source/Calamari.AzureResourceGroup.Tests/Calamari.AzureResourceGroup.Tests.csproj
+++ b/source/Calamari.AzureResourceGroup.Tests/Calamari.AzureResourceGroup.Tests.csproj
@@ -27,9 +27,6 @@
PreserveNewest
-
- PreserveNewest
-
PreserveNewest
diff --git a/source/Calamari.AzureResourceGroup.Tests/DeployAzureBicepTemplateCommandFixture.cs b/source/Calamari.AzureResourceGroup.Tests/DeployAzureBicepTemplateCommandFixture.cs
index 04d4630225..6ce8922365 100644
--- a/source/Calamari.AzureResourceGroup.Tests/DeployAzureBicepTemplateCommandFixture.cs
+++ b/source/Calamari.AzureResourceGroup.Tests/DeployAzureBicepTemplateCommandFixture.cs
@@ -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;
@@ -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;
@@ -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()
{
@@ -128,7 +131,6 @@ await CommandTestBuilder.CreateAsync()
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()
.WithArrange(context =>
@@ -136,7 +138,7 @@ await CommandTestBuilder.CreateAsync()
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();
}
@@ -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");
- }
}
}
diff --git a/source/Calamari.AzureResourceGroup.Tests/Packages/Bicep/parameters.json b/source/Calamari.AzureResourceGroup.Tests/Packages/Bicep/parameters.json
deleted file mode 100644
index edbd857422..0000000000
--- a/source/Calamari.AzureResourceGroup.Tests/Packages/Bicep/parameters.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "storageAccountName": {
- "value": "#{StorageAccountName}"
- },
- "location": {
- "value": "#{Location}"
- },
- "sku": {
- "value": "#{SKU}"
- }
-}
\ No newline at end of file
diff --git a/source/Calamari.AzureResourceGroup/Bicep/BicepCli.cs b/source/Calamari.AzureResourceGroup/Bicep/BicepCli.cs
new file mode 100644
index 0000000000..dc5ea17491
--- /dev/null
+++ b/source/Calamari.AzureResourceGroup/Bicep/BicepCli.cs
@@ -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;
+}
\ No newline at end of file
diff --git a/source/Calamari.AzureResourceGroup/Bicep/BicepToArmParameterMapper.cs b/source/Calamari.AzureResourceGroup/Bicep/BicepToArmParameterMapper.cs
new file mode 100644
index 0000000000..44ea4566ce
--- /dev/null
+++ b/source/Calamari.AzureResourceGroup/Bicep/BicepToArmParameterMapper.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Calamari.Common.Plumbing.Variables;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Calamari.AzureResourceGroup.Bicep;
+
+public static class BicepToArmParameterMapper
+{
+ record ArmParameter(string Name, string Type);
+
+ public static string Map(string bicepParametersString, string armTemplateJson, IVariables variables)
+ {
+ var armParameters = JObject.Parse(armTemplateJson)["parameters"]?.Children()
+ .Select(p => new ArmParameter(
+ Name: p.Name,
+ Type: p.Value["type"]?.Value() ?? "string"
+ ))
+ .ToList()
+ ?? [];
+
+ if (armParameters.Count == 0 || string.IsNullOrEmpty(bicepParametersString))
+ {
+ return string.Empty;
+ }
+
+ var parameterKeyValuePairs = JArray.Parse(bicepParametersString)
+ .Select(item => new KeyValuePair(
+ item["Key"]!.Value()!,
+ item["Value"]!.Value()!
+ ))
+ .ToList();
+
+ var result = new JObject();
+
+ var matched = parameterKeyValuePairs.Join(
+ armParameters,
+ kvp => kvp.Key,
+ p => p.Name,
+ (kvp, armParameter) => new { kvp, armParameter }
+ );
+
+ foreach (var match in matched)
+ {
+ var specifiedValue = GenerateValue(match.kvp, match.armParameter, variables);
+ if (specifiedValue != null)
+ {
+ result[match.kvp.Key] = new JObject { ["value"] = specifiedValue };
+ }
+ }
+
+
+ return result.ToString();
+ }
+
+ static JToken? GenerateValue(KeyValuePair property, ArmParameter parameter, IVariables variables)
+ {
+ var evaluatedValue = variables.Evaluate(property.Value);
+
+ var valueToken = parameter.Type switch
+ {
+ // NOTE: Array and Object are not defined here, but Server ignores them too
+ "int" => int.TryParse(evaluatedValue, out var intResult) ? new JValue(intResult) : null,
+ "bool" => bool.TryParse(evaluatedValue, out var boolResult) ? new JValue(boolResult) : null,
+ "secureString" or "secureObject" => property.Value,
+ _ => !string.IsNullOrEmpty(evaluatedValue) ? ParseStringOrObject(evaluatedValue) : null
+ };
+
+ return valueToken;
+ }
+
+ // Used for handling Objects passed around as strings which
+ // is part of what triggered https://slipway.octopushq.com/software-products/OctopusServer/problems/SoftwareReleaseProblems-9261
+ static JToken ParseStringOrObject(string value)
+ {
+ var trimmed = value.Trim();
+ if (trimmed.StartsWith("{") && trimmed.EndsWith("}"))
+ {
+ try
+ {
+ var obj = JObject.Parse(trimmed);
+ return new JValue(obj.ToString(Formatting.None));
+ }
+ catch (JsonException)
+ {
+ // not valid JSON, fall through to string
+ }
+ }
+
+ return new JValue(value);
+ }
+}
\ No newline at end of file
diff --git a/source/Calamari.AzureResourceGroup/Bicep/DeployAzureBicepTemplateCommand.cs b/source/Calamari.AzureResourceGroup/Bicep/DeployAzureBicepTemplateCommand.cs
new file mode 100644
index 0000000000..68ff5c6fd9
--- /dev/null
+++ b/source/Calamari.AzureResourceGroup/Bicep/DeployAzureBicepTemplateCommand.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using Calamari.Common.Commands;
+using Calamari.Common.Plumbing.Pipeline;
+
+namespace Calamari.AzureResourceGroup.Bicep;
+
+[Command("deploy-azure-bicep-template", Description = "Deploy a Bicep template to Azure")]
+// ReSharper disable once ClassNeverInstantiated.Global
+public class DeployAzureBicepTemplateCommand : PipelineCommand
+{
+ protected override IEnumerable Deploy(DeployResolver resolver)
+ {
+ yield return resolver.Create();
+ }
+}
\ No newline at end of file
diff --git a/source/Calamari.AzureResourceGroup/Bicep/DeployBicepTemplateBehaviour.cs b/source/Calamari.AzureResourceGroup/Bicep/DeployBicepTemplateBehaviour.cs
new file mode 100644
index 0000000000..9b5226f88e
--- /dev/null
+++ b/source/Calamari.AzureResourceGroup/Bicep/DeployBicepTemplateBehaviour.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Threading.Tasks;
+using Azure;
+using Azure.ResourceManager;
+using Azure.ResourceManager.Resources;
+using Azure.ResourceManager.Resources.Models;
+using Calamari.Azure;
+using Calamari.CloudAccounts;
+using Calamari.Common.Commands;
+using Calamari.Common.Features.Processes;
+using Calamari.Common.Plumbing.Extensions;
+using Calamari.Common.Plumbing.Logging;
+using Calamari.Common.Plumbing.Pipeline;
+using Calamari.Common.Plumbing.Variables;
+
+namespace Calamari.AzureResourceGroup.Bicep;
+
+// ReSharper disable once ClassNeverInstantiated.Global
+class DeployBicepTemplateBehaviour(ICommandLineRunner commandLineRunner, TemplateService templateService, AzureResourceGroupOperator resourceGroupOperator, IResourceGroupTemplateNormalizer parameterNormalizer, ILog log)
+ : IDeployBehaviour
+{
+ public bool IsEnabled(RunningDeployment context)
+ {
+ return true;
+ }
+
+ public async Task Execute(RunningDeployment context)
+ {
+ var accountType = context.Variables.GetRequiredVariable(AzureScripting.SpecialVariables.Account.AccountType);
+ IAzureAccount account = accountType == nameof(AccountType.AzureOidc) ? new AzureOidcAccount(context.Variables) : new AzureServicePrincipalAccount(context.Variables);
+
+ var armClient = account.CreateArmClient();
+
+ var resourceGroupName = context.Variables.GetRequiredVariable(SpecialVariables.Action.Azure.ResourceGroupName);
+ var resourceGroupLocation = context.Variables.GetRequiredVariable(SpecialVariables.Action.Azure.ResourceGroupLocation);
+ var subscriptionId = context.Variables.GetRequiredVariable(AzureAccountVariables.SubscriptionId);
+ var deploymentModeVariable = context.Variables.GetRequiredVariable(SpecialVariables.Action.Azure.ResourceGroupDeploymentMode);
+ var deploymentMode = (ArmDeploymentMode)Enum.Parse(typeof(ArmDeploymentMode), deploymentModeVariable);
+
+ var resourceGroup = await GetOrCreateResourceGroup(armClient, subscriptionId, resourceGroupName, resourceGroupLocation);
+
+ var (template, parameters) = GetArmTemplateAndParameters(context);
+
+ var armDeploymentName = DeploymentName.FromStepName(context.Variables[ActionVariables.Name]);
+ log.Verbose($"Deployment Name: {armDeploymentName}, set to variable \"AzureRmOutputs[DeploymentName]\"");
+ log.SetOutputVariable("AzureRmOutputs[DeploymentName]", armDeploymentName, context.Variables);
+
+ var deploymentOperation = await resourceGroupOperator.CreateDeployment(resourceGroup,
+ armDeploymentName,
+ deploymentMode,
+ template,
+ parameters);
+ await resourceGroupOperator.PollForCompletion(deploymentOperation, context.Variables);
+ await resourceGroupOperator.FinalizeDeployment(deploymentOperation, context.Variables);
+ }
+
+ async Task GetOrCreateResourceGroup(ArmClient armClient, string subscriptionId, string resourceGroupName, string location)
+ {
+ var subscription = armClient.GetSubscriptionResource(SubscriptionResource.CreateResourceIdentifier(subscriptionId));
+
+ var resourceGroups = subscription.GetResourceGroups();
+ var existing = await resourceGroups.GetIfExistsAsync(resourceGroupName);
+
+ if (existing.HasValue && existing.Value != null)
+ return existing.Value;
+
+ log.Info($"The resource group with the name {resourceGroupName} does not exist");
+ log.Info($"Creating resource group {resourceGroupName} in location {location}");
+
+ var resourceGroupData = new ResourceGroupData(location);
+ var armOperation = await resourceGroups.CreateOrUpdateAsync(WaitUntil.Completed, resourceGroupName, resourceGroupData);
+ return armOperation.Value;
+ }
+
+ (string template, string? parameters) GetArmTemplateAndParameters(RunningDeployment context)
+ {
+ var bicepCli = new BicepCli(log, commandLineRunner, context.CurrentDirectory);
+
+ var bicepTemplateFile = context.Variables.Get(SpecialVariables.Action.Azure.BicepTemplateFile, "template.bicep");
+ var templateSource = context.Variables.Get(SpecialVariables.Action.Azure.TemplateSource, string.Empty);
+
+ var filesInPackageOrRepository = templateSource is "Package" or "GitRepository";
+ if (filesInPackageOrRepository)
+ {
+ bicepTemplateFile = context.Variables.Get(SpecialVariables.Action.Azure.BicepTemplate);
+ }
+
+ log.Info($"Processing Bicep file: {bicepTemplateFile}");
+ var armTemplateFile = bicepCli.BuildArmTemplate(bicepTemplateFile!);
+ log.Info("Bicep file processed");
+
+ var template = templateService.GetSubstitutedTemplateContent(armTemplateFile, filesInPackageOrRepository, context.Variables);
+
+
+ var parametersValue = context.Variables.GetRaw(SpecialVariables.Action.Azure.BicepTemplateParameters) ?? string.Empty;
+
+ var parameters = BicepToArmParameterMapper.Map(parametersValue, template, context.Variables);
+
+ return (template, parameters);
+ }
+}
\ No newline at end of file
diff --git a/source/Calamari.AzureResourceGroup/BicepCli.cs b/source/Calamari.AzureResourceGroup/BicepCli.cs
deleted file mode 100644
index 172fa4b73c..0000000000
--- a/source/Calamari.AzureResourceGroup/BicepCli.cs
+++ /dev/null
@@ -1,135 +0,0 @@
-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
-{
- public class BicepCli
- {
- public 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
- {
- public CommandResultWithOutput(CommandResult result, CaptureCommandOutput output)
- {
- Result = result;
- Output = output;
- }
-
- public CommandResult Result { get; }
-
- public CaptureCommandOutput Output { get; set; }
- }
-}
\ No newline at end of file
diff --git a/source/Calamari.AzureResourceGroup/Calamari.AzureResourceGroup.csproj b/source/Calamari.AzureResourceGroup/Calamari.AzureResourceGroup.csproj
index 28948f698e..a53fd67917 100644
--- a/source/Calamari.AzureResourceGroup/Calamari.AzureResourceGroup.csproj
+++ b/source/Calamari.AzureResourceGroup/Calamari.AzureResourceGroup.csproj
@@ -15,6 +15,7 @@
+
diff --git a/source/Calamari.AzureResourceGroup/DeployAzureBicepTemplateCommand.cs b/source/Calamari.AzureResourceGroup/DeployAzureBicepTemplateCommand.cs
deleted file mode 100644
index 3de6909bcb..0000000000
--- a/source/Calamari.AzureResourceGroup/DeployAzureBicepTemplateCommand.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System;
-using System.Collections.Generic;
-using Calamari.Common.Commands;
-using Calamari.Common.Plumbing.Pipeline;
-
-namespace Calamari.AzureResourceGroup
-{
- [Command("deploy-azure-bicep-template", Description = "Deploy a Bicep template to Azure")]
- public class DeployAzureBicepTemplateCommand : PipelineCommand
- {
- protected override IEnumerable Deploy(DeployResolver resolver)
- {
- yield return resolver.Create();
- }
- }
-}
\ No newline at end of file
diff --git a/source/Calamari.AzureResourceGroup/DeployAzureResourceGroupBehaviour.cs b/source/Calamari.AzureResourceGroup/DeployAzureResourceGroupBehaviour.cs
index 4ce32d8008..1a1513fb62 100644
--- a/source/Calamari.AzureResourceGroup/DeployAzureResourceGroupBehaviour.cs
+++ b/source/Calamari.AzureResourceGroup/DeployAzureResourceGroupBehaviour.cs
@@ -5,7 +5,6 @@
using Calamari.Azure;
using Calamari.CloudAccounts;
using Calamari.Common.Commands;
-using Calamari.Common.FeatureToggles;
using Calamari.Common.Plumbing.Extensions;
using Calamari.Common.Plumbing.Logging;
using Calamari.Common.Plumbing.Pipeline;
diff --git a/source/Calamari.AzureResourceGroup/DeployAzureResourceGroupCommand.cs b/source/Calamari.AzureResourceGroup/DeployAzureResourceGroupCommand.cs
index 9715009243..6993c555a1 100644
--- a/source/Calamari.AzureResourceGroup/DeployAzureResourceGroupCommand.cs
+++ b/source/Calamari.AzureResourceGroup/DeployAzureResourceGroupCommand.cs
@@ -6,6 +6,7 @@
namespace Calamari.AzureResourceGroup
{
[Command("deploy-azure-resource-group", Description = "Creates a new Azure Resource Group deployment")]
+ // ReSharper disable once ClassNeverInstantiated.Global
public class DeployAzureResourceGroupCommand : PipelineCommand
{
protected override IEnumerable Deploy(DeployResolver resolver)
diff --git a/source/Calamari.AzureResourceGroup/DeployBicepTemplateBehaviour.cs b/source/Calamari.AzureResourceGroup/DeployBicepTemplateBehaviour.cs
deleted file mode 100644
index 83400607ac..0000000000
--- a/source/Calamari.AzureResourceGroup/DeployBicepTemplateBehaviour.cs
+++ /dev/null
@@ -1,110 +0,0 @@
-using System;
-using System.Threading.Tasks;
-using Azure;
-using Azure.ResourceManager;
-using Azure.ResourceManager.Resources;
-using Azure.ResourceManager.Resources.Models;
-using Calamari.Azure;
-using Calamari.CloudAccounts;
-using Calamari.Common.Commands;
-using Calamari.Common.Features.Processes;
-using Calamari.Common.Plumbing.Extensions;
-using Calamari.Common.Plumbing.Logging;
-using Calamari.Common.Plumbing.Pipeline;
-using Calamari.Common.Plumbing.Variables;
-
-namespace Calamari.AzureResourceGroup
-{
- class DeployBicepTemplateBehaviour : IDeployBehaviour
- {
- readonly ICommandLineRunner commandLineRunner;
- readonly TemplateService templateService;
- readonly AzureResourceGroupOperator resourceGroupOperator;
- readonly ILog log;
-
- public DeployBicepTemplateBehaviour(ICommandLineRunner commandLineRunner, TemplateService templateService, AzureResourceGroupOperator resourceGroupOperator, ILog log)
- {
- this.commandLineRunner = commandLineRunner;
- this.templateService = templateService;
- this.resourceGroupOperator = resourceGroupOperator;
- this.log = log;
- }
-
- public bool IsEnabled(RunningDeployment context)
- {
- return true;
- }
-
- public async Task Execute(RunningDeployment context)
- {
- var accountType = context.Variables.GetRequiredVariable(AzureScripting.SpecialVariables.Account.AccountType);
- var account = accountType == AccountType.AzureOidc.ToString() ? (IAzureAccount)new AzureOidcAccount(context.Variables) : new AzureServicePrincipalAccount(context.Variables);
-
- var armClient = account.CreateArmClient();
-
- var resourceGroupName = context.Variables.GetRequiredVariable(SpecialVariables.Action.Azure.ResourceGroupName);
- var resourceGroupLocation = context.Variables.GetRequiredVariable(SpecialVariables.Action.Azure.ResourceGroupLocation);
- var subscriptionId = context.Variables.GetRequiredVariable(AzureAccountVariables.SubscriptionId);
- var deploymentModeVariable = context.Variables.GetRequiredVariable(SpecialVariables.Action.Azure.ResourceGroupDeploymentMode);
- var deploymentMode = (ArmDeploymentMode)Enum.Parse(typeof(ArmDeploymentMode), deploymentModeVariable);
-
- var resourceGroup = await GetOrCreateResourceGroup(armClient, subscriptionId, resourceGroupName, resourceGroupLocation);
-
- var (template, parameters) = GetArmTemplateAndParameters(context);
-
- var armDeploymentName = DeploymentName.FromStepName(context.Variables[ActionVariables.Name]);
- log.Verbose($"Deployment Name: {armDeploymentName}, set to variable \"AzureRmOutputs[DeploymentName]\"");
- log.SetOutputVariable("AzureRmOutputs[DeploymentName]", armDeploymentName, context.Variables);
-
- var deploymentOperation = await resourceGroupOperator.CreateDeployment(resourceGroup,
- armDeploymentName,
- deploymentMode,
- template,
- parameters);
- await resourceGroupOperator.PollForCompletion(deploymentOperation, context.Variables);
- await resourceGroupOperator.FinalizeDeployment(deploymentOperation, context.Variables);
- }
-
- async Task GetOrCreateResourceGroup(ArmClient armClient, string subscriptionId, string resourceGroupName, string location)
- {
- var subscription = armClient.GetSubscriptionResource(SubscriptionResource.CreateResourceIdentifier(subscriptionId));
-
- var resourceGroups = subscription.GetResourceGroups();
- var existing = await resourceGroups.GetIfExistsAsync(resourceGroupName);
-
- if (existing.HasValue && existing.Value != null)
- return existing.Value;
-
- log.Info($"The resource group with the name {resourceGroupName} does not exist");
- log.Info($"Creating resource group {resourceGroupName} in location {location}");
-
- var resourceGroupData = new ResourceGroupData(location);
- var armOperation = await resourceGroups.CreateOrUpdateAsync(WaitUntil.Completed, resourceGroupName, resourceGroupData);
- return armOperation.Value;
- }
-
- (string template, string? parameters) GetArmTemplateAndParameters(RunningDeployment context)
- {
- var bicepCli = new BicepCli(log, commandLineRunner, context.CurrentDirectory);
-
- var bicepTemplateFile = context.Variables.Get(SpecialVariables.Action.Azure.BicepTemplateFile, "template.bicep");
- var templateSource = context.Variables.Get(SpecialVariables.Action.Azure.TemplateSource, string.Empty);
-
- var filesInPackageOrRepository = templateSource is "Package" or "GitRepository";
- if (filesInPackageOrRepository)
- {
- bicepTemplateFile = context.Variables.Get(SpecialVariables.Action.Azure.BicepTemplate);
- }
-
- log.Info($"Processing Bicep file: {bicepTemplateFile}");
- var armTemplateFile = bicepCli.BuildArmTemplate(bicepTemplateFile!);
- log.Info("Bicep file processed");
-
- var template = templateService.GetSubstitutedTemplateContent(armTemplateFile, filesInPackageOrRepository, context.Variables);
-
- var parameters = templateService.GetSubstitutedTemplateContent("parameters.json", inPackage: false, context.Variables);
-
- return (template, parameters);
- }
- }
-}
\ No newline at end of file
diff --git a/source/Calamari.AzureResourceGroup/ResourceGroupTemplateNormalizer.cs b/source/Calamari.AzureResourceGroup/ResourceGroupTemplateNormalizer.cs
index b8a4f4aab9..5931c14ec1 100644
--- a/source/Calamari.AzureResourceGroup/ResourceGroupTemplateNormalizer.cs
+++ b/source/Calamari.AzureResourceGroup/ResourceGroupTemplateNormalizer.cs
@@ -14,7 +14,7 @@ public string Normalize(string json)
try
{
var envelope = JsonConvert.DeserializeObject(json);
- return JsonConvert.SerializeObject(envelope.Parameters);
+ return JsonConvert.SerializeObject(envelope?.Parameters);
}
catch (JsonSerializationException)
{
@@ -25,7 +25,7 @@ public string Normalize(string json)
class ParameterEnvelope
{
[JsonProperty("parameters", Required = Required.Always)]
- public JObject Parameters { get; set; }
+ public required JObject Parameters { get; set; }
}
}
}
\ No newline at end of file
diff --git a/source/Calamari.AzureResourceGroup/SpecialVariables.cs b/source/Calamari.AzureResourceGroup/SpecialVariables.cs
index 21fa54b272..00ad95b050 100644
--- a/source/Calamari.AzureResourceGroup/SpecialVariables.cs
+++ b/source/Calamari.AzureResourceGroup/SpecialVariables.cs
@@ -18,6 +18,7 @@ public static class Azure
public static readonly string ArmDeploymentTimeout = "Octopus.Action.Azure.ArmDeploymentTimeout";
public static readonly string BicepTemplate = "Octopus.Action.Azure.BicepTemplate";
public static readonly string BicepTemplateFile = "Octopus.Action.Azure.BicepTemplateFile";
+ public static readonly string BicepTemplateParameters = "Octopus.Action.Azure.BicepTemplateParameters";
}
}
}