diff --git a/src/Bicep.Cli.IntegrationTests/packages.lock.json b/src/Bicep.Cli.IntegrationTests/packages.lock.json index c3e096a25e3..88d8e8aa605 100644 --- a/src/Bicep.Cli.IntegrationTests/packages.lock.json +++ b/src/Bicep.Cli.IntegrationTests/packages.lock.json @@ -132,8 +132,8 @@ }, "Azure.Deployments.Core": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "Zev+8/PldTvNwUEI84e7szmJm7/ClHyr4fe0hlMylZ9lwD4wCOyc3ijWe6LmI8J92WtXX7Mh1K1FtIcHOZC0iw==", + "resolved": "1.54.0", + "contentHash": "dItBSPwB83gv9BXwPEcef4VYufmuP7w59Bg/xpjCSlBEPlj3UukVypUqMmI2hRObhNnz2TQHiMw+Y/S8HtJbyA==", "dependencies": { "Microsoft.PowerPlatform.ResourceStack": "7.0.0.2007", "Newtonsoft.Json": "13.0.2", @@ -143,19 +143,19 @@ }, "Azure.Deployments.DiffEngine": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "JqDITGajqF4tjR0sqgl8YlwWEC92r7LOHjzP0cH6K0mQIyY3rRr2c1k3fbYRrbG1D69o4IiNXqqShDd8K4lSMg==" + "resolved": "1.54.0", + "contentHash": "Z+1Q/Vpy/LF0xKm3/0szoU/0AhZuNbnQOVMagtfejpRNteqnmeryLjdHG59AeKIpzgbHr3XxusfGz0jg4wjLdA==" }, "Azure.Deployments.Engine": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "xP1lnwgceE74Tmp5N5R1SdcB0iICwH2QiwVWpZnjqzA5EXGr9tesEbV1EO7GTgfCXtTWYbcQlySkAKtDPuMt0A==", + "resolved": "1.54.0", + "contentHash": "8mx0e0JUq43Ax/5I1BEcvUsFmDIUdsRnrKIMG3+QOz11SMbI2nHAPQYFkIkWnxgaxpObaZvx6EcJXQmptEgehg==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", - "Azure.Deployments.DiffEngine": "1.34.0", - "Azure.Deployments.Extensibility": "1.34.0", + "Azure.Deployments.Core": "1.54.0", + "Azure.Deployments.DiffEngine": "1.54.0", + "Azure.Deployments.Extensibility": "1.54.0", "Azure.Deployments.ResourceMetadata": "1.0.1265", - "Azure.Deployments.Templates": "1.34.0", + "Azure.Deployments.Templates": "1.54.0", "Microsoft.AspNet.WebApi.Client": "5.2.9", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Newtonsoft.Json": "13.0.2", @@ -165,23 +165,34 @@ }, "Azure.Deployments.Expression": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "4XIed8jUdWsc8IG2o2tdqJl8ioWjwN9MR2y9L9PV/5wlDUyb2I6MXVwU/f5d3ZMCD+svc4rE6HRgxMIBYHZmSQ==", + "resolved": "1.54.0", + "contentHash": "2nhYxVun701mghHxK5h5qZoL9RjaVCo83/VTc2HgPKJkIcRW3oYFbxnMQrP5pr9k0GsCxKmvOkwsoET9gy851A==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", + "Azure.Deployments.Core": "1.54.0", "IPNetwork2": "2.6.598", "Newtonsoft.Json": "13.0.2" } }, "Azure.Deployments.Extensibility": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "3n64LvNi90r70Puobb8OImKlivz6/JaCW7zVImwb8VXeXTJJtoPTuY0z81QroGqdQMA/aU8123edFBTM4hiBVg==", + "resolved": "1.54.0", + "contentHash": "CiMXvV8NVW/xQKMQVX7gmKj20GP/h1k+4nuCNyCoS5gek4WI9yk7pH0XHwmx1TVf2Ms1vtwc9bOYohq1kd27tg==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", + "Azure.Deployments.Core": "1.54.0", "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.JsonPath": { "type": "Transitive", "resolved": "1.0.1265", @@ -206,12 +217,12 @@ }, "Azure.Deployments.Templates": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "AlstKsqlGEv8XNEjtCZ9KsI//ZKtnKn0m18TSuSQT36w+RjWiCo35uE6U/HTIXmTRS/jJVOuwMSPU4FhTLW/jg==", + "resolved": "1.54.0", + "contentHash": "TNq6JNNcDVuNSS4sqBUETVTm8KDIYvt28vWSPaV+rHyTogo0qJHciSQglEwOk0zoTp1Qf8plT7QqXS0uM/A2AA==", "dependencies": { "Azure.Bicep.Types": "0.5.9", - "Azure.Deployments.Core": "1.34.0", - "Azure.Deployments.Expression": "1.34.0", + "Azure.Deployments.Core": "1.54.0", + "Azure.Deployments.Expression": "1.54.0", "Microsoft.Automata.SRM": "1.2.2", "Newtonsoft.Json": "13.0.2" } @@ -359,6 +370,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" + } + }, "MediatR": { "type": "Transitive", "resolved": "8.1.0", @@ -1635,7 +1654,8 @@ "dependencies": { "Azure.Bicep.Core": "[1.0.0, )", "Azure.Bicep.Local.Extension": "[1.0.0, )", - "Azure.Deployments.Engine": "[1.34.0, )", + "Azure.Deployments.Engine": "[1.54.0, )", + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "Microsoft.AspNet.WebApi.Client": "[6.0.0, )" } }, diff --git a/src/Bicep.Cli.UnitTests/packages.lock.json b/src/Bicep.Cli.UnitTests/packages.lock.json index fec9fd76b62..b3b8688d8f7 100644 --- a/src/Bicep.Cli.UnitTests/packages.lock.json +++ b/src/Bicep.Cli.UnitTests/packages.lock.json @@ -122,8 +122,8 @@ }, "Azure.Deployments.Core": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "Zev+8/PldTvNwUEI84e7szmJm7/ClHyr4fe0hlMylZ9lwD4wCOyc3ijWe6LmI8J92WtXX7Mh1K1FtIcHOZC0iw==", + "resolved": "1.54.0", + "contentHash": "dItBSPwB83gv9BXwPEcef4VYufmuP7w59Bg/xpjCSlBEPlj3UukVypUqMmI2hRObhNnz2TQHiMw+Y/S8HtJbyA==", "dependencies": { "Microsoft.PowerPlatform.ResourceStack": "7.0.0.2007", "Newtonsoft.Json": "13.0.2", @@ -133,19 +133,19 @@ }, "Azure.Deployments.DiffEngine": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "JqDITGajqF4tjR0sqgl8YlwWEC92r7LOHjzP0cH6K0mQIyY3rRr2c1k3fbYRrbG1D69o4IiNXqqShDd8K4lSMg==" + "resolved": "1.54.0", + "contentHash": "Z+1Q/Vpy/LF0xKm3/0szoU/0AhZuNbnQOVMagtfejpRNteqnmeryLjdHG59AeKIpzgbHr3XxusfGz0jg4wjLdA==" }, "Azure.Deployments.Engine": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "xP1lnwgceE74Tmp5N5R1SdcB0iICwH2QiwVWpZnjqzA5EXGr9tesEbV1EO7GTgfCXtTWYbcQlySkAKtDPuMt0A==", + "resolved": "1.54.0", + "contentHash": "8mx0e0JUq43Ax/5I1BEcvUsFmDIUdsRnrKIMG3+QOz11SMbI2nHAPQYFkIkWnxgaxpObaZvx6EcJXQmptEgehg==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", - "Azure.Deployments.DiffEngine": "1.34.0", - "Azure.Deployments.Extensibility": "1.34.0", + "Azure.Deployments.Core": "1.54.0", + "Azure.Deployments.DiffEngine": "1.54.0", + "Azure.Deployments.Extensibility": "1.54.0", "Azure.Deployments.ResourceMetadata": "1.0.1265", - "Azure.Deployments.Templates": "1.34.0", + "Azure.Deployments.Templates": "1.54.0", "Microsoft.AspNet.WebApi.Client": "5.2.9", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Newtonsoft.Json": "13.0.2", @@ -155,23 +155,34 @@ }, "Azure.Deployments.Expression": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "4XIed8jUdWsc8IG2o2tdqJl8ioWjwN9MR2y9L9PV/5wlDUyb2I6MXVwU/f5d3ZMCD+svc4rE6HRgxMIBYHZmSQ==", + "resolved": "1.54.0", + "contentHash": "2nhYxVun701mghHxK5h5qZoL9RjaVCo83/VTc2HgPKJkIcRW3oYFbxnMQrP5pr9k0GsCxKmvOkwsoET9gy851A==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", + "Azure.Deployments.Core": "1.54.0", "IPNetwork2": "2.6.598", "Newtonsoft.Json": "13.0.2" } }, "Azure.Deployments.Extensibility": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "3n64LvNi90r70Puobb8OImKlivz6/JaCW7zVImwb8VXeXTJJtoPTuY0z81QroGqdQMA/aU8123edFBTM4hiBVg==", + "resolved": "1.54.0", + "contentHash": "CiMXvV8NVW/xQKMQVX7gmKj20GP/h1k+4nuCNyCoS5gek4WI9yk7pH0XHwmx1TVf2Ms1vtwc9bOYohq1kd27tg==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", + "Azure.Deployments.Core": "1.54.0", "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.JsonPath": { "type": "Transitive", "resolved": "1.0.1265", @@ -196,12 +207,12 @@ }, "Azure.Deployments.Templates": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "AlstKsqlGEv8XNEjtCZ9KsI//ZKtnKn0m18TSuSQT36w+RjWiCo35uE6U/HTIXmTRS/jJVOuwMSPU4FhTLW/jg==", + "resolved": "1.54.0", + "contentHash": "TNq6JNNcDVuNSS4sqBUETVTm8KDIYvt28vWSPaV+rHyTogo0qJHciSQglEwOk0zoTp1Qf8plT7QqXS0uM/A2AA==", "dependencies": { "Azure.Bicep.Types": "0.5.9", - "Azure.Deployments.Core": "1.34.0", - "Azure.Deployments.Expression": "1.34.0", + "Azure.Deployments.Core": "1.54.0", + "Azure.Deployments.Expression": "1.54.0", "Microsoft.Automata.SRM": "1.2.2", "Newtonsoft.Json": "13.0.2" } @@ -326,6 +337,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" + } + }, "MessagePack": { "type": "Transitive", "resolved": "2.5.108", @@ -1506,7 +1525,8 @@ "dependencies": { "Azure.Bicep.Core": "[1.0.0, )", "Azure.Bicep.Local.Extension": "[1.0.0, )", - "Azure.Deployments.Engine": "[1.34.0, )", + "Azure.Deployments.Engine": "[1.54.0, )", + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "Microsoft.AspNet.WebApi.Client": "[6.0.0, )" } }, diff --git a/src/Bicep.Cli/Commands/LocalDeployCommand.cs b/src/Bicep.Cli/Commands/LocalDeployCommand.cs index b43ad5acf5f..c6c2fd21c9c 100644 --- a/src/Bicep.Cli/Commands/LocalDeployCommand.cs +++ b/src/Bicep.Cli/Commands/LocalDeployCommand.cs @@ -65,8 +65,8 @@ parameters.Parameters is not { } parametersString || return 1; } - await using LocalExtensibilityHandler extensibilityHandler = new(moduleDispatcher, GrpcExtensibilityProvider.Start); - await extensibilityHandler.InitializeProviders(compilation); + await using LocalExtensibilityHostManager extensibilityHandler = new(moduleDispatcher, GrpcBuiltInLocalExtension.Start); + await extensibilityHandler.InitializeExtensions(compilation); var result = await LocalDeployment.Deploy(extensibilityHandler, templateString, parametersString, cancellationToken); diff --git a/src/Bicep.Cli/packages.lock.json b/src/Bicep.Cli/packages.lock.json index 21e55a04892..cb4b1933554 100644 --- a/src/Bicep.Cli/packages.lock.json +++ b/src/Bicep.Cli/packages.lock.json @@ -140,8 +140,8 @@ }, "Azure.Deployments.Core": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "Zev+8/PldTvNwUEI84e7szmJm7/ClHyr4fe0hlMylZ9lwD4wCOyc3ijWe6LmI8J92WtXX7Mh1K1FtIcHOZC0iw==", + "resolved": "1.54.0", + "contentHash": "dItBSPwB83gv9BXwPEcef4VYufmuP7w59Bg/xpjCSlBEPlj3UukVypUqMmI2hRObhNnz2TQHiMw+Y/S8HtJbyA==", "dependencies": { "Microsoft.PowerPlatform.ResourceStack": "7.0.0.2007", "Newtonsoft.Json": "13.0.2", @@ -151,19 +151,19 @@ }, "Azure.Deployments.DiffEngine": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "JqDITGajqF4tjR0sqgl8YlwWEC92r7LOHjzP0cH6K0mQIyY3rRr2c1k3fbYRrbG1D69o4IiNXqqShDd8K4lSMg==" + "resolved": "1.54.0", + "contentHash": "Z+1Q/Vpy/LF0xKm3/0szoU/0AhZuNbnQOVMagtfejpRNteqnmeryLjdHG59AeKIpzgbHr3XxusfGz0jg4wjLdA==" }, "Azure.Deployments.Engine": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "xP1lnwgceE74Tmp5N5R1SdcB0iICwH2QiwVWpZnjqzA5EXGr9tesEbV1EO7GTgfCXtTWYbcQlySkAKtDPuMt0A==", + "resolved": "1.54.0", + "contentHash": "8mx0e0JUq43Ax/5I1BEcvUsFmDIUdsRnrKIMG3+QOz11SMbI2nHAPQYFkIkWnxgaxpObaZvx6EcJXQmptEgehg==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", - "Azure.Deployments.DiffEngine": "1.34.0", - "Azure.Deployments.Extensibility": "1.34.0", + "Azure.Deployments.Core": "1.54.0", + "Azure.Deployments.DiffEngine": "1.54.0", + "Azure.Deployments.Extensibility": "1.54.0", "Azure.Deployments.ResourceMetadata": "1.0.1265", - "Azure.Deployments.Templates": "1.34.0", + "Azure.Deployments.Templates": "1.54.0", "Microsoft.AspNet.WebApi.Client": "5.2.9", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Newtonsoft.Json": "13.0.2", @@ -173,23 +173,34 @@ }, "Azure.Deployments.Expression": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "4XIed8jUdWsc8IG2o2tdqJl8ioWjwN9MR2y9L9PV/5wlDUyb2I6MXVwU/f5d3ZMCD+svc4rE6HRgxMIBYHZmSQ==", + "resolved": "1.54.0", + "contentHash": "2nhYxVun701mghHxK5h5qZoL9RjaVCo83/VTc2HgPKJkIcRW3oYFbxnMQrP5pr9k0GsCxKmvOkwsoET9gy851A==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", + "Azure.Deployments.Core": "1.54.0", "IPNetwork2": "2.6.598", "Newtonsoft.Json": "13.0.2" } }, "Azure.Deployments.Extensibility": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "3n64LvNi90r70Puobb8OImKlivz6/JaCW7zVImwb8VXeXTJJtoPTuY0z81QroGqdQMA/aU8123edFBTM4hiBVg==", + "resolved": "1.54.0", + "contentHash": "CiMXvV8NVW/xQKMQVX7gmKj20GP/h1k+4nuCNyCoS5gek4WI9yk7pH0XHwmx1TVf2Ms1vtwc9bOYohq1kd27tg==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", + "Azure.Deployments.Core": "1.54.0", "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.JsonPath": { "type": "Transitive", "resolved": "1.0.1265", @@ -214,12 +225,12 @@ }, "Azure.Deployments.Templates": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "AlstKsqlGEv8XNEjtCZ9KsI//ZKtnKn0m18TSuSQT36w+RjWiCo35uE6U/HTIXmTRS/jJVOuwMSPU4FhTLW/jg==", + "resolved": "1.54.0", + "contentHash": "TNq6JNNcDVuNSS4sqBUETVTm8KDIYvt28vWSPaV+rHyTogo0qJHciSQglEwOk0zoTp1Qf8plT7QqXS0uM/A2AA==", "dependencies": { "Azure.Bicep.Types": "0.5.9", - "Azure.Deployments.Core": "1.34.0", - "Azure.Deployments.Expression": "1.34.0", + "Azure.Deployments.Core": "1.54.0", + "Azure.Deployments.Expression": "1.54.0", "Microsoft.Automata.SRM": "1.2.2", "Newtonsoft.Json": "13.0.2" } @@ -344,6 +355,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" + } + }, "MessagePack": { "type": "Transitive", "resolved": "2.5.108", @@ -1404,7 +1423,8 @@ "dependencies": { "Azure.Bicep.Core": "[1.0.0, )", "Azure.Bicep.Local.Extension": "[1.0.0, )", - "Azure.Deployments.Engine": "[1.34.0, )", + "Azure.Deployments.Engine": "[1.54.0, )", + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "Microsoft.AspNet.WebApi.Client": "[6.0.0, )" } }, diff --git a/src/Bicep.Core.IntegrationTests/RegistryProviderTests.cs b/src/Bicep.Core.IntegrationTests/RegistryProviderTests.cs index afc053c3995..24aae48a262 100644 --- a/src/Bicep.Core.IntegrationTests/RegistryProviderTests.cs +++ b/src/Bicep.Core.IntegrationTests/RegistryProviderTests.cs @@ -20,6 +20,7 @@ namespace Bicep.Core.IntegrationTests; public class RegistryProviderTests : TestBase { private static readonly FeatureProviderOverrides AllFeaturesEnabled = new(ExtensibilityEnabled: true, ExtensionRegistry: true, DynamicTypeLoadingEnabled: true); + private static readonly FeatureProviderOverrides AllFeaturesEnabledForLocalDeploy = new(ExtensibilityEnabled: true, LocalDeployEnabled: true, ExtensionRegistry: true, DynamicTypeLoadingEnabled: true); [TestMethod] [TestCategory(BaselineHelper.BaselineTestCategory)] @@ -349,6 +350,89 @@ public async Task Missing_required_provider_configuration_blocks_compilation() }); } + [TestMethod] + public async Task Correct_local_deploy_provider_configuration_result_in_successful_compilation() + { + // tgzData provideds configType with the properties namespace, config, and context + var services = await ProviderTestHelper.GetServiceBuilderWithPublishedProvider(ThirdPartyTypeHelper.GetTestTypesTgzWithFallbackAndConfiguration(), AllFeaturesEnabledForLocalDeploy); + + var result = await CompilationHelper.RestoreAndCompile(services, """ +extension 'br:example.azurecr.io/providers/foo:1.2.3' with { + namespace: 'ThirdPartyNamespace' + config: 'Some path to config file' + context: 'Some ThirdParty context' +} + +resource dadJoke 'fooType@v1' = { + identifier: 'foo' + joke: 'dad joke' +} + +output joke string = dadJoke.joke +"""); + + result.Template.Should().NotBeNull(); + + result.Template.Should().HaveValueAtPath("$.extensions['ThirdPartyProvider']['name']", "ThirdPartyProvider"); + result.Template.Should().HaveValueAtPath("$.extensions['ThirdPartyProvider']['version']", "1.0.0"); + + result.Template.Should().HaveValueAtPath("$.extensions['ThirdPartyProvider']['config']['namespace']['type']", "string"); + result.Template.Should().HaveValueAtPath("$.extensions['ThirdPartyProvider']['config']['namespace']['defaultValue']", "ThirdPartyNamespace"); + result.Template.Should().HaveValueAtPath("$.extensions['ThirdPartyProvider']['config']['config']['type']", "string"); + result.Template.Should().HaveValueAtPath("$.extensions['ThirdPartyProvider']['config']['config']['defaultValue']", "Some path to config file"); + result.Template.Should().HaveValueAtPath("$.extensions['ThirdPartyProvider']['config']['context']['type']", "string"); + result.Template.Should().HaveValueAtPath("$.extensions['ThirdPartyProvider']['config']['context']['defaultValue']", "Some ThirdParty context"); + + result.Should().NotHaveAnyDiagnostics(); + } + + [TestMethod] + public async Task Local_deploy_provider_with_configuration_defined_and_empty_configuration_provided_throws_errors() + { + // tgzData provideds configType with the properties namespace, config, and context + var services = await ProviderTestHelper.GetServiceBuilderWithPublishedProvider(ThirdPartyTypeHelper.GetTestTypesTgzWithFallbackAndConfiguration(), AllFeaturesEnabledForLocalDeploy); + + var result = await CompilationHelper.RestoreAndCompile(services, """ +extension 'br:example.azurecr.io/providers/foo:1.2.3' with { } + +resource dadJoke 'fooType@v1' = { + identifier: 'foo' + joke: 'dad joke' +} + +output joke string = dadJoke.joke +"""); + + result.Template.Should().BeNull(); + + result.Should().HaveDiagnostics([("BCP035", DiagnosticLevel.Error, "The specified \"object\" declaration is missing the following required properties: \"config\", \"namespace\".")], because: "Type checking should block the template compilation because required provider config properties hasn't been supplied."); + } + + [TestMethod] + public async Task Local_deploy_provider_without_configuration_defined_but_configuration_provided_throws_errors() + { + var services = await ProviderTestHelper.GetServiceBuilderWithPublishedProvider(ThirdPartyTypeHelper.GetTestTypesTgz(), AllFeaturesEnabledForLocalDeploy); + + var result = await CompilationHelper.RestoreAndCompile(services, """ +extension 'br:example.azurecr.io/providers/foo:1.2.3' with { + namespace: 'ThirdPartyNamespace' + config: 'Some path to config file' + context: 'Some ThirdParty context' +} + +resource fooRes 'fooType@v1' existing = { + identifier: 'foo' +} + +output baz string = fooRes.convertBarToBaz('bar') + +"""); + + result.Template.Should().BeNull(); + + result.Should().HaveDiagnostics([("BCP205", DiagnosticLevel.Error, "Extension \"ThirdPartyProvider\" does not support configuration.")], because: "Type checking should block the template compilation because provider does not support configuration but one has been provided."); + } + [TestMethod] public async Task Correct_provider_configuration_result_in_successful_compilation() { diff --git a/src/Bicep.Core.IntegrationTests/packages.lock.json b/src/Bicep.Core.IntegrationTests/packages.lock.json index 21ff66f072c..50c87c3acca 100644 --- a/src/Bicep.Core.IntegrationTests/packages.lock.json +++ b/src/Bicep.Core.IntegrationTests/packages.lock.json @@ -122,8 +122,8 @@ }, "Azure.Deployments.Core": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "Zev+8/PldTvNwUEI84e7szmJm7/ClHyr4fe0hlMylZ9lwD4wCOyc3ijWe6LmI8J92WtXX7Mh1K1FtIcHOZC0iw==", + "resolved": "1.54.0", + "contentHash": "dItBSPwB83gv9BXwPEcef4VYufmuP7w59Bg/xpjCSlBEPlj3UukVypUqMmI2hRObhNnz2TQHiMw+Y/S8HtJbyA==", "dependencies": { "Microsoft.PowerPlatform.ResourceStack": "7.0.0.2007", "Newtonsoft.Json": "13.0.2", @@ -133,19 +133,19 @@ }, "Azure.Deployments.DiffEngine": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "JqDITGajqF4tjR0sqgl8YlwWEC92r7LOHjzP0cH6K0mQIyY3rRr2c1k3fbYRrbG1D69o4IiNXqqShDd8K4lSMg==" + "resolved": "1.54.0", + "contentHash": "Z+1Q/Vpy/LF0xKm3/0szoU/0AhZuNbnQOVMagtfejpRNteqnmeryLjdHG59AeKIpzgbHr3XxusfGz0jg4wjLdA==" }, "Azure.Deployments.Engine": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "xP1lnwgceE74Tmp5N5R1SdcB0iICwH2QiwVWpZnjqzA5EXGr9tesEbV1EO7GTgfCXtTWYbcQlySkAKtDPuMt0A==", + "resolved": "1.54.0", + "contentHash": "8mx0e0JUq43Ax/5I1BEcvUsFmDIUdsRnrKIMG3+QOz11SMbI2nHAPQYFkIkWnxgaxpObaZvx6EcJXQmptEgehg==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", - "Azure.Deployments.DiffEngine": "1.34.0", - "Azure.Deployments.Extensibility": "1.34.0", + "Azure.Deployments.Core": "1.54.0", + "Azure.Deployments.DiffEngine": "1.54.0", + "Azure.Deployments.Extensibility": "1.54.0", "Azure.Deployments.ResourceMetadata": "1.0.1265", - "Azure.Deployments.Templates": "1.34.0", + "Azure.Deployments.Templates": "1.54.0", "Microsoft.AspNet.WebApi.Client": "5.2.9", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Newtonsoft.Json": "13.0.2", @@ -155,23 +155,34 @@ }, "Azure.Deployments.Expression": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "4XIed8jUdWsc8IG2o2tdqJl8ioWjwN9MR2y9L9PV/5wlDUyb2I6MXVwU/f5d3ZMCD+svc4rE6HRgxMIBYHZmSQ==", + "resolved": "1.54.0", + "contentHash": "2nhYxVun701mghHxK5h5qZoL9RjaVCo83/VTc2HgPKJkIcRW3oYFbxnMQrP5pr9k0GsCxKmvOkwsoET9gy851A==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", + "Azure.Deployments.Core": "1.54.0", "IPNetwork2": "2.6.598", "Newtonsoft.Json": "13.0.2" } }, "Azure.Deployments.Extensibility": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "3n64LvNi90r70Puobb8OImKlivz6/JaCW7zVImwb8VXeXTJJtoPTuY0z81QroGqdQMA/aU8123edFBTM4hiBVg==", + "resolved": "1.54.0", + "contentHash": "CiMXvV8NVW/xQKMQVX7gmKj20GP/h1k+4nuCNyCoS5gek4WI9yk7pH0XHwmx1TVf2Ms1vtwc9bOYohq1kd27tg==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", + "Azure.Deployments.Core": "1.54.0", "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.JsonPath": { "type": "Transitive", "resolved": "1.0.1265", @@ -196,12 +207,12 @@ }, "Azure.Deployments.Templates": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "AlstKsqlGEv8XNEjtCZ9KsI//ZKtnKn0m18TSuSQT36w+RjWiCo35uE6U/HTIXmTRS/jJVOuwMSPU4FhTLW/jg==", + "resolved": "1.54.0", + "contentHash": "TNq6JNNcDVuNSS4sqBUETVTm8KDIYvt28vWSPaV+rHyTogo0qJHciSQglEwOk0zoTp1Qf8plT7QqXS0uM/A2AA==", "dependencies": { "Azure.Bicep.Types": "0.5.9", - "Azure.Deployments.Core": "1.34.0", - "Azure.Deployments.Expression": "1.34.0", + "Azure.Deployments.Core": "1.54.0", + "Azure.Deployments.Expression": "1.54.0", "Microsoft.Automata.SRM": "1.2.2", "Newtonsoft.Json": "13.0.2" } @@ -349,6 +360,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" + } + }, "MediatR": { "type": "Transitive", "resolved": "8.1.0", @@ -1538,7 +1557,8 @@ "dependencies": { "Azure.Bicep.Core": "[1.0.0, )", "Azure.Bicep.Local.Extension": "[1.0.0, )", - "Azure.Deployments.Engine": "[1.34.0, )", + "Azure.Deployments.Engine": "[1.54.0, )", + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "Microsoft.AspNet.WebApi.Client": "[6.0.0, )" } }, diff --git a/src/Bicep.Core.Samples/packages.lock.json b/src/Bicep.Core.Samples/packages.lock.json index ca951706903..8f697d60247 100644 --- a/src/Bicep.Core.Samples/packages.lock.json +++ b/src/Bicep.Core.Samples/packages.lock.json @@ -117,8 +117,8 @@ }, "Azure.Deployments.Core": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "Zev+8/PldTvNwUEI84e7szmJm7/ClHyr4fe0hlMylZ9lwD4wCOyc3ijWe6LmI8J92WtXX7Mh1K1FtIcHOZC0iw==", + "resolved": "1.54.0", + "contentHash": "dItBSPwB83gv9BXwPEcef4VYufmuP7w59Bg/xpjCSlBEPlj3UukVypUqMmI2hRObhNnz2TQHiMw+Y/S8HtJbyA==", "dependencies": { "Microsoft.PowerPlatform.ResourceStack": "7.0.0.2007", "Newtonsoft.Json": "13.0.2", @@ -128,19 +128,19 @@ }, "Azure.Deployments.DiffEngine": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "JqDITGajqF4tjR0sqgl8YlwWEC92r7LOHjzP0cH6K0mQIyY3rRr2c1k3fbYRrbG1D69o4IiNXqqShDd8K4lSMg==" + "resolved": "1.54.0", + "contentHash": "Z+1Q/Vpy/LF0xKm3/0szoU/0AhZuNbnQOVMagtfejpRNteqnmeryLjdHG59AeKIpzgbHr3XxusfGz0jg4wjLdA==" }, "Azure.Deployments.Engine": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "xP1lnwgceE74Tmp5N5R1SdcB0iICwH2QiwVWpZnjqzA5EXGr9tesEbV1EO7GTgfCXtTWYbcQlySkAKtDPuMt0A==", + "resolved": "1.54.0", + "contentHash": "8mx0e0JUq43Ax/5I1BEcvUsFmDIUdsRnrKIMG3+QOz11SMbI2nHAPQYFkIkWnxgaxpObaZvx6EcJXQmptEgehg==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", - "Azure.Deployments.DiffEngine": "1.34.0", - "Azure.Deployments.Extensibility": "1.34.0", + "Azure.Deployments.Core": "1.54.0", + "Azure.Deployments.DiffEngine": "1.54.0", + "Azure.Deployments.Extensibility": "1.54.0", "Azure.Deployments.ResourceMetadata": "1.0.1265", - "Azure.Deployments.Templates": "1.34.0", + "Azure.Deployments.Templates": "1.54.0", "Microsoft.AspNet.WebApi.Client": "5.2.9", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Newtonsoft.Json": "13.0.2", @@ -150,23 +150,34 @@ }, "Azure.Deployments.Expression": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "4XIed8jUdWsc8IG2o2tdqJl8ioWjwN9MR2y9L9PV/5wlDUyb2I6MXVwU/f5d3ZMCD+svc4rE6HRgxMIBYHZmSQ==", + "resolved": "1.54.0", + "contentHash": "2nhYxVun701mghHxK5h5qZoL9RjaVCo83/VTc2HgPKJkIcRW3oYFbxnMQrP5pr9k0GsCxKmvOkwsoET9gy851A==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", + "Azure.Deployments.Core": "1.54.0", "IPNetwork2": "2.6.598", "Newtonsoft.Json": "13.0.2" } }, "Azure.Deployments.Extensibility": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "3n64LvNi90r70Puobb8OImKlivz6/JaCW7zVImwb8VXeXTJJtoPTuY0z81QroGqdQMA/aU8123edFBTM4hiBVg==", + "resolved": "1.54.0", + "contentHash": "CiMXvV8NVW/xQKMQVX7gmKj20GP/h1k+4nuCNyCoS5gek4WI9yk7pH0XHwmx1TVf2Ms1vtwc9bOYohq1kd27tg==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", + "Azure.Deployments.Core": "1.54.0", "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.JsonPath": { "type": "Transitive", "resolved": "1.0.1265", @@ -191,12 +202,12 @@ }, "Azure.Deployments.Templates": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "AlstKsqlGEv8XNEjtCZ9KsI//ZKtnKn0m18TSuSQT36w+RjWiCo35uE6U/HTIXmTRS/jJVOuwMSPU4FhTLW/jg==", + "resolved": "1.54.0", + "contentHash": "TNq6JNNcDVuNSS4sqBUETVTm8KDIYvt28vWSPaV+rHyTogo0qJHciSQglEwOk0zoTp1Qf8plT7QqXS0uM/A2AA==", "dependencies": { "Azure.Bicep.Types": "0.5.9", - "Azure.Deployments.Core": "1.34.0", - "Azure.Deployments.Expression": "1.34.0", + "Azure.Deployments.Core": "1.54.0", + "Azure.Deployments.Expression": "1.54.0", "Microsoft.Automata.SRM": "1.2.2", "Newtonsoft.Json": "13.0.2" } @@ -344,6 +355,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" + } + }, "MediatR": { "type": "Transitive", "resolved": "8.1.0", @@ -1485,7 +1504,8 @@ "dependencies": { "Azure.Bicep.Core": "[1.0.0, )", "Azure.Bicep.Local.Extension": "[1.0.0, )", - "Azure.Deployments.Engine": "[1.34.0, )", + "Azure.Deployments.Engine": "[1.54.0, )", + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "Microsoft.AspNet.WebApi.Client": "[6.0.0, )" } }, diff --git a/src/Bicep.Core.UnitTests/packages.lock.json b/src/Bicep.Core.UnitTests/packages.lock.json index 9701fc63f87..ed2071ed28e 100644 --- a/src/Bicep.Core.UnitTests/packages.lock.json +++ b/src/Bicep.Core.UnitTests/packages.lock.json @@ -165,8 +165,8 @@ }, "Azure.Deployments.Core": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "Zev+8/PldTvNwUEI84e7szmJm7/ClHyr4fe0hlMylZ9lwD4wCOyc3ijWe6LmI8J92WtXX7Mh1K1FtIcHOZC0iw==", + "resolved": "1.54.0", + "contentHash": "dItBSPwB83gv9BXwPEcef4VYufmuP7w59Bg/xpjCSlBEPlj3UukVypUqMmI2hRObhNnz2TQHiMw+Y/S8HtJbyA==", "dependencies": { "Microsoft.PowerPlatform.ResourceStack": "7.0.0.2007", "Newtonsoft.Json": "13.0.2", @@ -176,19 +176,19 @@ }, "Azure.Deployments.DiffEngine": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "JqDITGajqF4tjR0sqgl8YlwWEC92r7LOHjzP0cH6K0mQIyY3rRr2c1k3fbYRrbG1D69o4IiNXqqShDd8K4lSMg==" + "resolved": "1.54.0", + "contentHash": "Z+1Q/Vpy/LF0xKm3/0szoU/0AhZuNbnQOVMagtfejpRNteqnmeryLjdHG59AeKIpzgbHr3XxusfGz0jg4wjLdA==" }, "Azure.Deployments.Engine": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "xP1lnwgceE74Tmp5N5R1SdcB0iICwH2QiwVWpZnjqzA5EXGr9tesEbV1EO7GTgfCXtTWYbcQlySkAKtDPuMt0A==", + "resolved": "1.54.0", + "contentHash": "8mx0e0JUq43Ax/5I1BEcvUsFmDIUdsRnrKIMG3+QOz11SMbI2nHAPQYFkIkWnxgaxpObaZvx6EcJXQmptEgehg==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", - "Azure.Deployments.DiffEngine": "1.34.0", - "Azure.Deployments.Extensibility": "1.34.0", + "Azure.Deployments.Core": "1.54.0", + "Azure.Deployments.DiffEngine": "1.54.0", + "Azure.Deployments.Extensibility": "1.54.0", "Azure.Deployments.ResourceMetadata": "1.0.1265", - "Azure.Deployments.Templates": "1.34.0", + "Azure.Deployments.Templates": "1.54.0", "Microsoft.AspNet.WebApi.Client": "5.2.9", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Newtonsoft.Json": "13.0.2", @@ -198,23 +198,34 @@ }, "Azure.Deployments.Expression": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "4XIed8jUdWsc8IG2o2tdqJl8ioWjwN9MR2y9L9PV/5wlDUyb2I6MXVwU/f5d3ZMCD+svc4rE6HRgxMIBYHZmSQ==", + "resolved": "1.54.0", + "contentHash": "2nhYxVun701mghHxK5h5qZoL9RjaVCo83/VTc2HgPKJkIcRW3oYFbxnMQrP5pr9k0GsCxKmvOkwsoET9gy851A==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", + "Azure.Deployments.Core": "1.54.0", "IPNetwork2": "2.6.598", "Newtonsoft.Json": "13.0.2" } }, "Azure.Deployments.Extensibility": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "3n64LvNi90r70Puobb8OImKlivz6/JaCW7zVImwb8VXeXTJJtoPTuY0z81QroGqdQMA/aU8123edFBTM4hiBVg==", + "resolved": "1.54.0", + "contentHash": "CiMXvV8NVW/xQKMQVX7gmKj20GP/h1k+4nuCNyCoS5gek4WI9yk7pH0XHwmx1TVf2Ms1vtwc9bOYohq1kd27tg==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", + "Azure.Deployments.Core": "1.54.0", "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.JsonPath": { "type": "Transitive", "resolved": "1.0.1265", @@ -239,12 +250,12 @@ }, "Azure.Deployments.Templates": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "AlstKsqlGEv8XNEjtCZ9KsI//ZKtnKn0m18TSuSQT36w+RjWiCo35uE6U/HTIXmTRS/jJVOuwMSPU4FhTLW/jg==", + "resolved": "1.54.0", + "contentHash": "TNq6JNNcDVuNSS4sqBUETVTm8KDIYvt28vWSPaV+rHyTogo0qJHciSQglEwOk0zoTp1Qf8plT7QqXS0uM/A2AA==", "dependencies": { "Azure.Bicep.Types": "0.5.9", - "Azure.Deployments.Core": "1.34.0", - "Azure.Deployments.Expression": "1.34.0", + "Azure.Deployments.Core": "1.54.0", + "Azure.Deployments.Expression": "1.54.0", "Microsoft.Automata.SRM": "1.2.2", "Newtonsoft.Json": "13.0.2" } @@ -379,6 +390,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" + } + }, "MediatR": { "type": "Transitive", "resolved": "8.1.0", @@ -1491,7 +1510,8 @@ "dependencies": { "Azure.Bicep.Core": "[1.0.0, )", "Azure.Bicep.Local.Extension": "[1.0.0, )", - "Azure.Deployments.Engine": "[1.34.0, )", + "Azure.Deployments.Engine": "[1.54.0, )", + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "Microsoft.AspNet.WebApi.Client": "[6.0.0, )" } }, diff --git a/src/Bicep.Core/Emit/TemplateWriter.cs b/src/Bicep.Core/Emit/TemplateWriter.cs index 6f46af423cc..e1439ffb535 100644 --- a/src/Bicep.Core/Emit/TemplateWriter.cs +++ b/src/Bicep.Core/Emit/TemplateWriter.cs @@ -109,7 +109,14 @@ public void Write(SourceAwareJsonTextWriter writer) if (Context.Settings.UseExperimentalTemplateLanguageVersion) { - emitter.EmitProperty(LanguageVersionPropertyName, "2.1-experimental"); + if (Context.SemanticModel.Features.LocalDeployEnabled) + { + emitter.EmitProperty(LanguageVersionPropertyName, "2.2-experimental"); + } + else + { + emitter.EmitProperty(LanguageVersionPropertyName, "2.1-experimental"); + } } else if (Context.Settings.EnableSymbolicNames) { @@ -128,7 +135,7 @@ public void Write(SourceAwareJsonTextWriter writer) this.EmitVariablesIfPresent(emitter, program.Variables.Concat(Context.ImportClosureInfo.ImportedVariablesInClosure)); - this.EmitProviders(emitter, program.Providers); + this.EmitExtensionsIfPresent(emitter, program.Providers); this.EmitResources(jsonWriter, emitter, program.Resources, program.Modules); @@ -987,18 +994,26 @@ private void EmitVariablesIfPresent(ExpressionEmitter emitter, IEnumerable providers) + private void EmitExtensionsIfPresent(ExpressionEmitter emitter, ImmutableArray providers) { if (!providers.Any()) { return; } - if (this.Context.SemanticModel.Features.LocalDeployEnabled) + // TODO: Remove if statement once all providers got migrated to extensions (extensibility v2 contract). + if (Context.SemanticModel.Features.LocalDeployEnabled) + { + EmitExtensions(emitter, providers.Add(GetExtensionForLocalDeploy())); + } + else { - providers = providers.Add(GetProviderForLocalDeploy()); + EmitProviders(emitter, providers); } + } + private static void EmitProviders(ExpressionEmitter emitter, ImmutableArray providers) + { emitter.EmitObjectProperty("imports", () => { foreach (var provider in providers) @@ -1018,7 +1033,102 @@ private void EmitProviders(ExpressionEmitter emitter, ImmutableArray providers) + { + emitter.EmitObjectProperty("extensions", () => + { + foreach (var provider in providers) + { + var settings = provider.Settings; + + emitter.EmitObjectProperty(provider.Name, () => + { + emitter.EmitProperty("name", settings.ArmTemplateProviderName); + emitter.EmitProperty("version", settings.ArmTemplateProviderVersion); + + EmitExtensionConfig(provider, emitter); + }, + provider.SourceSyntax); + } + }); + } + + private void EmitExtensionConfig(DeclaredProviderExpression provider, ExpressionEmitter emitter) + { + if (provider.Config is null) + { + return; + } + + if (provider.Config is not ObjectExpression providerConfig) + { + throw new UnreachableException($"Provider config type expected to be of type: '{nameof(ObjectExpression)}' but received: '{provider.Config.GetType()}'"); + } + + emitter.EmitObjectProperty("config", () => + { + foreach (var providerConfigProperty in providerConfig.Properties) + { + // Type checking should have validated that the config name is not an expression (e.g. string interpolation), if we get a null value it means something + // was wrong with type checking validation. + var extensionConfigName = providerConfigProperty.TryGetKeyText() ?? throw new UnreachableException("Expressions are not allowed as config names."); + var configType = provider.Settings.ConfigurationType ?? throw new UnreachableException("Config type must be specified."); + var extensionConfigType = GetExtensionConfigType(extensionConfigName, configType); + + emitter.EmitObjectProperty(extensionConfigName, () => + { + switch (extensionConfigType) + { + case StringType: + if (extensionConfigType.ValidationFlags.HasFlag(TypeSymbolValidationFlags.IsSecure)) + { + emitter.EmitProperty("type", "secureString"); + } + else + { + emitter.EmitProperty("type", "string"); + } + break; + case IntegerType: + emitter.EmitProperty("type", "int"); + break; + case BooleanType: + emitter.EmitProperty("type", "bool"); + break; + case ArrayType: + emitter.EmitProperty("type", "array"); + break; + case ObjectType: + if (extensionConfigType.ValidationFlags.HasFlag(TypeSymbolValidationFlags.IsSecure)) + { + emitter.EmitProperty("type", "secureObject"); + } + else + { + emitter.EmitProperty("type", "object"); + } + break; + default: + throw new ArgumentException($"Config name: '{extensionConfigName}' specified an unsupported type: '{extensionConfigType}'. Supported types are: 'string', 'secureString', 'int', 'bool', 'array', 'secureObject', 'object'."); + } + + emitter.EmitProperty("defaultValue", providerConfigProperty.Value); + }); + } + }); + } + + private TypeSymbol GetExtensionConfigType(string configName, ObjectType configType) + { + if (configType.Properties.TryGetValue(configName) is { } configItem) + { + return configItem.TypeReference.Type; + } + + throw new UnreachableException($"Configuration name: '{configName}' does not exist as part of provider configuration."); + } + + private DeclaredProviderExpression GetExtensionForLocalDeploy() { return new( null, @@ -1103,7 +1213,14 @@ private void EmitResource(ExpressionEmitter emitter, DeclaredResourceExpression var importSymbol = Context.SemanticModel.Root.ProviderDeclarations.FirstOrDefault(i => metadata.Type.DeclaringNamespace.AliasNameEquals(i.Name)); if (importSymbol is not null) { - emitter.EmitProperty("import", importSymbol.Name); + if (this.Context.SemanticModel.Features.LocalDeployEnabled) + { + emitter.EmitProperty("extension", importSymbol.Name); + } + else + { + emitter.EmitProperty("import", importSymbol.Name); + } } if (metadata.IsAzResource) @@ -1225,7 +1342,7 @@ private void EmitModuleForLocalDeploy(PositionTrackingJsonTextWriter jsonWriter, { emitter.EmitObject(() => { - emitter.EmitProperty("import", "az0synthesized"); + emitter.EmitProperty("extension", "az0synthesized"); var body = module.Body; if (body is ForLoopExpression forLoop) diff --git a/src/Bicep.Decompiler.IntegrationTests/packages.lock.json b/src/Bicep.Decompiler.IntegrationTests/packages.lock.json index 30fca8119a5..47a355756a8 100644 --- a/src/Bicep.Decompiler.IntegrationTests/packages.lock.json +++ b/src/Bicep.Decompiler.IntegrationTests/packages.lock.json @@ -122,8 +122,8 @@ }, "Azure.Deployments.Core": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "Zev+8/PldTvNwUEI84e7szmJm7/ClHyr4fe0hlMylZ9lwD4wCOyc3ijWe6LmI8J92WtXX7Mh1K1FtIcHOZC0iw==", + "resolved": "1.54.0", + "contentHash": "dItBSPwB83gv9BXwPEcef4VYufmuP7w59Bg/xpjCSlBEPlj3UukVypUqMmI2hRObhNnz2TQHiMw+Y/S8HtJbyA==", "dependencies": { "Microsoft.PowerPlatform.ResourceStack": "7.0.0.2007", "Newtonsoft.Json": "13.0.2", @@ -133,19 +133,19 @@ }, "Azure.Deployments.DiffEngine": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "JqDITGajqF4tjR0sqgl8YlwWEC92r7LOHjzP0cH6K0mQIyY3rRr2c1k3fbYRrbG1D69o4IiNXqqShDd8K4lSMg==" + "resolved": "1.54.0", + "contentHash": "Z+1Q/Vpy/LF0xKm3/0szoU/0AhZuNbnQOVMagtfejpRNteqnmeryLjdHG59AeKIpzgbHr3XxusfGz0jg4wjLdA==" }, "Azure.Deployments.Engine": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "xP1lnwgceE74Tmp5N5R1SdcB0iICwH2QiwVWpZnjqzA5EXGr9tesEbV1EO7GTgfCXtTWYbcQlySkAKtDPuMt0A==", + "resolved": "1.54.0", + "contentHash": "8mx0e0JUq43Ax/5I1BEcvUsFmDIUdsRnrKIMG3+QOz11SMbI2nHAPQYFkIkWnxgaxpObaZvx6EcJXQmptEgehg==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", - "Azure.Deployments.DiffEngine": "1.34.0", - "Azure.Deployments.Extensibility": "1.34.0", + "Azure.Deployments.Core": "1.54.0", + "Azure.Deployments.DiffEngine": "1.54.0", + "Azure.Deployments.Extensibility": "1.54.0", "Azure.Deployments.ResourceMetadata": "1.0.1265", - "Azure.Deployments.Templates": "1.34.0", + "Azure.Deployments.Templates": "1.54.0", "Microsoft.AspNet.WebApi.Client": "5.2.9", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Newtonsoft.Json": "13.0.2", @@ -155,23 +155,34 @@ }, "Azure.Deployments.Expression": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "4XIed8jUdWsc8IG2o2tdqJl8ioWjwN9MR2y9L9PV/5wlDUyb2I6MXVwU/f5d3ZMCD+svc4rE6HRgxMIBYHZmSQ==", + "resolved": "1.54.0", + "contentHash": "2nhYxVun701mghHxK5h5qZoL9RjaVCo83/VTc2HgPKJkIcRW3oYFbxnMQrP5pr9k0GsCxKmvOkwsoET9gy851A==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", + "Azure.Deployments.Core": "1.54.0", "IPNetwork2": "2.6.598", "Newtonsoft.Json": "13.0.2" } }, "Azure.Deployments.Extensibility": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "3n64LvNi90r70Puobb8OImKlivz6/JaCW7zVImwb8VXeXTJJtoPTuY0z81QroGqdQMA/aU8123edFBTM4hiBVg==", + "resolved": "1.54.0", + "contentHash": "CiMXvV8NVW/xQKMQVX7gmKj20GP/h1k+4nuCNyCoS5gek4WI9yk7pH0XHwmx1TVf2Ms1vtwc9bOYohq1kd27tg==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", + "Azure.Deployments.Core": "1.54.0", "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.JsonPath": { "type": "Transitive", "resolved": "1.0.1265", @@ -196,12 +207,12 @@ }, "Azure.Deployments.Templates": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "AlstKsqlGEv8XNEjtCZ9KsI//ZKtnKn0m18TSuSQT36w+RjWiCo35uE6U/HTIXmTRS/jJVOuwMSPU4FhTLW/jg==", + "resolved": "1.54.0", + "contentHash": "TNq6JNNcDVuNSS4sqBUETVTm8KDIYvt28vWSPaV+rHyTogo0qJHciSQglEwOk0zoTp1Qf8plT7QqXS0uM/A2AA==", "dependencies": { "Azure.Bicep.Types": "0.5.9", - "Azure.Deployments.Core": "1.34.0", - "Azure.Deployments.Expression": "1.34.0", + "Azure.Deployments.Core": "1.54.0", + "Azure.Deployments.Expression": "1.54.0", "Microsoft.Automata.SRM": "1.2.2", "Newtonsoft.Json": "13.0.2" } @@ -349,6 +360,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" + } + }, "MediatR": { "type": "Transitive", "resolved": "8.1.0", @@ -1538,7 +1557,8 @@ "dependencies": { "Azure.Bicep.Core": "[1.0.0, )", "Azure.Bicep.Local.Extension": "[1.0.0, )", - "Azure.Deployments.Engine": "[1.34.0, )", + "Azure.Deployments.Engine": "[1.54.0, )", + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "Microsoft.AspNet.WebApi.Client": "[6.0.0, )" } }, diff --git a/src/Bicep.Decompiler.UnitTests/packages.lock.json b/src/Bicep.Decompiler.UnitTests/packages.lock.json index 30fca8119a5..47a355756a8 100644 --- a/src/Bicep.Decompiler.UnitTests/packages.lock.json +++ b/src/Bicep.Decompiler.UnitTests/packages.lock.json @@ -122,8 +122,8 @@ }, "Azure.Deployments.Core": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "Zev+8/PldTvNwUEI84e7szmJm7/ClHyr4fe0hlMylZ9lwD4wCOyc3ijWe6LmI8J92WtXX7Mh1K1FtIcHOZC0iw==", + "resolved": "1.54.0", + "contentHash": "dItBSPwB83gv9BXwPEcef4VYufmuP7w59Bg/xpjCSlBEPlj3UukVypUqMmI2hRObhNnz2TQHiMw+Y/S8HtJbyA==", "dependencies": { "Microsoft.PowerPlatform.ResourceStack": "7.0.0.2007", "Newtonsoft.Json": "13.0.2", @@ -133,19 +133,19 @@ }, "Azure.Deployments.DiffEngine": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "JqDITGajqF4tjR0sqgl8YlwWEC92r7LOHjzP0cH6K0mQIyY3rRr2c1k3fbYRrbG1D69o4IiNXqqShDd8K4lSMg==" + "resolved": "1.54.0", + "contentHash": "Z+1Q/Vpy/LF0xKm3/0szoU/0AhZuNbnQOVMagtfejpRNteqnmeryLjdHG59AeKIpzgbHr3XxusfGz0jg4wjLdA==" }, "Azure.Deployments.Engine": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "xP1lnwgceE74Tmp5N5R1SdcB0iICwH2QiwVWpZnjqzA5EXGr9tesEbV1EO7GTgfCXtTWYbcQlySkAKtDPuMt0A==", + "resolved": "1.54.0", + "contentHash": "8mx0e0JUq43Ax/5I1BEcvUsFmDIUdsRnrKIMG3+QOz11SMbI2nHAPQYFkIkWnxgaxpObaZvx6EcJXQmptEgehg==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", - "Azure.Deployments.DiffEngine": "1.34.0", - "Azure.Deployments.Extensibility": "1.34.0", + "Azure.Deployments.Core": "1.54.0", + "Azure.Deployments.DiffEngine": "1.54.0", + "Azure.Deployments.Extensibility": "1.54.0", "Azure.Deployments.ResourceMetadata": "1.0.1265", - "Azure.Deployments.Templates": "1.34.0", + "Azure.Deployments.Templates": "1.54.0", "Microsoft.AspNet.WebApi.Client": "5.2.9", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Newtonsoft.Json": "13.0.2", @@ -155,23 +155,34 @@ }, "Azure.Deployments.Expression": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "4XIed8jUdWsc8IG2o2tdqJl8ioWjwN9MR2y9L9PV/5wlDUyb2I6MXVwU/f5d3ZMCD+svc4rE6HRgxMIBYHZmSQ==", + "resolved": "1.54.0", + "contentHash": "2nhYxVun701mghHxK5h5qZoL9RjaVCo83/VTc2HgPKJkIcRW3oYFbxnMQrP5pr9k0GsCxKmvOkwsoET9gy851A==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", + "Azure.Deployments.Core": "1.54.0", "IPNetwork2": "2.6.598", "Newtonsoft.Json": "13.0.2" } }, "Azure.Deployments.Extensibility": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "3n64LvNi90r70Puobb8OImKlivz6/JaCW7zVImwb8VXeXTJJtoPTuY0z81QroGqdQMA/aU8123edFBTM4hiBVg==", + "resolved": "1.54.0", + "contentHash": "CiMXvV8NVW/xQKMQVX7gmKj20GP/h1k+4nuCNyCoS5gek4WI9yk7pH0XHwmx1TVf2Ms1vtwc9bOYohq1kd27tg==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", + "Azure.Deployments.Core": "1.54.0", "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.JsonPath": { "type": "Transitive", "resolved": "1.0.1265", @@ -196,12 +207,12 @@ }, "Azure.Deployments.Templates": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "AlstKsqlGEv8XNEjtCZ9KsI//ZKtnKn0m18TSuSQT36w+RjWiCo35uE6U/HTIXmTRS/jJVOuwMSPU4FhTLW/jg==", + "resolved": "1.54.0", + "contentHash": "TNq6JNNcDVuNSS4sqBUETVTm8KDIYvt28vWSPaV+rHyTogo0qJHciSQglEwOk0zoTp1Qf8plT7QqXS0uM/A2AA==", "dependencies": { "Azure.Bicep.Types": "0.5.9", - "Azure.Deployments.Core": "1.34.0", - "Azure.Deployments.Expression": "1.34.0", + "Azure.Deployments.Core": "1.54.0", + "Azure.Deployments.Expression": "1.54.0", "Microsoft.Automata.SRM": "1.2.2", "Newtonsoft.Json": "13.0.2" } @@ -349,6 +360,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" + } + }, "MediatR": { "type": "Transitive", "resolved": "8.1.0", @@ -1538,7 +1557,8 @@ "dependencies": { "Azure.Bicep.Core": "[1.0.0, )", "Azure.Bicep.Local.Extension": "[1.0.0, )", - "Azure.Deployments.Engine": "[1.34.0, )", + "Azure.Deployments.Engine": "[1.54.0, )", + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "Microsoft.AspNet.WebApi.Client": "[6.0.0, )" } }, diff --git a/src/Bicep.LangServer.IntegrationTests/packages.lock.json b/src/Bicep.LangServer.IntegrationTests/packages.lock.json index 26bfe2c899b..93510f5c470 100644 --- a/src/Bicep.LangServer.IntegrationTests/packages.lock.json +++ b/src/Bicep.LangServer.IntegrationTests/packages.lock.json @@ -148,8 +148,8 @@ }, "Azure.Deployments.Core": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "Zev+8/PldTvNwUEI84e7szmJm7/ClHyr4fe0hlMylZ9lwD4wCOyc3ijWe6LmI8J92WtXX7Mh1K1FtIcHOZC0iw==", + "resolved": "1.54.0", + "contentHash": "dItBSPwB83gv9BXwPEcef4VYufmuP7w59Bg/xpjCSlBEPlj3UukVypUqMmI2hRObhNnz2TQHiMw+Y/S8HtJbyA==", "dependencies": { "Microsoft.PowerPlatform.ResourceStack": "7.0.0.2007", "Newtonsoft.Json": "13.0.2", @@ -159,19 +159,19 @@ }, "Azure.Deployments.DiffEngine": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "JqDITGajqF4tjR0sqgl8YlwWEC92r7LOHjzP0cH6K0mQIyY3rRr2c1k3fbYRrbG1D69o4IiNXqqShDd8K4lSMg==" + "resolved": "1.54.0", + "contentHash": "Z+1Q/Vpy/LF0xKm3/0szoU/0AhZuNbnQOVMagtfejpRNteqnmeryLjdHG59AeKIpzgbHr3XxusfGz0jg4wjLdA==" }, "Azure.Deployments.Engine": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "xP1lnwgceE74Tmp5N5R1SdcB0iICwH2QiwVWpZnjqzA5EXGr9tesEbV1EO7GTgfCXtTWYbcQlySkAKtDPuMt0A==", + "resolved": "1.54.0", + "contentHash": "8mx0e0JUq43Ax/5I1BEcvUsFmDIUdsRnrKIMG3+QOz11SMbI2nHAPQYFkIkWnxgaxpObaZvx6EcJXQmptEgehg==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", - "Azure.Deployments.DiffEngine": "1.34.0", - "Azure.Deployments.Extensibility": "1.34.0", + "Azure.Deployments.Core": "1.54.0", + "Azure.Deployments.DiffEngine": "1.54.0", + "Azure.Deployments.Extensibility": "1.54.0", "Azure.Deployments.ResourceMetadata": "1.0.1265", - "Azure.Deployments.Templates": "1.34.0", + "Azure.Deployments.Templates": "1.54.0", "Microsoft.AspNet.WebApi.Client": "5.2.9", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Newtonsoft.Json": "13.0.2", @@ -181,23 +181,34 @@ }, "Azure.Deployments.Expression": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "4XIed8jUdWsc8IG2o2tdqJl8ioWjwN9MR2y9L9PV/5wlDUyb2I6MXVwU/f5d3ZMCD+svc4rE6HRgxMIBYHZmSQ==", + "resolved": "1.54.0", + "contentHash": "2nhYxVun701mghHxK5h5qZoL9RjaVCo83/VTc2HgPKJkIcRW3oYFbxnMQrP5pr9k0GsCxKmvOkwsoET9gy851A==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", + "Azure.Deployments.Core": "1.54.0", "IPNetwork2": "2.6.598", "Newtonsoft.Json": "13.0.2" } }, "Azure.Deployments.Extensibility": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "3n64LvNi90r70Puobb8OImKlivz6/JaCW7zVImwb8VXeXTJJtoPTuY0z81QroGqdQMA/aU8123edFBTM4hiBVg==", + "resolved": "1.54.0", + "contentHash": "CiMXvV8NVW/xQKMQVX7gmKj20GP/h1k+4nuCNyCoS5gek4WI9yk7pH0XHwmx1TVf2Ms1vtwc9bOYohq1kd27tg==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", + "Azure.Deployments.Core": "1.54.0", "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.JsonPath": { "type": "Transitive", "resolved": "1.0.1265", @@ -222,12 +233,12 @@ }, "Azure.Deployments.Templates": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "AlstKsqlGEv8XNEjtCZ9KsI//ZKtnKn0m18TSuSQT36w+RjWiCo35uE6U/HTIXmTRS/jJVOuwMSPU4FhTLW/jg==", + "resolved": "1.54.0", + "contentHash": "TNq6JNNcDVuNSS4sqBUETVTm8KDIYvt28vWSPaV+rHyTogo0qJHciSQglEwOk0zoTp1Qf8plT7QqXS0uM/A2AA==", "dependencies": { "Azure.Bicep.Types": "0.5.9", - "Azure.Deployments.Core": "1.34.0", - "Azure.Deployments.Expression": "1.34.0", + "Azure.Deployments.Core": "1.54.0", + "Azure.Deployments.Expression": "1.54.0", "Microsoft.Automata.SRM": "1.2.2", "Newtonsoft.Json": "13.0.2" } @@ -375,6 +386,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" + } + }, "MediatR": { "type": "Transitive", "resolved": "8.1.0", @@ -1496,7 +1515,8 @@ "dependencies": { "Azure.Bicep.Core": "[1.0.0, )", "Azure.Bicep.Local.Extension": "[1.0.0, )", - "Azure.Deployments.Engine": "[1.34.0, )", + "Azure.Deployments.Engine": "[1.54.0, )", + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "Microsoft.AspNet.WebApi.Client": "[6.0.0, )" } }, diff --git a/src/Bicep.LangServer.UnitTests/packages.lock.json b/src/Bicep.LangServer.UnitTests/packages.lock.json index 720e8769d66..9c611f8e6a9 100644 --- a/src/Bicep.LangServer.UnitTests/packages.lock.json +++ b/src/Bicep.LangServer.UnitTests/packages.lock.json @@ -158,8 +158,8 @@ }, "Azure.Deployments.Core": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "Zev+8/PldTvNwUEI84e7szmJm7/ClHyr4fe0hlMylZ9lwD4wCOyc3ijWe6LmI8J92WtXX7Mh1K1FtIcHOZC0iw==", + "resolved": "1.54.0", + "contentHash": "dItBSPwB83gv9BXwPEcef4VYufmuP7w59Bg/xpjCSlBEPlj3UukVypUqMmI2hRObhNnz2TQHiMw+Y/S8HtJbyA==", "dependencies": { "Microsoft.PowerPlatform.ResourceStack": "7.0.0.2007", "Newtonsoft.Json": "13.0.2", @@ -169,19 +169,19 @@ }, "Azure.Deployments.DiffEngine": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "JqDITGajqF4tjR0sqgl8YlwWEC92r7LOHjzP0cH6K0mQIyY3rRr2c1k3fbYRrbG1D69o4IiNXqqShDd8K4lSMg==" + "resolved": "1.54.0", + "contentHash": "Z+1Q/Vpy/LF0xKm3/0szoU/0AhZuNbnQOVMagtfejpRNteqnmeryLjdHG59AeKIpzgbHr3XxusfGz0jg4wjLdA==" }, "Azure.Deployments.Engine": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "xP1lnwgceE74Tmp5N5R1SdcB0iICwH2QiwVWpZnjqzA5EXGr9tesEbV1EO7GTgfCXtTWYbcQlySkAKtDPuMt0A==", + "resolved": "1.54.0", + "contentHash": "8mx0e0JUq43Ax/5I1BEcvUsFmDIUdsRnrKIMG3+QOz11SMbI2nHAPQYFkIkWnxgaxpObaZvx6EcJXQmptEgehg==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", - "Azure.Deployments.DiffEngine": "1.34.0", - "Azure.Deployments.Extensibility": "1.34.0", + "Azure.Deployments.Core": "1.54.0", + "Azure.Deployments.DiffEngine": "1.54.0", + "Azure.Deployments.Extensibility": "1.54.0", "Azure.Deployments.ResourceMetadata": "1.0.1265", - "Azure.Deployments.Templates": "1.34.0", + "Azure.Deployments.Templates": "1.54.0", "Microsoft.AspNet.WebApi.Client": "5.2.9", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Newtonsoft.Json": "13.0.2", @@ -191,23 +191,34 @@ }, "Azure.Deployments.Expression": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "4XIed8jUdWsc8IG2o2tdqJl8ioWjwN9MR2y9L9PV/5wlDUyb2I6MXVwU/f5d3ZMCD+svc4rE6HRgxMIBYHZmSQ==", + "resolved": "1.54.0", + "contentHash": "2nhYxVun701mghHxK5h5qZoL9RjaVCo83/VTc2HgPKJkIcRW3oYFbxnMQrP5pr9k0GsCxKmvOkwsoET9gy851A==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", + "Azure.Deployments.Core": "1.54.0", "IPNetwork2": "2.6.598", "Newtonsoft.Json": "13.0.2" } }, "Azure.Deployments.Extensibility": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "3n64LvNi90r70Puobb8OImKlivz6/JaCW7zVImwb8VXeXTJJtoPTuY0z81QroGqdQMA/aU8123edFBTM4hiBVg==", + "resolved": "1.54.0", + "contentHash": "CiMXvV8NVW/xQKMQVX7gmKj20GP/h1k+4nuCNyCoS5gek4WI9yk7pH0XHwmx1TVf2Ms1vtwc9bOYohq1kd27tg==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", + "Azure.Deployments.Core": "1.54.0", "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.JsonPath": { "type": "Transitive", "resolved": "1.0.1265", @@ -232,12 +243,12 @@ }, "Azure.Deployments.Templates": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "AlstKsqlGEv8XNEjtCZ9KsI//ZKtnKn0m18TSuSQT36w+RjWiCo35uE6U/HTIXmTRS/jJVOuwMSPU4FhTLW/jg==", + "resolved": "1.54.0", + "contentHash": "TNq6JNNcDVuNSS4sqBUETVTm8KDIYvt28vWSPaV+rHyTogo0qJHciSQglEwOk0zoTp1Qf8plT7QqXS0uM/A2AA==", "dependencies": { "Azure.Bicep.Types": "0.5.9", - "Azure.Deployments.Core": "1.34.0", - "Azure.Deployments.Expression": "1.34.0", + "Azure.Deployments.Core": "1.54.0", + "Azure.Deployments.Expression": "1.54.0", "Microsoft.Automata.SRM": "1.2.2", "Newtonsoft.Json": "13.0.2" } @@ -385,6 +396,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" + } + }, "MediatR": { "type": "Transitive", "resolved": "8.1.0", @@ -1558,7 +1577,8 @@ "dependencies": { "Azure.Bicep.Core": "[1.0.0, )", "Azure.Bicep.Local.Extension": "[1.0.0, )", - "Azure.Deployments.Engine": "[1.34.0, )", + "Azure.Deployments.Engine": "[1.54.0, )", + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "Microsoft.AspNet.WebApi.Client": "[6.0.0, )" } }, diff --git a/src/Bicep.LangServer/Handlers/LocalDeployHandler.cs b/src/Bicep.LangServer/Handlers/LocalDeployHandler.cs index 2334c5bc602..9dc9ad7a127 100644 --- a/src/Bicep.LangServer/Handlers/LocalDeployHandler.cs +++ b/src/Bicep.LangServer/Handlers/LocalDeployHandler.cs @@ -81,8 +81,8 @@ public async Task Handle(LocalDeployRequest request, Cancel throw new InvalidOperationException("Bicep file had errors."); } - await using LocalExtensibilityHandler extensibilityHandler = new(moduleDispatcher, GrpcExtensibilityProvider.Start); - await extensibilityHandler.InitializeProviders(context.Compilation); + await using LocalExtensibilityHostManager extensibilityHandler = new(moduleDispatcher, GrpcBuiltInLocalExtension.Start); + await extensibilityHandler.InitializeExtensions(context.Compilation); var result = await LocalDeployment.Deploy(extensibilityHandler, templateString, parametersString, cancellationToken); diff --git a/src/Bicep.LangServer/packages.lock.json b/src/Bicep.LangServer/packages.lock.json index 6e309ec372a..a9b763fd5ed 100644 --- a/src/Bicep.LangServer/packages.lock.json +++ b/src/Bicep.LangServer/packages.lock.json @@ -135,8 +135,8 @@ }, "Azure.Deployments.Core": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "Zev+8/PldTvNwUEI84e7szmJm7/ClHyr4fe0hlMylZ9lwD4wCOyc3ijWe6LmI8J92WtXX7Mh1K1FtIcHOZC0iw==", + "resolved": "1.54.0", + "contentHash": "dItBSPwB83gv9BXwPEcef4VYufmuP7w59Bg/xpjCSlBEPlj3UukVypUqMmI2hRObhNnz2TQHiMw+Y/S8HtJbyA==", "dependencies": { "Microsoft.PowerPlatform.ResourceStack": "7.0.0.2007", "Newtonsoft.Json": "13.0.2", @@ -146,19 +146,19 @@ }, "Azure.Deployments.DiffEngine": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "JqDITGajqF4tjR0sqgl8YlwWEC92r7LOHjzP0cH6K0mQIyY3rRr2c1k3fbYRrbG1D69o4IiNXqqShDd8K4lSMg==" + "resolved": "1.54.0", + "contentHash": "Z+1Q/Vpy/LF0xKm3/0szoU/0AhZuNbnQOVMagtfejpRNteqnmeryLjdHG59AeKIpzgbHr3XxusfGz0jg4wjLdA==" }, "Azure.Deployments.Engine": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "xP1lnwgceE74Tmp5N5R1SdcB0iICwH2QiwVWpZnjqzA5EXGr9tesEbV1EO7GTgfCXtTWYbcQlySkAKtDPuMt0A==", + "resolved": "1.54.0", + "contentHash": "8mx0e0JUq43Ax/5I1BEcvUsFmDIUdsRnrKIMG3+QOz11SMbI2nHAPQYFkIkWnxgaxpObaZvx6EcJXQmptEgehg==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", - "Azure.Deployments.DiffEngine": "1.34.0", - "Azure.Deployments.Extensibility": "1.34.0", + "Azure.Deployments.Core": "1.54.0", + "Azure.Deployments.DiffEngine": "1.54.0", + "Azure.Deployments.Extensibility": "1.54.0", "Azure.Deployments.ResourceMetadata": "1.0.1265", - "Azure.Deployments.Templates": "1.34.0", + "Azure.Deployments.Templates": "1.54.0", "Microsoft.AspNet.WebApi.Client": "5.2.9", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Newtonsoft.Json": "13.0.2", @@ -168,23 +168,34 @@ }, "Azure.Deployments.Expression": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "4XIed8jUdWsc8IG2o2tdqJl8ioWjwN9MR2y9L9PV/5wlDUyb2I6MXVwU/f5d3ZMCD+svc4rE6HRgxMIBYHZmSQ==", + "resolved": "1.54.0", + "contentHash": "2nhYxVun701mghHxK5h5qZoL9RjaVCo83/VTc2HgPKJkIcRW3oYFbxnMQrP5pr9k0GsCxKmvOkwsoET9gy851A==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", + "Azure.Deployments.Core": "1.54.0", "IPNetwork2": "2.6.598", "Newtonsoft.Json": "13.0.2" } }, "Azure.Deployments.Extensibility": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "3n64LvNi90r70Puobb8OImKlivz6/JaCW7zVImwb8VXeXTJJtoPTuY0z81QroGqdQMA/aU8123edFBTM4hiBVg==", + "resolved": "1.54.0", + "contentHash": "CiMXvV8NVW/xQKMQVX7gmKj20GP/h1k+4nuCNyCoS5gek4WI9yk7pH0XHwmx1TVf2Ms1vtwc9bOYohq1kd27tg==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", + "Azure.Deployments.Core": "1.54.0", "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.JsonPath": { "type": "Transitive", "resolved": "1.0.1265", @@ -209,12 +220,12 @@ }, "Azure.Deployments.Templates": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "AlstKsqlGEv8XNEjtCZ9KsI//ZKtnKn0m18TSuSQT36w+RjWiCo35uE6U/HTIXmTRS/jJVOuwMSPU4FhTLW/jg==", + "resolved": "1.54.0", + "contentHash": "TNq6JNNcDVuNSS4sqBUETVTm8KDIYvt28vWSPaV+rHyTogo0qJHciSQglEwOk0zoTp1Qf8plT7QqXS0uM/A2AA==", "dependencies": { "Azure.Bicep.Types": "0.5.9", - "Azure.Deployments.Core": "1.34.0", - "Azure.Deployments.Expression": "1.34.0", + "Azure.Deployments.Core": "1.54.0", + "Azure.Deployments.Expression": "1.54.0", "Microsoft.Automata.SRM": "1.2.2", "Newtonsoft.Json": "13.0.2" } @@ -334,6 +345,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" + } + }, "MediatR": { "type": "Transitive", "resolved": "8.1.0", @@ -1381,7 +1400,8 @@ "dependencies": { "Azure.Bicep.Core": "[1.0.0, )", "Azure.Bicep.Local.Extension": "[1.0.0, )", - "Azure.Deployments.Engine": "[1.34.0, )", + "Azure.Deployments.Engine": "[1.54.0, )", + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "Microsoft.AspNet.WebApi.Client": "[6.0.0, )" } }, diff --git a/src/Bicep.Local.Deploy.IntegrationTests/EndToEndDeploymentTests.cs b/src/Bicep.Local.Deploy.IntegrationTests/EndToEndDeploymentTests.cs index 314f5a7eb1b..ebd2d079f5c 100644 --- a/src/Bicep.Local.Deploy.IntegrationTests/EndToEndDeploymentTests.cs +++ b/src/Bicep.Local.Deploy.IntegrationTests/EndToEndDeploymentTests.cs @@ -2,7 +2,11 @@ // Licensed under the MIT License. using System.IO.Abstractions; +using System.Text; +using System.Text.Json.Nodes; using Azure.Deployments.Core.Definitions; +using Azure.Deployments.Engine.Host.Azure.ExtensibilityV2.Contract.Models; +using Azure.Deployments.Extensibility.Core.V2.Models; using Azure.Deployments.Extensibility.Messages; using Bicep.Core.Configuration; using Bicep.Core.Features; @@ -18,6 +22,7 @@ using Bicep.Local.Extension; using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.WindowsAzure.ResourceStack.Common.Json; using Moq; using Newtonsoft.Json.Linq; @@ -45,7 +50,7 @@ public async Task End_to_end_deployment_basic() } """), ("main.bicep", """ -provider http +extension http param coords { lattitude: string @@ -90,11 +95,17 @@ param coords { var parametersFile = result.Compilation.Emitter.Parameters().Parameters!; var templateFile = result.Compilation.Emitter.Parameters().Template!.Template!; - var providerMock = StrictMock.Of(); - providerMock.Setup(x => x.Save(It.Is(req => req.Resource.Properties["uri"]!.ToString() == "https://api.weather.gov/points/47.6363726,-122.1357068"), It.IsAny())) - .Returns((req, _) => + JsonObject identifiers = new() + { + { "name", "someName" }, + { "namespace", "someNamespace" } + }; + + 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, _) => { - req.Resource.Properties["body"] = """ + req.Properties["body"] = """ { "properties": { "gridId": "SEW", @@ -103,13 +114,13 @@ param coords { } } """; - - return Task.FromResult(new(req.Resource, null, null)); + return Task.FromResult(new LocalExtensibilityOperationResponse(new Resource(req.Type, req.ApiVersion, identifiers, req.Properties, "Succeeded"), null)); }); - providerMock.Setup(x => x.Save(It.Is(req => req.Resource.Properties["uri"]!.ToString() == "https://api.weather.gov/gridpoints/SEW/131,68/forecast"), It.IsAny())) - .Returns((req, _) => + + providerMock.Setup(x => x.CreateOrUpdate(It.Is(req => req.Properties["uri"]!.ToString() == "https://api.weather.gov/gridpoints/SEW/131,68/forecast"), It.IsAny())) + .Returns((req, _) => { - req.Resource.Properties["body"] = """ + req.Properties["body"] = """ { "properties": { "periods": [ @@ -127,13 +138,12 @@ param coords { } } """; - - return Task.FromResult(new(req.Resource, null, null)); + return Task.FromResult(new LocalExtensibilityOperationResponse(new Resource(req.Type, req.ApiVersion, identifiers, req.Properties, "Succeeded"), null)); }); var dispatcher = BicepTestConstants.CreateModuleDispatcher(services.Build().Construct()); - await using LocalExtensibilityHandler extensibilityHandler = new(dispatcher, uri => Task.FromResult(providerMock.Object)); - await extensibilityHandler.InitializeProviders(result.Compilation); + await using LocalExtensibilityHostManager extensibilityHandler = new(dispatcher, uri => Task.FromResult(providerMock.Object)); + await extensibilityHandler.InitializeExtensions(result.Compilation); var localDeployResult = await LocalDeployment.Deploy(extensibilityHandler, templateFile, parametersFile, TestContext.CancellationTokenSource.Token); @@ -151,4 +161,307 @@ param coords { ] """)); } + + [TestMethod] + public async Task Provider_returning_resource_and_error_data_should_fail() + { + var services = await ProviderTestHelper.GetServiceBuilderWithPublishedProvider(ThirdPartyTypeHelper.GetHttpProviderTypesTgz(), new(ExtensibilityEnabled: true, ExtensionRegistry: true, LocalDeployEnabled: true)); + + var result = await CompilationHelper.RestoreAndCompileParams(services, + ("bicepconfig.json", """ +{ + "extensions": { + "http": "br:example.azurecr.io/providers/foo:1.2.3" + }, + "experimentalFeaturesEnabled": { + "extensibility": true, + "extensionRegistry": true, + "localDeploy": true + } +} +"""), + ("main.bicep", """ +extension http + +param coords { + lattitude: string + longitude: string +} + +resource gridpointsReq 'request@v1' = { + uri: 'https://api.weather.gov/points/${coords.lattitude},${coords.longitude}' + format: 'raw' +} + +var gridpoints = json(gridpointsReq.body).properties + +resource forecastReq 'request@v1' = { + uri: 'https://api.weather.gov/gridpoints/${gridpoints.gridId}/${gridpoints.gridX},${gridpoints.gridY}/forecast' + format: 'raw' +} + +var forecast = json(forecastReq.body).properties + +type forecastType = { + name: string + temperature: int +} + +output forecast forecastType[] = map(forecast.periods, p => { + name: p.name + temperature: p.temperature +}) +"""), + ("parameters.bicepparam", """ +using 'main.bicep' + +param coords = { + lattitude: '47.6363726' + longitude: '-122.1357068' +} +""")); + + result.Should().NotHaveAnyDiagnostics(); + + var parametersFile = result.Compilation.Emitter.Parameters().Parameters!; + var templateFile = result.Compilation.Emitter.Parameters().Template!.Template!; + + JsonObject identifiers = new() + { + { "name", "someName" }, + { "namespace", "someNamespace" } + }; + + 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, _) => + { + req.Properties["body"] = """ +{ + "properties": { + "gridId": "SEW", + "gridX": "131", + "gridY": "68" + } +} +"""; + return Task.FromResult(new LocalExtensibilityOperationResponse(new Resource(req.Type, req.ApiVersion, identifiers, req.Properties, "Succeeded"), new ErrorData(new Error() { Code = "Code", Message = "Error message" }))); + }); + + var dispatcher = BicepTestConstants.CreateModuleDispatcher(services.Build().Construct()); + await using LocalExtensibilityHostManager extensibilityHandler = new(dispatcher, uri => Task.FromResult(providerMock.Object)); + await extensibilityHandler.InitializeExtensions(result.Compilation); + + var localDeployResult = await LocalDeployment.Deploy(extensibilityHandler, templateFile, parametersFile, TestContext.CancellationTokenSource.Token); + + localDeployResult.Deployment.Properties.ProvisioningState.Should().Be(ProvisioningState.Failed, because: $"Provider returned '{nameof(Resource)}' and '{nameof(ErrorData)}' as part of its response and it is not allowed. Providers should return one or the other to indicate success or failure respectively."); + localDeployResult.Deployment.Properties.Error.Should().NotBeNull(); + + localDeployResult.Deployment.Properties.Error.Code.Should().Be("DeploymentFailed"); + localDeployResult.Deployment.Properties.Error.Details.Should().NotBeNullOrEmpty(); + localDeployResult.Deployment.Properties.Error.Details[0].Code.Should().Be("ResourceDeploymentFailure"); + localDeployResult.Deployment.Properties.Error.Details[0].Target.Should().Be("/resources/gridpointsReq", because: $"Expect a failure when mocking a response for \"/resources/gridpointsReq\" since it is returning '{nameof(Resource)}' and '{nameof(ErrorData)}' when only one type should be returned to indicate success or failure."); + } + + [TestMethod] + public async Task Provider_not_returning_resource_or_error_data_should_fail() + { + var services = await ProviderTestHelper.GetServiceBuilderWithPublishedProvider(ThirdPartyTypeHelper.GetHttpProviderTypesTgz(), new(ExtensibilityEnabled: true, ExtensionRegistry: true, LocalDeployEnabled: true)); + + var result = await CompilationHelper.RestoreAndCompileParams(services, + ("bicepconfig.json", """ +{ + "extensions": { + "http": "br:example.azurecr.io/providers/foo:1.2.3" + }, + "experimentalFeaturesEnabled": { + "extensibility": true, + "extensionRegistry": true, + "localDeploy": true + } +} +"""), + ("main.bicep", """ +extension http + +param coords { + lattitude: string + longitude: string +} + +resource gridpointsReq 'request@v1' = { + uri: 'https://api.weather.gov/points/${coords.lattitude},${coords.longitude}' + format: 'raw' +} + +var gridpoints = json(gridpointsReq.body).properties + +resource forecastReq 'request@v1' = { + uri: 'https://api.weather.gov/gridpoints/${gridpoints.gridId}/${gridpoints.gridX},${gridpoints.gridY}/forecast' + format: 'raw' +} + +var forecast = json(forecastReq.body).properties + +type forecastType = { + name: string + temperature: int +} + +output forecast forecastType[] = map(forecast.periods, p => { + name: p.name + temperature: p.temperature +}) +"""), + ("parameters.bicepparam", """ +using 'main.bicep' + +param coords = { + lattitude: '47.6363726' + longitude: '-122.1357068' +} +""")); + + result.Should().NotHaveAnyDiagnostics(); + + var parametersFile = result.Compilation.Emitter.Parameters().Parameters!; + var templateFile = result.Compilation.Emitter.Parameters().Template!.Template!; + + JsonObject identifiers = new() + { + { "name", "someName" }, + { "namespace", "someNamespace" } + }; + + 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, _) => + { + req.Properties["body"] = """ +{ + "properties": { + "gridId": "SEW", + "gridX": "131", + "gridY": "68" + } +} +"""; + return Task.FromResult(new LocalExtensibilityOperationResponse(new Resource(req.Type, req.ApiVersion, identifiers, req.Properties, "Succeeded"), new ErrorData(new Error() { Code = "Code", Message = "Error message" }))); + }); + + var dispatcher = BicepTestConstants.CreateModuleDispatcher(services.Build().Construct()); + await using LocalExtensibilityHostManager extensibilityHandler = new(dispatcher, uri => Task.FromResult(providerMock.Object)); + await extensibilityHandler.InitializeExtensions(result.Compilation); + + var localDeployResult = await LocalDeployment.Deploy(extensibilityHandler, templateFile, parametersFile, TestContext.CancellationTokenSource.Token); + + localDeployResult.Deployment.Properties.ProvisioningState.Should().Be(ProvisioningState.Failed, because: $"Provider did not return '{nameof(Resource)}' or '{nameof(ErrorData)}' as part of its response. Providers should return one or the other to indicate success or failure respectively."); + localDeployResult.Deployment.Properties.Error.Should().NotBeNull(); + + localDeployResult.Deployment.Properties.Error.Code.Should().Be("DeploymentFailed"); + localDeployResult.Deployment.Properties.Error.Details.Should().NotBeNullOrEmpty(); + localDeployResult.Deployment.Properties.Error.Details[0].Code.Should().Be("ResourceDeploymentFailure"); + localDeployResult.Deployment.Properties.Error.Details[0].Target.Should().Be("/resources/gridpointsReq", because: $"Expect a failure when mocking a response for \"/resources/gridpointsReq\" because provider it is not returning '{nameof(Resource)}' or '{nameof(ErrorData)}' and one must be returned to indicate success or failure."); + } + + [TestMethod] + public async Task Provider_returning_error_data_should_fail() + { + var services = await ProviderTestHelper.GetServiceBuilderWithPublishedProvider(ThirdPartyTypeHelper.GetHttpProviderTypesTgz(), new(ExtensibilityEnabled: true, ExtensionRegistry: true, LocalDeployEnabled: true)); + + var result = await CompilationHelper.RestoreAndCompileParams(services, + ("bicepconfig.json", """ +{ + "extensions": { + "http": "br:example.azurecr.io/providers/foo:1.2.3" + }, + "experimentalFeaturesEnabled": { + "extensibility": true, + "extensionRegistry": true, + "localDeploy": true + } +} +"""), + ("main.bicep", """ +extension http + +param coords { + lattitude: string + longitude: string +} + +resource gridpointsReq 'request@v1' = { + uri: 'https://api.weather.gov/points/${coords.lattitude},${coords.longitude}' + format: 'raw' +} + +var gridpoints = json(gridpointsReq.body).properties + +resource forecastReq 'request@v1' = { + uri: 'https://api.weather.gov/gridpoints/${gridpoints.gridId}/${gridpoints.gridX},${gridpoints.gridY}/forecast' + format: 'raw' +} + +var forecast = json(forecastReq.body).properties + +type forecastType = { + name: string + temperature: int +} + +output forecast forecastType[] = map(forecast.periods, p => { + name: p.name + temperature: p.temperature +}) +"""), + ("parameters.bicepparam", """ +using 'main.bicep' + +param coords = { + lattitude: '47.6363726' + longitude: '-122.1357068' +} +""")); + + result.Should().NotHaveAnyDiagnostics(); + + var parametersFile = result.Compilation.Emitter.Parameters().Parameters!; + var templateFile = result.Compilation.Emitter.Parameters().Template!.Template!; + + JsonObject identifiers = new() + { + { "name", "someName" }, + { "namespace", "someNamespace" } + }; + + 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, _) => + { + req.Properties["body"] = """ +{ + "properties": { + "gridId": "SEW", + "gridX": "131", + "gridY": "68" + } +} +"""; + return Task.FromResult(new LocalExtensibilityOperationResponse(new Resource(req.Type, req.ApiVersion, identifiers, req.Properties, "Succeeded"), new ErrorData(new Error() { Code = "Code", Message = "Error message" }))); + }); + + var dispatcher = BicepTestConstants.CreateModuleDispatcher(services.Build().Construct()); + await using LocalExtensibilityHostManager extensibilityHandler = new(dispatcher, uri => Task.FromResult(providerMock.Object)); + await extensibilityHandler.InitializeExtensions(result.Compilation); + + var localDeployResult = await LocalDeployment.Deploy(extensibilityHandler, templateFile, parametersFile, TestContext.CancellationTokenSource.Token); + + localDeployResult.Deployment.Properties.ProvisioningState.Should().Be(ProvisioningState.Failed, because: "Provider returned a failure when attempting to create a resource."); + localDeployResult.Deployment.Properties.Error.Should().NotBeNull(); + + localDeployResult.Deployment.Properties.Error.Code.Should().Be("DeploymentFailed"); + localDeployResult.Deployment.Properties.Error.Details.Should().NotBeNullOrEmpty(); + localDeployResult.Deployment.Properties.Error.Details[0].Code.Should().Be("ResourceDeploymentFailure"); + localDeployResult.Deployment.Properties.Error.Details[0].Target.Should().Be("/resources/gridpointsReq", because: $"Expect a failure when mocking a response for \"/resources/gridpointsReq\" because provider returned '{nameof(ErrorData)}' to indicate a failure."); + } } diff --git a/src/Bicep.Local.Deploy.IntegrationTests/ProviderExtensionTests.cs b/src/Bicep.Local.Deploy.IntegrationTests/ProviderExtensionTests.cs index 6076d7fc603..65beae1b0df 100644 --- a/src/Bicep.Local.Deploy.IntegrationTests/ProviderExtensionTests.cs +++ b/src/Bicep.Local.Deploy.IntegrationTests/ProviderExtensionTests.cs @@ -1,8 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Diagnostics; using System.IO.Pipes; using System.Text.Json; +using System.Text.Json.Nodes; using Bicep.Core.UnitTests; using Bicep.Core.UnitTests.Assertions; using Bicep.Core.UnitTests.Mock; @@ -60,34 +62,42 @@ await Task.WhenAll( [TestMethod] public async Task Save_request_works_as_expected() { + JsonObject identifiers = new() + { + { "name", "someName" }, + { "namespace", "someNamespace" } + }; + var handlerMock = StrictMock.Of(); - handlerMock.SetupGet(x => x.ResourceType).Returns("apps/Deployment@v1"); + handlerMock.SetupGet(x => x.ResourceType).Returns("apps/Deployment"); - handlerMock.Setup(x => x.Save(It.IsAny(), It.IsAny())) - .Returns((req, _) => - Task.FromResult(new Protocol.ExtensibilityOperationResponse(req.Resource, null, null))); + handlerMock.Setup(x => x.CreateOrUpdate(It.IsAny(), It.IsAny())) + .Returns((req, _) => + Task.FromResult(new Protocol.LocalExtensibilityOperationResponse( + new Protocol.Resource(req.Type, req.ApiVersion, "Succeeded", identifiers, req.Config, req.Properties), + null))); await RunExtensionTest( builder => builder.AddHandler(handlerMock.Object), async (client, token) => { - var request = new Extension.Rpc.ExtensibilityOperationRequest + var request = new Extension.Rpc.ResourceSpecification { - Import = new() - { - Provider = "Kubernetes", - Version = "1.0.0", - Config = """ + ApiVersion = "v1", + Type = "apps/Deployment", + Config = """ { - "kubeConfig": "redacted", - "namespace": "default" + "kubeConfig": { + "type": "string", + "defaultValue": "redacted" + }, + "namespace": { + "type": "string", + "defaultValue": "default" + } } - """ - }, - Resource = new() - { - Type = "apps/Deployment@v1", - Properties = """ + """, + Properties = """ { "metadata": { "name": "echo-server" @@ -124,13 +134,18 @@ await RunExtensionTest( } } """ - } }; - var response = await client.SaveAsync(request, cancellationToken: token); + var response = await client.CreateOrUpdateAsync(request, cancellationToken: token); response.Should().NotBeNull(); - response.Resource!.Type.Should().Be("apps/Deployment@v1"); + response.Resource.Should().NotBeNull(); + response.Resource.Type.Should().Be("apps/Deployment"); + response.Resource.Identifiers.Should().NotBeNullOrEmpty(); + var responseIdentifiers = JsonObject.Parse(response.Resource.Identifiers)!.AsObject(); + responseIdentifiers.Should().NotBeNullOrEmpty(); + responseIdentifiers["name"]!.GetValue().Should().Be(identifiers["name"]!.GetValue()); + responseIdentifiers["namespace"]!.GetValue().Should().Be(identifiers["namespace"]!.GetValue()); }); } } diff --git a/src/Bicep.Local.Deploy.IntegrationTests/packages.lock.json b/src/Bicep.Local.Deploy.IntegrationTests/packages.lock.json index dbf770d7442..e456e1e65a4 100644 --- a/src/Bicep.Local.Deploy.IntegrationTests/packages.lock.json +++ b/src/Bicep.Local.Deploy.IntegrationTests/packages.lock.json @@ -140,8 +140,8 @@ }, "Azure.Deployments.Core": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "Zev+8/PldTvNwUEI84e7szmJm7/ClHyr4fe0hlMylZ9lwD4wCOyc3ijWe6LmI8J92WtXX7Mh1K1FtIcHOZC0iw==", + "resolved": "1.54.0", + "contentHash": "dItBSPwB83gv9BXwPEcef4VYufmuP7w59Bg/xpjCSlBEPlj3UukVypUqMmI2hRObhNnz2TQHiMw+Y/S8HtJbyA==", "dependencies": { "Microsoft.PowerPlatform.ResourceStack": "7.0.0.2007", "Newtonsoft.Json": "13.0.2", @@ -151,19 +151,19 @@ }, "Azure.Deployments.DiffEngine": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "JqDITGajqF4tjR0sqgl8YlwWEC92r7LOHjzP0cH6K0mQIyY3rRr2c1k3fbYRrbG1D69o4IiNXqqShDd8K4lSMg==" + "resolved": "1.54.0", + "contentHash": "Z+1Q/Vpy/LF0xKm3/0szoU/0AhZuNbnQOVMagtfejpRNteqnmeryLjdHG59AeKIpzgbHr3XxusfGz0jg4wjLdA==" }, "Azure.Deployments.Engine": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "xP1lnwgceE74Tmp5N5R1SdcB0iICwH2QiwVWpZnjqzA5EXGr9tesEbV1EO7GTgfCXtTWYbcQlySkAKtDPuMt0A==", + "resolved": "1.54.0", + "contentHash": "8mx0e0JUq43Ax/5I1BEcvUsFmDIUdsRnrKIMG3+QOz11SMbI2nHAPQYFkIkWnxgaxpObaZvx6EcJXQmptEgehg==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", - "Azure.Deployments.DiffEngine": "1.34.0", - "Azure.Deployments.Extensibility": "1.34.0", + "Azure.Deployments.Core": "1.54.0", + "Azure.Deployments.DiffEngine": "1.54.0", + "Azure.Deployments.Extensibility": "1.54.0", "Azure.Deployments.ResourceMetadata": "1.0.1265", - "Azure.Deployments.Templates": "1.34.0", + "Azure.Deployments.Templates": "1.54.0", "Microsoft.AspNet.WebApi.Client": "5.2.9", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Newtonsoft.Json": "13.0.2", @@ -173,23 +173,34 @@ }, "Azure.Deployments.Expression": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "4XIed8jUdWsc8IG2o2tdqJl8ioWjwN9MR2y9L9PV/5wlDUyb2I6MXVwU/f5d3ZMCD+svc4rE6HRgxMIBYHZmSQ==", + "resolved": "1.54.0", + "contentHash": "2nhYxVun701mghHxK5h5qZoL9RjaVCo83/VTc2HgPKJkIcRW3oYFbxnMQrP5pr9k0GsCxKmvOkwsoET9gy851A==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", + "Azure.Deployments.Core": "1.54.0", "IPNetwork2": "2.6.598", "Newtonsoft.Json": "13.0.2" } }, "Azure.Deployments.Extensibility": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "3n64LvNi90r70Puobb8OImKlivz6/JaCW7zVImwb8VXeXTJJtoPTuY0z81QroGqdQMA/aU8123edFBTM4hiBVg==", + "resolved": "1.54.0", + "contentHash": "CiMXvV8NVW/xQKMQVX7gmKj20GP/h1k+4nuCNyCoS5gek4WI9yk7pH0XHwmx1TVf2Ms1vtwc9bOYohq1kd27tg==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", + "Azure.Deployments.Core": "1.54.0", "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.JsonPath": { "type": "Transitive", "resolved": "1.0.1265", @@ -214,12 +225,12 @@ }, "Azure.Deployments.Templates": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "AlstKsqlGEv8XNEjtCZ9KsI//ZKtnKn0m18TSuSQT36w+RjWiCo35uE6U/HTIXmTRS/jJVOuwMSPU4FhTLW/jg==", + "resolved": "1.54.0", + "contentHash": "TNq6JNNcDVuNSS4sqBUETVTm8KDIYvt28vWSPaV+rHyTogo0qJHciSQglEwOk0zoTp1Qf8plT7QqXS0uM/A2AA==", "dependencies": { "Azure.Bicep.Types": "0.5.9", - "Azure.Deployments.Core": "1.34.0", - "Azure.Deployments.Expression": "1.34.0", + "Azure.Deployments.Core": "1.54.0", + "Azure.Deployments.Expression": "1.54.0", "Microsoft.Automata.SRM": "1.2.2", "Newtonsoft.Json": "13.0.2" } @@ -367,6 +378,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" + } + }, "MediatR": { "type": "Transitive", "resolved": "8.1.0", @@ -1548,7 +1567,8 @@ "dependencies": { "Azure.Bicep.Core": "[1.0.0, )", "Azure.Bicep.Local.Extension": "[1.0.0, )", - "Azure.Deployments.Engine": "[1.34.0, )", + "Azure.Deployments.Engine": "[1.54.0, )", + "Azure.Deployments.Extensibility.Core": "[0.1.55, )", "Microsoft.AspNet.WebApi.Client": "[6.0.0, )" } }, diff --git a/src/Bicep.Local.Deploy/Bicep.Local.Deploy.csproj b/src/Bicep.Local.Deploy/Bicep.Local.Deploy.csproj index 92063d4265d..dcf896c29a7 100644 --- a/src/Bicep.Local.Deploy/Bicep.Local.Deploy.csproj +++ b/src/Bicep.Local.Deploy/Bicep.Local.Deploy.csproj @@ -19,7 +19,8 @@ - + + diff --git a/src/Bicep.Local.Deploy/Extensibility/AzExtensibilityProvider.cs b/src/Bicep.Local.Deploy/Extensibility/AzExtensibilityProvider.cs deleted file mode 100644 index 5621d9ca6a8..00000000000 --- a/src/Bicep.Local.Deploy/Extensibility/AzExtensibilityProvider.cs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Threading; -using System.Threading.Tasks; -using Azure.Deployments.Core.Definitions; -using Azure.Deployments.Extensibility.Contract; -using Azure.Deployments.Extensibility.Data; -using Azure.Deployments.Extensibility.Messages; -using Microsoft.WindowsAzure.ResourceStack.Common.Extensions; -using Microsoft.WindowsAzure.ResourceStack.Common.Json; -using Newtonsoft.Json.Linq; - -namespace Bicep.Local.Deploy.Extensibility; - -public class AzExtensibilityProvider : LocalExtensibilityProvider -{ - private readonly LocalExtensibilityHandler extensibilityHandler; - - public AzExtensibilityProvider(LocalExtensibilityHandler extensibilityHandler) - { - this.extensibilityHandler = extensibilityHandler; - } - - public override Task Delete(ExtensibilityOperationRequest request, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public override Task Get(ExtensibilityOperationRequest request, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public override Task PreviewSave(ExtensibilityOperationRequest request, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public override async Task Save(ExtensibilityOperationRequest request, CancellationToken cancellationToken) - { - switch (request.Resource.Type) - { - case "Microsoft.Resources/deployments": - { - var template = request.Resource.Properties["template"]!.ToString(); - var parameters = new JObject - { - ["$schema"] = "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", - ["contentVersion"] = "1.0.0.0", - ["parameters"] = request.Resource.Properties["parameters"], - }; - - var result = await LocalDeployment.Deploy(extensibilityHandler, template, parameters.ToJson(), cancellationToken); - - if (result.Deployment.Properties.ProvisioningState != ProvisioningState.Succeeded) - { - return new( - null, - null, - result.Deployment.Properties.Error.Details.SelectArray(x => new ExtensibilityError(x.Code, x.Message, x.Target))); - } - - return new( - new ExtensibleResourceData(request.Resource.Type, new JObject - { - ["outputs"] = result.Deployment.Properties.Outputs?.ToJToken(), - }), - null, - null); - } - } - - throw new NotImplementedException(); - } -} diff --git a/src/Bicep.Local.Deploy/Extensibility/GrpcBuiltInLocalExtension.cs b/src/Bicep.Local.Deploy/Extensibility/GrpcBuiltInLocalExtension.cs new file mode 100644 index 00000000000..28979bf1557 --- /dev/null +++ b/src/Bicep.Local.Deploy/Extensibility/GrpcBuiltInLocalExtension.cs @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics; +using System.Text.Json.Nodes; +using Azure.Deployments.Extensibility.Core.V2.Json; +using Bicep.Local.Extension.Rpc; +using Google.Protobuf.Collections; +using Json.Pointer; +using Microsoft.WindowsAzure.ResourceStack.Common.Json; +using Newtonsoft.Json.Linq; +using ExtensibilityV2 = Azure.Deployments.Extensibility.Core.V2.Models; +using Rpc = Bicep.Local.Extension.Rpc; + +namespace Bicep.Local.Deploy.Extensibility; + +public class GrpcBuiltInLocalExtension : LocalExtensibilityHost +{ + private readonly BicepExtension.BicepExtensionClient client; + private readonly Process process; + + private GrpcBuiltInLocalExtension(BicepExtension.BicepExtensionClient client, Process process) + { + this.client = client; + this.process = process; + } + + public static async Task Start(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 GrpcBuiltInLocalExtension(client, process); + } + catch (Exception ex) + { + await TerminateProcess(process); + throw new InvalidOperationException($"Failed to connect to provider {pathToBinary.LocalPath}", ex); + } + } + + public override async Task CreateOrUpdate(ExtensibilityV2.ResourceSpecification request, CancellationToken cancellationToken) + => Convert(await client.CreateOrUpdateAsync(Convert(request), cancellationToken: cancellationToken)); + + public override async Task Delete(ExtensibilityV2.ResourceReference request, CancellationToken cancellationToken) + => Convert(await client.DeleteAsync(Convert(request), cancellationToken: cancellationToken)); + + public override async Task Get(ExtensibilityV2.ResourceReference request, CancellationToken cancellationToken) + => Convert(await client.GetAsync(Convert(request), cancellationToken: cancellationToken)); + + public override 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 LocalExtensibilityOperationResponse 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) + => innerError is null ? 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 override 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/GrpcExtensibilityProvider.cs b/src/Bicep.Local.Deploy/Extensibility/GrpcExtensibilityProvider.cs deleted file mode 100644 index 4ee61d8d84d..00000000000 --- a/src/Bicep.Local.Deploy/Extensibility/GrpcExtensibilityProvider.cs +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Configuration; -using System.Diagnostics; -using System.IO; -using System.IO.Pipes; -using System.Net; -using System.Net.Sockets; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Azure.Deployments.Extensibility.Contract; -using Azure.Deployments.Extensibility.Messages; -using Bicep.Local.Extension.Rpc; -using Grpc.Net.Client; -using Microsoft.WindowsAzure.ResourceStack.Common.Json; -using Newtonsoft.Json.Linq; -using Data = Azure.Deployments.Extensibility.Data; -using Messages = Azure.Deployments.Extensibility.Messages; -using Rpc = Bicep.Local.Extension.Rpc; - -namespace Bicep.Local.Deploy.Extensibility; - -public class GrpcExtensibilityProvider : LocalExtensibilityProvider -{ - private readonly BicepExtension.BicepExtensionClient client; - private readonly Process process; - - private GrpcExtensibilityProvider(BicepExtension.BicepExtensionClient client, Process process) - { - this.client = client; - this.process = process; - } - - public static async Task Start(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 GrpcExtensibilityProvider(client, process); - } - catch (Exception ex) - { - await TerminateProcess(process); - throw new InvalidOperationException($"Failed to connect to provider {pathToBinary.LocalPath}", ex); - } - } - - public async override Task Delete(Messages.ExtensibilityOperationRequest request, CancellationToken cancellationToken) - { - return Convert(await client.DeleteAsync(Convert(request), cancellationToken: cancellationToken)); - } - - public async override Task Get(Messages.ExtensibilityOperationRequest request, CancellationToken cancellationToken) - { - return Convert(await client.GetAsync(Convert(request), cancellationToken: cancellationToken)); - } - - public async override Task PreviewSave(Messages.ExtensibilityOperationRequest request, CancellationToken cancellationToken) - { - return Convert(await client.PreviewSaveAsync(Convert(request), cancellationToken: cancellationToken)); - } - - public async override Task Save(Messages.ExtensibilityOperationRequest request, CancellationToken cancellationToken) - { - return Convert(await client.SaveAsync(Convert(request), cancellationToken: cancellationToken)); - } - - private static Rpc.ExtensibilityOperationRequest Convert(Messages.ExtensibilityOperationRequest request) - => new() - { - Import = new Rpc.ExtensibleImportData - { - Provider = request.Import.Provider, - Version = request.Import.Version, - Config = request.Import.Config?.ToJson(), - }, - Resource = new Rpc.ExtensibleResourceData - { - Type = request.Resource.Type, - Properties = request.Resource.Properties?.ToJson(), - }, - }; - - private static Messages.ExtensibilityOperationResponse Convert(Rpc.ExtensibilityOperationResponse response) - => new( - response.Resource is { } resource ? new(resource.Type, resource.Properties?.FromJson()) : null, - response.ResourceMetadata is { } metadata ? new(metadata.ReadOnlyProperties.ToArray(), metadata.ImmutableProperties.ToArray(), metadata.DynamicProperties.ToArray()) : null, - response.Errors is { } errors ? errors.Select(error => new Data.ExtensibilityError(error.Code, error.Message, error.Target)).ToArray() : null); - - public override 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/LocalExtensibilityHandler.cs b/src/Bicep.Local.Deploy/Extensibility/LocalExtensibilityHandler.cs deleted file mode 100644 index a43ba44c771..00000000000 --- a/src/Bicep.Local.Deploy/Extensibility/LocalExtensibilityHandler.cs +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Concurrent; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; -using Azure.Deployments.Extensibility.Contract; -using Azure.Deployments.Extensibility.Messages; -using Bicep.Core.Extensions; -using Bicep.Core.Registry; -using Bicep.Core.Semantics; -using Bicep.Core.Semantics.Namespaces; -using Bicep.Core.TypeSystem.Types; -using Microsoft.WindowsAzure.ResourceStack.Common.Utilities; -using IAsyncDisposable = System.IAsyncDisposable; - -namespace Bicep.Local.Deploy.Extensibility; - -public class LocalExtensibilityHandler : IAsyncDisposable -{ - private record ProviderKey( - string Name, - string Version); - - private Dictionary RegisteredProviders = new(); - private readonly IModuleDispatcher moduleDispatcher; - private readonly Func> providerFactory; - - public LocalExtensibilityHandler(IModuleDispatcher moduleDispatcher, Func> providerFactory) - { - this.moduleDispatcher = moduleDispatcher; - this.providerFactory = providerFactory; - // Built in provider for handling nested deployments - RegisteredProviders[new("LocalNested", "0.0.0")] = new AzExtensibilityProvider(this); - } - - private async Task CallProvider(string method, IExtensibilityProvider provider, ExtensibilityOperationRequest request, CancellationToken cancellationToken) - { - return method switch - { - "get" => await provider.Get(request, cancellationToken), - "delete" => await provider.Delete(request, cancellationToken), - "save" => await provider.Save(request, cancellationToken), - "previewSave" => await provider.PreviewSave(request, cancellationToken), - _ => throw new NotImplementedException($"Unsupported method {method}"), - }; - } - - public async Task CallExtensibilityHost( - string method, - ExtensibilityOperationRequest request, - CancellationToken cancellationToken) - { - var provider = RegisteredProviders[new(request.Import.Provider, request.Import.Version)]; - - return await CallProvider(method, provider, request, cancellationToken); - } - - private IEnumerable<(NamespaceType namespaceType, Uri binaryUri)> GetBinaryProviders(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 (namespaceType, binaryUri); - } - } - } - - public async Task InitializeProviders(Compilation compilation) - { - var binaryProviders = GetBinaryProviders(compilation).DistinctBy(x => x.binaryUri); - - foreach (var (namespaceType, binaryUri) in binaryProviders) - { - ProviderKey providerKey = new(namespaceType.Settings.ArmTemplateProviderName, namespaceType.Settings.ArmTemplateProviderVersion); - RegisteredProviders[providerKey] = await providerFactory(binaryUri); - } - } - - public async ValueTask DisposeAsync() - { - await Task.WhenAll(RegisteredProviders.Values.Select(async provider => - { - try - { - await provider.DisposeAsync(); - } - catch - { - // TODO: handle errors shutting down processes gracefully - } - })); - } -} diff --git a/src/Bicep.Local.Deploy/Extensibility/LocalExtensibilityHost.cs b/src/Bicep.Local.Deploy/Extensibility/LocalExtensibilityHost.cs new file mode 100644 index 00000000000..269fd5d6446 --- /dev/null +++ b/src/Bicep.Local.Deploy/Extensibility/LocalExtensibilityHost.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.Deployments.Extensibility.Contract; +using Azure.Deployments.Extensibility.Core.V2.Models; +using Azure.Deployments.Extensibility.Messages; + +namespace Bicep.Local.Deploy.Extensibility; + +public record LocalExtensibilityOperationResponse(Resource? Resource, ErrorData? ErrorData); + +[JsonSerializable(typeof(LocalExtensibilityOperationResponse))] +public partial class LocalExtensibilityOperationResponseContext : JsonSerializerContext { } + +public static class LocalExtensibilityOperationResponseJsonDefaults +{ + public readonly static JsonSerializerOptions SerializerOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + }; + + public readonly static LocalExtensibilityOperationResponseContext SerializerContext = new(SerializerOptions); +} + +public abstract class LocalExtensibilityHost : IAsyncDisposable +{ + public abstract Task Delete(ResourceReference request, CancellationToken cancellationToken); + + public abstract Task Get(ResourceReference request, CancellationToken cancellationToken); + + public abstract Task Preview(ResourceSpecification request, CancellationToken cancellationToken); + + public abstract Task CreateOrUpdate(ResourceSpecification request, CancellationToken cancellationToken); + + public virtual ValueTask DisposeAsync() + { + return ValueTask.CompletedTask; + } +} diff --git a/src/Bicep.Local.Deploy/Extensibility/LocalExtensibilityHostManager.cs b/src/Bicep.Local.Deploy/Extensibility/LocalExtensibilityHostManager.cs new file mode 100644 index 00000000000..cc760712407 --- /dev/null +++ b/src/Bicep.Local.Deploy/Extensibility/LocalExtensibilityHostManager.cs @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Bicep.Core.Extensions; +using Bicep.Core.Registry; +using Bicep.Core.Semantics; +using Bicep.Core.TypeSystem.Types; +using Microsoft.WindowsAzure.ResourceStack.Common.Json; +using IAsyncDisposable = System.IAsyncDisposable; +using Azure.Deployments.Extensibility.Core.V2.Models; +using System.Text.Json.Nodes; +using System.Text.Json; +using Azure.Deployments.Extensibility.Core.V2.Json; +using System.IO; +using Azure.Deployments.Engine.Host.Azure.ExtensibilityV2.Contract.Models; +using System.Diagnostics; +using System.Net.Http.Headers; +using System.Threading; +using System.Text.Json.Serialization.Metadata; + + +namespace Bicep.Local.Deploy.Extensibility; + +public class LocalExtensibilityHostManager : IAsyncDisposable +{ + private record ExtensionKey( + string Name, + string Version); + + private Dictionary RegisteredExtensions = new(); + private readonly IModuleDispatcher moduleDispatcher; + private readonly Func> extensionFactory; + + public LocalExtensibilityHostManager(IModuleDispatcher moduleDispatcher, Func> extensionFactory) + { + this.moduleDispatcher = moduleDispatcher; + this.extensionFactory = extensionFactory; + // Built in provider for handling nested deployments + RegisteredExtensions[new("LocalNested", "0.0.0")] = new NestedDeploymentBuiltInLocalExtension(this); + } + + public async Task CallExtensibilityHost( + LocalDeploymentEngineHost.ExtensionInfo extensionInfo, + HttpContent content, + CancellationToken cancellationToken) + { + var extension = RegisteredExtensions[new(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, + LocalExtensibilityHost 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(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(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(LocalExtensibilityOperationResponse 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."); + } + + private IEnumerable<(NamespaceType namespaceType, Uri binaryUri)> 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 (namespaceType, binaryUri); + } + } + } + + public async Task InitializeExtensions(Compilation compilation) + { + var binaryExtensions = GetBinaryExtensions(compilation).DistinctBy(x => x.binaryUri); + + foreach (var (namespaceType, binaryUri) in binaryExtensions) + { + ExtensionKey providerKey = new(namespaceType.Settings.ArmTemplateProviderName, namespaceType.Settings.ArmTemplateProviderVersion); + RegisteredExtensions[providerKey] = await extensionFactory(binaryUri); + } + } + + public async ValueTask DisposeAsync() + { + await Task.WhenAll(RegisteredExtensions.Values.Select(async provider => + { + try + { + await provider.DisposeAsync(); + } + catch + { + // TODO: handle errors shutting down processes gracefully + } + })); + } +} diff --git a/src/Bicep.Local.Deploy/Extensibility/LocalExtensibilityProvider.cs b/src/Bicep.Local.Deploy/Extensibility/LocalExtensibilityProvider.cs deleted file mode 100644 index e26b2998cde..00000000000 --- a/src/Bicep.Local.Deploy/Extensibility/LocalExtensibilityProvider.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Azure.Deployments.Extensibility.Contract; -using Azure.Deployments.Extensibility.Messages; - -namespace Bicep.Local.Deploy.Extensibility; - -public abstract class LocalExtensibilityProvider : IExtensibilityProvider, IAsyncDisposable -{ - public abstract Task Delete(ExtensibilityOperationRequest request, CancellationToken cancellationToken); - - public abstract Task Get(ExtensibilityOperationRequest request, CancellationToken cancellationToken); - - public abstract Task PreviewSave(ExtensibilityOperationRequest request, CancellationToken cancellationToken); - - public abstract Task Save(ExtensibilityOperationRequest request, CancellationToken cancellationToken); - - public virtual ValueTask DisposeAsync() - { - return ValueTask.CompletedTask; - } -} diff --git a/src/Bicep.Local.Deploy/Extensibility/NestedDeploymentBuiltInLocalExtension.cs b/src/Bicep.Local.Deploy/Extensibility/NestedDeploymentBuiltInLocalExtension.cs new file mode 100644 index 00000000000..e3f8f36b158 --- /dev/null +++ b/src/Bicep.Local.Deploy/Extensibility/NestedDeploymentBuiltInLocalExtension.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.Text.Json.Nodes; +using System.Threading; +using System.Threading.Tasks; +using Azure.Deployments.Core.Definitions; +using Azure.Deployments.Extensibility.Contract; +using Azure.Deployments.Extensibility.Core.V2.Models; +using Azure.Deployments.Extensibility.Data; +using Azure.Deployments.Extensibility.Messages; +using Json.More; +using Json.Pointer; +using Microsoft.WindowsAzure.ResourceStack.Common.Extensions; +using Microsoft.WindowsAzure.ResourceStack.Common.Json; +using Newtonsoft.Json.Linq; + +namespace Bicep.Local.Deploy.Extensibility; + +public class NestedDeploymentBuiltInLocalExtension : LocalExtensibilityHost +{ + private readonly LocalExtensibilityHostManager extensibilityHostManager; + + public NestedDeploymentBuiltInLocalExtension(LocalExtensibilityHostManager extensibilityHandler) + { + this.extensibilityHostManager = extensibilityHandler; + } + + private record DeploymentIdentifiers (string DeploymentName); + + private JsonObject CreateDeploymentIdentifiers(DeploymentContent deployment) + => JsonObject.Parse(new DeploymentIdentifiers(deployment.Name).ToJsonStream())?.AsObject() ?? throw new UnreachableException("Serialization is not expected to fail."); + + public override async Task CreateOrUpdate(ResourceSpecification request, CancellationToken cancellationToken) + { + switch (request.Type) + { + case "Microsoft.Resources/deployments": + { + var template = request.Properties["template"]!.ToString(); + var parameters = new JsonObject + { + ["$schema"] = "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + ["contentVersion"] = "1.0.0.0", + ["parameters"] = request.Properties["parameters"], + }; + + var result = await LocalDeployment.Deploy(extensibilityHostManager, template, parameters.ToJson(), cancellationToken); + + if (result.Deployment.Properties.ProvisioningState != ProvisioningState.Succeeded) + { + return new LocalExtensibilityOperationResponse( + Resource: null, + ErrorData: new ErrorData( + error: new Error( + result.Deployment.Properties.Error.Code, + result.Deployment.Properties.Error.Message, + JsonPointer.Empty, + result.Deployment.Properties.Error.Details.SelectArray(x => new ErrorDetail(x.Code, x.Message, JsonPointer.Empty))))); + } + + return new LocalExtensibilityOperationResponse( + Resource: new Resource( + identifiers: CreateDeploymentIdentifiers(result.Deployment), + type: request.Type, + apiVersion: request.ApiVersion, + status: result.Deployment.Properties.ProvisioningState.ToString(), + properties: JsonObject.Parse(result.Deployment.Properties.ToJsonStream())?.AsObject() ?? throw new UnreachableException("Serialization is not expected to fail.")), + ErrorData: null); + } + } + + throw new NotImplementedException(); + } + + public override Task Delete(ResourceReference request, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public override Task Get(ResourceReference request, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public override Task Preview(ResourceSpecification request, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } +} diff --git a/src/Bicep.Local.Deploy/IServiceCollectionExtensions.cs b/src/Bicep.Local.Deploy/IServiceCollectionExtensions.cs index 8f61d94f660..e631f9ef7f4 100644 --- a/src/Bicep.Local.Deploy/IServiceCollectionExtensions.cs +++ b/src/Bicep.Local.Deploy/IServiceCollectionExtensions.cs @@ -32,7 +32,7 @@ namespace Bicep.Local.Deploy; public static class IServiceCollectionExtensions { - public static IServiceCollection RegisterLocalDeployServices(this IServiceCollection services, LocalExtensibilityHandler extensibilityHandler) + public static IServiceCollection RegisterLocalDeployServices(this IServiceCollection services, LocalExtensibilityHostManager extensibilityHandler) { var eventSource = new TraceEventSource(); services.AddSingleton(eventSource); diff --git a/src/Bicep.Local.Deploy/LocalDeployment.cs b/src/Bicep.Local.Deploy/LocalDeployment.cs index 75f4b17cc14..254278ffff2 100644 --- a/src/Bicep.Local.Deploy/LocalDeployment.cs +++ b/src/Bicep.Local.Deploy/LocalDeployment.cs @@ -17,7 +17,7 @@ public record Result( DeploymentContent Deployment, ImmutableArray Operations); - public static async Task Deploy(LocalExtensibilityHandler extensibilityHandler, string templateString, string parametersString, CancellationToken cancellationToken) + public static async Task Deploy(LocalExtensibilityHostManager extensibilityHandler, string templateString, string parametersString, CancellationToken cancellationToken) { var services = new ServiceCollection() .RegisterLocalDeployServices(extensibilityHandler) diff --git a/src/Bicep.Local.Deploy/LocalDeploymentEngine.cs b/src/Bicep.Local.Deploy/LocalDeploymentEngine.cs index c2b1bada1f1..5642d154d2a 100644 --- a/src/Bicep.Local.Deploy/LocalDeploymentEngine.cs +++ b/src/Bicep.Local.Deploy/LocalDeploymentEngine.cs @@ -77,9 +77,9 @@ private static (Template template, Dictionary x.Import is null)) + if (template.Resources.Any(x => x.Extension is null)) { - throw new NotImplementedException("Only resources with imports are supported"); + throw new NotImplementedException("Only resources with extensions are supported"); } var context = DeploymentContextWithScopeDefinition.CreateAtResourceGroup( diff --git a/src/Bicep.Local.Deploy/LocalDeploymentEngineHost.cs b/src/Bicep.Local.Deploy/LocalDeploymentEngineHost.cs index 3e2f2b75a03..5e1cc3e1a03 100644 --- a/src/Bicep.Local.Deploy/LocalDeploymentEngineHost.cs +++ b/src/Bicep.Local.Deploy/LocalDeploymentEngineHost.cs @@ -1,41 +1,23 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; -using System.Collections.Generic; -using System.Linq; using System.Net; -using System.Net.Http; using System.Net.Http.Headers; -using System.Threading; -using System.Threading.Tasks; +using System.Text; +using System.Text.Json.Nodes; using Azure.Deployments.Core.Definitions; using Azure.Deployments.Core.Definitions.Identifiers; -using Azure.Deployments.Core.Definitions.Resources; using Azure.Deployments.Core.Entities; -using Azure.Deployments.Core.ErrorResponses; using Azure.Deployments.Core.EventSources; using Azure.Deployments.Core.Exceptions; -using Azure.Deployments.Core.Extensions; using Azure.Deployments.Core.FeatureEnablement; -using Azure.Deployments.Core.Helpers; -using Azure.Deployments.Core.PerformanceCounters; -using Azure.Deployments.Core.Uri; -using Azure.Deployments.Engine.Extensions; -using Azure.Deployments.Engine.Helpers; -using Azure.Deployments.Engine.Host.Azure.Constants; -using Azure.Deployments.Engine.Host.Azure.Definitions; -using Azure.Deployments.Engine.Host.Azure.Exceptions; using Azure.Deployments.Engine.Host.Azure.Interfaces; using Azure.Deployments.Engine.Host.Azure.Workers.Metadata; using Azure.Deployments.Engine.Host.External; using Azure.Deployments.Engine.Interfaces; -using Azure.Deployments.Extensibility.Messages; using Azure.Deployments.ResourceMetadata.Contracts; using Bicep.Local.Deploy.Extensibility; using Microsoft.WindowsAzure.ResourceStack.Common.BackgroundJobs; -using Microsoft.WindowsAzure.ResourceStack.Common.Extensions; -using Microsoft.WindowsAzure.ResourceStack.Common.Instrumentation; using Microsoft.WindowsAzure.ResourceStack.Common.Json; using Microsoft.WindowsAzure.ResourceStack.Common.Services.ADAuthentication; using Newtonsoft.Json.Linq; @@ -46,10 +28,12 @@ namespace Bicep.Local.Deploy; public class LocalDeploymentEngineHost : DeploymentEngineHostBase { - private readonly LocalExtensibilityHandler extensibilityHandler; + private readonly LocalExtensibilityHostManager extensibilityHandler; + public readonly record struct ExtensionInfo(string ExtensionName, string ExtensionVersion, string Method); + public LocalDeploymentEngineHost( - LocalExtensibilityHandler extensibilityHandler, + LocalExtensibilityHostManager extensibilityHandler, IDeploymentsRequestContext requestContext, IDeploymentEventSource deploymentEventSource, IKeyVaultDataProvider keyVaultDataProvider, @@ -140,19 +124,20 @@ protected override async Task TryReadAsString(HttpContent content, bool } } - public override async Task CallExtensibilityHost( + public override async Task CallExtensibilityHostV2( HttpMethod requestMethod, Uri requestUri, - ExtensibilityOperationRequest request, + HttpContent content, AuthenticationToken extensibilityHostToken, CancellationToken cancellationToken) { - var response = await extensibilityHandler.CallExtensibilityHost(requestUri.Segments[^1], request, cancellationToken); + var extensionName = requestUri.Segments[^4].TrimEnd('/'); + var extensionVersion = requestUri.Segments[^3].TrimEnd('/'); + var method = requestUri.Segments[^1].TrimEnd('/'); - return new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(response.ToJson()), - }; + var extensionInfo = new ExtensionInfo(extensionName, extensionVersion, method); + + return await extensibilityHandler.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 ff3d4a2e8a2..d29f8a8d951 100644 --- a/src/Bicep.Local.Deploy/packages.lock.json +++ b/src/Bicep.Local.Deploy/packages.lock.json @@ -4,15 +4,15 @@ "net8.0": { "Azure.Deployments.Engine": { "type": "Direct", - "requested": "[1.34.0, )", - "resolved": "1.34.0", - "contentHash": "xP1lnwgceE74Tmp5N5R1SdcB0iICwH2QiwVWpZnjqzA5EXGr9tesEbV1EO7GTgfCXtTWYbcQlySkAKtDPuMt0A==", + "requested": "[1.54.0, )", + "resolved": "1.54.0", + "contentHash": "8mx0e0JUq43Ax/5I1BEcvUsFmDIUdsRnrKIMG3+QOz11SMbI2nHAPQYFkIkWnxgaxpObaZvx6EcJXQmptEgehg==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", - "Azure.Deployments.DiffEngine": "1.34.0", - "Azure.Deployments.Extensibility": "1.34.0", + "Azure.Deployments.Core": "1.54.0", + "Azure.Deployments.DiffEngine": "1.54.0", + "Azure.Deployments.Extensibility": "1.54.0", "Azure.Deployments.ResourceMetadata": "1.0.1265", - "Azure.Deployments.Templates": "1.34.0", + "Azure.Deployments.Templates": "1.54.0", "Microsoft.AspNet.WebApi.Client": "5.2.9", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Newtonsoft.Json": "13.0.2", @@ -20,6 +20,18 @@ "System.Diagnostics.DiagnosticSource": "5.0.1" } }, + "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, )", @@ -122,8 +134,8 @@ }, "Azure.Deployments.Core": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "Zev+8/PldTvNwUEI84e7szmJm7/ClHyr4fe0hlMylZ9lwD4wCOyc3ijWe6LmI8J92WtXX7Mh1K1FtIcHOZC0iw==", + "resolved": "1.54.0", + "contentHash": "dItBSPwB83gv9BXwPEcef4VYufmuP7w59Bg/xpjCSlBEPlj3UukVypUqMmI2hRObhNnz2TQHiMw+Y/S8HtJbyA==", "dependencies": { "Microsoft.PowerPlatform.ResourceStack": "7.0.0.2007", "Newtonsoft.Json": "13.0.2", @@ -133,25 +145,25 @@ }, "Azure.Deployments.DiffEngine": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "JqDITGajqF4tjR0sqgl8YlwWEC92r7LOHjzP0cH6K0mQIyY3rRr2c1k3fbYRrbG1D69o4IiNXqqShDd8K4lSMg==" + "resolved": "1.54.0", + "contentHash": "Z+1Q/Vpy/LF0xKm3/0szoU/0AhZuNbnQOVMagtfejpRNteqnmeryLjdHG59AeKIpzgbHr3XxusfGz0jg4wjLdA==" }, "Azure.Deployments.Expression": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "4XIed8jUdWsc8IG2o2tdqJl8ioWjwN9MR2y9L9PV/5wlDUyb2I6MXVwU/f5d3ZMCD+svc4rE6HRgxMIBYHZmSQ==", + "resolved": "1.54.0", + "contentHash": "2nhYxVun701mghHxK5h5qZoL9RjaVCo83/VTc2HgPKJkIcRW3oYFbxnMQrP5pr9k0GsCxKmvOkwsoET9gy851A==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", + "Azure.Deployments.Core": "1.54.0", "IPNetwork2": "2.6.598", "Newtonsoft.Json": "13.0.2" } }, "Azure.Deployments.Extensibility": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "3n64LvNi90r70Puobb8OImKlivz6/JaCW7zVImwb8VXeXTJJtoPTuY0z81QroGqdQMA/aU8123edFBTM4hiBVg==", + "resolved": "1.54.0", + "contentHash": "CiMXvV8NVW/xQKMQVX7gmKj20GP/h1k+4nuCNyCoS5gek4WI9yk7pH0XHwmx1TVf2Ms1vtwc9bOYohq1kd27tg==", "dependencies": { - "Azure.Deployments.Core": "1.34.0", + "Azure.Deployments.Core": "1.54.0", "Newtonsoft.Json": "13.0.2" } }, @@ -179,12 +191,12 @@ }, "Azure.Deployments.Templates": { "type": "Transitive", - "resolved": "1.34.0", - "contentHash": "AlstKsqlGEv8XNEjtCZ9KsI//ZKtnKn0m18TSuSQT36w+RjWiCo35uE6U/HTIXmTRS/jJVOuwMSPU4FhTLW/jg==", + "resolved": "1.54.0", + "contentHash": "TNq6JNNcDVuNSS4sqBUETVTm8KDIYvt28vWSPaV+rHyTogo0qJHciSQglEwOk0zoTp1Qf8plT7QqXS0uM/A2AA==", "dependencies": { "Azure.Bicep.Types": "0.5.9", - "Azure.Deployments.Core": "1.34.0", - "Azure.Deployments.Expression": "1.34.0", + "Azure.Deployments.Core": "1.54.0", + "Azure.Deployments.Expression": "1.54.0", "Microsoft.Automata.SRM": "1.2.2", "Newtonsoft.Json": "13.0.2" } @@ -309,6 +321,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", diff --git a/src/Bicep.Local.Extension.Mock/Handlers/EchoResourceHandler.cs b/src/Bicep.Local.Extension.Mock/Handlers/EchoResourceHandler.cs index f1ab1e6fca7..dd468d192b3 100644 --- a/src/Bicep.Local.Extension.Mock/Handlers/EchoResourceHandler.cs +++ b/src/Bicep.Local.Extension.Mock/Handlers/EchoResourceHandler.cs @@ -5,6 +5,7 @@ using System.Text.Json.Nodes; using Bicep.Core.Json; using Bicep.Local.Extension.Protocol; +using Newtonsoft.Json.Linq; namespace Bicep.Local.Extension.Mock.Handlers; @@ -18,26 +19,30 @@ public class EchoResourceHandler : IResourceHandler { public string ResourceType => "echo"; - public Task Delete(ExtensibilityOperationRequest request, CancellationToken cancellationToken) + public Task Delete(ResourceReference request, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task Get(ExtensibilityOperationRequest request, CancellationToken cancellationToken) + public Task Get(ResourceReference request, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task PreviewSave(ExtensibilityOperationRequest request, CancellationToken cancellationToken) + public Task Preview(ResourceSpecification request, CancellationToken cancellationToken) => throw new NotImplementedException(); - public async Task Save(ExtensibilityOperationRequest request, CancellationToken cancellationToken) + public async Task CreateOrUpdate(ResourceSpecification request, CancellationToken cancellationToken) { await Task.Yield(); - var requestBody = JsonSerializer.Deserialize(request.Resource.Properties, SerializationContext.Default.EchoRequest) + var requestBody = JsonSerializer.Deserialize(request.Properties, SerializationContext.Default.EchoRequest) ?? throw new InvalidOperationException("Failed to deserialize request body"); - var responseBody = new EchoResponse(requestBody.Payload); - var response = new ExtensibleResourceData( - request.Resource.Type, - JsonNode.Parse(JsonSerializer.Serialize(responseBody, SerializationContext.Default.EchoResponse))!.AsObject()); + JsonObject identifiers = new() + { + { "name", "someName" }, + { "namespace", "someNamespace" } + }; - return new ExtensibilityOperationResponse(response, null, null); + var responseBody = new EchoResponse(requestBody.Payload); + return new LocalExtensibilityOperationResponse( + Resource: new Resource(request.Type, request.ApiVersion, "Succeeded", identifiers, null, JsonNode.Parse(JsonSerializer.Serialize(responseBody, SerializationContext.Default.EchoResponse))!.AsObject()), + ErrorData: null); } } diff --git a/src/Bicep.Local.Extension.Mock/Program.cs b/src/Bicep.Local.Extension.Mock/Program.cs index 01c950e8ad9..963846b2857 100644 --- a/src/Bicep.Local.Extension.Mock/Program.cs +++ b/src/Bicep.Local.Extension.Mock/Program.cs @@ -25,8 +25,6 @@ public static async Task Main(string[] args) return; } - var extension = new KestrelProviderExtension(); - await ProviderExtension.Run(new KestrelProviderExtension(), RegisterHandlers, args); } diff --git a/src/Bicep.Local.Extension/Protocol/IResourceHandler.cs b/src/Bicep.Local.Extension/Protocol/IResourceHandler.cs index 91267f2207f..fe9d3b7fcd1 100644 --- a/src/Bicep.Local.Extension/Protocol/IResourceHandler.cs +++ b/src/Bicep.Local.Extension/Protocol/IResourceHandler.cs @@ -4,53 +4,65 @@ using System.Collections.Immutable; using System.Text.Json.Nodes; using System.Text.Json.Serialization.Metadata; +using Bicep.Local.Extension.Rpc; namespace Bicep.Local.Extension.Protocol; -public record ExtensibilityOperationRequest( - ExtensibleImportData Import, - ExtensibleResourceData Resource); - -public record ExtensibilityOperationResponse( - ExtensibleResourceData? Resource, - ExtensibleResourceMetadata? ResourceMetadata, - ImmutableArray? Errors); +public record Resource( + string Type, + string? ApiVersion, + string? Status, + JsonObject Identifiers, + JsonObject? Config, + JsonObject Properties); -public record ExtensibleImportData( - string Provider, - string Version, +public record ResourceSpecification( + string Type, + string? ApiVersion, + JsonObject Properties, JsonObject? Config); -public record ExtensibleResourceData( +public record ResourceReference( string Type, - JsonObject? Properties); + string? ApiVersion, + JsonObject Identifiers, + JsonObject? Config); -public record ExtensibleResourceMetadata( - ImmutableArray? ReadOnlyProperties, - ImmutableArray? ImmutableProperties, - ImmutableArray? DynamicProperties); +public record ErrorData( + Error Error); -public record ExtensibilityError( +public record Error( string Code, + string Target, string Message, - string Target); + ErrorDetail[]? Details, + JsonObject? InnerError); + +public record ErrorDetail( + string Code, + string Target, + string Message); + +public record LocalExtensibilityOperationResponse( + Resource? Resource, + ErrorData? ErrorData); public interface IGenericResourceHandler { - Task Save( - ExtensibilityOperationRequest request, + Task CreateOrUpdate( + ResourceSpecification request, CancellationToken cancellationToken); - Task PreviewSave( - ExtensibilityOperationRequest request, + Task Preview( + ResourceSpecification request, CancellationToken cancellationToken); - Task Get( - ExtensibilityOperationRequest request, + Task Get( + ResourceReference request, CancellationToken cancellationToken); - Task Delete( - ExtensibilityOperationRequest request, + Task Delete( + ResourceReference request, CancellationToken cancellationToken); } diff --git a/src/Bicep.Local.Extension/Rpc/BicepExtensionImpl.cs b/src/Bicep.Local.Extension/Rpc/BicepExtensionImpl.cs index f680cdec326..4f8c97b03ab 100644 --- a/src/Bicep.Local.Extension/Rpc/BicepExtensionImpl.cs +++ b/src/Bicep.Local.Extension/Rpc/BicepExtensionImpl.cs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Linq; using System.Text.Json.Nodes; using Bicep.Local.Extension.Protocol; +using Google.Protobuf.Collections; using Grpc.Core; using Microsoft.Extensions.Logging; @@ -20,61 +20,118 @@ public BicepExtensionImpl(ILogger logger, ResourceDispatcher this.dispatcher = dispatcher; } - public override Task Save(ExtensibilityOperationRequest request, ServerCallContext context) - => WrapExceptions(async () => Convert(await dispatcher.GetHandler(request.Resource.Type).Save(Convert(request), context.CancellationToken))); + public override Task CreateOrUpdate(ResourceSpecification request, ServerCallContext context) + => WrapExceptions(async () => Convert(await dispatcher.GetHandler(request.Type).CreateOrUpdate(Convert(request), context.CancellationToken))); - public override Task PreviewSave(ExtensibilityOperationRequest request, ServerCallContext context) - => WrapExceptions(async () => Convert(await dispatcher.GetHandler(request.Resource.Type).PreviewSave(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(ExtensibilityOperationRequest request, ServerCallContext context) - => WrapExceptions(async () => Convert(await dispatcher.GetHandler(request.Resource.Type).Get(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(ExtensibilityOperationRequest request, ServerCallContext context) - => WrapExceptions(async () => Convert(await dispatcher.GetHandler(request.Resource.Type).Delete(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 static Protocol.ExtensibilityOperationRequest Convert(ExtensibilityOperationRequest request) + private Protocol.ResourceSpecification Convert(ResourceSpecification request) { - return new( - new(request.Import.Provider, request.Import.Version, request.Import.Config is { } ? JsonNode.Parse(request.Import.Config) as JsonObject : null), - new(request.Resource.Type, request.Resource.Properties is { } ? JsonNode.Parse(request.Resource.Properties) as JsonObject : null)); + JsonObject? config = GetExtensionConfig(request.Config); + var properties = ToJsonObject(request.Properties, "Parsing resource properties failed. Please ensure is non-null or empty and is a valid JSON object."); + + return new(request.Type, request.ApiVersion, properties, config); } - private static ExtensibilityOperationResponse Convert(Protocol.ExtensibilityOperationResponse response) + private Protocol.ResourceReference Convert(ResourceReference request) { - var output = new ExtensibilityOperationResponse(); - if (response.Resource is { }) + JsonObject identifiers = ToJsonObject(request.Identifiers, "Parsing resource identifiers failed. Please ensure is non-null or empty and is a valid JSON object."); + JsonObject? config = GetExtensionConfig(request.Config); + + return new(request.Type, request.ApiVersion, identifiers, config); + } + + private JsonObject? GetExtensionConfig(string extensionConfig) + { + JsonObject? config = null; + if (!string.IsNullOrEmpty(extensionConfig)) { - output.Resource = new ExtensibleResourceData + 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(Protocol.Resource? response) + => response is null ? null : + new() { - Type = response.Resource.Type, - Properties = response.Resource.Properties?.ToString() + Identifiers = response.Identifiers.ToJsonString(), + Properties = response.Properties.ToJsonString(), + Status = response.Status, + Type = response.Type, + ApiVersion = response.ApiVersion, }; + + private ErrorData? Convert(Protocol.ErrorData? response) + { + if (response is null) + { + return null; } - if (response.ResourceMetadata is { } metadata) + var errorData = new ErrorData() + { + Error = new Error() + { + Code = response.Error.Code, + Message = response.Error.Message, + InnerError = response.Error.InnerError?.ToJsonString(), + Target = response.Error.Target, + } + }; + + var errorDetails = Convert(response.Error.Details); + if (errorDetails is not null) { - output.ResourceMetadata.ReadOnlyProperties.AddRange(metadata.ReadOnlyProperties); - output.ResourceMetadata.ImmutableProperties.AddRange(metadata.ImmutableProperties); - output.ResourceMetadata.DynamicProperties.AddRange(metadata.DynamicProperties); + errorData.Error.Details.AddRange(errorDetails); } + return errorData; + } - if (response.Errors is { } errors) + private RepeatedField? Convert(Protocol.ErrorDetail[]? response) + { + if (response is null) { - output.Errors.AddRange(errors.Select(error => new ExtensibilityError - { - Code = error.Code, - Message = error.Message, - Target = error.Target - })); + return null; } - return output; + var list = new RepeatedField(); + foreach (var item in response) + { + list.Add(Convert(item)); + } + return list; } - private static async Task WrapExceptions(Func> func) + private ErrorDetail Convert(Protocol.ErrorDetail response) + => new() + { + Code = response.Code, + Message = response.Message, + Target = response.Target, + }; + + private LocalExtensibilityOperationResponse Convert(Protocol.LocalExtensibilityOperationResponse response) + => new() + { + ErrorData = Convert(response.ErrorData), + Resource = Convert(response.Resource) + }; + + private static async Task WrapExceptions(Func> func) { try { @@ -82,13 +139,19 @@ private static async Task WrapExceptions(Func