From f1ab60093f809675b7b51e0cab08b32c4b5c45e1 Mon Sep 17 00:00:00 2001 From: Jorge Cotillo Date: Sun, 28 Jul 2024 23:06:15 -0700 Subject: [PATCH] initial commit --- .../packages.lock.json | 1 + src/Bicep.Cli.UnitTests/packages.lock.json | 1 + src/Bicep.Cli/Commands/LocalDeployCommand.cs | 56 +++++- .../Helpers/ServiceCollectionExtensions.cs | 1 + src/Bicep.Cli/packages.lock.json | 1 + .../packages.lock.json | 1 + src/Bicep.Core.Samples/packages.lock.json | 1 + src/Bicep.Core.UnitTests/packages.lock.json | 1 + .../packages.lock.json | 1 + .../packages.lock.json | 1 + .../packages.lock.json | 1 + .../packages.lock.json | 1 + src/Bicep.LangServer/packages.lock.json | 1 + .../EndToEndDeploymentTests.cs | 46 ++++- .../packages.lock.json | 1 + .../Extensibility/GrpcProxyLocalExtension.cs | 174 ++++++++++++++++++ .../Extensibility/ILocalExtensionFactory.cs | 17 ++ .../ILocalExtensionFactoryManager.cs | 18 ++ .../Extensibility/ILocalExtensionHost.cs | 16 ++ .../Extensibility/ILocalExtensionLifecycle.cs | 15 ++ .../LocalExtensionFactoryManager.cs | 53 ++++++ .../Extensibility/LocalExtensionHost.cs | 134 ++++++++++++++ .../LocalExtensionHostManager.cs | 59 ++++++ .../Extensibility/LocalExtensionRegistry.cs | 40 ++++ .../IServiceCollectionExtensions.cs | 95 ++++++++++ src/Bicep.Local.Deploy/LocalDeployment.cs | 48 +++++ .../LocalDeploymentEngine.cs | 2 +- .../LocalDeploymentEngineHost.cs | 8 +- src/Bicep.Local.Deploy/packages.lock.json | 1 + .../packages.lock.json | 20 ++ .../Bicep.Local.Extension.csproj | 1 + .../BicepExtensionHostBase.cs | 117 ++++++++++++ .../Protocol/ILocalExtension.cs | 56 ++++++ .../Protocol/IResourceHandler.cs | 9 + .../Protocol/LocalExtensionDispatcher.cs | 41 +++++ .../LocalExtensionDispatcherBuilder.cs | 47 +++++ .../Rpc/BicepGrpcService.cs | 156 ++++++++++++++++ src/Bicep.Local.Extension/packages.lock.json | 55 ++++++ .../packages.lock.json | 1 + .../packages.lock.json | 1 + .../packages.lock.json | 1 + src/Bicep.Tools.Benchmark/packages.lock.json | 1 + 42 files changed, 1285 insertions(+), 16 deletions(-) create mode 100644 src/Bicep.Local.Deploy/Extensibility/GrpcProxyLocalExtension.cs create mode 100644 src/Bicep.Local.Deploy/Extensibility/ILocalExtensionFactory.cs create mode 100644 src/Bicep.Local.Deploy/Extensibility/ILocalExtensionFactoryManager.cs create mode 100644 src/Bicep.Local.Deploy/Extensibility/ILocalExtensionHost.cs create mode 100644 src/Bicep.Local.Deploy/Extensibility/ILocalExtensionLifecycle.cs create mode 100644 src/Bicep.Local.Deploy/Extensibility/LocalExtensionFactoryManager.cs create mode 100644 src/Bicep.Local.Deploy/Extensibility/LocalExtensionHost.cs create mode 100644 src/Bicep.Local.Deploy/Extensibility/LocalExtensionHostManager.cs create mode 100644 src/Bicep.Local.Deploy/Extensibility/LocalExtensionRegistry.cs create mode 100644 src/Bicep.Local.Extension/BicepExtensionHostBase.cs create mode 100644 src/Bicep.Local.Extension/Protocol/ILocalExtension.cs create mode 100644 src/Bicep.Local.Extension/Protocol/LocalExtensionDispatcher.cs create mode 100644 src/Bicep.Local.Extension/Protocol/LocalExtensionDispatcherBuilder.cs create mode 100644 src/Bicep.Local.Extension/Rpc/BicepGrpcService.cs diff --git a/src/Bicep.Cli.IntegrationTests/packages.lock.json b/src/Bicep.Cli.IntegrationTests/packages.lock.json index dddfeb1919f..d40452e337a 100644 --- a/src/Bicep.Cli.IntegrationTests/packages.lock.json +++ b/src/Bicep.Cli.IntegrationTests/packages.lock.json @@ -1663,6 +1663,7 @@ "Azure.Bicep.Local.Extension": { "type": "Project", "dependencies": { + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "CommandLineParser": "[2.9.1, )", "Google.Protobuf": "[3.27.1, )", "Grpc.Net.Client": "[2.63.0, )" diff --git a/src/Bicep.Cli.UnitTests/packages.lock.json b/src/Bicep.Cli.UnitTests/packages.lock.json index 5e7c11a091c..e7168ab2bed 100644 --- a/src/Bicep.Cli.UnitTests/packages.lock.json +++ b/src/Bicep.Cli.UnitTests/packages.lock.json @@ -1535,6 +1535,7 @@ "Azure.Bicep.Local.Extension": { "type": "Project", "dependencies": { + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "CommandLineParser": "[2.9.1, )", "Google.Protobuf": "[3.27.1, )", "Grpc.Net.Client": "[2.63.0, )" diff --git a/src/Bicep.Cli/Commands/LocalDeployCommand.cs b/src/Bicep.Cli/Commands/LocalDeployCommand.cs index c6c2fd21c9c..9b18a920d3b 100644 --- a/src/Bicep.Cli/Commands/LocalDeployCommand.cs +++ b/src/Bicep.Cli/Commands/LocalDeployCommand.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections.Immutable; +using System.Web.Services.Description; using Azure.Deployments.Core.Json; using Bicep.Cli.Arguments; using Bicep.Cli.Helpers; @@ -12,12 +14,20 @@ using Bicep.Core.TypeSystem.Types; using Bicep.Local.Deploy; using Bicep.Local.Deploy.Extensibility; +using Bicep.Local.Extension.Protocol; using Bicep.Local.Extension.Rpc; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Newtonsoft.Json; namespace Bicep.Cli.Commands; +public class LocalDeployExtensionFactory : ILocalExtensionFactory +{ + public Task CreateLocalExtensionAsync(LocalExtensionKey extensionKey, Uri extensionBinaryUri) + => GrpcProxyLocalExtension.CreateGrpcExtension(extensionBinaryUri); +} + public class LocalDeployCommand : ICommand { private readonly IModuleDispatcher moduleDispatcher; @@ -65,16 +75,54 @@ parameters.Parameters is not { } parametersString || return 1; } - await using LocalExtensibilityHostManager extensibilityHandler = new(moduleDispatcher, GrpcBuiltInLocalExtension.Start); - await extensibilityHandler.InitializeExtensions(compilation); + var serviceProvider = RegisterLocalDeployServices(); + var localExtensionHostManager = serviceProvider.GetService(); + var localDeploy = serviceProvider.GetService(); - var result = await LocalDeployment.Deploy(extensibilityHandler, templateString, parametersString, cancellationToken); + if (localExtensionHostManager is null) + { + throw new ArgumentNullException(nameof(localExtensionHostManager)); + } + + if (localDeploy is null) + { + throw new ArgumentNullException(nameof(localDeploy)); + } + + localExtensionHostManager.InitializeLocalExtensions(GetBinaryExtensions(compilation).ToImmutableList()); + + var result = await localDeploy.Deploy(templateString, parametersString, cancellationToken); await WriteSummary(result); return 0; } - private async Task WriteSummary(LocalDeployment.Result result) + private ServiceProvider RegisterLocalDeployServices() + { + var service = new Microsoft.Extensions.DependencyInjection.ServiceCollection(); + service.RegisterLocalDeployServices(); + service.AddSingleton(); + return service.BuildServiceProvider(); + } + + private IEnumerable GetBinaryExtensions(Compilation compilation) + { + var namespaceTypes = compilation.GetAllBicepModels() + .Select(x => x.Root.NamespaceResolver) + .SelectMany(x => x.GetNamespaceNames().Select(x.TryGetNamespace)) + .WhereNotNull(); + + foreach (var namespaceType in namespaceTypes) + { + if (namespaceType.Artifact is { } artifact && + moduleDispatcher.TryGetProviderBinary(artifact) is { } binaryUri) + { + yield return new(namespaceType, binaryUri); + } + } + } + + private async Task WriteSummary(LocalDeploy.Result result) { if (result.Deployment.Properties.Outputs is { } outputs) { diff --git a/src/Bicep.Cli/Helpers/ServiceCollectionExtensions.cs b/src/Bicep.Cli/Helpers/ServiceCollectionExtensions.cs index eb578dd9fef..468143b0d11 100644 --- a/src/Bicep.Cli/Helpers/ServiceCollectionExtensions.cs +++ b/src/Bicep.Cli/Helpers/ServiceCollectionExtensions.cs @@ -16,6 +16,7 @@ using Bicep.Core.TypeSystem.Providers; using Bicep.Core.Utils; using Bicep.Decompiler; +using Bicep.Local.Deploy.Extensibility; using Microsoft.Extensions.DependencyInjection; using Environment = Bicep.Core.Utils.Environment; using IOFileSystem = System.IO.Abstractions.FileSystem; diff --git a/src/Bicep.Cli/packages.lock.json b/src/Bicep.Cli/packages.lock.json index 6ed90e8af4b..3ecabe0d5b8 100644 --- a/src/Bicep.Cli/packages.lock.json +++ b/src/Bicep.Cli/packages.lock.json @@ -1432,6 +1432,7 @@ "Azure.Bicep.Local.Extension": { "type": "Project", "dependencies": { + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "CommandLineParser": "[2.9.1, )", "Google.Protobuf": "[3.27.1, )", "Grpc.Net.Client": "[2.63.0, )" diff --git a/src/Bicep.Core.IntegrationTests/packages.lock.json b/src/Bicep.Core.IntegrationTests/packages.lock.json index e95b672f331..60cb4d0657f 100644 --- a/src/Bicep.Core.IntegrationTests/packages.lock.json +++ b/src/Bicep.Core.IntegrationTests/packages.lock.json @@ -1567,6 +1567,7 @@ "Azure.Bicep.Local.Extension": { "type": "Project", "dependencies": { + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "CommandLineParser": "[2.9.1, )", "Google.Protobuf": "[3.27.1, )", "Grpc.Net.Client": "[2.63.0, )" diff --git a/src/Bicep.Core.Samples/packages.lock.json b/src/Bicep.Core.Samples/packages.lock.json index 21e53156ca2..efadda3a8ed 100644 --- a/src/Bicep.Core.Samples/packages.lock.json +++ b/src/Bicep.Core.Samples/packages.lock.json @@ -1513,6 +1513,7 @@ "Azure.Bicep.Local.Extension": { "type": "Project", "dependencies": { + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "CommandLineParser": "[2.9.1, )", "Google.Protobuf": "[3.27.1, )", "Grpc.Net.Client": "[2.63.0, )" diff --git a/src/Bicep.Core.UnitTests/packages.lock.json b/src/Bicep.Core.UnitTests/packages.lock.json index 8b8a7b52e78..a364508d50d 100644 --- a/src/Bicep.Core.UnitTests/packages.lock.json +++ b/src/Bicep.Core.UnitTests/packages.lock.json @@ -1519,6 +1519,7 @@ "Azure.Bicep.Local.Extension": { "type": "Project", "dependencies": { + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "CommandLineParser": "[2.9.1, )", "Google.Protobuf": "[3.27.1, )", "Grpc.Net.Client": "[2.63.0, )" diff --git a/src/Bicep.Decompiler.IntegrationTests/packages.lock.json b/src/Bicep.Decompiler.IntegrationTests/packages.lock.json index 151a5b4f4f5..ca867c8a067 100644 --- a/src/Bicep.Decompiler.IntegrationTests/packages.lock.json +++ b/src/Bicep.Decompiler.IntegrationTests/packages.lock.json @@ -1567,6 +1567,7 @@ "Azure.Bicep.Local.Extension": { "type": "Project", "dependencies": { + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "CommandLineParser": "[2.9.1, )", "Google.Protobuf": "[3.27.1, )", "Grpc.Net.Client": "[2.63.0, )" diff --git a/src/Bicep.Decompiler.UnitTests/packages.lock.json b/src/Bicep.Decompiler.UnitTests/packages.lock.json index 151a5b4f4f5..ca867c8a067 100644 --- a/src/Bicep.Decompiler.UnitTests/packages.lock.json +++ b/src/Bicep.Decompiler.UnitTests/packages.lock.json @@ -1567,6 +1567,7 @@ "Azure.Bicep.Local.Extension": { "type": "Project", "dependencies": { + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "CommandLineParser": "[2.9.1, )", "Google.Protobuf": "[3.27.1, )", "Grpc.Net.Client": "[2.63.0, )" diff --git a/src/Bicep.LangServer.IntegrationTests/packages.lock.json b/src/Bicep.LangServer.IntegrationTests/packages.lock.json index 4f607361323..5462dc4cf27 100644 --- a/src/Bicep.LangServer.IntegrationTests/packages.lock.json +++ b/src/Bicep.LangServer.IntegrationTests/packages.lock.json @@ -1524,6 +1524,7 @@ "Azure.Bicep.Local.Extension": { "type": "Project", "dependencies": { + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "CommandLineParser": "[2.9.1, )", "Google.Protobuf": "[3.27.1, )", "Grpc.Net.Client": "[2.63.0, )" diff --git a/src/Bicep.LangServer.UnitTests/packages.lock.json b/src/Bicep.LangServer.UnitTests/packages.lock.json index a1a0adf4114..5117d41c8bb 100644 --- a/src/Bicep.LangServer.UnitTests/packages.lock.json +++ b/src/Bicep.LangServer.UnitTests/packages.lock.json @@ -1587,6 +1587,7 @@ "Azure.Bicep.Local.Extension": { "type": "Project", "dependencies": { + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "CommandLineParser": "[2.9.1, )", "Google.Protobuf": "[3.27.1, )", "Grpc.Net.Client": "[2.63.0, )" diff --git a/src/Bicep.LangServer/packages.lock.json b/src/Bicep.LangServer/packages.lock.json index 19ebb980045..5f941da9540 100644 --- a/src/Bicep.LangServer/packages.lock.json +++ b/src/Bicep.LangServer/packages.lock.json @@ -1409,6 +1409,7 @@ "Azure.Bicep.Local.Extension": { "type": "Project", "dependencies": { + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "CommandLineParser": "[2.9.1, )", "Google.Protobuf": "[3.27.1, )", "Grpc.Net.Client": "[2.63.0, )" diff --git a/src/Bicep.Local.Deploy.IntegrationTests/EndToEndDeploymentTests.cs b/src/Bicep.Local.Deploy.IntegrationTests/EndToEndDeploymentTests.cs index ebd2d079f5c..8fc815a6a07 100644 --- a/src/Bicep.Local.Deploy.IntegrationTests/EndToEndDeploymentTests.cs +++ b/src/Bicep.Local.Deploy.IntegrationTests/EndToEndDeploymentTests.cs @@ -1,10 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections.Immutable; using System.IO.Abstractions; using System.Text; using System.Text.Json.Nodes; using Azure.Deployments.Core.Definitions; +using Azure.Deployments.Core.Extensions; using Azure.Deployments.Engine.Host.Azure.ExtensibilityV2.Contract.Models; using Azure.Deployments.Extensibility.Core.V2.Models; using Azure.Deployments.Extensibility.Messages; @@ -12,6 +14,7 @@ using Bicep.Core.Features; using Bicep.Core.FileSystem; using Bicep.Core.Registry; +using Bicep.Core.Semantics; using Bicep.Core.UnitTests; using Bicep.Core.UnitTests.Assertions; using Bicep.Core.UnitTests.Features; @@ -21,6 +24,8 @@ using Bicep.Local.Deploy.Extensibility; using Bicep.Local.Extension; using FluentAssertions; +using Microsoft.Azure.Deployments.Service.Shared.Jobs; +using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.WindowsAzure.ResourceStack.Common.Json; using Moq; @@ -101,7 +106,7 @@ param coords { { "namespace", "someNamespace" } }; - var providerMock = StrictMock.Of(); + var providerMock = StrictMock.Of(); providerMock.Setup(x => x.CreateOrUpdate(It.Is(req => req.Properties["uri"]!.ToString() == "https://api.weather.gov/points/47.6363726,-122.1357068"), It.IsAny())) .Returns((req, _) => { @@ -114,7 +119,7 @@ param coords { } } """; - return Task.FromResult(new LocalExtensibilityOperationResponse(new Resource(req.Type, req.ApiVersion, identifiers, req.Properties, "Succeeded"), null)); + return Task.FromResult(new Extension.Protocol.LocalExtensionOperationResponse(new Resource(req.Type, req.ApiVersion, identifiers, req.Properties, "Succeeded"), null)); }); providerMock.Setup(x => x.CreateOrUpdate(It.Is(req => req.Properties["uri"]!.ToString() == "https://api.weather.gov/gridpoints/SEW/131,68/forecast"), It.IsAny())) @@ -138,14 +143,24 @@ param coords { } } """; - return Task.FromResult(new LocalExtensibilityOperationResponse(new Resource(req.Type, req.ApiVersion, identifiers, req.Properties, "Succeeded"), null)); + return Task.FromResult(new Extension.Protocol.LocalExtensionOperationResponse(new Resource(req.Type, req.ApiVersion, identifiers, req.Properties, "Succeeded"), null)); }); - var dispatcher = BicepTestConstants.CreateModuleDispatcher(services.Build().Construct()); - await using LocalExtensibilityHostManager extensibilityHandler = new(dispatcher, uri => Task.FromResult(providerMock.Object)); - await extensibilityHandler.InitializeExtensions(result.Compilation); + var localExtensionFactory = StrictMock.Of(); + localExtensionFactory + .Setup(x => x.CreateLocalExtensionAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(providerMock.Object); - var localDeployResult = await LocalDeployment.Deploy(extensibilityHandler, templateFile, parametersFile, TestContext.CancellationTokenSource.Token); + var moduleDispatcher = BicepTestConstants.CreateModuleDispatcher(services.Build().Construct()); + + var extensionFactoryManager = new LocalExtensionFactoryManager(localExtensionFactory.Object); + var extensionHostManager = new LocalExtensionHostManager( + extensionFactoryManager, + new LocalExtensionHost(extensionFactoryManager)); + + extensionHostManager.InitializeLocalExtensions(GetBinaryExtensions(result.Compilation, moduleDispatcher).ToImmutableList()); + + var localDeployResult = await LocalDeployment.Deploy(extensionHostManager, templateFile, parametersFile, TestContext.CancellationTokenSource.Token); localDeployResult.Deployment.Properties.ProvisioningState.Should().Be(ProvisioningState.Succeeded); localDeployResult.Deployment.Properties.Outputs["forecast"].Value.Should().DeepEqual(JToken.Parse(""" @@ -162,6 +177,23 @@ param coords { """)); } + private IEnumerable GetBinaryExtensions(Compilation compilation, IModuleDispatcher moduleDispatcher) + { + var namespaceTypes = compilation.GetAllBicepModels() + .Select(x => x.Root.NamespaceResolver) + .SelectMany(x => x.GetNamespaceNames().Select(x.TryGetNamespace)) + .WhereNotNull(); + + foreach (var namespaceType in namespaceTypes) + { + if (namespaceType.Artifact is { } artifact && + moduleDispatcher.TryGetProviderBinary(artifact) is { } binaryUri) + { + yield return new(namespaceType, binaryUri); + } + } + } + [TestMethod] public async Task Provider_returning_resource_and_error_data_should_fail() { diff --git a/src/Bicep.Local.Deploy.IntegrationTests/packages.lock.json b/src/Bicep.Local.Deploy.IntegrationTests/packages.lock.json index 9ea55d7aa27..c2619f5ee8e 100644 --- a/src/Bicep.Local.Deploy.IntegrationTests/packages.lock.json +++ b/src/Bicep.Local.Deploy.IntegrationTests/packages.lock.json @@ -1577,6 +1577,7 @@ "Azure.Bicep.Local.Extension": { "type": "Project", "dependencies": { + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "CommandLineParser": "[2.9.1, )", "Google.Protobuf": "[3.27.1, )", "Grpc.Net.Client": "[2.63.0, )" diff --git a/src/Bicep.Local.Deploy/Extensibility/GrpcProxyLocalExtension.cs b/src/Bicep.Local.Deploy/Extensibility/GrpcProxyLocalExtension.cs new file mode 100644 index 00000000000..83e3b56a347 --- /dev/null +++ b/src/Bicep.Local.Deploy/Extensibility/GrpcProxyLocalExtension.cs @@ -0,0 +1,174 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Text.Json.Nodes; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core; +using Bicep.Local.Extension.Protocol; +using Bicep.Local.Extension.Rpc; +using Google.Protobuf.Collections; +using Json.Pointer; +using ExtensibilityV2 = Azure.Deployments.Extensibility.Core.V2.Models; +using Rpc = Bicep.Local.Extension.Rpc; + +namespace Bicep.Local.Deploy.Extensibility +{ + public class GrpcProxyLocalExtension : ILocalExtension, IAsyncDisposable + { + private readonly BicepExtension.BicepExtensionClient client; + private readonly Process process; + + private GrpcProxyLocalExtension(BicepExtension.BicepExtensionClient client, Process process) + { + this.client = client; + this.process = process; + } + + public static async Task CreateGrpcExtension(Uri pathToBinary) + { + var socketName = $"{Guid.NewGuid()}.tmp"; + var socketPath = Path.Combine(Path.GetTempPath(), socketName); + + if (File.Exists(socketPath)) + { + File.Delete(socketPath); + } + + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = pathToBinary.LocalPath, + Arguments = $"--socket {socketPath}", + UseShellExecute = false, + RedirectStandardError = true, + RedirectStandardOutput = true, + }, + }; + + try + { + // 30s timeout for starting up the RPC connection + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + + process.EnableRaisingEvents = true; + process.Exited += (sender, e) => cts.Cancel(); + process.OutputDataReceived += (sender, e) => Trace.WriteLine($"{pathToBinary} stdout: {e.Data}"); + process.ErrorDataReceived += (sender, e) => Trace.WriteLine($"{pathToBinary} stderr: {e.Data}"); + + process.Start(); + + process.BeginErrorReadLine(); + process.BeginOutputReadLine(); + + var channel = GrpcChannelHelper.CreateChannel(socketPath); + var client = new BicepExtension.BicepExtensionClient(channel); + + await GrpcChannelHelper.WaitForConnectionAsync(client, cts.Token); + + return new GrpcProxyLocalExtension(client, process); + } + catch (Exception ex) + { + await TerminateProcess(process); + throw new InvalidOperationException($"Failed to connect to provider {pathToBinary.LocalPath}", ex); + } + } + + public async Task CreateOrUpdate(ExtensibilityV2.ResourceSpecification request, CancellationToken cancellationToken) + => Convert(await client.CreateOrUpdateAsync(Convert(request), cancellationToken: cancellationToken)); + + public async Task Delete(ExtensibilityV2.ResourceReference request, CancellationToken cancellationToken) + => Convert(await client.DeleteAsync(Convert(request), cancellationToken: cancellationToken)); + + public async Task Get(ExtensibilityV2.ResourceReference request, CancellationToken cancellationToken) + => Convert(await client.GetAsync(Convert(request), cancellationToken: cancellationToken)); + + public async Task Preview(ExtensibilityV2.ResourceSpecification request, CancellationToken cancellationToken) + => Convert(await client.PreviewAsync(Convert(request), cancellationToken: cancellationToken)); + + private static Rpc.ResourceReference Convert(ExtensibilityV2.ResourceReference request) + { + Rpc.ResourceReference output = new() + { + Type = request.Type, + Identifiers = request.Identifiers.ToJsonString(), + }; + + if (request.ApiVersion is { }) + { + output.ApiVersion = request.ApiVersion; + } + if (request.Config is { }) + { + output.Config = request.Config.ToJsonString(); + } + + return output; + } + + private static Rpc.ResourceSpecification Convert(ExtensibilityV2.ResourceSpecification request) + { + Rpc.ResourceSpecification output = new() + { + Type = request.Type, + Properties = request.Properties.ToJsonString(), + }; + + if (request.ApiVersion is { }) + { + output.ApiVersion = request.ApiVersion; + } + if (request.Config is { }) + { + output.Config = request.Config.ToJsonString(); + } + + return output; + } + + private static ExtensibilityV2.ErrorData Convert(Rpc.ErrorData errorData) + => new(new ExtensibilityV2.Error(errorData.Error.Code, errorData.Error.Message, JsonPointer.Empty, Convert(errorData.Error.Details), ConvertInnerError(errorData.Error.InnerError))); + + private static ExtensibilityV2.ErrorDetail[]? Convert(RepeatedField? details) + => details is not null ? details.Select(Convert).ToArray() : null; + + private static ExtensibilityV2.ErrorDetail Convert(Rpc.ErrorDetail detail) + => new(detail.Code, detail.Message, JsonPointer.Empty); + + private static LocalExtensionOperationResponse Convert(Rpc.LocalExtensibilityOperationResponse response) + => new( + response.Resource is { } ? new(response.Resource.Type, response.Resource.ApiVersion, ToJsonObject(response.Resource.Identifiers, "Parsing response identifiers failed. Please ensure is non-null or empty and is a valid JSON object."), ToJsonObject(response.Resource.Properties, "Parsing response properties failed. Please ensure is non-null or empty and is ensure is a valid JSON object."), response.Resource.Status) : null, + response.ErrorData is { } ? Convert(response.ErrorData) : null); + + private static JsonObject? ConvertInnerError(string innerError) + => string.IsNullOrEmpty(innerError) ? null : ToJsonObject(innerError, "Parsing innerError failed. Please ensure is non-null or empty and is a valid JSON object."); + + private static JsonObject ToJsonObject(string json, string errorMessage) + => JsonNode.Parse(json)?.AsObject() ?? throw new ArgumentNullException(errorMessage); + + public async ValueTask DisposeAsync() + { + await TerminateProcess(process); + } + + private static async Task TerminateProcess(Process process) + { + try + { + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15)); + await process.WaitForExitAsync(cts.Token); + } + finally + { + process.Kill(); + } + } + } +} diff --git a/src/Bicep.Local.Deploy/Extensibility/ILocalExtensionFactory.cs b/src/Bicep.Local.Deploy/Extensibility/ILocalExtensionFactory.cs new file mode 100644 index 00000000000..7b39c3c7576 --- /dev/null +++ b/src/Bicep.Local.Deploy/Extensibility/ILocalExtensionFactory.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Bicep.Local.Extension.Protocol; + +namespace Bicep.Local.Deploy.Extensibility +{ + public interface ILocalExtensionFactory + { + Task CreateLocalExtensionAsync(LocalExtensionKey extensionKey, Uri extensionBinaryUri); + } +} diff --git a/src/Bicep.Local.Deploy/Extensibility/ILocalExtensionFactoryManager.cs b/src/Bicep.Local.Deploy/Extensibility/ILocalExtensionFactoryManager.cs new file mode 100644 index 00000000000..0f807810345 --- /dev/null +++ b/src/Bicep.Local.Deploy/Extensibility/ILocalExtensionFactoryManager.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Bicep.Local.Extension.Protocol; + +namespace Bicep.Local.Deploy.Extensibility +{ + public interface ILocalExtensionFactoryManager + { + void InitializeLocalExtensions(IReadOnlyList binaryExtensions); + Task GetLocalExtensionAsync(string providerName, string providerVersion); + } +} diff --git a/src/Bicep.Local.Deploy/Extensibility/ILocalExtensionHost.cs b/src/Bicep.Local.Deploy/Extensibility/ILocalExtensionHost.cs new file mode 100644 index 00000000000..37ba17f9528 --- /dev/null +++ b/src/Bicep.Local.Deploy/Extensibility/ILocalExtensionHost.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bicep.Local.Deploy.Extensibility +{ + public interface ILocalExtensionHost + { + Task CallExtensibilityHost(LocalDeploymentEngineHost.ExtensionInfo extensionInfo, HttpContent content, CancellationToken cancellationToken); + } +} diff --git a/src/Bicep.Local.Deploy/Extensibility/ILocalExtensionLifecycle.cs b/src/Bicep.Local.Deploy/Extensibility/ILocalExtensionLifecycle.cs new file mode 100644 index 00000000000..79b125d7af2 --- /dev/null +++ b/src/Bicep.Local.Deploy/Extensibility/ILocalExtensionLifecycle.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bicep.Local.Deploy.Extensibility +{ + public interface ILocalExtensionLifecycle : IDisposable + { + } +} diff --git a/src/Bicep.Local.Deploy/Extensibility/LocalExtensionFactoryManager.cs b/src/Bicep.Local.Deploy/Extensibility/LocalExtensionFactoryManager.cs new file mode 100644 index 00000000000..44529591660 --- /dev/null +++ b/src/Bicep.Local.Deploy/Extensibility/LocalExtensionFactoryManager.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Bicep.Core.TypeSystem.Types; +using Bicep.Local.Extension.Protocol; +using Google.Protobuf; + +namespace Bicep.Local.Deploy.Extensibility +{ + public record LocalExtensionKey(string ExtensionName, string ExtensionVersion); + + public class LocalExtensionFactoryManager(ILocalExtensionFactory extensionFactory) : ILocalExtensionFactoryManager + { + private readonly Dictionary>> RegisteredLocalExtensions = []; + private readonly Dictionary InitializedLocalExtensions = []; + + public void InitializeLocalExtensions(IReadOnlyList binaryExtensions) + { + foreach (var (namespaceType, binaryUri) in binaryExtensions) + { + LocalExtensionKey providerKey = new(namespaceType.Settings.ArmTemplateProviderName, namespaceType.Settings.ArmTemplateProviderVersion); + RegisteredLocalExtensions[providerKey] = () => extensionFactory.CreateLocalExtensionAsync(providerKey, binaryUri); + } + + RegisterBuiltInExtensions(); + } + + private void RegisterBuiltInExtensions() { } + + public async Task GetLocalExtensionAsync(string providerName, string providerVersion) + { + LocalExtensionKey providerKey = new(providerName, providerVersion); + if (!RegisteredLocalExtensions.TryGetValue(providerKey, out var localExtension)) + { + throw new ArgumentException($"Provider name: '{providerName}' and version: '{providerVersion}' was not found."); + } + + // Ensure loading the extension once and on-demand. + if (!InitializedLocalExtensions.TryGetValue(providerKey, out var initializedLocalExtension)) + { + initializedLocalExtension = await localExtension(); + InitializedLocalExtensions[providerKey] = initializedLocalExtension; + } + + return initializedLocalExtension; + } + } +} diff --git a/src/Bicep.Local.Deploy/Extensibility/LocalExtensionHost.cs b/src/Bicep.Local.Deploy/Extensibility/LocalExtensionHost.cs new file mode 100644 index 00000000000..6d1cf7ca645 --- /dev/null +++ b/src/Bicep.Local.Deploy/Extensibility/LocalExtensionHost.cs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using System.Threading.Tasks; +using Azure.Deployments.Extensibility.Core.V2.Json; +using Bicep.Local.Extension.Protocol; + +namespace Bicep.Local.Deploy.Extensibility +{ + public class LocalExtensionHost : ILocalExtensionHost + { + private readonly ILocalExtensionFactoryManager extensionFactoryManager; + + public LocalExtensionHost(ILocalExtensionFactoryManager extensionFactoryManager) + { + this.extensionFactoryManager = extensionFactoryManager; + } + + public async Task CallExtensibilityHost(LocalDeploymentEngineHost.ExtensionInfo extensionInfo, HttpContent content, CancellationToken cancellationToken) + { + var extension = await extensionFactoryManager.GetLocalExtensionAsync(extensionInfo.ExtensionName, extensionInfo.ExtensionVersion); + var response = await CallExtension(extensionInfo.Method, extension, content, cancellationToken); + + // DeploymentEngine performs header validation and expects these two to always be set. + response.Headers.Add("Location", "local"); + response.Headers.Add("Version", extensionInfo.ExtensionVersion); + + return response; + } + + private async Task CallExtension(string method, ILocalExtension provider, HttpContent content, CancellationToken cancellationToken) + { + switch (method) + { + case "createOrUpdate": + { + var resourceSpecification = await GetResourceSpecificationAsync(await content.ReadAsStreamAsync(cancellationToken), cancellationToken); + var extensionResponse = await provider.CreateOrUpdate(resourceSpecification, cancellationToken); + + return await GetHttpResponseMessageAsync(extensionResponse, cancellationToken); + } + case "delete": + { + var resourceReference = await GetResourceReferenceAsync(await content.ReadAsStreamAsync(cancellationToken), cancellationToken); + var extensionResponse = await provider.Delete(resourceReference, cancellationToken); + + return await GetHttpResponseMessageAsync(extensionResponse, cancellationToken); + } + case "get": + { + var resourceReference = await GetResourceReferenceAsync(await content.ReadAsStreamAsync(cancellationToken), cancellationToken); + var extensionResponse = await provider.Delete(resourceReference, cancellationToken); + + return await GetHttpResponseMessageAsync(extensionResponse, cancellationToken); + } + case "preview": + { + var resourceSpecification = await GetResourceSpecificationAsync(await content.ReadAsStreamAsync(cancellationToken), cancellationToken); + var extensionResponse = await provider.CreateOrUpdate(resourceSpecification, cancellationToken); + + return await GetHttpResponseMessageAsync(extensionResponse, cancellationToken); + } + default: + throw new NotImplementedException($"Unsupported method {method}"); + } + } + + private async Task GetResourceSpecificationAsync(Stream stream, CancellationToken cancellationToken) + => await DeserializeAsync( + stream, + JsonDefaults.SerializerContext.ResourceSpecification, + $"Deserializing '{nameof(global::Azure.Deployments.Extensibility.Core.V2.Models.ResourceSpecification)}' failed. Please ensure the request body contains a valid JSON object.", + cancellationToken); + + private async Task GetResourceReferenceAsync(Stream stream, CancellationToken cancellationToken) + => await DeserializeAsync( + stream, + JsonDefaults.SerializerContext.ResourceReference, + $"Deserializing '{nameof(Azure.Deployments.Extensibility.Core.V2.Models.ResourceReference)}' failed. Please ensure the request body contains a valid JSON object.", + cancellationToken); + + private async Task DeserializeAsync(Stream stream, JsonTypeInfo typeInfo, string errorMessage, CancellationToken cancellationToken) + => await JsonSerializer.DeserializeAsync(stream, typeInfo, cancellationToken) ?? throw new ArgumentNullException(errorMessage); + + private async Task GetHttpResponseMessageAsync(LocalExtensionOperationResponse extensionResponse, CancellationToken cancellationToken) + { + if (extensionResponse.Resource is { } && extensionResponse.ErrorData is { }) + { + throw new ArgumentException($"Setting '{nameof(LocalExtensibilityOperationResponse.ErrorData)}' and '{nameof(LocalExtensibilityOperationResponse.Resource)}' is not valid. Please make sure to set one of these properties."); + } + + if (extensionResponse.Resource is not { } && extensionResponse.ErrorData is not { }) + { + throw new ArgumentException($"'{nameof(LocalExtensibilityOperationResponse.ErrorData)}' and '{nameof(LocalExtensibilityOperationResponse.Resource)}' cannot be both empty. Please make sure to set one of these properties."); + } + + var memoryStream = new MemoryStream(); + if (extensionResponse.ErrorData is { }) + { + await JsonSerializer.SerializeAsync(memoryStream, extensionResponse.ErrorData, JsonDefaults.SerializerContext.ErrorData, cancellationToken); + memoryStream.Position = 0; + var streamContent = new StreamContent(memoryStream); + streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + + return new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest) + { + Content = streamContent + }; + } + else if (extensionResponse.Resource is { }) + { + await JsonSerializer.SerializeAsync(memoryStream, extensionResponse.Resource, JsonDefaults.SerializerContext.Resource, cancellationToken); + memoryStream.Position = 0; + var streamContent = new StreamContent(memoryStream); + streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + + return new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = streamContent + }; + } + + throw new UnreachableException($"Should not reach here, either '{nameof(LocalExtensibilityOperationResponse.ErrorData)}' or '{nameof(LocalExtensibilityOperationResponse.Resource)}' should have been set."); + } + } +} diff --git a/src/Bicep.Local.Deploy/Extensibility/LocalExtensionHostManager.cs b/src/Bicep.Local.Deploy/Extensibility/LocalExtensionHostManager.cs new file mode 100644 index 00000000000..fecd1a73e95 --- /dev/null +++ b/src/Bicep.Local.Deploy/Extensibility/LocalExtensionHostManager.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Metadata.Ecma335; +using System.Text; +using System.Threading.Tasks; +using Bicep.Core.TypeSystem.Types; +using Google.Protobuf; + +namespace Bicep.Local.Deploy.Extensibility +{ + public record BinaryExtensionReference(NamespaceType NamespaceType, Uri BinaryExtensionUri); + + public class LocalExtensionHostManager : IAsyncDisposable, ILocalExtensionHost + { + private readonly ILocalExtensionFactoryManager localExtensionFactoryManager; + private readonly ILocalExtensionHost localExtensionHost; + private bool LocalExtensionsInitialized; + + public LocalExtensionHostManager(ILocalExtensionFactoryManager localExtensionFactoryManager, ILocalExtensionHost localExtensionHost) + { + this.localExtensionFactoryManager = localExtensionFactoryManager; + this.localExtensionHost = localExtensionHost; + } + + public void InitializeLocalExtensions(IReadOnlyList binaryExtensions) + { + if (LocalExtensionsInitialized == false) + { + localExtensionFactoryManager.InitializeLocalExtensions(binaryExtensions); + LocalExtensionsInitialized = true; + } + } + + public void InitializeLocalExtensionsLifecycleManagement() { } + + public Task CallExtensibilityHost(LocalDeploymentEngineHost.ExtensionInfo extensionInfo, HttpContent content, CancellationToken cancellationToken) + { + EnsureLocalExtensionsInitialized(); + return localExtensionHost.CallExtensibilityHost(extensionInfo, content, cancellationToken); + } + + private void EnsureLocalExtensionsInitialized() + { + if (LocalExtensionsInitialized == false) + { + throw new ArgumentNullException($"Local extensions not initialized. Make sure to invoke: '{nameof(InitializeLocalExtensions)}' first."); + } + } + + ValueTask IAsyncDisposable.DisposeAsync() + { + return ValueTask.CompletedTask; + } + } +} diff --git a/src/Bicep.Local.Deploy/Extensibility/LocalExtensionRegistry.cs b/src/Bicep.Local.Deploy/Extensibility/LocalExtensionRegistry.cs new file mode 100644 index 00000000000..c35bc30dc8c --- /dev/null +++ b/src/Bicep.Local.Deploy/Extensibility/LocalExtensionRegistry.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bicep.Local.Deploy.Extensibility +{ + public class LocalExtensionRegistry + { + public LocalExtensionRegistry(ILocalExtensionFactory factory) + { + } + + public void InitializeLocalExtensions() + { + RetrieveLocalExtensions(); + RegisterLocalExtensions(); + LoadLocalExtensions(); + } + + private void RetrieveLocalExtensions() + { + + } + + private void RegisterLocalExtensions() + { + + } + + private void LoadLocalExtensions() + { + + } + } +} diff --git a/src/Bicep.Local.Deploy/IServiceCollectionExtensions.cs b/src/Bicep.Local.Deploy/IServiceCollectionExtensions.cs index 818c9a41f2b..f4b17f867da 100644 --- a/src/Bicep.Local.Deploy/IServiceCollectionExtensions.cs +++ b/src/Bicep.Local.Deploy/IServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Linq; +using System.Web.Services.Description; using Azure.Deployments.Core.EventSources; using Azure.Deployments.Core.Exceptions; using Azure.Deployments.Core.FeatureEnablement; @@ -72,6 +73,100 @@ public static IServiceCollection RegisterLocalDeployServices(this IServiceCollec services.AddSingleton(extensibilityHandler); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + return services; + } + + public static IServiceCollection RegisterLocalDeployServices(this IServiceCollection services) + { + var eventSource = new TraceEventSource(); + services.AddSingleton(eventSource); + services.AddSingleton(eventSource); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + var jobConfiguration = new JobConfigurationBase + { + Location = "local", + EventSource = eventSource, + }; + services.AddSingleton(jobConfiguration); + RegisterJobsAsService(services); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + return services; + } + + public static IServiceCollection RegisterLocalDeployServices(this IServiceCollection services, LocalExtensionHostManager extensionHostManager) + { + var eventSource = new TraceEventSource(); + services.AddSingleton(eventSource); + services.AddSingleton(eventSource); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + var jobConfiguration = new JobConfigurationBase + { + Location = "local", + EventSource = eventSource, + }; + services.AddSingleton(jobConfiguration); + RegisterJobsAsService(services); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(extensionHostManager); + + services.AddSingleton(); + services.AddSingleton(); return services; } diff --git a/src/Bicep.Local.Deploy/LocalDeployment.cs b/src/Bicep.Local.Deploy/LocalDeployment.cs index 254278ffff2..88977a7c6c2 100644 --- a/src/Bicep.Local.Deploy/LocalDeployment.cs +++ b/src/Bicep.Local.Deploy/LocalDeployment.cs @@ -11,6 +11,35 @@ namespace Bicep.Local.Deploy; +public class LocalDeploy +{ + public record Result( + DeploymentContent Deployment, + ImmutableArray Operations); + + private readonly LocalDeploymentEngine deploymentEngine; + private readonly WorkerJobDispatcherClient dispatcherClient; + + public LocalDeploy(LocalDeploymentEngine deploymentEngine, WorkerJobDispatcherClient dispatcherClient) + { + this.deploymentEngine = deploymentEngine; + this.dispatcherClient = dispatcherClient; + } + + public async Task Deploy(string templateString, string parametersString, CancellationToken cancellationToken) + { + try + { + var result = await deploymentEngine.Deploy(templateString, parametersString, cancellationToken); + return new(result.Deployment, result.Operations); + } + finally + { + await dispatcherClient.StopAsync(); + } + } +} + public static class LocalDeployment { public record Result( @@ -35,4 +64,23 @@ public static async Task Deploy(LocalExtensibilityHostManager extensibil await dispatcher.StopAsync(); } } + + public static async Task Deploy(LocalExtensionHostManager extensionHostManager, string templateString, string parametersString, CancellationToken cancellationToken) + { + var services = new ServiceCollection() + .RegisterLocalDeployServices(extensionHostManager) + .BuildServiceProvider(); + + var engine = services.GetRequiredService(); + var dispatcher = services.GetRequiredService(); + + try + { + return await engine.Deploy(templateString, parametersString, cancellationToken); + } + finally + { + await dispatcher.StopAsync(); + } + } } diff --git a/src/Bicep.Local.Deploy/LocalDeploymentEngine.cs b/src/Bicep.Local.Deploy/LocalDeploymentEngine.cs index 5642d154d2a..8a877a8e27f 100644 --- a/src/Bicep.Local.Deploy/LocalDeploymentEngine.cs +++ b/src/Bicep.Local.Deploy/LocalDeploymentEngine.cs @@ -31,7 +31,7 @@ namespace Bicep.Local.Deploy; -internal class LocalDeploymentEngine +public class LocalDeploymentEngine { public LocalDeploymentEngine( diff --git a/src/Bicep.Local.Deploy/LocalDeploymentEngineHost.cs b/src/Bicep.Local.Deploy/LocalDeploymentEngineHost.cs index 5db42f3fa6c..8df184eec0b 100644 --- a/src/Bicep.Local.Deploy/LocalDeploymentEngineHost.cs +++ b/src/Bicep.Local.Deploy/LocalDeploymentEngineHost.cs @@ -28,12 +28,12 @@ namespace Bicep.Local.Deploy; public class LocalDeploymentEngineHost : DeploymentEngineHostBase { - private readonly LocalExtensibilityHostManager extensibilityHandler; + private readonly LocalExtensionHostManager extensionHostManager; public readonly record struct ExtensionInfo(string ExtensionName, string ExtensionVersion, string Method); public LocalDeploymentEngineHost( - LocalExtensibilityHostManager extensibilityHandler, + LocalExtensionHostManager extensionHostManager, IDeploymentsRequestContext requestContext, IDeploymentEventSource deploymentEventSource, IKeyVaultDataProvider keyVaultDataProvider, @@ -43,7 +43,7 @@ public LocalDeploymentEngineHost( IEnablementConfigProvider enablementConfigProvider) : base(settings, deploymentEventSource, keyVaultDataProvider, requestContext, dataProviderHolder, exceptionHandler, enablementConfigProvider) { - this.extensibilityHandler = extensibilityHandler; + this.extensionHostManager = extensionHostManager; } public override Task DownloadContent(Uri requestUri, CancellationToken cancellationToken) @@ -138,7 +138,7 @@ public override async Task CallExtensibilityHostV2( var extensionInfo = new ExtensionInfo(extensionName, extensionVersion, method); - return await extensibilityHandler.CallExtensibilityHost(extensionInfo, content, cancellationToken); + return await extensionHostManager.CallExtensibilityHost(extensionInfo, content, cancellationToken); } protected override Task GetEnvironmentKey() diff --git a/src/Bicep.Local.Deploy/packages.lock.json b/src/Bicep.Local.Deploy/packages.lock.json index 25de4edc7e0..bc82d4ef6d5 100644 --- a/src/Bicep.Local.Deploy/packages.lock.json +++ b/src/Bicep.Local.Deploy/packages.lock.json @@ -1280,6 +1280,7 @@ "Azure.Bicep.Local.Extension": { "type": "Project", "dependencies": { + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "CommandLineParser": "[2.9.1, )", "Google.Protobuf": "[3.27.1, )", "Grpc.Net.Client": "[2.63.0, )" diff --git a/src/Bicep.Local.Extension.Mock/packages.lock.json b/src/Bicep.Local.Extension.Mock/packages.lock.json index c5dacdeed3b..1bb528a38a9 100644 --- a/src/Bicep.Local.Extension.Mock/packages.lock.json +++ b/src/Bicep.Local.Extension.Mock/packages.lock.json @@ -117,6 +117,17 @@ "Newtonsoft.Json": "13.0.2" } }, + "Azure.Deployments.Extensibility.Core": { + "type": "Transitive", + "resolved": "0.1.55", + "contentHash": "iMZhx89YLqHaPGA20LXlzDBty7ov/UgOdxLudJtYwBXkalfSRHLPNKRnJVeGM3EZc9897LeoPyfJ8NvyLeZcgQ==", + "dependencies": { + "JsonPatch.Net": "3.1.0", + "JsonPath.Net": "1.1.0", + "JsonPointer.Net": "5.0.0", + "JsonSchema.Net": "7.0.4" + } + }, "Azure.Deployments.Templates": { "type": "Transitive", "resolved": "1.71.0", @@ -267,6 +278,14 @@ "Json.More.Net": "2.0.1.2" } }, + "JsonSchema.Net": { + "type": "Transitive", + "resolved": "7.0.4", + "contentHash": "R0Hk2Tr/np4Q1NO8CBjyQsoiD1iFJyEQP20Sw7JnZCNGJoaSBe+g4b+nZqnBXPQhiqY5LGZ8JZwZkRh/eKZhEQ==", + "dependencies": { + "JsonPointer.Net": "5.0.0" + } + }, "Microsoft.Automata.SRM": { "type": "Transitive", "resolved": "1.2.2", @@ -1068,6 +1087,7 @@ "Azure.Bicep.Local.Extension": { "type": "Project", "dependencies": { + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "CommandLineParser": "[2.9.1, )", "Google.Protobuf": "[3.27.1, )", "Grpc.Net.Client": "[2.63.0, )" diff --git a/src/Bicep.Local.Extension/Bicep.Local.Extension.csproj b/src/Bicep.Local.Extension/Bicep.Local.Extension.csproj index 03745e3764b..6f39aa7d387 100644 --- a/src/Bicep.Local.Extension/Bicep.Local.Extension.csproj +++ b/src/Bicep.Local.Extension/Bicep.Local.Extension.csproj @@ -22,6 +22,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Bicep.Local.Extension/BicepExtensionHostBase.cs b/src/Bicep.Local.Extension/BicepExtensionHostBase.cs new file mode 100644 index 00000000000..5a531a252a2 --- /dev/null +++ b/src/Bicep.Local.Extension/BicepExtensionHostBase.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics; +using Bicep.Local.Extension.Protocol; +using Bicep.Local.Extension.Rpc; +using CommandLine; + +namespace Bicep.Local.Extension +{ + public abstract class BicepExtensionHostBase + { + internal class CommandLineOptions + { + [Option("socket", Required = false, HelpText = "The path to the domain socket to connect on")] + public string? Socket { get; set; } + + [Option("wait-for-debugger", Required = false, HelpText = "If set, wait for a dotnet debugger to be attached before starting the server")] + public bool WaitForDebugger { get; set; } + } + + public static async Task Run(BicepExtensionHostBase extension, Action registerHandlers, string[] args) + => await RunWithCancellationAsync(async cancellationToken => + { + if (IsTracingEnabled) + { + Trace.Listeners.Add(new TextWriterTraceListener(Console.Error)); + } + + await extension.RunAsync(args, registerHandlers, cancellationToken); + }); + + public async Task RunAsync(string[] args, Action registerHandlers, CancellationToken cancellationToken) + { + var parser = new Parser(settings => + { + settings.IgnoreUnknownArguments = true; + }); + + await parser.ParseArguments(args) + .WithNotParsed((x) => System.Environment.Exit(1)) + .WithParsedAsync(async options => await RunServer(registerHandlers, options, cancellationToken)); + } + + protected abstract Task RunServer(string socketPath, LocalExtensionDispatcher dispatcher, CancellationToken cancellationToken); + + private async Task RunServer(Action registerHandlers, CommandLineOptions options, CancellationToken cancellationToken) + { + if (options.WaitForDebugger) + { + // exit if we don't have a debugger attached within 5 minutes + var debuggerTimeoutToken = CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken, + new CancellationTokenSource(TimeSpan.FromMinutes(5)).Token).Token; + + while (!Debugger.IsAttached) + { + await Task.Delay(100, debuggerTimeoutToken); + } + + Debugger.Break(); + } + + var handlerBuilder = new LocalExtensionDispatcherBuilder(); + registerHandlers(handlerBuilder); + var dispatcher = handlerBuilder.Build(); + + if (options.Socket is { } socketPath) + { + await Task.WhenAny(RunServer(socketPath, dispatcher, cancellationToken), WaitForCancellation(cancellationToken)); + return; + } + + throw new NotImplementedException(); + } + + private static async Task WaitForCancellation(CancellationToken cancellationToken) + { + try + { + await Task.Delay(-1, cancellationToken); + } + catch (TaskCanceledException ex) when (ex.CancellationToken == cancellationToken) + { + // don't throw - continue + } + } + + private static async Task RunWithCancellationAsync(Func runFunc) + { + var cancellationTokenSource = new CancellationTokenSource(); + + Console.CancelKeyPress += (sender, e) => + { + cancellationTokenSource.Cancel(); + e.Cancel = true; + }; + + AppDomain.CurrentDomain.ProcessExit += (sender, e) => + { + cancellationTokenSource.Cancel(); + }; + + try + { + await runFunc(cancellationTokenSource.Token); + } + catch (OperationCanceledException exception) when (exception.CancellationToken == cancellationTokenSource.Token) + { + // this is expected - no need to rethrow + } + } + + private static bool IsTracingEnabled + => bool.TryParse(Environment.GetEnvironmentVariable("BICEP_TRACING_ENABLED"), out var value) && value; + } +} diff --git a/src/Bicep.Local.Extension/Protocol/ILocalExtension.cs b/src/Bicep.Local.Extension/Protocol/ILocalExtension.cs new file mode 100644 index 00000000000..444374ea8ad --- /dev/null +++ b/src/Bicep.Local.Extension/Protocol/ILocalExtension.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bicep.Local.Extension.Protocol +{ + public abstract class LocalExtension : ILocalExtension + { + private bool initialized; + private ILocalExtension? InitializedExtension; + + public abstract Task InitializeAsync(); + + public async Task EnsureInitializedAsync() + { + if (initialized == false || InitializedExtension is null) + { + InitializedExtension = await InitializeAsync(); + initialized = true; + } + return InitializedExtension; + } + + public abstract Task CreateOrUpdate(Azure.Deployments.Extensibility.Core.V2.Models.ResourceSpecification request, CancellationToken cancellationToken); + + public abstract Task Delete(Azure.Deployments.Extensibility.Core.V2.Models.ResourceReference request, CancellationToken cancellationToken); + + public abstract Task Get(Azure.Deployments.Extensibility.Core.V2.Models.ResourceReference request, CancellationToken cancellationToken); + + public abstract Task Preview(Azure.Deployments.Extensibility.Core.V2.Models.ResourceSpecification request, CancellationToken cancellationToken); + } + + public interface ILocalExtension + { + Task CreateOrUpdate( + Azure.Deployments.Extensibility.Core.V2.Models.ResourceSpecification request, + CancellationToken cancellationToken); + + Task Delete( + Azure.Deployments.Extensibility.Core.V2.Models.ResourceReference request, + CancellationToken cancellationToken); + + Task Get( + Azure.Deployments.Extensibility.Core.V2.Models.ResourceReference request, + CancellationToken cancellationToken); + + Task Preview( + Azure.Deployments.Extensibility.Core.V2.Models.ResourceSpecification request, + CancellationToken cancellationToken); + } +} diff --git a/src/Bicep.Local.Extension/Protocol/IResourceHandler.cs b/src/Bicep.Local.Extension/Protocol/IResourceHandler.cs index fe9d3b7fcd1..8f1d7bf56ef 100644 --- a/src/Bicep.Local.Extension/Protocol/IResourceHandler.cs +++ b/src/Bicep.Local.Extension/Protocol/IResourceHandler.cs @@ -47,6 +47,10 @@ public record LocalExtensibilityOperationResponse( Resource? Resource, ErrorData? ErrorData); +public record LocalExtensionOperationResponse( + Azure.Deployments.Extensibility.Core.V2.Models.Resource? Resource, + Azure.Deployments.Extensibility.Core.V2.Models.ErrorData? ErrorData); + public interface IGenericResourceHandler { Task CreateOrUpdate( @@ -70,3 +74,8 @@ public interface IResourceHandler : IGenericResourceHandler { string ResourceType { get; } } + +public interface ILocalExtensionHandler : ILocalExtension +{ + string ResourceType { get; } +} diff --git a/src/Bicep.Local.Extension/Protocol/LocalExtensionDispatcher.cs b/src/Bicep.Local.Extension/Protocol/LocalExtensionDispatcher.cs new file mode 100644 index 00000000000..ca19eb3a5ff --- /dev/null +++ b/src/Bicep.Local.Extension/Protocol/LocalExtensionDispatcher.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bicep.Local.Extension.Protocol +{ + public class LocalExtensionDispatcher + { + private readonly ILocalExtension? genericResourceHandler; + private readonly ImmutableDictionary resourceHandlers; + + public LocalExtensionDispatcher( + ILocalExtension? genericResourceHandler, + ImmutableDictionary resourceHandlers) + { + this.genericResourceHandler = genericResourceHandler; + this.resourceHandlers = resourceHandlers; + } + + public ILocalExtension GetHandler(string resourceType) + { + if (this.resourceHandlers.TryGetValue(resourceType, out var handler)) + { + return handler; + } + + if (this.genericResourceHandler is { }) + { + return this.genericResourceHandler; + } + + throw new ArgumentException($"Resource type '{resourceType}' is not supported."); + } + } +} diff --git a/src/Bicep.Local.Extension/Protocol/LocalExtensionDispatcherBuilder.cs b/src/Bicep.Local.Extension/Protocol/LocalExtensionDispatcherBuilder.cs new file mode 100644 index 00000000000..0c1b7ad7368 --- /dev/null +++ b/src/Bicep.Local.Extension/Protocol/LocalExtensionDispatcherBuilder.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bicep.Local.Extension.Protocol +{ + public class LocalExtensionDispatcherBuilder + { + private ILocalExtension? genericResourceHandler; + private readonly Dictionary resourceHandlers = new(StringComparer.OrdinalIgnoreCase); + + public LocalExtensionDispatcherBuilder AddHandler(ILocalExtensionHandler handler) + { + if (!this.resourceHandlers.TryAdd(handler.ResourceType, handler)) + { + throw new ArgumentException($"Resource type '{handler.ResourceType}' has already been registered."); + } + + this.resourceHandlers[handler.ResourceType] = handler; + return this; + } + + public LocalExtensionDispatcherBuilder AddGenericHandler(ILocalExtension handler) + { + if (this.genericResourceHandler is not null) + { + throw new ArgumentException($"Generic resource handler has already been registered."); + } + + this.genericResourceHandler = handler; + return this; + } + + public LocalExtensionDispatcher Build() + { + return new( + this.genericResourceHandler, + this.resourceHandlers.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase)); + } + } +} diff --git a/src/Bicep.Local.Extension/Rpc/BicepGrpcService.cs b/src/Bicep.Local.Extension/Rpc/BicepGrpcService.cs new file mode 100644 index 00000000000..0033afa4bf7 --- /dev/null +++ b/src/Bicep.Local.Extension/Rpc/BicepGrpcService.cs @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Bicep.Local.Extension.Protocol; +using Google.Protobuf.Collections; +using Grpc.Core; +using Json.More; +using Microsoft.Extensions.Logging; + +namespace Bicep.Local.Extension.Rpc +{ + public class BicepGrpcService : BicepExtension.BicepExtensionBase + { + private readonly ILogger logger; + private readonly LocalExtensionDispatcher dispatcher; + + public BicepGrpcService(ILogger logger, LocalExtensionDispatcher dispatcher) + { + this.logger = logger; + this.dispatcher = dispatcher; + } + + public override Task CreateOrUpdate(ResourceSpecification request, ServerCallContext context) + => WrapExceptions(async () => Convert(await dispatcher.GetHandler(request.Type).CreateOrUpdate(Convert(request), context.CancellationToken))); + + public override Task Preview(ResourceSpecification request, ServerCallContext context) + => WrapExceptions(async () => Convert(await dispatcher.GetHandler(request.Type).Preview(Convert(request), context.CancellationToken))); + + public override Task Get(ResourceReference request, ServerCallContext context) + => WrapExceptions(async () => Convert(await dispatcher.GetHandler(request.Type).Get(Convert(request), context.CancellationToken))); + + public override Task Delete(ResourceReference request, ServerCallContext context) + => WrapExceptions(async () => Convert(await dispatcher.GetHandler(request.Type).Delete(Convert(request), context.CancellationToken))); + + public override Task Ping(Empty request, ServerCallContext context) + => Task.FromResult(new Empty()); + + private Azure.Deployments.Extensibility.Core.V2.Models.ResourceSpecification Convert(ResourceSpecification request) + => new(request.Type, request.ApiVersion, ToJsonObject(request.Properties, "Parsing extension properties failed. Please ensure is a valid JSON object."), GetExtensionConfig(request.Config)); + + private Azure.Deployments.Extensibility.Core.V2.Models.ResourceReference Convert(ResourceReference request) + => new(request.Type, request.ApiVersion, ToJsonObject(request.Identifiers, "Parsing extension identifiers failed. Please ensure is a valid JSON object."), GetExtensionConfig(request.Config)); + + private JsonObject? GetExtensionConfig(string extensionConfig) + { + JsonObject? config = null; + if (!string.IsNullOrEmpty(extensionConfig)) + { + config = ToJsonObject(extensionConfig, "Parsing extension config failed. Please ensure is a valid JSON object."); + } + return config; + } + + private JsonObject ToJsonObject(string json, string errorMessage) + => JsonNode.Parse(json)?.AsObject() ?? throw new ArgumentNullException(errorMessage); + + private Resource? Convert(Azure.Deployments.Extensibility.Core.V2.Models.Resource? response) + => response is null ? null : + new() + { + Identifiers = response.Identifiers.ToJsonString(), + Properties = response.Properties.ToJsonString(), + Status = response.Status, + Type = response.Type, + ApiVersion = response.ApiVersion, + }; + + private ErrorData? Convert(Azure.Deployments.Extensibility.Core.V2.Models.ErrorData? response) + { + if (response is null) + { + return null; + } + + var errorData = new ErrorData() + { + Error = new Error() + { + Code = response.Error.Code, + Message = response.Error.Message, + InnerError = response.Error.InnerError?.ToJsonString(), + Target = response.Error.Target?.ToString() ?? string.Empty, + } + }; + + var errorDetails = Convert(response.Error.Details); + if (errorDetails is not null) + { + errorData.Error.Details.AddRange(errorDetails); + } + return errorData; + } + + private RepeatedField? Convert(IList? response) + { + if (response is null) + { + return null; + } + + var list = new RepeatedField(); + foreach (var item in response) + { + list.Add(Convert(item)); + } + return list; + } + + private ErrorDetail Convert(Azure.Deployments.Extensibility.Core.V2.Models.ErrorDetail response) + => new() + { + Code = response.Code, + Message = response.Message, + Target = response.Target?.ToString() ?? string.Empty, + }; + + private LocalExtensibilityOperationResponse Convert(LocalExtensionOperationResponse response) + => new() + { + ErrorData = Convert(response.ErrorData), + Resource = Convert(response.Resource) + }; + + private static async Task WrapExceptions(Func> func) + { + try + { + return await func(); + } + catch (Exception ex) + { + var response = new LocalExtensibilityOperationResponse + { + Resource = null, + ErrorData = new ErrorData + { + Error = new Error + { + Message = $"Rpc request failed: {ex}", + Code = "RpcException", + Target = "" + } + } + }; + + return response; + } + } + } +} diff --git a/src/Bicep.Local.Extension/packages.lock.json b/src/Bicep.Local.Extension/packages.lock.json index 309f7fb8377..c21ecc06fea 100644 --- a/src/Bicep.Local.Extension/packages.lock.json +++ b/src/Bicep.Local.Extension/packages.lock.json @@ -2,6 +2,18 @@ "version": 1, "dependencies": { "net8.0": { + "Azure.Deployments.Extensibility.Core": { + "type": "Direct", + "requested": "[0.1.55, )", + "resolved": "0.1.55", + "contentHash": "iMZhx89YLqHaPGA20LXlzDBty7ov/UgOdxLudJtYwBXkalfSRHLPNKRnJVeGM3EZc9897LeoPyfJ8NvyLeZcgQ==", + "dependencies": { + "JsonPatch.Net": "3.1.0", + "JsonPath.Net": "1.1.0", + "JsonPointer.Net": "5.0.0", + "JsonSchema.Net": "7.0.4" + } + }, "Azure.Deployments.Internal.GenerateNotice": { "type": "Direct", "requested": "[0.1.38, )", @@ -77,6 +89,49 @@ "Grpc.Core.Api": "2.63.0" } }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" + }, + "Json.More.Net": { + "type": "Transitive", + "resolved": "2.0.1.2", + "contentHash": "uF3QeiaXEfH92emz0/BWUiNtMSfxIIvgynuB0Bf1vF4s8eWTcZitBx9l+g/FDaJk5XxqBv9buQXizXKQcXFG1w==" + }, + "JsonPatch.Net": { + "type": "Transitive", + "resolved": "3.1.0", + "contentHash": "BS/8aoXm5mEA/8f7SzhfXEitsEbqERME7xzuRbCDvBXtS2mcAoPAq11L324rhARJfRZ/nGso/KFfggIIdyytww==", + "dependencies": { + "JsonPointer.Net": "5.0.0" + } + }, + "JsonPath.Net": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "Njbt3xuyiJ41zkut0nrKbHL7Hpxb39siV/KchPnXKVNGnhnYqIUmiWh653EfRK4lG8H+ds08bNrw5/3jl9ZC3A==", + "dependencies": { + "Json.More.Net": "2.0.1.2" + } + }, + "JsonPointer.Net": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "fm4T5w20AY6C+p5/pJr0vrXRNGgtSfHl34I1LxC9zdPwS9S3j0GiR1Mz/CVPWKDXXGDpCt1APHpCq7kn5adCfA==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Json.More.Net": "2.0.1.2" + } + }, + "JsonSchema.Net": { + "type": "Transitive", + "resolved": "7.0.4", + "contentHash": "R0Hk2Tr/np4Q1NO8CBjyQsoiD1iFJyEQP20Sw7JnZCNGJoaSBe+g4b+nZqnBXPQhiqY5LGZ8JZwZkRh/eKZhEQ==", + "dependencies": { + "JsonPointer.Net": "5.0.0" + } + }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", "resolved": "8.0.0", diff --git a/src/Bicep.RegistryModuleTool.IntegrationTests/packages.lock.json b/src/Bicep.RegistryModuleTool.IntegrationTests/packages.lock.json index f791b396c20..0c2d1fd91f6 100644 --- a/src/Bicep.RegistryModuleTool.IntegrationTests/packages.lock.json +++ b/src/Bicep.RegistryModuleTool.IntegrationTests/packages.lock.json @@ -1785,6 +1785,7 @@ "Azure.Bicep.Local.Extension": { "type": "Project", "dependencies": { + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "CommandLineParser": "[2.9.1, )", "Google.Protobuf": "[3.27.1, )", "Grpc.Net.Client": "[2.63.0, )" diff --git a/src/Bicep.RegistryModuleTool.TestFixtures/packages.lock.json b/src/Bicep.RegistryModuleTool.TestFixtures/packages.lock.json index b1e60ec7ea7..2d446463660 100644 --- a/src/Bicep.RegistryModuleTool.TestFixtures/packages.lock.json +++ b/src/Bicep.RegistryModuleTool.TestFixtures/packages.lock.json @@ -1725,6 +1725,7 @@ "Azure.Bicep.Local.Extension": { "type": "Project", "dependencies": { + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "CommandLineParser": "[2.9.1, )", "Google.Protobuf": "[3.27.1, )", "Grpc.Net.Client": "[2.63.0, )" diff --git a/src/Bicep.RegistryModuleTool.UnitTests/packages.lock.json b/src/Bicep.RegistryModuleTool.UnitTests/packages.lock.json index f791b396c20..0c2d1fd91f6 100644 --- a/src/Bicep.RegistryModuleTool.UnitTests/packages.lock.json +++ b/src/Bicep.RegistryModuleTool.UnitTests/packages.lock.json @@ -1785,6 +1785,7 @@ "Azure.Bicep.Local.Extension": { "type": "Project", "dependencies": { + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "CommandLineParser": "[2.9.1, )", "Google.Protobuf": "[3.27.1, )", "Grpc.Net.Client": "[2.63.0, )" diff --git a/src/Bicep.Tools.Benchmark/packages.lock.json b/src/Bicep.Tools.Benchmark/packages.lock.json index 7d01671feed..0ae15775bf2 100644 --- a/src/Bicep.Tools.Benchmark/packages.lock.json +++ b/src/Bicep.Tools.Benchmark/packages.lock.json @@ -1611,6 +1611,7 @@ "Azure.Bicep.Local.Extension": { "type": "Project", "dependencies": { + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "CommandLineParser": "[2.9.1, )", "Google.Protobuf": "[3.27.1, )", "Grpc.Net.Client": "[2.63.0, )"