diff --git a/engine/BenchPress.TestEngine.Tests/ArmDeploymentServiceTests.cs b/engine/BenchPress.TestEngine.Tests/ArmDeploymentServiceTests.cs index 8ecb125..e0b1154 100644 --- a/engine/BenchPress.TestEngine.Tests/ArmDeploymentServiceTests.cs +++ b/engine/BenchPress.TestEngine.Tests/ArmDeploymentServiceTests.cs @@ -12,6 +12,7 @@ public class ArmDeploymentServiceTests { private readonly Mock subscriptionDeploymentsMock; private const string validSubId = "a3a01f37-665c-4ee8-9bc3-3adf7ebcec0d"; private const string validRgName = "test-rg"; + private const string validLocation = "eastus"; private const string smapleFiles = "../../../SampleFiles"; private const string standaloneTemplate = $"{smapleFiles}/storage-account.json"; private const string templateWithParams = $"{smapleFiles}/storage-account-needs-params.json"; @@ -92,19 +93,20 @@ public class ArmDeploymentServiceTests { { var subMock = SetUpSubscriptionMock(validSubId); SetUpDeploymentsMock(subscriptionDeploymentsMock); - await armDeploymentService.DeployArmToSubscriptionAsync(validSubId, templateWithParams, parameters); + await armDeploymentService.DeployArmToSubscriptionAsync(validSubId, validLocation, templateWithParams, parameters); VerifyDeploymentsMock(subscriptionDeploymentsMock); } [Theory] - [InlineData("", "a3a01f37-665c-4ee8-9bc3-3adf7ebcec0d")] - [InlineData("main.bicep", "")] - public async Task DeployArmToSubscriptionAsync_MissingParameter_ThrowsException(string templatePath, string subId) + [InlineData("main.bicep", "", "a3a01f37-665c-4ee8-9bc3-3adf7ebcec0d")] + [InlineData("", "eastus", "a3a01f37-665c-4ee8-9bc3-3adf7ebcec0d")] + [InlineData("main.bicep", "eastus", "")] + public async Task DeployArmToSubscriptionAsync_MissingParameter_ThrowsException(string templatePath, string location, string subId) { var subMock = SetUpSubscriptionMock(subId); SetUpDeploymentsMock(subscriptionDeploymentsMock); var ex = await Assert.ThrowsAsync( - async () => await armDeploymentService.DeployArmToSubscriptionAsync(subId, templatePath) + async () => await armDeploymentService.DeployArmToSubscriptionAsync(subId, location, templatePath) ); Assert.Equal("One or more parameters were missing or empty", ex.Message); } @@ -116,7 +118,7 @@ public class ArmDeploymentServiceTests { var excepectedMessage = "Deployment template validation failed"; SetUpDeploymentExceptionMock(subscriptionDeploymentsMock, new RequestFailedException(excepectedMessage)); var ex = await Assert.ThrowsAsync( - async () => await armDeploymentService.DeployArmToSubscriptionAsync(validSubId, templateWithParams) + async () => await armDeploymentService.DeployArmToSubscriptionAsync(validSubId, validLocation, templateWithParams) ); Assert.Equal(excepectedMessage, ex.Message); } @@ -127,13 +129,22 @@ public class ArmDeploymentServiceTests { var subMock = SetUpSubscriptionMock(validSubId); SetUpDeploymentsMock(subscriptionDeploymentsMock); var ex = await Assert.ThrowsAsync( - async () => await armDeploymentService.DeployArmToSubscriptionAsync("The Wrong Subscription", standaloneTemplate) + async () => await armDeploymentService.DeployArmToSubscriptionAsync("The Wrong Subscription", validLocation, standaloneTemplate) ); Assert.Equal("Subscription Not Found", ex.Message); } [Fact] public async Task CreateDeploymentContent_WithoutParameters() + { + var subMock = SetUpSubscriptionMock(validSubId); + SetUpDeploymentsMock(subscriptionDeploymentsMock); + await armDeploymentService.DeployArmToSubscriptionAsync(validSubId, validLocation, standaloneTemplate); + VerifyDeploymentsMock(subscriptionDeploymentsMock); + } + + [Fact] + public async Task CreateDeploymentContent_WithoutLocation() { var subMock = SetUpSubscriptionMock(validSubId); var rgMock = SetUpResourceGroupMock(subMock, validRgName); diff --git a/engine/BenchPress.TestEngine.Tests/DeploymentServiceTests.cs b/engine/BenchPress.TestEngine.Tests/DeploymentServiceTests.cs index 3f289bd..f428a12 100644 --- a/engine/BenchPress.TestEngine.Tests/DeploymentServiceTests.cs +++ b/engine/BenchPress.TestEngine.Tests/DeploymentServiceTests.cs @@ -78,6 +78,64 @@ public class DeploymentServiceTests Assert.Equal(expectedMessage, result.ErrorMessage); } + [Fact] + public async Task DeploymentSubCreate_DeploysToSub_WithTranspiledFiles() + { + SetUpSuccessfulTranspilation(validSubRequest.BicepFilePath, templatePath); + SetUpSuccessfulSubDeployment(validSubRequest, templatePath); + var result = await deploymentService.DeploymentSubCreate(validSubRequest, context); + Assert.True(result.Success); + VerifySubDeployment(validSubRequest, templatePath); + } + + [Theory] + [InlineData("main.bicep", "", "a3a01f37-665c-4ee8-9bc3-3adf7ebcec0d")] + [InlineData("", "eastus", "a3a01f37-665c-4ee8-9bc3-3adf7ebcec0d")] + [InlineData("main.bicep", "eastus", "")] + public async Task DeploymentSubCreate_FailsOnMissingParameters(string bicepFilePath, string location, string subscriptionNameOrId) + { + var request = SetUpSubRequest(bicepFilePath, location, subscriptionNameOrId); + SetUpSuccessfulTranspilation(validSubRequest.BicepFilePath, templatePath); + SetUpSuccessfulSubDeployment(request, templatePath); + var result = await deploymentService.DeploymentSubCreate(request, context); + Assert.False(result.Success); + VerifyNoTranspilation(); + VerifyNoDeployments(); + } + + [Fact] + public async Task DeploymentSubCreate_ReturnsFailureOnTranspileException() + { + var expectedMessage = "the bicep file was malformed"; + SetUpExceptionThrowingTranspilation(new Exception(expectedMessage)); + SetUpSuccessfulSubDeployment(validSubRequest, "template.json"); + var result = await deploymentService.DeploymentSubCreate(validSubRequest, context); + Assert.False(result.Success); + Assert.Equal(expectedMessage, result.ErrorMessage); + } + + [Fact] + public async Task DeploymentSubCreate_ReturnsFailureOnFailedDeployment() + { + SetUpSuccessfulTranspilation(validSubRequest.BicepFilePath, templatePath); + var expectedReason = "Failure occured during deployment"; + SetUpFailedSubDeployment(expectedReason); + var result = await deploymentService.DeploymentSubCreate(validSubRequest, context); + Assert.False(result.Success); + Assert.Equal(expectedReason, result.ErrorMessage); + } + + [Fact] + public async Task DeploymentSubpCreate_ReturnsFailureOnDeploymentException() + { + SetUpSuccessfulTranspilation(validSubRequest.BicepFilePath, templatePath); + var expectedMessage = "the template was malformed"; + SetUpExceptionThrowingSubDeployment(new Exception(expectedMessage)); + var result = await deploymentService.DeploymentSubCreate(validSubRequest, context); + Assert.False(result.Success); + Assert.Equal(expectedMessage, result.ErrorMessage); + } + [Fact(Skip = "Not Implemented")] public async Task DeleteGroup_DeletesAllResources() { @@ -97,6 +155,13 @@ public class DeploymentServiceTests SubscriptionNameOrId = Guid.NewGuid().ToString() }; + private readonly DeploymentSubRequest validSubRequest = new DeploymentSubRequest + { + BicepFilePath = "main.bicep", + Location = "eastus", + SubscriptionNameOrId = Guid.NewGuid().ToString() + }; + private DeploymentGroupRequest SetUpGroupRequest(string bicepFilePath, string resourceGroupName, string subscriptionNameOrId) { return new DeploymentGroupRequest @@ -107,6 +172,16 @@ public class DeploymentServiceTests }; } + private DeploymentSubRequest SetUpSubRequest(string bicepFilePath, string location, string subscriptionNameOrId) + { + return new DeploymentSubRequest + { + BicepFilePath = bicepFilePath, + Location = location, + SubscriptionNameOrId = subscriptionNameOrId + }; + } + private void SetUpSuccessfulTranspilation(string bicepFilePath, string armTemplatePath) { bicepTranspileServiceMock.Setup(x => x.BuildAsync(bicepFilePath)).ReturnsAsync(armTemplatePath); @@ -183,6 +258,52 @@ public class DeploymentServiceTests Times.Once); } + private void SetUpSuccessfulSubDeployment(DeploymentSubRequest request, string templatePath) + { + var operation = SetupDeploymentOperation(true, "OK"); + armDeploymentMock.Setup(x => x.DeployArmToSubscriptionAsync( + request.SubscriptionNameOrId, + request.Location, + templatePath, + request.ParameterFilePath, + It.IsAny())) + .ReturnsAsync(operation); + } + + private void SetUpFailedSubDeployment(string reason) + { + var operation = SetupDeploymentOperation(false, reason); + armDeploymentMock.Setup(x => x.DeployArmToSubscriptionAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(operation); + } + + private void SetUpExceptionThrowingSubDeployment(Exception ex) + { + armDeploymentMock.Setup(x => x.DeployArmToSubscriptionAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ThrowsAsync(ex); + } + + private void VerifySubDeployment(DeploymentSubRequest request, string templatePath) + { + armDeploymentMock.Verify(x => x.DeployArmToSubscriptionAsync( + request.SubscriptionNameOrId, + request.Location, + templatePath, + request.ParameterFilePath, + It.IsAny()), + Times.Once); + } + private void VerifyNoDeployments() { armDeploymentMock.Verify(x => x.DeployArmToResourceGroupAsync( @@ -192,5 +313,13 @@ public class DeploymentServiceTests It.IsAny(), It.IsAny()), Times.Never); + + armDeploymentMock.Verify(x => x.DeployArmToSubscriptionAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); } } \ No newline at end of file diff --git a/engine/BenchPress.TestEngine/Services/ArmDeploymentService.cs b/engine/BenchPress.TestEngine/Services/ArmDeploymentService.cs index da7f6c2..231eb90 100644 --- a/engine/BenchPress.TestEngine/Services/ArmDeploymentService.cs +++ b/engine/BenchPress.TestEngine/Services/ArmDeploymentService.cs @@ -20,11 +20,11 @@ public class ArmDeploymentService : IArmDeploymentService { return await CreateGroupDeployment(rg, waitUntil, NewDeploymentName, deploymentContent); } - public async Task> DeployArmToSubscriptionAsync(string subscriptionNameOrId, string armTemplatePath, string? parametersPath = null, WaitUntil waitUtil = WaitUntil.Completed) + public async Task> DeployArmToSubscriptionAsync(string subscriptionNameOrId, string location, string armTemplatePath, string? parametersPath = null, WaitUntil waitUtil = WaitUntil.Completed) { - ValidateParameters(subscriptionNameOrId, armTemplatePath); + ValidateParameters(subscriptionNameOrId, location, armTemplatePath); SubscriptionResource sub = await client.GetSubscriptions().GetAsync(subscriptionNameOrId); - var deploymentContent = await CreateDeploymentContent(armTemplatePath, parametersPath); + var deploymentContent = await CreateDeploymentContent(armTemplatePath, parametersPath, location); return await CreateSubscriptionDeployment(sub, waitUtil, NewDeploymentName, deploymentContent); } @@ -34,18 +34,26 @@ public class ArmDeploymentService : IArmDeploymentService { } } - private async Task CreateDeploymentContent(string armTemplatePath, string? parametersPath) { + private async Task CreateDeploymentContent(string armTemplatePath, string? parametersPath, string? location = null) { var templateContent = (await File.ReadAllTextAsync(armTemplatePath)).TrimEnd(); - var properties = new ArmDeploymentProperties(ArmDeploymentMode.Incremental) { + var properties = new ArmDeploymentProperties(ArmDeploymentMode.Incremental) + { Template = BinaryData.FromString(templateContent) }; - - if (!string.IsNullOrWhiteSpace(parametersPath)) { + + if (!string.IsNullOrWhiteSpace(parametersPath)) + { var paramteresContent = (await File.ReadAllTextAsync(parametersPath)).TrimEnd(); properties.Parameters = BinaryData.FromString(parametersPath); } - return new ArmDeploymentContent(properties); + var content = new ArmDeploymentContent(properties); + if (!string.IsNullOrWhiteSpace(location)) + { + content.Location = location; + } + + return content; } // These extension methods are wrapped to allow mocking in our tests diff --git a/engine/BenchPress.TestEngine/Services/DeploymentService.cs b/engine/BenchPress.TestEngine/Services/DeploymentService.cs index 7c745e2..48d9190 100644 --- a/engine/BenchPress.TestEngine/Services/DeploymentService.cs +++ b/engine/BenchPress.TestEngine/Services/DeploymentService.cs @@ -49,6 +49,41 @@ public class DeploymentService : Deployment.DeploymentBase } } + public override async Task DeploymentSubCreate(DeploymentSubRequest request, ServerCallContext context) + { + if (string.IsNullOrWhiteSpace(request.BicepFilePath) + || string.IsNullOrWhiteSpace(request.Location) + || string.IsNullOrWhiteSpace(request.SubscriptionNameOrId)) + { + return new DeploymentResult + { + Success = false, + ErrorMessage = $"One or more of the following required parameters was missing: {nameof(request.BicepFilePath)}, {nameof(request.Location)}, and {nameof(request.SubscriptionNameOrId)}" + }; + } + + try + { + var armTemplatePath = await bicepTranspileService.BuildAsync(request.BicepFilePath); + var deployment = await armDeploymentService.DeployArmToSubscriptionAsync(request.SubscriptionNameOrId, request.Location, armTemplatePath, request.ParameterFilePath); + var response = deployment.WaitForCompletionResponse(); + + return new DeploymentResult + { + Success = !response.IsError, + ErrorMessage = response.ReasonPhrase + }; + } + catch (Exception ex) + { + return new DeploymentResult + { + Success = false, + ErrorMessage = ex.Message + }; + } + } + public override async Task DeleteGroup(DeleteGroupRequest request, ServerCallContext context) { throw new NotImplementedException(); diff --git a/engine/BenchPress.TestEngine/Services/IArmDeploymentService.cs b/engine/BenchPress.TestEngine/Services/IArmDeploymentService.cs index 349d768..ae6be69 100644 --- a/engine/BenchPress.TestEngine/Services/IArmDeploymentService.cs +++ b/engine/BenchPress.TestEngine/Services/IArmDeploymentService.cs @@ -2,5 +2,5 @@ namespace BenchPress.TestEngine.Services; public interface IArmDeploymentService { Task> DeployArmToResourceGroupAsync(string subscriptionNameOrId, string resourceGroupName, string armTemplatePath, string? parametersPath = null, Azure.WaitUntil waitUtil = Azure.WaitUntil.Completed); - Task> DeployArmToSubscriptionAsync(string subscriptionNameOrId, string armTemplatePath, string? parametersPath = null, Azure.WaitUntil waitUtil = Azure.WaitUntil.Completed); + Task> DeployArmToSubscriptionAsync(string subscriptionNameOrId, string location, string armTemplatePath, string? parametersPath = null, Azure.WaitUntil waitUtil = Azure.WaitUntil.Completed); } diff --git a/protos/deployment.proto b/protos/deployment.proto index 625e760..f0a097f 100644 --- a/protos/deployment.proto +++ b/protos/deployment.proto @@ -8,6 +8,7 @@ option csharp_namespace = "BenchPress.TestEngine"; // Other scopes: subscription, management group, and tenant. service Deployment { rpc DeploymentGroupCreate (DeploymentGroupRequest) returns (DeploymentResult); + rpc DeploymentSubCreate (DeploymentSubRequest) returns (DeploymentResult); rpc DeleteGroup (DeleteGroupRequest) returns (DeploymentResult); } @@ -18,6 +19,13 @@ message DeploymentGroupRequest { string subscription_name_or_id = 4; } +message DeploymentSubRequest { + string bicep_file_path = 1; + string parameter_file_path = 2; + string location = 3; + string subscription_name_or_id = 4; +} + message DeleteGroupRequest { string resource_group_name = 1; string subscription_name_or_id = 2;