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
Expand Up @@ -117,6 +117,18 @@ private ILocalExtension GetExtensionMock()
return extensionMock.Object;
}

private ILocalExtension GetFailingExtensionMock()
{
var extensionMock = StrictMock.Of<ILocalExtension>();
extensionMock.Setup(x => x.CreateOrUpdate(It.IsAny<ResourceSpecification>(), It.IsAny<CancellationToken>()))
.Returns<ResourceSpecification, CancellationToken>((req, _) =>
{
return Task.FromResult(new LocalExtensionOperationResponse(null, new(new("MyErrorCode", "Dummy error message"))));
});

return extensionMock.Object;
}

[TestMethod]
public async Task Local_deploy_should_succeed()
{
Expand Down Expand Up @@ -268,7 +280,7 @@ public async Task Local_deploy_with_azure_should_succeed(bool async)
}
},
},
[]);
[], []);
});

var services = await ExtensionTestHelper.GetServiceBuilderWithPublishedExtension(GetMockLocalDeployPackage(), new(LocalDeployEnabled: true));
Expand Down Expand Up @@ -299,4 +311,70 @@ public async Task Local_deploy_with_azure_should_succeed(bool async)

""");
}

[TestMethod]
public async Task Local_deploy_should_report_nested_operations()
{
var paramFile = new EmbeddedFile(typeof(LocalDeployCommandTests).Assembly, "Files/LocalDeployCommandTests/weather/nested.bicepparam");
var baselineFolder = BaselineFolder.BuildOutputFolder(TestContext, paramFile);

var services = await ExtensionTestHelper.GetServiceBuilderWithPublishedExtension(GetMockLocalDeployPackage(), new(LocalDeployEnabled: true));
var clientFactory = services.Build().Construct<IContainerRegistryClientFactory>();

var cacheDirectory = FileHelper.GetCacheRootDirectory(TestContext).EnsureExists();

var result = await Bicep(
new InvocationSettings(ClientFactory: clientFactory, FeatureOverrides: new(CacheRootDirectory: cacheDirectory)),
services => RegisterExtensionMocks(services, GetExtensionMock()),
TestContext.CancellationTokenSource.Token,
["local-deploy", baselineFolder.EntryFile.OutputFilePath]);

result.Should().NotHaveStderr().And.Succeed();

result.WithoutAnsi().WithoutDurations().Stdout.Should().BeEquivalentToIgnoringNewlines("""
╭───────────────────────┬──────────┬───────────╮
│ Resource │ Duration │ Status │
├───────────────────────┼──────────┼───────────┤
│ main │ │ Succeeded │
│ main -> gridpointsReq │ │ Succeeded │
│ main -> forecastReq │ │ Succeeded │
╰───────────────────────┴──────────┴───────────╯
Comment thread
shenglol marked this conversation as resolved.

""");
}

[TestMethod]
public async Task Local_deploy_should_report_nested_operation_failures()
{
var paramFile = new EmbeddedFile(typeof(LocalDeployCommandTests).Assembly, "Files/LocalDeployCommandTests/weather/nested.bicepparam");
var baselineFolder = BaselineFolder.BuildOutputFolder(TestContext, paramFile);

var services = await ExtensionTestHelper.GetServiceBuilderWithPublishedExtension(GetMockLocalDeployPackage(), new(LocalDeployEnabled: true));
var clientFactory = services.Build().Construct<IContainerRegistryClientFactory>();

var cacheDirectory = FileHelper.GetCacheRootDirectory(TestContext).EnsureExists();

var result = await Bicep(
new InvocationSettings(ClientFactory: clientFactory, FeatureOverrides: new(CacheRootDirectory: cacheDirectory)),
services => RegisterExtensionMocks(services, GetFailingExtensionMock()),
TestContext.CancellationTokenSource.Token,
["local-deploy", baselineFolder.EntryFile.OutputFilePath]);

result.Should().NotHaveStderr().And.Fail();

result.WithoutAnsi().WithoutDurations().Stdout.Should().BeEquivalentToIgnoringNewlines("""
╭───────────────────────┬──────────┬───────────────────────────────────────────╮
│ Resource │ Duration │ Status │
├───────────────────────┼──────────┼───────────────────────────────────────────┤
│ main │ │ DeploymentFailed: At least one resource │
│ │ │ deployment operation failed. Please list │
│ │ │ deployment operations for details. Please │
│ │ │ see │
│ │ │ https://aka.ms/arm-deployment-operations │
│ │ │ for usage details. │
│ main -> gridpointsReq │ │ MyErrorCode: Dummy error message │
╰───────────────────────┴──────────┴───────────────────────────────────────────╯

""");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
targetScope = 'local'

param coords {
latitude: string
longitude: string
}

module main 'main.bicep' = {
params: {
coords: coords
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using 'nested.bicep'

param coords = {
latitude: '47.6363726'
longitude: '-122.1357068'
}
2 changes: 1 addition & 1 deletion src/Bicep.Cli/Commands/LocalDeployCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ private async Task ProcessDeployment(Compilation compilation, string templateStr
await using var dispatcher = dispatcherFactory.Create();

await dispatcher.InitializeExtensions(compilation);
await dispatcher.Deploy(templateString, parametersString, result => onUpdate(new(DeploymentProcessor.GetDeploymentView(result.Deployment, result.Operations), null)), cancellationToken);
await dispatcher.Deploy(templateString, parametersString, result => onUpdate(new(DeploymentProcessor.GetDeploymentView(result), null)), cancellationToken);
}
catch (Exception exception)
{
Expand Down
25 changes: 22 additions & 3 deletions src/Bicep.Cli/Helpers/Deploy/DeploymentProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
using Bicep.Core.TypeSystem;
using Bicep.Core.Utils;
using Bicep.Core.Utils.Deployments;
using Bicep.Local.Deploy;
using Microsoft.WindowsAzure.ResourceStack.Common.Json;
using Newtonsoft.Json.Linq;

Expand Down Expand Up @@ -480,10 +481,12 @@ private static DeploymentView GetDeploymentView(ArmDeploymentResource deployment
Outputs: GetOutputs(deployment.Data));
}

public static DeploymentView GetDeploymentView(DeploymentContent deployment, IEnumerable<DeploymentOperationDefinition> operations)
public static DeploymentView GetDeploymentView(LocalDeploymentResult result)
{
var deployment = result.Deployment;

List<DeploymentOperationView> operationViews = [];
foreach (var operation in operations)
foreach (var (prefix, operation) in FlattenOperations(result, string.Empty))
{
if (operation.Properties.TargetResource is null)
{
Expand All @@ -501,7 +504,7 @@ public static DeploymentView GetDeploymentView(DeploymentContent deployment, IEn
operationViews.Add(new(
Id: operation.Properties.TargetResource.Id,
Name: operation.Properties.TargetResource.ResourceName,
SymbolicName: operation.Properties.TargetResource.SymbolicName,
SymbolicName: $"{prefix}{operation.Properties.TargetResource.SymbolicName}",
Type: operation.Properties.TargetResource.ResourceType!,
State: operationState,
StartTime: operation.Properties.Timestamp!.Value,
Expand All @@ -520,4 +523,20 @@ public static DeploymentView GetDeploymentView(DeploymentContent deployment, IEn
Error: GetError(deployment),
Outputs: GetOutputs(deployment));
}

private static IEnumerable<(string prefix, DeploymentOperationDefinition operation)> FlattenOperations(LocalDeploymentResult result, string prefix)
{
foreach (var operation in result.Operations)
{
yield return (prefix, operation);
}

foreach (var (name, childDeployment) in result.ChildDeployments)
{
foreach (var item in FlattenOperations(childDeployment, $"{prefix}{name} -> "))
{
yield return item;
}
}
}
}
4 changes: 2 additions & 2 deletions src/Bicep.Cli/Helpers/Deploy/DeploymentRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ public bool RenderDeployment(Table table, DeploymentWrapperView? view, ref bool
}

var orderedOperations = deployment.Operations
.OrderBy(x => x.EndTime is { } ? x.EndTime.Value : DateTime.MaxValue)
.ThenBy(x => x.StartTime)
.OrderBy(x => x.StartTime)
.ThenBy(x => x.EndTime is { } ? x.EndTime.Value : DateTime.MaxValue)
.ToList();

table.Rows.Clear();
Expand Down
2 changes: 1 addition & 1 deletion src/Bicep.Local.Deploy/Azure/ArmDeploymentProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,6 @@ public async Task<LocalDeploymentResult> CheckDeployment(RootConfiguration confi
var response = await deploymentsClient.GetAsync(deploymentLocator.DeploymentName, cancellationToken);
var content = response.GetRawResponse().Content.ToString().FromDeploymentsJson<DeploymentContent>();

return new(content, []);
return new(content, [], []);
}
}
99 changes: 70 additions & 29 deletions src/Bicep.Local.Deploy/Engine/LocalDeploymentEngine.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Collections.Immutable;
using System.Net;
using Azure.Deployments.Core.Constants;
using Azure.Deployments.Core.Definitions;
Expand All @@ -19,36 +20,19 @@

namespace Bicep.Local.Deploy.Engine;

public class LocalDeploymentEngine
public class LocalDeploymentEngine(
LocalRequestContext requestContext,
IAzureDeploymentSettings settings,
AzureDeploymentEngine deploymentEngine,
IDataProviderHolder dataProviderHolder,
IDeploymentJobsDataProvider jobProvider,
IDeploymentEntityFactory deploymentEntityFactory)
{
static LocalDeploymentEngine()
{
CoreConstants.ResourcesLimitOverrideForLocalDeploy = int.MaxValue;
}

public LocalDeploymentEngine(
LocalRequestContext requestContext,
IAzureDeploymentSettings settings,
AzureDeploymentEngine deploymentEngine,
IDataProviderHolder dataProviderHolder,
IDeploymentJobsDataProvider jobProvider,
IDeploymentEntityFactory deploymentEntityFactory)
{
this.requestContext = requestContext;
this.settings = settings;
this.deploymentEngine = deploymentEngine;
this.dataProviderHolder = dataProviderHolder;
this.jobProvider = jobProvider;
this.deploymentEntityFactory = deploymentEntityFactory;
}

private readonly LocalRequestContext requestContext;
private readonly IAzureDeploymentSettings settings;
private readonly AzureDeploymentEngine deploymentEngine;
private readonly IDataProviderHolder dataProviderHolder;
private readonly IDeploymentJobsDataProvider jobProvider;
private readonly IDeploymentEntityFactory deploymentEntityFactory;

private static (Template template, Dictionary<string, DeploymentParameterDefinition> parameters, OrdinalDictionary<OrdinalDictionary<DeploymentExtensionConfigItem>>? extensionConfigs) ParseTemplateAndParameters(string templateString, string parametersString)
{
var template = TemplateParsingEngine.ParseTemplate(templateString);
Expand Down Expand Up @@ -95,7 +79,7 @@ public async Task StartDeployment(string name, string templateString, string par
var oboToken = "dummyToken";
var oboCorrelationId = RequestCorrelationContext.Current.CorrelationId;

var deploymentPlan = await this.deploymentEngine.ProcessDeployment(
var deploymentPlan = await deploymentEngine.ProcessDeployment(
preflightSettings: new(settings, syncMode: true),
deploymentContext: context,
definition: definition,
Expand Down Expand Up @@ -130,13 +114,70 @@ public async Task<LocalDeploymentResult> CheckDeployment(string name)

var deploymentDataProvider = await dataProviderHolder.GetDeploymentDataProviderAsync(requestContext.Location);
var entity = await deploymentDataProvider.FindDeployment(context.SubscriptionId, context.ResourceGroupName, context.DeploymentName);
var operationEntities = await deploymentDataProvider.FindDeploymentOperations(context.SubscriptionId, context.ResourceGroupName, context.DeploymentName, entity.SequenceId, -1);
var operationEntities = await deploymentDataProvider.FindDeploymentOperations(
context.SubscriptionId,
context.ResourceGroupName,
context.DeploymentName,
entity.SequenceId,
int.MaxValue,
includeOrphanSequences: true);

var deploymentEntityBySequence = new Dictionary<string, IDeploymentEntity>(StringComparer.OrdinalIgnoreCase)
{
[entity.SequenceId] = entity,
};

async Task<IDeploymentEntity> GetDeploymentEntityForSequence(string deploymentSequenceId)
{
if (deploymentEntityBySequence.TryGetValue(deploymentSequenceId, out var cachedEntity))
{
return cachedEntity;
}

var deploymentEntity = await deploymentDataProvider.FindDeployment(
context.SubscriptionId,
context.ResourceGroupName,
context.DeploymentName,
deploymentSequenceId);

deploymentEntityBySequence[deploymentSequenceId] = deploymentEntity;

return deploymentEntity;
}

List<DeploymentOperationDefinition> operationDefinitions = [];
foreach (var operation in operationEntities.Where(operation => operation.TargetResource is not null))
{
var deploymentEntity = await GetDeploymentEntityForSequence(operation.DeploymentSequenceId);
operationDefinitions.Add(deploymentEngine.CreateDeploymentOperationDefinition(deploymentEntity, operation, requestContext.Location));
}

ImmutableArray<DeploymentOperationDefinition> operations = [.. operationDefinitions];

var childDeployments = ImmutableDictionary.CreateBuilder<string, LocalDeploymentResult>(StringComparer.OrdinalIgnoreCase);
foreach (var operation in operations)
{
if (operation.Properties.TargetResource is not { } targetResource ||
targetResource.Extension?.Alias is not "az0synthesized" ||
targetResource.Identifiers?["name"]?.ToString() is not { } childDeploymentName)
{
continue;
}

// Only recurse into deployments that exist locally. Modules deployed to other scopes
// (e.g. Azure) are not tracked here, so their wrapper operation is kept as a single row.
if (await deploymentDataProvider.FindDeployment(context.SubscriptionId, context.ResourceGroupName, childDeploymentName) is null)
{
continue;
}

childDeployments[targetResource.SymbolicName] = await CheckDeployment(childDeploymentName);
}

return new(
deploymentEngine.CreateDeploymentDefinition(entity, requestContext.ApiVersion),
[.. operationEntities
.Where(operation => operation.TargetResource is not null)
.Select(operation => deploymentEngine.CreateDeploymentOperationDefinition(entity, operation, requestContext.Location))]);
operations,
childDeployments.ToImmutable());
}
}

Expand Down
5 changes: 1 addition & 4 deletions src/Bicep.Local.Deploy/Engine/LocalDeploymentEngineHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,7 @@ public override void AddAsyncNotificationUri(HttpRequestHeaders httpHeaders, Bac
=> throw new NotImplementedException();

public override EnablementConfig GetEnablementConfig(PreviewDeploymentFunction feature)
=> new()
{
FeatureName = feature.ToString()
};
=> enablementConfigProvider.GetEnablementConfig(feature);

public override IResourceTypeMetadataProvider GetResourceTypeMetadataProvider()
=> throw new NotImplementedException();
Expand Down
4 changes: 2 additions & 2 deletions src/Bicep.Local.Deploy/Engine/LocalDeploymentSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,9 @@ public class LocalDeploymentSettings : IAzureDeploymentSettings

public string[] AsyncOperationCallbackAllowedProviders { get; set; } = [];

public TimeSpan ResourceMaximumRetryInterval { get; set; } = TimeSpan.FromMinutes(10);
public TimeSpan ResourceMaximumRetryInterval { get; set; } = TimeSpan.FromSeconds(10);

public TimeSpan ResourceMinimumRetryInterval { get; set; } = TimeSpan.FromSeconds(5);
public TimeSpan ResourceMinimumRetryInterval { get; set; } = TimeSpan.FromSeconds(1);

public TimeSpan ResourceNotificationBasedDefaultRetryInterval { get; set; } = TimeSpan.FromMinutes(2);

Expand Down
7 changes: 2 additions & 5 deletions src/Bicep.Local.Deploy/LocalDeploymentResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,11 @@
// Licensed under the MIT License.

using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Azure.Deployments.Core.Definitions;
using Bicep.Local.Deploy.Extensibility;
using Microsoft.Extensions.DependencyInjection;

namespace Bicep.Local.Deploy;

public record LocalDeploymentResult(
DeploymentContent Deployment,
ImmutableArray<DeploymentOperationDefinition> Operations);
ImmutableArray<DeploymentOperationDefinition> Operations,
ImmutableDictionary<string, LocalDeploymentResult> ChildDeployments);
Loading