diff --git a/src/Bicep.Cli.IntegrationTests/Bicep.Cli.IntegrationTests.csproj b/src/Bicep.Cli.IntegrationTests/Bicep.Cli.IntegrationTests.csproj index a769458fdd0..6974ba1ac86 100644 --- a/src/Bicep.Cli.IntegrationTests/Bicep.Cli.IntegrationTests.csproj +++ b/src/Bicep.Cli.IntegrationTests/Bicep.Cli.IntegrationTests.csproj @@ -18,12 +18,14 @@ + + diff --git a/src/Bicep.Cli.IntegrationTests/BuildCommandTests.cs b/src/Bicep.Cli.IntegrationTests/BuildCommandTests.cs index ee788628d8b..6d3fd0f8632 100644 --- a/src/Bicep.Cli.IntegrationTests/BuildCommandTests.cs +++ b/src/Bicep.Cli.IntegrationTests/BuildCommandTests.cs @@ -342,8 +342,7 @@ public async Task Build_Invalid_SingleFile_ShouldFail_WithExpectedErrorMessage(D { var outputDirectory = dataSet.SaveFilesToTestDirectory(TestContext); var bicepFilePath = Path.Combine(outputDirectory, DataSet.TestFileMain); - var defaultSettings = CreateDefaultSettings(); - var diagnostics = await GetAllDiagnostics(bicepFilePath, defaultSettings.ClientFactory, defaultSettings.TemplateSpecRepositoryFactory); + var diagnostics = await GetAllDiagnostics(bicepFilePath, InvocationSettings.Default.ClientFactory, InvocationSettings.Default.TemplateSpecRepositoryFactory); var (output, error, result) = await Bicep("build", bicepFilePath); @@ -367,8 +366,7 @@ public async Task Build_Invalid_SingleFile_ToStdOut_ShouldFail_WithExpectedError result.Should().Be(1); output.Should().BeEmpty(); - var defaultSettings = CreateDefaultSettings(); - var diagnostics = await GetAllDiagnostics(bicepFilePath, defaultSettings.ClientFactory, defaultSettings.TemplateSpecRepositoryFactory); + var diagnostics = await GetAllDiagnostics(bicepFilePath, InvocationSettings.Default.ClientFactory, InvocationSettings.Default.TemplateSpecRepositoryFactory); error.Should().ContainAll(diagnostics); } diff --git a/src/Bicep.Cli.IntegrationTests/BuildParamsCommandTests.cs b/src/Bicep.Cli.IntegrationTests/BuildParamsCommandTests.cs index fe23dabe174..293edfa1159 100644 --- a/src/Bicep.Cli.IntegrationTests/BuildParamsCommandTests.cs +++ b/src/Bicep.Cli.IntegrationTests/BuildParamsCommandTests.cs @@ -29,7 +29,7 @@ namespace Bicep.Cli.IntegrationTests public class BuildParamsCommandTests : TestBase { private InvocationSettings Settings - => CreateDefaultSettings() with + => new() { Environment = TestEnvironment.Create( ("stringEnvVariableName", "test"), diff --git a/src/Bicep.Cli.IntegrationTests/LintCommandTests.cs b/src/Bicep.Cli.IntegrationTests/LintCommandTests.cs index ea7a92d33b1..44754785c78 100644 --- a/src/Bicep.Cli.IntegrationTests/LintCommandTests.cs +++ b/src/Bicep.Cli.IntegrationTests/LintCommandTests.cs @@ -13,7 +13,13 @@ using Microsoft.CodeAnalysis.Sarif; using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.WindowsAzure.ResourceStack.Common.Json; +using Microsoft.Extensions.DependencyInjection; using Moq; +using Bicep.Core.Registry.PublicRegistry; +using System.Collections.Immutable; +using System.Diagnostics; +using Bicep.Cli.UnitTests; +using FileSystem = System.IO.Abstractions.FileSystem; namespace Bicep.Cli.IntegrationTests; @@ -154,8 +160,7 @@ public async Task Lint_Invalid_SingleFile_ShouldFail_WithExpectedErrorMessage(Da { var outputDirectory = dataSet.SaveFilesToTestDirectory(TestContext); var bicepFilePath = Path.Combine(outputDirectory, DataSet.TestFileMain); - var defaultSettings = CreateDefaultSettings(); - var diagnostics = await GetAllDiagnostics(bicepFilePath, defaultSettings.ClientFactory, defaultSettings.TemplateSpecRepositoryFactory); + var diagnostics = await GetAllDiagnostics(bicepFilePath, InvocationSettings.Default.ClientFactory, InvocationSettings.Default.TemplateSpecRepositoryFactory, InvocationSettings.Default.ModuleMetadataClient); var (output, error, result) = await Bicep("lint", bicepFilePath); @@ -173,8 +178,10 @@ public async Task Lint_WithEmptyBicepConfig_ShouldProduceConfigurationError() string testOutputPath = FileHelper.GetUniqueTestOutputPath(TestContext); var inputFile = FileHelper.SaveResultFile(TestContext, "main.bicep", DataSets.Empty.Bicep, testOutputPath); var configurationPath = FileHelper.SaveResultFile(TestContext, "bicepconfig.json", string.Empty, testOutputPath); + var settings = new InvocationSettings() { ModuleMetadataClient = PublicRegistryModuleMetadataClientMock.CreateToThrow(new Exception("unit test failed: shouldn't call this")).Object }; + + var (output, error, result) = await Bicep(settings, "lint", inputFile); - var (output, error, result) = await Bicep("lint", inputFile); result.Should().Be(1); output.Should().BeEmpty(); @@ -254,7 +261,6 @@ public async Task Lint_with_sarif_diagnostics_format_should_output_valid_sarif() sarifLog.Runs[0].Results[0].RuleId.Should().Be("no-unused-params"); sarifLog.Runs[0].Results[0].Message.Text.Should().Contain("is declared but never used"); } - private static IEnumerable GetValidDataSetsWithoutWarnings() => DataSets .AllDataSets .Where(ds => ds.IsValid) diff --git a/src/Bicep.Cli.IntegrationTests/PublishProviderCommandTests.cs b/src/Bicep.Cli.IntegrationTests/PublishProviderCommandTests.cs index 31d78437436..9a8b69c9665 100644 --- a/src/Bicep.Cli.IntegrationTests/PublishProviderCommandTests.cs +++ b/src/Bicep.Cli.IntegrationTests/PublishProviderCommandTests.cs @@ -122,7 +122,7 @@ public async Task Publish_provider_should_fail_for_malformed_target() var outputDirectory = FileHelper.GetUniqueTestOutputPath(TestContext); var indexPath = Path.Combine(outputDirectory, "index.json"); - var result = await Bicep(CreateDefaultSettings(), "publish-provider", indexPath, "--target", $"asdf:123"); + var result = await Bicep(InvocationSettings.Default, "publish-provider", indexPath, "--target", $"asdf:123"); result.Should().Fail().And.HaveStderrMatch("*The specified module reference scheme \"asdf\" is not recognized.*"); } @@ -132,7 +132,7 @@ public async Task Publish_provider_should_fail_for_missing_index_path() var outputDirectory = FileHelper.GetUniqueTestOutputPath(TestContext); var indexPath = Path.Combine(outputDirectory, "index.json"); - var result = await Bicep(CreateDefaultSettings(), "publish-provider", indexPath, "--target", $"br:example.com/test/provider:0.0.1"); + var result = await Bicep(InvocationSettings.Default, "publish-provider", indexPath, "--target", $"br:example.com/test/provider:0.0.1"); result.Should().Fail().And.HaveStderrMatch("*Provider package creation failed: Could not find a part of the path '*'.*"); } @@ -142,7 +142,7 @@ public async Task Publish_provider_should_fail_for_malformed_index() var outputDirectory = FileHelper.GetUniqueTestOutputPath(TestContext); var indexPath = FileHelper.SaveResultFile(TestContext, "index.json", "malformed", outputDirectory); - var result = await Bicep(CreateDefaultSettings(), "publish-provider", indexPath, "--target", $"br:example.com/test/provider:0.0.1"); + var result = await Bicep(InvocationSettings.Default, "publish-provider", indexPath, "--target", $"br:example.com/test/provider:0.0.1"); result.Should().Fail().And.HaveStderrMatch("*Provider package creation failed: 'm' is an invalid start of a value.*"); } @@ -161,7 +161,7 @@ public async Task Publish_provider_should_fail_for_missing_referenced_types_json } """, outputDirectory); - var result = await Bicep(CreateDefaultSettings(), "publish-provider", indexPath, "--target", $"br:example.com/test/provider:0.0.1"); + var result = await Bicep(InvocationSettings.Default, "publish-provider", indexPath, "--target", $"br:example.com/test/provider:0.0.1"); result.Should().Fail().And.HaveStderrMatch("*Provider package creation failed: Could not find file '*types.json'.*"); } @@ -181,7 +181,7 @@ public async Task Publish_provider_should_fail_for_malformed_types_json() """, outputDirectory); FileHelper.SaveResultFile(TestContext, "v1/types.json", "malformed", outputDirectory); - var result = await Bicep(CreateDefaultSettings(), "publish-provider", indexPath, "--target", $"br:example.com/test/provider:0.0.1"); + var result = await Bicep(InvocationSettings.Default, "publish-provider", indexPath, "--target", $"br:example.com/test/provider:0.0.1"); result.Should().Fail().And.HaveStderrMatch("*Provider package creation failed: 'm' is an invalid start of a value.*"); } @@ -213,7 +213,7 @@ public async Task Publish_provider_should_fail_for_bad_type_location() ] """, outputDirectory); - var result = await Bicep(CreateDefaultSettings(), "publish-provider", indexPath, "--target", $"br:example.com/test/provider:0.0.1"); + var result = await Bicep(InvocationSettings.Default, "publish-provider", indexPath, "--target", $"br:example.com/test/provider:0.0.1"); result.Should().Fail().And.HaveStderrMatch("*Provider package creation failed: Index was outside the bounds of the array.*"); } } diff --git a/src/Bicep.Cli.IntegrationTests/RootCommandTests.cs b/src/Bicep.Cli.IntegrationTests/RootCommandTests.cs index 6e1f701e1db..1fc1320216b 100644 --- a/src/Bicep.Cli.IntegrationTests/RootCommandTests.cs +++ b/src/Bicep.Cli.IntegrationTests/RootCommandTests.cs @@ -44,7 +44,7 @@ public async Task BicepVersionShouldPrintVersionInformation() [TestMethod] public async Task BicepHelpShouldPrintHelp() { - var settings = CreateDefaultSettings() with { FeatureOverrides = new(RegistryEnabled: true) }; + var settings = new InvocationSettings() { FeatureOverrides = new(RegistryEnabled: true) }; var (output, error, result) = await Bicep(settings, "--help"); @@ -127,7 +127,7 @@ public async Task BicepHelpShouldAlwaysIncludePublish() { // disable registry to ensure `bicep --help` is not consulting the feature provider before // preparing the help text (as features can only be determined when an input file is specified) - var settings = CreateDefaultSettings() with { FeatureOverrides = new(RegistryEnabled: false) }; + var settings = new InvocationSettings() { FeatureOverrides = new(RegistryEnabled: false) }; var (output, error, result) = await Bicep(settings, "--help"); diff --git a/src/Bicep.Cli.IntegrationTests/TestBase.cs b/src/Bicep.Cli.IntegrationTests/TestBase.cs index 4cd9e3f18bb..7b164d1ab05 100644 --- a/src/Bicep.Cli.IntegrationTests/TestBase.cs +++ b/src/Bicep.Cli.IntegrationTests/TestBase.cs @@ -1,17 +1,21 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections.Immutable; using Bicep.Cli.UnitTests; using Bicep.Core; using Bicep.Core.Extensions; using Bicep.Core.FileSystem; using Bicep.Core.Registry; +using Bicep.Core.Registry.PublicRegistry; using Bicep.Core.Text; using Bicep.Core.UnitTests; using Bicep.Core.UnitTests.Features; +using Bicep.Core.UnitTests.Mock; using Bicep.Core.UnitTests.Utils; using Bicep.Core.Utils; using FluentAssertions; +using FluentAssertions.Common; using Microsoft.Extensions.DependencyInjection; using Moq; @@ -19,49 +23,83 @@ namespace Bicep.Cli.IntegrationTests { public abstract class TestBase : Bicep.Core.UnitTests.TestBase { - private static BicepCompiler CreateCompiler(IContainerRegistryClientFactory clientFactory, ITemplateSpecRepositoryFactory templateSpecRepositoryFactory) + private static BicepCompiler CreateCompiler(IContainerRegistryClientFactory clientFactory, ITemplateSpecRepositoryFactory templateSpecRepositoryFactory, IPublicRegistryModuleMetadataClient? moduleMetadataClient) => ServiceBuilder.Create( - x => x.AddSingleton(clientFactory).AddSingleton(templateSpecRepositoryFactory)).GetCompiler(); + services => + { + services + .AddSingleton(clientFactory) + .AddSingleton(templateSpecRepositoryFactory) + .AddSingleton(); + + IServiceCollectionExtensions.AddMockHttpClientIfNotNull(services, moduleMetadataClient); + } + ).GetCompiler(); protected const string BuildSummaryFailedRegex = @"Build failed: (\d*) Warning\(s\), ([1-9][0-9]*) Error\(s\)"; protected const string BuildSummarySucceededRegex = @"Build succeeded: (\d*) Warning\(s\), 0 Error\(s\)"; protected static readonly MockRepository Repository = new(MockBehavior.Strict); - protected record InvocationSettings( - FeatureProviderOverrides? FeatureOverrides, - IContainerRegistryClientFactory ClientFactory, - ITemplateSpecRepositoryFactory TemplateSpecRepositoryFactory, - IEnvironment? Environment = null); - - protected static Task Bicep(params string[] args) => Bicep(CreateDefaultSettings(), args); + protected record InvocationSettings + { + public static readonly InvocationSettings Default = new(); + + public FeatureProviderOverrides? FeatureOverrides { get; init; } + public IContainerRegistryClientFactory ClientFactory { get; init; } + public ITemplateSpecRepositoryFactory TemplateSpecRepositoryFactory { get; init; } + public IEnvironment? Environment { get; init; } + public IPublicRegistryModuleMetadataClient ModuleMetadataClient { get; init; } + + public InvocationSettings( + FeatureProviderOverrides? FeatureOverrides = null, + IContainerRegistryClientFactory? ClientFactory = null, + ITemplateSpecRepositoryFactory? TemplateSpecRepositoryFactory = null, + IEnvironment? Environment = null, + IPublicRegistryModuleMetadataClient? ModuleMetadataClient = null) + { + this.FeatureOverrides = FeatureOverrides; + this.ClientFactory = ClientFactory ?? Repository.Create().Object; + this.TemplateSpecRepositoryFactory = TemplateSpecRepositoryFactory ?? Repository.Create().Object; + this.Environment = Environment; - protected static InvocationSettings CreateDefaultSettings() => new( - FeatureOverrides: null, - ClientFactory: Repository.Create().Object, - TemplateSpecRepositoryFactory: Repository.Create().Object); + this.ModuleMetadataClient = ModuleMetadataClient ?? StrictMock.Of().Object; + } + } - protected static Task Bicep(Action registerAction, CancellationToken cancellationToken, params string[] args) + protected static Task Bicep(InvocationSettings settings, Action? registerAction, CancellationToken cancellationToken, params string?[] args /*null args are ignored*/) => TextWriterHelper.InvokeWriterAction((@out, err) - => new Program(new(Output: @out, Error: err), registerAction) - .RunAsync(args, cancellationToken)); + => new Program( + new(Output: @out, Error: err), + services => + { + if (settings.FeatureOverrides is { }) + { + services.WithFeatureOverrides(settings.FeatureOverrides); + } - protected static Task Bicep(Action registerAction, params string[] args) - => Bicep(registerAction, CancellationToken.None, args); + IServiceCollectionExtensions.AddMockHttpClientIfNotNull(services, settings.ModuleMetadataClient); - protected static Task Bicep(InvocationSettings settings, params string?[] args /*null args are ignored*/) - => Bicep(services => - { - if (settings.FeatureOverrides is { }) - { - services.WithFeatureOverrides(settings.FeatureOverrides); + services + .AddSingletonIfNotNull(settings.Environment ?? BicepTestConstants.EmptyEnvironment) + .AddSingletonIfNotNull(settings.ClientFactory) + .AddSingletonIfNotNull(settings.TemplateSpecRepositoryFactory); + + registerAction?.Invoke(services); } + ) + .RunAsync(args.ToArrayExcludingNull(), cancellationToken)); - services - .AddSingleton(settings.Environment ?? BicepTestConstants.EmptyEnvironment) - .AddSingleton(settings.ClientFactory) - .AddSingleton(settings.TemplateSpecRepositoryFactory); - }, CancellationToken.None, args.ToArrayExcludingNull()); + protected static Task Bicep(params string[] args) => Bicep(InvocationSettings.Default, args); + + protected static Task Bicep(Action registerAction, params string[] args) + => Bicep(InvocationSettings.Default, registerAction, CancellationToken.None, args); + + protected static Task Bicep(Action registerAction, CancellationToken cancellationToken, params string[] args) + => Bicep(InvocationSettings.Default, registerAction, cancellationToken, args); + + protected static Task Bicep(InvocationSettings settings, params string?[] args /*null args are ignored*/) + => Bicep(settings, null, CancellationToken.None, args); protected static void AssertNoErrors(string error) { @@ -71,9 +109,9 @@ protected static void AssertNoErrors(string error) } } - protected static async Task> GetAllDiagnostics(string bicepFilePath, IContainerRegistryClientFactory clientFactory, ITemplateSpecRepositoryFactory templateSpecRepositoryFactory) + protected static async Task> GetAllDiagnostics(string bicepFilePath, IContainerRegistryClientFactory clientFactory, ITemplateSpecRepositoryFactory templateSpecRepositoryFactory, IPublicRegistryModuleMetadataClient? moduleMetadataClient = null) { - var compilation = await CreateCompiler(clientFactory, templateSpecRepositoryFactory).CreateCompilation(PathHelper.FilePathToFileUrl(bicepFilePath)); + var compilation = await CreateCompiler(clientFactory, templateSpecRepositoryFactory, moduleMetadataClient).CreateCompilation(PathHelper.FilePathToFileUrl(bicepFilePath)); var output = new List(); foreach (var (bicepFile, diagnostics) in compilation.GetAllDiagnosticsByBicepFile()) diff --git a/src/Bicep.Cli.IntegrationTests/UseRecentModuleVersionsIntegrationTests.cs b/src/Bicep.Cli.IntegrationTests/UseRecentModuleVersionsIntegrationTests.cs new file mode 100644 index 00000000000..cf2afd1905b --- /dev/null +++ b/src/Bicep.Cli.IntegrationTests/UseRecentModuleVersionsIntegrationTests.cs @@ -0,0 +1,804 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Immutable; +using System.IO.Abstractions; +using System.IO.Abstractions.TestingHelpers; +using Azure; +using Azure.Containers.ContainerRegistry; +using Azure.Identity; +using Bicep.Cli.UnitTests; +using Bicep.Cli.UnitTests.Assertions; +using Bicep.Core.Analyzers.Linter.Rules; +using Bicep.Core.Configuration; +using Bicep.Core.Modules; +using Bicep.Core.Registry; +using Bicep.Core.Registry.Oci; +using Bicep.Core.Registry.PublicRegistry; +using Bicep.Core.Samples; +using Bicep.Core.UnitTests; +using Bicep.Core.UnitTests.Assertions; +using Bicep.Core.UnitTests.Baselines; +using Bicep.Core.UnitTests.Mock; +using Bicep.Core.UnitTests.Registry; +using Bicep.Core.UnitTests.Utils; +using FluentAssertions; +using FluentAssertions.Execution; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Bicep.Cli.IntegrationTests; + +[TestClass] +public class UseRecentModuleVersionsIntegrationTests : TestBase +{ + public static BicepModuleMetadata[] DefaultModulesMetadata = [ + new BicepModuleMetadata( + "fake/avm/res/app/container-app", + ["0.2.0"], + ImmutableDictionary.Empty), + ]; + + private const string PREFIX = "br:mcr.microsoft.com/bicep"; + private const string NotRestoredErrorCode = "BCP190"; + private const string UnableToRestoreErrorCode = "BCP192"; + + private string CacheRoot => Path.Join(FileHelper.GetUniqueTestOutputPath(TestContext), "cacheRoot"); + + private class Options(string CacheRoot) + { + private IPublicRegistryModuleMetadataClient? _metadataClient = null; + private string? _config = null; + + public string Bicep { get; init; } = "/* bicep contents */"; + public (string module, string[] versions)[] ModulesMetadata { get; init; } = []; + public string[] PublishedModules { get; init; } = []; + public bool NoRestore { get; init; } = false; + + public string DiagnosticLevel { get; init; } = "Warning"; // This rule normally defaults to "off" + + public string? BicepConfig + { + set { _config = value; } + get + { + return _config ?? """ + { + "cacheRootDirectory": "{CACHEROOT}", + "analyzers": { + "core": { + "rules": { + "use-recent-module-versions": { + "level": "{DiagnosticLevel}" + } + } + } + } + } + """.Replace("{DiagnosticLevel}", this.DiagnosticLevel) + .Replace("{CACHEROOT}", CacheRoot.Replace("\\", "\\\\")); + } + } + + // Automatically created from ModulesMetadata by default + public IPublicRegistryModuleMetadataClient MetadataClient + { + set + { + _metadataClient = value; + } + get => _metadataClient is { } ? _metadataClient : PublicRegistryModuleMetadataClientMock.Create( + ModulesMetadata.Select(mm => new BicepModuleMetadata( + mm.module, + new List(mm.versions), + new Dictionary().ToImmutableDictionary()))).Object; + } + + } + + private async Task Test(Options options) + { + string testOutputPath = FileHelper.GetUniqueTestOutputPath(TestContext); + + // compile and publish modules using throwaway file system + var clientFactory = await RegistryHelper.CreateMockRegistryClientWithPublishedModulesAsync( + new MockFileSystem(), + options.PublishedModules.Select(x => (x, "", true)).ToArray()); + + // create files + var mainFile = FileHelper.SaveResultFile(TestContext, "main.bicep", options.Bicep, testOutputPath); + var configurationPath = options.BicepConfig is null ? null : FileHelper.SaveResultFile(TestContext, "bicepconfig.json", options.BicepConfig, testOutputPath); + + // act + var settings = new InvocationSettings() + { + ModuleMetadataClient = options.MetadataClient, + ClientFactory = clientFactory + }; + return await Bicep(settings, "lint", mainFile, options.NoRestore ? "--no-restore" : null); + } + + [TestMethod] + public async Task IfLevelIsOff_ShouldNotDownloadModuleMetadata() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m1 '{PREFIX}/fake/avm/res/app/container-app:0.2.0' = { + name: 'm1' + } + """.Replace("{PREFIX}", PREFIX), + DiagnosticLevel = "off", + PublishedModules = [$"{PREFIX}/fake/avm/res/app/container-app:0.2.0"], + MetadataClient = PublicRegistryModuleMetadataClientMock.CreateToThrow(new Exception("unit test failed: shouldn't try to download in this scenario")).Object, + }); + + result.Should().NotHaveStderr(); + result.Should().HaveStdout(""); + result.Should().Succeed(); + } + + [TestMethod] + // We don't currently cache to disk, but rather on every check to restore modules. + public async Task IfNoRestoreSpecified_ThenShouldFailBecauseNoCache() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m1 '{PREFIX}/fake/avm/res/app/container-app:0.2.0' = { + name: 'm1' + } + """.Replace("{PREFIX}", PREFIX), + PublishedModules = [$"{PREFIX}/fake/avm/res/app/container-app:0.2.0"], + ModulesMetadata = [("fake/avm/res/app/container-app", ["0.2.0"])], + NoRestore = true, + }); + + result.Should().HaveStderrMatch($"*Error {NotRestoredErrorCode}: The artifact with reference \"br:mcr.microsoft.com/bicep/fake/avm/res/app/container-app:0.2.0\" has not been restored.*"); + result.Should().HaveStderrMatch("*Warning use-recent-module-versions: Available module versions have not yet been downloaded. If running from the command line, be sure --no-restore is not specified.*"); + result.Should().HaveStdout(""); + result.Should().Fail(); + } + + [TestMethod] + // We don't currently cache to disk, but rather on every check to restore modules. + public async Task IfMetadataDownloadFails_ThenShouldFail() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m1 '{PREFIX}/fake/avm/res/app/container-app:0.2.0' = { + name: 'm1' + } + """.Replace("{PREFIX}", PREFIX), + PublishedModules = [$"{PREFIX}/fake/avm/res/app/container-app:0.2.0"], + MetadataClient = PublicRegistryModuleMetadataClientMock.CreateToThrow(new Exception("Download failed.")).Object, + }); + + result.Should().HaveStderrMatch($"*Warning use-recent-module-versions: Could not download available module versions: Download failed.*"); + result.Should().HaveStdout(""); + result.Should().Succeed(); + } + + [TestMethod] + public async Task SimpleFailure() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m1 '{PREFIX}/fake/avm/res/app/container-app:0.2.0' = { + name: 'm1' + } + """.Replace("{PREFIX}", PREFIX), + PublishedModules = [ + $"{PREFIX}/fake/avm/res/app/container-app:0.2.0", + $"{PREFIX}/fake/avm/res/app/container-app:0.3.0" + ], + ModulesMetadata = [("fake/avm/res/app/container-app", ["0.2.0", "0.3.0"])], + }); + + result.Should().HaveStderrMatch($"*Warning use-recent-module-versions: Use a more recent version of module 'fake/avm/res/app/container-app'. The most recent version is 0.3.0.*"); + result.Should().HaveStdout(""); + result.Should().Succeed(); // only a warning + } + + [TestMethod] + public async Task IfDoesntMatchCase_ThenShouldIgnore() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m1 '{PREFIX}/fake/avm/res/app/container-app:0.2.0' = { + name: 'm1' + } + """.Replace("{PREFIX}", PREFIX), + PublishedModules = [ + $"{PREFIX}/fake/avm/res/app/container-app:0.2.0", + $"{PREFIX}/fake/avm/res/app/container-app:0.3.0" + ], + ModulesMetadata = [("fake/avm/res/app/CONTAINER-app", ["0.2.0", "0.3.0"])], + }); + + result.Should().NotHaveStderr(); + result.Should().HaveStdout(""); + result.Should().Succeed(); // only a warning + } + + [TestMethod] + public async Task IfLevelIsError_ShouldShowFailuresAsErrors() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m1 '{PREFIX}/fake/avm/res/app/container-app:0.2.0' = { + name: 'm1' + } + """.Replace("{PREFIX}", PREFIX), + DiagnosticLevel = "Error", + PublishedModules = [ + $"{PREFIX}/fake/avm/res/app/container-app:0.2.0", + $"{PREFIX}/fake/avm/res/app/container-app:0.3.0" + ], + ModulesMetadata = [("fake/avm/res/app/container-app", ["0.2.0", "0.3.0"])], + }); + + result.Should().HaveStderrMatch($"*Error use-recent-module-versions: Use a more recent version of module 'fake/avm/res/app/container-app'. The most recent version is 0.3.0.*"); + result.Should().HaveStdout(""); + result.Should().Fail(); + } + + [TestMethod] + public async Task MetadataOrderShouldntMatter1() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m1 '{PREFIX}/fake/avm/res/app/container-app:0.2.0' = { + name: 'm1' + } + """.Replace("{PREFIX}", PREFIX), + PublishedModules = [ + $"{PREFIX}/fake/avm/res/app/container-app:0.2.0", + $"{PREFIX}/fake/avm/res/app/container-app:0.3.0" + ], + ModulesMetadata = [("fake/avm/res/app/container-app", ["0.2.0", "0.3.0"])], + }); + + result.Should().HaveStderrMatch($"*Warning use-recent-module-versions: Use a more recent version of module 'fake/avm/res/app/container-app'. The most recent version is 0.3.0.*"); + result.Should().HaveStdout(""); + result.Should().Succeed(); + } + + [TestMethod] + public async Task MetadataOrderShouldntMatter2() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m1 '{PREFIX}/fake/avm/res/app/container-app:0.2.0' = { + name: 'm1' + } + """.Replace("{PREFIX}", PREFIX), + PublishedModules = [ + $"{PREFIX}/fake/avm/res/app/container-app:0.2.0", + $"{PREFIX}/fake/avm/res/app/container-app:0.3.0" + ], + ModulesMetadata = [("fake/avm/res/app/container-app", ["0.3.0", "0.2.0"])], + }); + + result.Should().HaveStderrMatch($"*Warning use-recent-module-versions: Use a more recent version of module 'fake/avm/res/app/container-app'. The most recent version is 0.3.0.*"); + result.Should().HaveStdout(""); + result.Should().Succeed(); + } + + [TestMethod] + public async Task IfModuleNotFound_ShouldIgnore() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m1 '{PREFIX}/fake/avm/res/app/unknown-module:0.3.0' = { // Our metadata doesn't recognize this + name: 'm1' + } + """.Replace("{PREFIX}", PREFIX), + PublishedModules = [$"{PREFIX}/fake/avm/res/app/unknown-module:unknown-version"], // Required for mock to work + ModulesMetadata = [("fake/avm/res/app/container-app", ["0.3.0", "0.2.0"])], + }); + + result.Should().HaveCompileError(UnableToRestoreErrorCode); + result.Should().NotHaveRecentModuleVersionsRuleFailure(); + result.Should().HaveStdout(""); + result.Should().Fail(); + } + + [TestMethod] + public async Task IfModuleUsesVersionsGreaterThanOurCacheRecognizes_ShouldPass() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m1 '{PREFIX}/fake/avm/res/app/container-app:0.3.0' = { // Our metadata only recognizes up to 0.2.0, but 0.3.0 does exist + name: 'm1' + } + """.Replace("{PREFIX}", PREFIX), + PublishedModules = [$"{PREFIX}/fake/avm/res/app/container-app:0.3.0"], + ModulesMetadata = [("fake/avm/res/app/container-app", ["0.1.0", "0.2.0"])], + }); + + result.Should().NotHaveStderr(); + result.Should().HaveStdout(""); + result.Should().Succeed(); + } + + [TestMethod] + public async Task IfModuleUsesUnknownVersion_ThatIsLowerThanOurCacheRecognizes_ShouldFail() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m1 '{PREFIX}/fake/avm/res/app/container-app:0.1.0' = { // Our metadata recognizes 0.2.0, 0.4.0, but 0.1.0 does exist + name: 'm1' + } + """.Replace("{PREFIX}", PREFIX), + PublishedModules = [$"{PREFIX}/fake/avm/res/app/container-app:0.1.0"], + ModulesMetadata = [("fake/avm/res/app/container-app", ["0.2.0", "0.4.0"])], + }); + + result.Should().HaveStderrMatch("*Warning use-recent-module-versions: Use a more recent version of module 'fake/avm/res/app/container-app'. The most recent version is 0.4.0.*"); + result.Should().HaveStdout(""); + result.Should().Succeed(); // only a warning + } + + [TestMethod] + public async Task IfModuleUsesUnknownVersion_ThatIsLowerThanOurCacheHighestVersion_ShouldFail() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m1 '{PREFIX}/fake/avm/res/app/container-app:0.3.0' = { // Our metadata recognizes 0.1.0, 0.2.0, 0.4.0, but 0.3.0 does exist + name: 'm1' + } + """.Replace("{PREFIX}", PREFIX), + PublishedModules = [$"{PREFIX}/fake/avm/res/app/container-app:0.3.0"], + ModulesMetadata = [("fake/avm/res/app/container-app", ["0.1.0", "0.2.0", "0.4.0"])], + }); + + result.Should().HaveStderrMatch("*Warning use-recent-module-versions: Use a more recent version of module 'fake/avm/res/app/container-app'. The most recent version is 0.4.0.*"); + result.Should().HaveStdout(""); + result.Should().Succeed(); // only a warning + } + + [TestMethod] //asfdg fails when run with others + public async Task IfModuleVersionIsNotPublished_ButCacheHasMoreRecentVersion_ThenShouldGiveCompilerError_AndLinterShouldFail() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m1 '{PREFIX}/fake/avm/res/app/container-app:0.3.0' = { // 0.3.0 does not exist + name: 'm1' + } + """.Replace("{PREFIX}", PREFIX), + PublishedModules = [ + $"{PREFIX}/fake/avm/res/app/container-app:0.1.0", + $"{PREFIX}/fake/avm/res/app/container-app:0.2.0", + $"{PREFIX}/fake/avm/res/app/container-app:0.4.0"], + ModulesMetadata = [("fake/avm/res/app/container-app", ["0.1.0", "0.2.0", "0.4.0"])], + }); + + result.Should().HaveCompileError(UnableToRestoreErrorCode); + result.Should().HaveStderrMatch("*Warning use-recent-module-versions: Use a more recent version of module 'fake/avm/res/app/container-app'. The most recent version is 0.4.0.*"); + result.Should().HaveStdout(""); + result.Should().Fail(); + } + + [TestMethod] + public async Task IfModuleVersionIsHigherThanPublished_ThenShouldGiveCompileError_ButLinterShouldIgnore() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m1 '{PREFIX}/fake/avm/res/app/container-app:0.3.1' = { // 0.1.0 and 0.2.0 exist + name: 'm1' + } + """.Replace("{PREFIX}", PREFIX), + PublishedModules = [ + $"{PREFIX}/fake/avm/res/app/container-app:0.1.0", + $"{PREFIX}/fake/avm/res/app/container-app:0.2.0"], + ModulesMetadata = [("fake/avm/res/app/container-app", ["0.1.0", "0.2.0"])], + }); + + result.Should().HaveCompileError(UnableToRestoreErrorCode); + result.Should().NotHaveRecentModuleVersionsRuleFailure(); + result.Should().HaveStdout(""); + } + + [TestMethod] + public async Task IfModuleNotRecognizedByMetadata_ThenCompileError_ButLinterShouldIgnore() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m1 '{PREFIX}/fake/avm/res/app/unknown:0.1.0' = { + name: 'm1' + } + """.Replace("{PREFIX}", PREFIX), + PublishedModules = [ + $"{PREFIX}/fake/avm/res/app/container-app:0.1.0", + $"{PREFIX}/fake/avm/res/app/container-app:0.2.0"], + ModulesMetadata = [("fake/avm/res/app/container-app", ["0.1.0", "0.2.0"])], + }); + + result.Should().HaveCompileError(UnableToRestoreErrorCode); + result.Should().NotHaveRecentModuleVersionsRuleFailure(); + result.Should().HaveStdout(""); + } + + [TestMethod] + public async Task IfModuleVersion_IsNotSemanticVersion_LinterShouldIgnore() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m1 '{PREFIX}/fake/avm/res/app/container-app:v0.1.0' = { + name: 'm1' + } + module m2 '{PREFIX}/fake/avm/res/app/container-app:abc' = { + name: 'm2' + } + module m3 '{PREFIX}/fake/avm/res/app/container-app:0.1.0.0' = { + name: 'm3' + } + module m4 '{PREFIX}/fake/avm/res/app/container-app:0.1' = { + name: 'm4' + } + module m5 '{PREFIX}/fake/avm/res/app/container-app:1' = { + name: 'm5' + } + """.Replace("{PREFIX}", PREFIX), + PublishedModules = [ + $"{PREFIX}/fake/avm/res/app/container-app:v0.1.0", + $"{PREFIX}/fake/avm/res/app/container-app:abc", + $"{PREFIX}/fake/avm/res/app/container-app:0.1.0.0", + $"{PREFIX}/fake/avm/res/app/container-app:0.1", + $"{PREFIX}/fake/avm/res/app/container-app:1", + ], + ModulesMetadata = [("fake/avm/res/app/container-app", ["10.0.0"])], + }); + + result.Should().NotHaveRecentModuleVersionsRuleFailure(); + result.Should().HaveStdout(""); + } + + [TestMethod] + public async Task IfModuleVersion_HasOldVersionWithSuffix_Fail() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m1a '{PREFIX}/fake/avm/res/app/container-app1a:0.1.0' = { // fail + name: 'm1a' + } + module m1b '{PREFIX}/fake/avm/res/app/container-app1b:0.1.0-beta' = { // fail + name: 'm1b' + } + """.Replace("{PREFIX}", PREFIX), + PublishedModules = [ + $"{PREFIX}/fake/avm/res/app/container-app1a:0.1.0", + $"{PREFIX}/fake/avm/res/app/container-app1b:0.1.0-beta", + ], + ModulesMetadata = [ + ("fake/avm/res/app/container-app1a", ["0.1.0", "0.2.0"]), + ("fake/avm/res/app/container-app1b", ["0.1.0", "0.2.0"]), + ], + }); + + result.Should().NotHaveCompileError(UnableToRestoreErrorCode); + result.Should().HaveStderrMatch("*main.bicep(1,12) : Warning use-recent-module-versions: Use a more recent version of module 'fake/avm/res/app/container-app1a'. The most recent version is 0.2.0. *"); + result.Should().HaveStderrMatch("*main.bicep(4,12) : Warning use-recent-module-versions: Use a more recent version of module 'fake/avm/res/app/container-app1b'. The most recent version is 0.2.0. *"); + + result.Should().HaveStdout(""); + } + + [TestMethod] + public async Task SuffixIsLowerThanVersionWithoutSuffix() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m2 '{PREFIX}/fake/avm/res/app/container-app2:0.2.0-alpha' = { // fail + name: 'm2' + } + """.Replace("{PREFIX}", PREFIX), + PublishedModules = [ + $"{PREFIX}/fake/avm/res/app/container-app2:0.2.0-alpha" + ], + ModulesMetadata = [ + ("fake/avm/res/app/container-app2", ["0.1.0", "0.2.0"]), + ], + }); + + result.Should().HaveStderrMatch("*Warning use-recent-module-versions: Use a more recent version of module 'fake/avm/res/app/container-app2'. The most recent version is 0.2.0.*"); + result.Should().HaveStdout(""); + } + + [TestMethod] + public async Task SuffixWithHigherVersion_IsHigherThanLowerVersionWithoutSuffix() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m2 '{PREFIX}/fake/avm/res/app/container-app2:0.3.0-alpha' = { // pass + name: 'm2' + } + """.Replace("{PREFIX}", PREFIX), + PublishedModules = [ + $"{PREFIX}/fake/avm/res/app/container-app2:0.3.0-alpha" + ], + ModulesMetadata = [ + ("fake/avm/res/app/container-app2", ["0.1.0", "0.2.0"]), + ], + }); + + result.Should().NotHaveStderr(); + result.Should().HaveStdout(""); + } + + [TestMethod] + public async Task IfModuleVersion_InvalidVersion_ThenCompileError_ButLinterShouldIgnore() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m1 '{PREFIX}/fake/avm/res/app/container-app:1.0.*' = { + name: 'm1' + } + module m2 '{PREFIX}/fake/avm/res/app/container-app:' = { + name: 'm2' + } + module m3 '{PREFIX}/fake/avm/res/app/container-app' = { + name: 'm3' + } + """.Replace("{PREFIX}", PREFIX), + PublishedModules = [], + ModulesMetadata = [("fake/avm/res/app/container-app", ["10.0.0"])], + }); + + result.Should().HaveCompileError("BCP198"); + result.Should().NotHaveRecentModuleVersionsRuleFailure(); + result.Should().HaveStdout(""); + } + + // Public modules don't currently have suffixes, but can't guarantee this won't happen in the future + [TestMethod] + public async Task IfModuleMetadataIsAlpha_AndReferencesAlpha_ShouldPass() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m1 '{PREFIX}/fake/avm/res/app/container-app:1.0.0-alpha' = { + name: 'm1' + } + """.Replace("{PREFIX}", PREFIX), + PublishedModules = [ + $"{PREFIX}/fake/avm/res/app/container-app:1.0.0-alpha" + ], + ModulesMetadata = [("fake/avm/res/app/container-app", ["1.0.0-alpha"])], + }); + + result.Should().NotHaveCompileError(); + result.Should().NotHaveRecentModuleVersionsRuleFailure(); + result.Should().HaveStdout(""); + } + + // Public modules don't currently have suffixes, but can't guarantee this won't happen in the future + [TestMethod] + public async Task IfModuleMetadataIsBeta_AndReferencesRelease_ShouldPass() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m1 '{PREFIX}/fake/avm/res/app/container-app:1.0.0' = { + name: 'm1' + } + """.Replace("{PREFIX}", PREFIX), + PublishedModules = [ + $"{PREFIX}/fake/avm/res/app/container-app:1.0.0" + ], + ModulesMetadata = [("fake/avm/res/app/container-app", ["1.0.0-beta"])], + }); + + result.Should().NotHaveCompileError(); + result.Should().NotHaveRecentModuleVersionsRuleFailure(); + result.Should().HaveStdout(""); + } + + // Public modules don't currently have suffixes, but can't guarantee this won't happen in the future + [TestMethod] + public async Task IfModuleMetadataIsRelease_AndReferencesBeta_ShouldFail() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m1 '{PREFIX}/fake/avm/res/app/container-app:1.0.0-beta' = { + name: 'm1' + } + """.Replace("{PREFIX}", PREFIX), + PublishedModules = [ + $"{PREFIX}/fake/avm/res/app/container-app:1.0.0-beta" + ], + ModulesMetadata = [("fake/avm/res/app/container-app", ["1.0.0"])], + }); + + result.Should().NotHaveCompileError(); + result.Should().HaveStderrMatch("*Warning use-recent-module-versions: Use a more recent version of module 'fake/avm/res/app/container-app'. The most recent version is 1.0.0.*"); + result.Should().HaveStdout(""); + } + + // Public modules don't currently have suffixes, but can't guarantee this won't happen in the future + [TestMethod] + public async Task IfModuleMetadataHasReleaseAndBeta_AndReferencesBeta_ShouldFail() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m1 '{PREFIX}/fake/avm/res/app/container-app:1.0.0-beta' = { + name: 'm1' + } + """.Replace("{PREFIX}", PREFIX), + PublishedModules = [ + $"{PREFIX}/fake/avm/res/app/container-app:1.0.0-beta" + ], + ModulesMetadata = [("fake/avm/res/app/container-app", ["1.0.0", "1.0.0-beta"])], + }); + + result.Should().NotHaveCompileError(); + result.Should().HaveStderrMatch("*Warning use-recent-module-versions: Use a more recent version of module 'fake/avm/res/app/container-app'. The most recent version is 1.0.0.*"); + result.Should().HaveStdout(""); + } + + // Public modules don't currently have suffixes, but can't guarantee this won't happen in the future + [TestMethod] + public async Task IfModuleMetadataHasReleaseAndBeta_AndReferencesBeta_ShouldFail_MetadataOrderShouldntMatter() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m1 '{PREFIX}/fake/avm/res/app/container-app:1.0.0-beta' = { + name: 'm1' + } + """.Replace("{PREFIX}", PREFIX), + PublishedModules = [ + $"{PREFIX}/fake/avm/res/app/container-app:1.0.0-beta" + ], + ModulesMetadata = [("fake/avm/res/app/container-app", ["1.0.0-beta", "1.0.0"])], + }); + + result.Should().NotHaveCompileError(); + result.Should().HaveStderrMatch("*Warning use-recent-module-versions: Use a more recent version of module 'fake/avm/res/app/container-app'. The most recent version is 1.0.0.*"); + result.Should().HaveStdout(""); + } + + // Public modules don't currently have suffixes, but can't guarantee this won't happen in the future + [TestMethod] + public async Task IfModuleMetadataHasReleaseAndBeta_FailureMessageShouldShowReleaseVersion() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m1 '{PREFIX}/fake/avm/res/app/container-app:1.0.0-beta' = { + name: 'm1' + } + """.Replace("{PREFIX}", PREFIX), + PublishedModules = [ + $"{PREFIX}/fake/avm/res/app/container-app:1.0.0-beta" + ], + ModulesMetadata = [("fake/avm/res/app/container-app", ["1.0.0", "1.0.0-beta", "2.0.0", "2.0.0-alpha", "2.0.0-beta"])], + }); + + result.Should().NotHaveCompileError(); + result.Should().HaveStderrMatch("*Warning use-recent-module-versions: Use a more recent version of module 'fake/avm/res/app/container-app'. The most recent version is 2.0.0.*"); + result.Should().HaveStdout(""); + } + + [TestMethod] + public async Task PublicBicepAlias_ShouldWork() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m1 'br/public:fake/avm/res/app/container-app:1.0.0' = { + name: 'm1' + } + """, + PublishedModules = [ + $"{PREFIX}/fake/avm/res/app/container-app:1.0.0" + ], + ModulesMetadata = [("fake/avm/res/app/container-app", ["1.0.1", "1.0.0"])], + }); + + result.Should().NotHaveCompileError(); + result.Should().HaveStderrMatch("*Warning use-recent-module-versions: Use a more recent version of module 'fake/avm/res/app/container-app'. The most recent version is 1.0.1.*"); + result.Should().HaveStdout(""); + } + + [TestMethod] + public async Task PrivateModules_ShouldCurrentlyBeIgnored() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m1 'br:mcr.private.com/bicep/fake/avm/res/app/container-app:1.0.0' = { + name: 'm1' + } + """, + PublishedModules = [ + $"br:mcr.private.com/bicep/fake/avm/res/app/container-app:1.0.0" + ], + ModulesMetadata = [("fake/avm/res/app/container-app", ["1.0.1", "1.0.0"])], + }); + + result.Should().NotHaveCompileError(); + result.Should().NotHaveRecentModuleVersionsRuleFailure(); + result.Should().HaveStdout(""); + } + + [TestMethod] + public async Task PrivateModules_ShouldCurrentlyBeIgnored_EvenIfNoCache() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m1 'br:mcr.private.com/bicep/fake/avm/res/app/container-app:1.0.0' = { + name: 'm1' + } + """, + PublishedModules = [ + $"br:mcr.private.com/bicep/fake/avm/res/app/container-app:1.0.0" + ], + ModulesMetadata = [("fake/avm/res/app/container-app", ["1.0.1", "1.0.0"])], + NoRestore = true, + }); + + result.Should().HaveCompileError(NotRestoredErrorCode); + result.Should().NotHaveRecentModuleVersionsRuleFailure(); + result.Should().HaveStdout(""); + } + + [TestMethod] + public async Task PublicModule_WithoutBicepPrefix_ShouldntHappen_ButShouldBeIgnoredIfItDoes() + { + var result = await Test(new Options(CacheRoot) + { + Bicep = """ + module m1 'br:mcr.microsoft.com/fake/avm/res/app/container-app:1.0.0' = { + name: 'm1' + } + """, + PublishedModules = [ + $"br:mcr.microsoft.com/fake/avm/res/app/container-app:1.0.0" + ], + ModulesMetadata = [("fake/avm/res/app/container-app", ["1.0.1", "1.0.0"])], + }); + + result.Should().NotHaveCompileError(); + result.Should().NotHaveStderr(); + result.Should().HaveStdout(""); + } +} + +public static class CliResultUseRecentModuleVersionsExtensions +{ + public static AndConstraint HaveRecentModuleVersionsRuleFailure(this CliResultAssertions instance, string because = "", params object[] becauseArgs) + { + instance.Subject.Should().HaveRuleFailure(UseRecentModuleVersionsRule.Code, because, becauseArgs); + + return new(instance); + } + + public static AndConstraint NotHaveRecentModuleVersionsRuleFailure(this CliResultAssertions instance, string because = "", params object[] becauseArgs) + { + instance.Subject.Should().NotHaveRuleFailure(UseRecentModuleVersionsRule.Code, because, becauseArgs); + + return new(instance); + } +} diff --git a/src/Bicep.Cli.IntegrationTests/packages.lock.json b/src/Bicep.Cli.IntegrationTests/packages.lock.json index c6c30eaad6a..c3e096a25e3 100644 --- a/src/Bicep.Cli.IntegrationTests/packages.lock.json +++ b/src/Bicep.Cli.IntegrationTests/packages.lock.json @@ -72,6 +72,16 @@ "resolved": "3.6.139", "contentHash": "rq0Ub/Jik7PtMtZtLn0tHuJ01Yt36RQ+eeBe+S7qnJ/EFOX6D4T9zuYD3vQPYKGI6Ro4t2iWgFm3fGDgjBrMfg==" }, + "TestableIO.System.IO.Abstractions.TestingHelpers": { + "type": "Direct", + "requested": "[21.0.2, )", + "resolved": "21.0.2", + "contentHash": "4s6EC7rGSlNPqC9/FOmYavFTjetO7zDBnc0H8Hyb8ABJll2UgeC7UvdNLwx28/hhUE8AvUJmwamjAII6LJWIQA==", + "dependencies": { + "TestableIO.System.IO.Abstractions": "21.0.2", + "TestableIO.System.IO.Abstractions.Wrappers": "21.0.2" + } + }, "Azure.Bicep.Types": { "type": "Transitive", "resolved": "0.5.9", @@ -890,6 +900,11 @@ "OmniSharp.Extensions.LanguageProtocol": "0.19.9" } }, + "RichardSzalay.MockHttp": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "QwnauYiaywp65QKFnP+wvgiQ2D8Pv888qB2dyfd7MSVDF06sIvxqASenk+RxsWybyyt+Hu1Y251wQxpHTv3UYg==" + }, "runtime.linux-arm.runtime.native.System.IO.Ports": { "type": "Transitive", "resolved": "6.0.0", @@ -967,6 +982,11 @@ "System.Text.Encoding.Extensions": "4.3.0" } }, + "Semver": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "4vYo1zqn6pJ1YrhjuhuOSbIIm0CpM47grbpTJ5ABjOlfGt/EhMEM9ed4MRK5Jr6gVnntWDqOUzGeUJp68PZGjw==" + }, "SharpYaml": { "type": "Transitive", "resolved": "2.1.1", @@ -1570,15 +1590,6 @@ "resolved": "21.0.2", "contentHash": "ViPtOy4lAlmWq9GDEwiE5C9XSgDGHUWMRvVvruI38pbikUXkgu7xnCVW7PYdQOjS+iWWcQHt+U7oyPedfnWSHg==" }, - "TestableIO.System.IO.Abstractions.TestingHelpers": { - "type": "Transitive", - "resolved": "21.0.2", - "contentHash": "4s6EC7rGSlNPqC9/FOmYavFTjetO7zDBnc0H8Hyb8ABJll2UgeC7UvdNLwx28/hhUE8AvUJmwamjAII6LJWIQA==", - "dependencies": { - "TestableIO.System.IO.Abstractions": "21.0.2", - "TestableIO.System.IO.Abstractions.Wrappers": "21.0.2" - } - }, "TestableIO.System.IO.Abstractions.Wrappers": { "type": "Transitive", "resolved": "21.0.2", @@ -1603,9 +1614,11 @@ "Microsoft.Extensions.Configuration.Binder": "[8.0.1, )", "Microsoft.Extensions.Configuration.Json": "[8.0.0, )", "Microsoft.Extensions.DependencyInjection": "[8.0.0, )", + "Microsoft.Extensions.Http": "[8.0.0, )", "Microsoft.Graph.Bicep.Types": "[0.1.6-preview, )", "Microsoft.PowerPlatform.ResourceStack": "[7.0.0.2007, )", "Newtonsoft.Json": "[13.0.3, )", + "Semver": "[2.3.0, )", "SharpYaml": "[2.1.1, )", "System.IO.Abstractions": "[21.0.2, )", "System.Private.Uri": "[4.3.2, )" @@ -1679,6 +1692,7 @@ "Microsoft.NET.Test.Sdk": "[17.10.0, )", "Moq": "[4.20.70, )", "Newtonsoft.Json.Schema": "[3.0.16, )", + "RichardSzalay.MockHttp": "[7.0.0, )", "System.IO.Abstractions.TestingHelpers": "[21.0.2, )" } }, diff --git a/src/Bicep.Cli.UnitTests/Assertions/CliResultAssertions.cs b/src/Bicep.Cli.UnitTests/Assertions/CliResultAssertions.cs index acea8cbafe1..bc075fa8417 100644 --- a/src/Bicep.Cli.UnitTests/Assertions/CliResultAssertions.cs +++ b/src/Bicep.Cli.UnitTests/Assertions/CliResultAssertions.cs @@ -1,9 +1,16 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Bicep.Core.Analyzers.Linter.Rules; +using FluentAssertions; using FluentAssertions.Primitives; namespace Bicep.Cli.UnitTests.Assertions; +public static class CliResultExtensions +{ + public static CliResultAssertions Should(this CliResult result) => new(result); +} + public class CliResultAssertions : ReferenceTypeAssertions { public CliResultAssertions(CliResult instance) @@ -12,4 +19,89 @@ public CliResultAssertions(CliResult instance) } protected override string Identifier => "result"; + + public AndConstraint Succeed(string because = "", params object[] becauseArgs) + { + Subject.ExitCode.Should().Be(0, because, becauseArgs); + + return new(this); + } + + public AndConstraint Fail(string because = "", params object[] becauseArgs) + { + Subject.ExitCode.Should().NotBe(0, because, becauseArgs); + + return new(this); + } + + public AndConstraint HaveStdout(string stdout, string because = "", params object[] becauseArgs) + { + Subject.Stdout.Should().Be(stdout, because, becauseArgs); + + return new(this); + } + + public AndConstraint HaveStdoutMatch(string stdout, string because = "", params object[] becauseArgs) + { + Subject.Stdout.Should().Match(stdout, because, becauseArgs); + + return new(this); + } + + public AndConstraint NotHaveStdout(string because = "", params object[] becauseArgs) + => HaveStdout("", because, becauseArgs); + + public AndConstraint HaveStderr(string stderr, string because = "", params object[] becauseArgs) + { + Subject.Stderr.Should().Be(stderr, because, becauseArgs); + + return new(this); + } + + public AndConstraint HaveStderrMatch(string stderr, string because = "", params object[] becauseArgs) + { + Subject.Stderr.Should().Match(stderr, because, becauseArgs); + + return new(this); + } + + public AndConstraint NotHaveStderrMatch(string stderr, string because = "", params object[] becauseArgs) + { + Subject.Stderr.Should().NotMatch(stderr, because, becauseArgs); + + return new(this); + } + + public AndConstraint NotHaveStderr(string because = "", params object[] becauseArgs) + => HaveStderr("", because, becauseArgs); + + public AndConstraint HaveCompileError(string? errorCode = null, string because = "", params object[] becauseArgs) + { + var errorMatch = errorCode is { } ? $"Error {errorCode}" : "Error"; + Subject.Should().HaveStderrMatch($"*{errorMatch}*", because, becauseArgs); + + return new(this); + } + + public AndConstraint NotHaveCompileError(string? errorCode = null, string because = "", params object[] becauseArgs) + { + var errorMatch = errorCode is { } ? $"Error {errorCode}" : "Error"; + Subject.Should().NotHaveStderrMatch($"*{errorMatch}*", because, becauseArgs); + + return new(this); + } + + public AndConstraint HaveRuleFailure(string linterRuleCode, string because = "", params object[] becauseArgs) + { + Subject.Should().HaveStderrMatch($"*{linterRuleCode}*", because, becauseArgs); + + return new(this); + } + + public AndConstraint NotHaveRuleFailure(string linterRuleCode, string because = "", params object[] becauseArgs) + { + Subject.Should().NotHaveStderrMatch($"*{linterRuleCode}*", because, becauseArgs); + + return new(this); + } } diff --git a/src/Bicep.Cli.UnitTests/Assertions/CliResultAssertionsExtensions.cs b/src/Bicep.Cli.UnitTests/Assertions/CliResultAssertionsExtensions.cs deleted file mode 100644 index d308eb528f7..00000000000 --- a/src/Bicep.Cli.UnitTests/Assertions/CliResultAssertionsExtensions.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -using FluentAssertions; - -namespace Bicep.Cli.UnitTests.Assertions; - -public static class CliResultAssertionsExtensions -{ - public static CliResultAssertions Should(this CliResult result) => new(result); - - public static AndConstraint Succeed(this CliResultAssertions instance, string because = "", params object[] becauseArgs) - { - instance.Subject.ExitCode.Should().Be(0, because, becauseArgs); - - return new(instance); - } - - public static AndConstraint Fail(this CliResultAssertions instance, string because = "", params object[] becauseArgs) - { - instance.Subject.ExitCode.Should().NotBe(0, because, becauseArgs); - - return new(instance); - } - - public static AndConstraint HaveStdout(this CliResultAssertions instance, string stdout, string because = "", params object[] becauseArgs) - { - instance.Subject.Stdout.Should().Be(stdout, because, becauseArgs); - - return new(instance); - } - - public static AndConstraint HaveStdoutMatch(this CliResultAssertions instance, string stdout, string because = "", params object[] becauseArgs) - { - instance.Subject.Stdout.Should().Match(stdout, because, becauseArgs); - - return new(instance); - } - - public static AndConstraint NotHaveStdout(this CliResultAssertions instance, string because = "", params object[] becauseArgs) - => HaveStdout(instance, "", because, becauseArgs); - - public static AndConstraint HaveStderr(this CliResultAssertions instance, string stderr, string because = "", params object[] becauseArgs) - { - instance.Subject.Stderr.Should().Be(stderr, because, becauseArgs); - - return new(instance); - } - - public static AndConstraint HaveStderrMatch(this CliResultAssertions instance, string stderr, string because = "", params object[] becauseArgs) - { - instance.Subject.Stderr.Should().Match(stderr, because, becauseArgs); - - return new(instance); - } - - public static AndConstraint NotHaveStderr(this CliResultAssertions instance, string because = "", params object[] becauseArgs) - => HaveStderr(instance, "", because, becauseArgs); -} diff --git a/src/Bicep.Cli.UnitTests/packages.lock.json b/src/Bicep.Cli.UnitTests/packages.lock.json index edf4ee17f3c..fec9fd76b62 100644 --- a/src/Bicep.Cli.UnitTests/packages.lock.json +++ b/src/Bicep.Cli.UnitTests/packages.lock.json @@ -460,6 +460,26 @@ "resolved": "8.0.0", "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==" }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "8.0.0", + "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "System.Diagnostics.DiagnosticSource": "8.0.0" + } + }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", "resolved": "8.0.0", @@ -483,6 +503,19 @@ "resolved": "8.0.0", "contentHash": "OK+670i7esqlQrPjdIKRbsyMCe9g5kSLpRRQGSr4Q58AOYEe/hCnfLZprh7viNisSUUQZmMrbbuDaIrP+V1ebQ==" }, + "Microsoft.Extensions.Http": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "cWz4caHwvx0emoYe7NkHPxII/KkTI8R/LC9qdqJqnKv2poTJ4e2qqPGQqvRoQ5kaSA4FU5IV3qFAuLuOhoqULQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Diagnostics": "8.0.0", + "Microsoft.Extensions.Logging": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0" + } + }, "Microsoft.Extensions.Logging": { "type": "Transitive", "resolved": "8.0.0", @@ -515,6 +548,18 @@ "Microsoft.Extensions.Primitives": "8.0.0" } }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.Configuration.Binder": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "8.0.0", @@ -826,6 +871,11 @@ "System.Text.Encoding.Extensions": "4.3.0" } }, + "Semver": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "4vYo1zqn6pJ1YrhjuhuOSbIIm0CpM47grbpTJ5ABjOlfGt/EhMEM9ed4MRK5Jr6gVnntWDqOUzGeUJp68PZGjw==" + }, "SharpYaml": { "type": "Transitive", "resolved": "2.1.1", @@ -1435,9 +1485,11 @@ "Microsoft.Extensions.Configuration.Binder": "[8.0.1, )", "Microsoft.Extensions.Configuration.Json": "[8.0.0, )", "Microsoft.Extensions.DependencyInjection": "[8.0.0, )", + "Microsoft.Extensions.Http": "[8.0.0, )", "Microsoft.Graph.Bicep.Types": "[0.1.6-preview, )", "Microsoft.PowerPlatform.ResourceStack": "[7.0.0.2007, )", "Newtonsoft.Json": "[13.0.3, )", + "Semver": "[2.3.0, )", "SharpYaml": "[2.1.1, )", "System.IO.Abstractions": "[21.0.2, )", "System.Private.Uri": "[4.3.2, )" diff --git a/src/Bicep.Cli/Arguments/LintArguments.cs b/src/Bicep.Cli/Arguments/LintArguments.cs index 7739bbfa373..6472704f15e 100644 --- a/src/Bicep.Cli/Arguments/LintArguments.cs +++ b/src/Bicep.Cli/Arguments/LintArguments.cs @@ -30,6 +30,7 @@ public LintArguments(string[] args) DiagnosticsFormat = ArgumentHelper.ToDiagnosticsFormat(args[i + 1]); i++; break; + default: if (args[i].StartsWith("--")) { diff --git a/src/Bicep.Cli/Helpers/ServiceCollectionExtensions.cs b/src/Bicep.Cli/Helpers/ServiceCollectionExtensions.cs index 6ae9802f873..eb578dd9fef 100644 --- a/src/Bicep.Cli/Helpers/ServiceCollectionExtensions.cs +++ b/src/Bicep.Cli/Helpers/ServiceCollectionExtensions.cs @@ -11,6 +11,7 @@ using Bicep.Core.FileSystem; using Bicep.Core.Registry; using Bicep.Core.Registry.Auth; +using Bicep.Core.Registry.PublicRegistry; using Bicep.Core.Semantics.Namespaces; using Bicep.Core.TypeSystem.Providers; using Bicep.Core.Utils; @@ -71,6 +72,7 @@ public static IServiceCollection AddBicepCore(this IServiceCollection services) .AddSingleton() .AddSingleton() .AddSingleton() + .AddPublicRegistryModuleMetadataProviderServices() .AddSingleton(); public static IServiceCollection AddBicepDecompiler(this IServiceCollection services) => services diff --git a/src/Bicep.Cli/packages.lock.json b/src/Bicep.Cli/packages.lock.json index e226a6bcfe6..21e55a04892 100644 --- a/src/Bicep.Cli/packages.lock.json +++ b/src/Bicep.Cli/packages.lock.json @@ -465,6 +465,26 @@ "resolved": "8.0.0", "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==" }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "8.0.0", + "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "System.Diagnostics.DiagnosticSource": "8.0.0" + } + }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", "resolved": "8.0.0", @@ -488,6 +508,19 @@ "resolved": "8.0.0", "contentHash": "OK+670i7esqlQrPjdIKRbsyMCe9g5kSLpRRQGSr4Q58AOYEe/hCnfLZprh7viNisSUUQZmMrbbuDaIrP+V1ebQ==" }, + "Microsoft.Extensions.Http": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "cWz4caHwvx0emoYe7NkHPxII/KkTI8R/LC9qdqJqnKv2poTJ4e2qqPGQqvRoQ5kaSA4FU5IV3qFAuLuOhoqULQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Diagnostics": "8.0.0", + "Microsoft.Extensions.Logging": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0" + } + }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", "resolved": "8.0.0", @@ -510,6 +543,18 @@ "Microsoft.Extensions.Primitives": "8.0.0" } }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.Configuration.Binder": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "8.0.0", @@ -749,6 +794,11 @@ "resolved": "4.4.0", "contentHash": "YhEdSQUsTx+C8m8Bw7ar5/VesXvCFMItyZF7G1AUY+OM0VPZUOeAVpJ4Wl6fydBGUYZxojTDR3I6Bj/+BPkJNA==" }, + "Semver": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "4vYo1zqn6pJ1YrhjuhuOSbIIm0CpM47grbpTJ5ABjOlfGt/EhMEM9ed4MRK5Jr6gVnntWDqOUzGeUJp68PZGjw==" + }, "SharpYaml": { "type": "Transitive", "resolved": "2.1.1", @@ -1333,9 +1383,11 @@ "Microsoft.Extensions.Configuration.Binder": "[8.0.1, )", "Microsoft.Extensions.Configuration.Json": "[8.0.0, )", "Microsoft.Extensions.DependencyInjection": "[8.0.0, )", + "Microsoft.Extensions.Http": "[8.0.0, )", "Microsoft.Graph.Bicep.Types": "[0.1.6-preview, )", "Microsoft.PowerPlatform.ResourceStack": "[7.0.0.2007, )", "Newtonsoft.Json": "[13.0.3, )", + "Semver": "[2.3.0, )", "SharpYaml": "[2.1.1, )", "System.IO.Abstractions": "[21.0.2, )", "System.Private.Uri": "[4.3.2, )" diff --git a/src/Bicep.Core.IntegrationTests/packages.lock.json b/src/Bicep.Core.IntegrationTests/packages.lock.json index 7a35aceae3b..21ff66f072c 100644 --- a/src/Bicep.Core.IntegrationTests/packages.lock.json +++ b/src/Bicep.Core.IntegrationTests/packages.lock.json @@ -853,6 +853,11 @@ "OmniSharp.Extensions.LanguageProtocol": "0.19.9" } }, + "RichardSzalay.MockHttp": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "QwnauYiaywp65QKFnP+wvgiQ2D8Pv888qB2dyfd7MSVDF06sIvxqASenk+RxsWybyyt+Hu1Y251wQxpHTv3UYg==" + }, "runtime.linux-arm.runtime.native.System.IO.Ports": { "type": "Transitive", "resolved": "6.0.0", @@ -915,6 +920,11 @@ "resolved": "4.4.0", "contentHash": "YhEdSQUsTx+C8m8Bw7ar5/VesXvCFMItyZF7G1AUY+OM0VPZUOeAVpJ4Wl6fydBGUYZxojTDR3I6Bj/+BPkJNA==" }, + "Semver": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "4vYo1zqn6pJ1YrhjuhuOSbIIm0CpM47grbpTJ5ABjOlfGt/EhMEM9ed4MRK5Jr6gVnntWDqOUzGeUJp68PZGjw==" + }, "SharpYaml": { "type": "Transitive", "resolved": "2.1.1", @@ -1507,9 +1517,11 @@ "Microsoft.Extensions.Configuration.Binder": "[8.0.1, )", "Microsoft.Extensions.Configuration.Json": "[8.0.0, )", "Microsoft.Extensions.DependencyInjection": "[8.0.0, )", + "Microsoft.Extensions.Http": "[8.0.0, )", "Microsoft.Graph.Bicep.Types": "[0.1.6-preview, )", "Microsoft.PowerPlatform.ResourceStack": "[7.0.0.2007, )", "Newtonsoft.Json": "[13.0.3, )", + "Semver": "[2.3.0, )", "SharpYaml": "[2.1.1, )", "System.IO.Abstractions": "[21.0.2, )", "System.Private.Uri": "[4.3.2, )" @@ -1562,6 +1574,7 @@ "Microsoft.NET.Test.Sdk": "[17.10.0, )", "Moq": "[4.20.70, )", "Newtonsoft.Json.Schema": "[3.0.16, )", + "RichardSzalay.MockHttp": "[7.0.0, )", "System.IO.Abstractions.TestingHelpers": "[21.0.2, )" } }, diff --git a/src/Bicep.Core.Samples/packages.lock.json b/src/Bicep.Core.Samples/packages.lock.json index 5e1295eced3..ca951706903 100644 --- a/src/Bicep.Core.Samples/packages.lock.json +++ b/src/Bicep.Core.Samples/packages.lock.json @@ -800,6 +800,11 @@ "OmniSharp.Extensions.LanguageProtocol": "0.19.9" } }, + "RichardSzalay.MockHttp": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "QwnauYiaywp65QKFnP+wvgiQ2D8Pv888qB2dyfd7MSVDF06sIvxqASenk+RxsWybyyt+Hu1Y251wQxpHTv3UYg==" + }, "runtime.linux-arm.runtime.native.System.IO.Ports": { "type": "Transitive", "resolved": "6.0.0", @@ -862,6 +867,11 @@ "resolved": "4.4.0", "contentHash": "YhEdSQUsTx+C8m8Bw7ar5/VesXvCFMItyZF7G1AUY+OM0VPZUOeAVpJ4Wl6fydBGUYZxojTDR3I6Bj/+BPkJNA==" }, + "Semver": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "4vYo1zqn6pJ1YrhjuhuOSbIIm0CpM47grbpTJ5ABjOlfGt/EhMEM9ed4MRK5Jr6gVnntWDqOUzGeUJp68PZGjw==" + }, "SharpYaml": { "type": "Transitive", "resolved": "2.1.1", @@ -1454,9 +1464,11 @@ "Microsoft.Extensions.Configuration.Binder": "[8.0.1, )", "Microsoft.Extensions.Configuration.Json": "[8.0.0, )", "Microsoft.Extensions.DependencyInjection": "[8.0.0, )", + "Microsoft.Extensions.Http": "[8.0.0, )", "Microsoft.Graph.Bicep.Types": "[0.1.6-preview, )", "Microsoft.PowerPlatform.ResourceStack": "[7.0.0.2007, )", "Newtonsoft.Json": "[13.0.3, )", + "Semver": "[2.3.0, )", "SharpYaml": "[2.1.1, )", "System.IO.Abstractions": "[21.0.2, )", "System.Private.Uri": "[4.3.2, )" @@ -1498,6 +1510,7 @@ "Microsoft.NET.Test.Sdk": "[17.10.0, )", "Moq": "[4.20.70, )", "Newtonsoft.Json.Schema": "[3.0.16, )", + "RichardSzalay.MockHttp": "[7.0.0, )", "System.IO.Abstractions.TestingHelpers": "[21.0.2, )" } }, diff --git a/src/Bicep.Core.UnitTests/Bicep.Core.UnitTests.csproj b/src/Bicep.Core.UnitTests/Bicep.Core.UnitTests.csproj index c32a83746b7..e92df878e38 100644 --- a/src/Bicep.Core.UnitTests/Bicep.Core.UnitTests.csproj +++ b/src/Bicep.Core.UnitTests/Bicep.Core.UnitTests.csproj @@ -17,6 +17,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Bicep.Core.UnitTests/BicepTestConstants.cs b/src/Bicep.Core.UnitTests/BicepTestConstants.cs index e8aefa939e1..e2fe151545d 100644 --- a/src/Bicep.Core.UnitTests/BicepTestConstants.cs +++ b/src/Bicep.Core.UnitTests/BicepTestConstants.cs @@ -13,6 +13,7 @@ using Bicep.Core.Json; using Bicep.Core.Registry; using Bicep.Core.Registry.Oci; +using Bicep.Core.Registry.PublicRegistry; using Bicep.Core.Semantics.Namespaces; using Bicep.Core.TypeSystem; using Bicep.Core.TypeSystem.Providers; @@ -54,7 +55,7 @@ public static class BicepTestConstants public static readonly ITemplateSpecRepositoryFactory TemplateSpecRepositoryFactory = StrictMock.Of().Object; // Linter rules added to this list will be automatically disabled for most tests. - public static readonly string[] NonStableAnalyzerRules = [UseRecentApiVersionRule.Code]; + public static readonly string[] NonStableAnalyzerRules = [UseRecentApiVersionRule.Code, UseRecentModuleVersionsRule.Code]; public static readonly RootConfiguration BuiltInConfigurationWithAllAnalyzersDisabled = IConfigurationManager.GetBuiltInConfiguration().WithAllAnalyzersDisabled(); public static readonly RootConfiguration BuiltInConfigurationWithStableAnalyzers = IConfigurationManager.GetBuiltInConfiguration().WithAllAnalyzers().WithAnalyzersDisabled(NonStableAnalyzerRules); @@ -67,9 +68,11 @@ public static class BicepTestConstants public static readonly IServiceProvider EmptyServiceProvider = new Mock(MockBehavior.Loose).Object; - public static readonly IArtifactRegistryProvider RegistryProvider = new DefaultArtifactRegistryProvider(EmptyServiceProvider, FileResolver, FileSystem, ClientFactory, TemplateSpecRepositoryFactory, FeatureProviderFactory, BuiltInOnlyConfigurationManager); + public static IArtifactRegistryProvider CreateRegistryProvider(IServiceProvider services) => + new DefaultArtifactRegistryProvider(services, FileResolver, FileSystem, ClientFactory, TemplateSpecRepositoryFactory, FeatureProviderFactory, BuiltInOnlyConfigurationManager); - public static readonly IModuleDispatcher ModuleDispatcher = new ModuleDispatcher(RegistryProvider, IConfigurationManager.WithStaticConfiguration(BuiltInConfiguration)); + public static IModuleDispatcher CreateModuleDispatcher(IServiceProvider services) => + new ModuleDispatcher(CreateRegistryProvider(services), IConfigurationManager.WithStaticConfiguration(BuiltInConfiguration)); public static readonly NamespaceResolver DefaultNamespaceResolver = NamespaceResolver.Create([ new("az", AzNamespaceType.Create("az", ResourceScope.ResourceGroup, AzNamespaceType.BuiltInTypeProvider, BicepSourceFileKind.BicepFile), null), @@ -77,7 +80,7 @@ public static class BicepTestConstants ]); // By default turns off only problematic analyzers - public static readonly LinterAnalyzer LinterAnalyzer = new(); + public static readonly LinterAnalyzer LinterAnalyzer = new(EmptyServiceProvider); public static IEnvironment EmptyEnvironment = new TestEnvironment(ImmutableDictionary.Empty); diff --git a/src/Bicep.Core.UnitTests/Configuration/BicepConfigSchemaTests.cs b/src/Bicep.Core.UnitTests/Configuration/BicepConfigSchemaTests.cs index 5a2e0433454..ad77c6c36cd 100644 --- a/src/Bicep.Core.UnitTests/Configuration/BicepConfigSchemaTests.cs +++ b/src/Bicep.Core.UnitTests/Configuration/BicepConfigSchemaTests.cs @@ -30,7 +30,7 @@ public class RuleAndSchemaTestDataAttribute : Attribute, ITestDataSource { public IEnumerable GetData(MethodInfo methodInfo) { - var analyzer = new LinterAnalyzer(); + var analyzer = new LinterAnalyzer(BicepTestConstants.EmptyServiceProvider); var ruleSet = analyzer.GetRuleSet().ToArray(); return AllRulesAndSchemasById.Values.Select(value => new object[] { value.Rule.Code, value.Rule, value.Schema }); @@ -72,7 +72,7 @@ private static ImmutableArray AllRules { get { - var linter = new LinterAnalyzer(); + var linter = new LinterAnalyzer(BicepTestConstants.EmptyServiceProvider); var ruleSet = linter.GetRuleSet(); ruleSet.Should().NotBeEmpty(); diff --git a/src/Bicep.Core.UnitTests/Diagnostics/LinterAnalyzerTests.cs b/src/Bicep.Core.UnitTests/Diagnostics/LinterAnalyzerTests.cs index b3139a29cfe..834b27b6861 100644 --- a/src/Bicep.Core.UnitTests/Diagnostics/LinterAnalyzerTests.cs +++ b/src/Bicep.Core.UnitTests/Diagnostics/LinterAnalyzerTests.cs @@ -23,7 +23,7 @@ public class TestDataAttribute : Attribute, ITestDataSource { public IEnumerable GetData(MethodInfo methodInfo) { - var analyzer = new LinterAnalyzer(); + var analyzer = new LinterAnalyzer(BicepTestConstants.EmptyServiceProvider); var ruleSet = analyzer.GetRuleSet().ToArray(); return ruleSet.Select(rule => new object[] { rule }); @@ -40,7 +40,7 @@ public IEnumerable GetData(MethodInfo methodInfo) [TestMethod] public void HasBuiltInRules() { - var linter = new LinterAnalyzer(); + var linter = new LinterAnalyzer(BicepTestConstants.EmptyServiceProvider); linter.GetRuleSet().Should().NotBeEmpty(); } @@ -53,14 +53,14 @@ public void HasBuiltInRules() public void BuiltInRulesExistSanityCheck(string ruleCode) { - var linter = new LinterAnalyzer(); + var linter = new LinterAnalyzer(BicepTestConstants.EmptyServiceProvider); linter.GetRuleSet().Should().Contain(r => r.Code == ruleCode); } [TestMethod] public void AllDefinedRulesAreListedInLinterRulesProvider() { - var linter = new LinterAnalyzer(); + var linter = new LinterAnalyzer(BicepTestConstants.EmptyServiceProvider); var ruleTypes = linter.GetRuleSet().Select(r => r.GetType()).ToArray(); var expectedRuleTypes = typeof(LinterAnalyzer).Assembly @@ -79,7 +79,7 @@ public void AllDefinedRulesAreListedInLinterRulesProvider() [TestMethod] public void AllRulesHaveUniqueDetails() { - var analyzer = new LinterAnalyzer(); + var analyzer = new LinterAnalyzer(BicepTestConstants.EmptyServiceProvider); var ruleSet = analyzer.GetRuleSet().ToArray(); var codeSet = ruleSet.Select(r => r.Code).ToHashSet(); @@ -92,7 +92,7 @@ public void AllRulesHaveUniqueDetails() [TestMethod] public void MostRulesEnabledByDefault() { - var analyzer = new LinterAnalyzer(); + var analyzer = new LinterAnalyzer(BicepTestConstants.EmptyServiceProvider); var ruleSet = analyzer.GetRuleSet().ToArray(); var numberEnabled = ruleSet.Where(r => r.DefaultDiagnosticLevel != DiagnosticLevel.Off).Count(); numberEnabled.Should().BeGreaterThan(ruleSet.Length / 2, "most rules should probably be enabled by default"); @@ -152,7 +152,7 @@ public void TestRuleThrowingException() var semanticModel = compilationResult.Compilation.GetSemanticModel(compilationResult.BicepFile); var throwRule = new LinterThrowsTestRule(); - var test = () => throwRule.Analyze(semanticModel).ToArray(); + var test = () => throwRule.Analyze(semanticModel, BicepTestConstants.EmptyServiceProvider).ToArray(); test.Should().Throw(); } } diff --git a/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/LinterRuleTestsBase.cs b/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/LinterRuleTestsBase.cs index 4ab21e09c5c..3817fb54d65 100644 --- a/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/LinterRuleTestsBase.cs +++ b/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/LinterRuleTestsBase.cs @@ -7,6 +7,7 @@ using Bicep.Core.Configuration; using Bicep.Core.Diagnostics; using Bicep.Core.Extensions; +using Bicep.Core.Semantics; using Bicep.Core.Text; using Bicep.Core.TypeSystem.Providers; using Bicep.Core.UnitTests.Assertions; diff --git a/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/UseRecentApiVersionRuleTests.cs b/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/UseRecentApiVersionRuleTests.cs index c52c3ab9700..c4ed2bac80d 100644 --- a/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/UseRecentApiVersionRuleTests.cs +++ b/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/UseRecentApiVersionRuleTests.cs @@ -818,7 +818,7 @@ private static bool DateIsEqualOrMoreRecentThan(DateOnly dt, DateOnly other) [DataTestMethod] [DynamicData(nameof(GetTestData), DynamicDataSourceType.Method, DynamicDataDisplayNameDeclaringType = typeof(TestData), DynamicDataDisplayName = nameof(TestData.GetDisplayName))] - public void Invariants(TestData data) + public void InvariantsTest(TestData data) { var (allVersions, allowedVersions) = UseRecentApiVersionRule.GetAcceptableApiVersions(RealApiVersionProvider, data.Today, data.MaxAgeInDays, data.ResourceScope, data.FullyQualifiedResourceType); diff --git a/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/UseRecentModuleVersionsRuleTests.cs b/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/UseRecentModuleVersionsRuleTests.cs new file mode 100644 index 00000000000..41ed9775739 --- /dev/null +++ b/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/UseRecentModuleVersionsRuleTests.cs @@ -0,0 +1,280 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Reflection; +using System.Text.RegularExpressions; +using Bicep.Core.Analyzers.Interfaces; +using Bicep.Core.Analyzers.Linter.ApiVersions; +using Bicep.Core.Analyzers.Linter.Rules; +using Bicep.Core.CodeAction; +using Bicep.Core.Configuration; +using Bicep.Core.Diagnostics; +using Bicep.Core.Json; +using Bicep.Core.Parsing; +using Bicep.Core.Registry.PublicRegistry; +using Bicep.Core.Resources; +using Bicep.Core.TypeSystem; +using Bicep.Core.UnitTests.Assertions; +using Bicep.Core.UnitTests.Mock; +using Bicep.Core.UnitTests.Utils; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using static Bicep.Core.UnitTests.Utils.CompilationHelper; + +namespace Bicep.Core.UnitTests.Diagnostics.LinterRuleTests +{ + // More extensive tests in src/Bicep.Cli.IntegrationTests/UseRecentModuleVersionsIntegrationTests.cs + + [TestClass] + public partial class UseRecentModuleVersionsRuleTests : LinterRuleTestsBase + { + private static readonly ServiceBuilder Services = new ServiceBuilder() + .WithRegistration(x => x.AddSingleton( + IConfigurationManager.WithStaticConfiguration( + IConfigurationManager.GetBuiltInConfiguration() + .WithAllAnalyzers()))); + + private static CompilationResult Compile( + string bicep, + string[] availableModules, + string[] availableVersions, // for simplicity, mock returns these same versions for all available modules + string? downloadError = null) + { + var publicRegistryModuleMetadataProvider = StrictMock.Of(); + publicRegistryModuleMetadataProvider.Setup(x => x.GetCachedModules()) + .Returns(availableModules.Select(m => new RegistryModule(m, null, null)).ToArray()); + publicRegistryModuleMetadataProvider.Setup(x => x.GetCachedModuleVersions(It.IsAny())) + .Returns((string module) => + { + return availableModules.Contains(module) ? + availableVersions.Select(v => new RegistryModuleVersion(v, null, null)).ToArray() : + []; + }); + publicRegistryModuleMetadataProvider.Setup(x => x.IsCached) + .Returns(availableModules.Length > 0); + publicRegistryModuleMetadataProvider.Setup(x => x.DownloadError) + .Returns(downloadError); + + var services = Services.WithRegistration(x => x.AddSingleton(publicRegistryModuleMetadataProvider.Object)); + var result = CompilationHelper.Compile(services, [("main.bicep", bicep)]); + return result; + } + + [TestMethod] + public void IfUsingOldVersionThatWeDontKnowAbout_Fail() + { + var result = Compile(""" + module m1 'br/public:avm/res/network/public-ip-address:0.4.0' = { + } + """, + ["avm/res/network/public-ip-address"], + ["1.0.0"] + ); + result.Diagnostics.Where(d => d.Code == UseRecentModuleVersionsRule.Code) + .Should() + .HaveDiagnostics([ + (UseRecentModuleVersionsRule.Code, DiagnosticLevel.Warning, "Use a more recent version of module 'avm/res/network/public-ip-address'. The most recent version is 1.0.0.") + ]); + } + + [TestMethod] + public void IfUsingNewVersionThatWeDontKnowAbout_Passes() + { + // This is the scenario where one or more versions have been published since the cache was updated, and the + // user is using a newer version not in our cache. + // Either + var result = Compile(""" + module m1 'br/public:avm/res/network/public-ip-address:1.4.0' = { + } + """, + ["avm/res/network/public-ip-address"], + ["1.0.0"] + ); + result.Diagnostics.Where(d => d.Code == UseRecentModuleVersionsRule.Code) + .Should() + .NotHaveAnyDiagnostics(); + } + + [TestMethod] + public void UsingNewestVersion_NoFailures() + { + var result = Compile(""" + module m1 'br/public:avm/res/network/public-ip-address:0.4.0' = { + } + """, + ["avm/res/network/public-ip-address"], + ["0.3.0", "0.3.1", "0.4.0"] + ); + result.Diagnostics.Where(d => d.Code == UseRecentModuleVersionsRule.Code) + .Should() + .NotHaveAnyDiagnostics(); + } + + [TestMethod] + public void IfVersionsNotDownloaded_ThenShowExactlyOneFailure() + { + var result = Compile(""" + module m1 'br/public:avm/res/network/public-ip-address:0.4.0' = { + } + module m1 'br/public:avm/res/network/public-ip-address:0.4.0' = { + } + module m1 'br/public:avm/res/network/public-ip-address:0.4.0' = { + } + """, + [], // not yet downloaded + [] + ); + result.Diagnostics.Where(d => d.Code == UseRecentModuleVersionsRule.Code) + .Should() + .HaveDiagnostics([ + // Should only show this warning for the first module we find + ("use-recent-module-versions", DiagnosticLevel.Warning, "Available module versions have not yet been downloaded. If running from the command line, be sure --no-restore is not specified.") + ]); + } + + [TestMethod] + public void IfVersionsDownloadFailed_ThenShowExactlyOneFailure() + { + var result = Compile(""" + module m1 'br/public:avm/res/network/public-ip-address:0.4.0' = { + } + module m1 'br/public:avm/res/network/public-ip-address:0.4.0' = { + } + module m1 'br/public:avm/res/network/public-ip-address:0.4.0' = { + } + """, + [], + [], + "download error" + ); + result.Diagnostics.Where(d => d.Code == UseRecentModuleVersionsRule.Code) + .Should() + .HaveDiagnostics([ + // Should only show this warning for the first module + ("use-recent-module-versions", DiagnosticLevel.Warning, "Could not download available module versions: download error") + ]); + } + + [TestMethod] + public void IgnoreNonPublicModules() + { + var result = Compile(""" + module m1 'br/whatever:avm/res/network/public-ip-address:0.4.0' = { + } + """, + [], + [] + ); + result.Diagnostics.Where(d => d.Code == UseRecentModuleVersionsRule.Code) + .Should() + .NotHaveAnyDiagnostics(); + } + + [TestMethod] + public void HasDownloadError() + { + var result = Compile(""" + module m1 'br/public:avm/res/network/public-ip-address:0.4.0' = { + } + """, + ["avm/res/network/public-ip-address"], + ["1.0.0"], + "My download error" + ); + result.Diagnostics.Where(d => d.Code == UseRecentModuleVersionsRule.Code) + .Should() + .HaveDiagnostics([ + ("use-recent-module-versions", DiagnosticLevel.Warning, "Could not download available module versions: My download error") + ]); + } + + [TestMethod] + public void SingleMinorVersionBehind() + { + var result = Compile(""" + module m1 'br/public:avm/res/network/public-ip-address:0.4.0' = { + } + """, + ["avm/res/network/public-ip-address"], + ["0.3.0", "0.4.0", "0.5.0", "1.0.1"] + ); + result.Diagnostics.Where(d => d.Code == UseRecentModuleVersionsRule.Code) + .Should() + .HaveDiagnostics([ + ("use-recent-module-versions", DiagnosticLevel.Warning, "Use a more recent version of module 'avm/res/network/public-ip-address'. The most recent version is 1.0.1.") + ]); + } + + [TestMethod] + public void SingleMajorVersionBehind() + { + var result = Compile(""" + module m1 'br/public:avm/res/network/public-ip-address:0.4.0' = { + } + """, + ["avm/res/network/public-ip-address"], + ["0.3.0", "0.4.0", "1.0.0"] + ); + result.Diagnostics.Where(d => d.Code == UseRecentModuleVersionsRule.Code) + .Should() + .HaveDiagnostics([ + ("use-recent-module-versions", DiagnosticLevel.Warning, "Use a more recent version of module 'avm/res/network/public-ip-address'. The most recent version is 1.0.0.") + ]); + } + + [TestMethod] + public void SinglePatchVersionBehind() + { + var result = Compile(""" + module m1 'br/public:avm/res/network/public-ip-address:0.4.1' = { + } + """, + ["avm/res/network/public-ip-address"], + ["0.3.0", "0.4.1", "0.4.2", "0.5.0"] + ); + result.Diagnostics.Where(d => d.Code == UseRecentModuleVersionsRule.Code) + .Should() + .HaveDiagnostics([ + ("use-recent-module-versions", DiagnosticLevel.Warning, "Use a more recent version of module 'avm/res/network/public-ip-address'. The most recent version is 0.5.0.") + ]); + } + + [TestMethod] + public void MultiplePatchVersionsBehind() + { + var result = Compile(""" + module m1 'br/public:avm/res/network/public-ip-address:0.4.1' = { + } + """, + ["avm/res/network/public-ip-address"], + ["0.3.0", "0.4.1", "0.4.2", "0.4.5" ] + ); + result.Diagnostics.Where(d => d.Code == UseRecentModuleVersionsRule.Code) + .Should() + .HaveDiagnostics([ + ("use-recent-module-versions", DiagnosticLevel.Warning, "Use a more recent version of module 'avm/res/network/public-ip-address'. The most recent version is 0.4.5.") + ]); + } + + [TestMethod] + public void HasFix() + { + var result = Compile(""" + module m1 'br/public:avm/res/network/public-ip-address:0.4.0' = { + } + """, + ["avm/res/network/public-ip-address"], + ["0.3.0", "0.4.0", "0.5.0", "1.0.1"] + ); + + var diag = (IBicepAnalyerFixableDiagnostic)result.Diagnostics.Where(d => d.Code == UseRecentModuleVersionsRule.Code).First(); + var fixes = diag.Fixes.ToArray(); + fixes.Should().HaveCount(1); + fixes[0].Replacements.Should().HaveCount(1); + fixes[0].Replacements.First().Text.Should().Be("1.0.1"); + fixes[0].Title.Should().Be("Replace with most recent version '1.0.1'"); + } + } +} diff --git a/src/Bicep.Core.UnitTests/IServiceCollectionExtensions.cs b/src/Bicep.Core.UnitTests/IServiceCollectionExtensions.cs index b3e10cc75e4..3624da2207c 100644 --- a/src/Bicep.Core.UnitTests/IServiceCollectionExtensions.cs +++ b/src/Bicep.Core.UnitTests/IServiceCollectionExtensions.cs @@ -8,12 +8,14 @@ using Bicep.Core.FileSystem; using Bicep.Core.Registry; using Bicep.Core.Registry.Auth; +using Bicep.Core.Registry.PublicRegistry; using Bicep.Core.Semantics.Namespaces; using Bicep.Core.TypeSystem.Providers; using Bicep.Core.TypeSystem.Providers.Az; using Bicep.Core.TypeSystem.Types; using Bicep.Core.UnitTests.Configuration; using Bicep.Core.UnitTests.Features; +using Bicep.Core.UnitTests.Mock; using Bicep.Core.UnitTests.Utils; using Bicep.Core.Utils; using Bicep.Core.Workspaces; @@ -28,22 +30,30 @@ namespace Bicep.Core.UnitTests; public static class IServiceCollectionExtensions { - public static IServiceCollection AddBicepCore(this IServiceCollection services) => services - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(TestEnvironment.Create()) - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); + public static IServiceCollection AddBicepCore(this IServiceCollection services) + { + services + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(TestEnvironment.Create()) + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddPublicRegistryModuleMetadataProviderServices() + .AddSingleton(); + + AddMockHttpClient(services, PublicRegistryModuleMetadataClientMock.Create([]).Object); + + return services; + } public static IServiceCollection AddBicepDecompiler(this IServiceCollection services) => services .AddSingleton(); @@ -122,7 +132,7 @@ public static IServiceCollection WithDeploymentHelper(this IServiceCollection se public static IServiceCollection WithEmptyAzResources(this IServiceCollection services) => services.WithAzResources([]); - public static IServiceCollection AddSingletonIfNonNull(this IServiceCollection services, TService? instance) + public static IServiceCollection AddSingletonIfNotNull(this IServiceCollection services, TService? instance) where TService : class { if (instance is not null) @@ -132,4 +142,27 @@ public static IServiceCollection AddSingletonIfNonNull(this IServiceCo return services; } + + public static IServiceCollection AddMockHttpClient(IServiceCollection services, TClient? httpClient) where TClient : class + { + return AddMockHttpClientIfNotNull(services, httpClient); + } + + public static IServiceCollection AddMockHttpClientIfNotNull(IServiceCollection services, TClient? httpClient) where TClient : class + { + if (!typeof(TClient).IsInterface) + { + throw new ArgumentException($"TClient must be an interface type, found: {typeof(TClient).FullName}"); + } + + if (httpClient is { }) + { + services.AddHttpClient(typeof(TClient).FullName!, httpClient => + { + }) + .AddTypedClient(c => httpClient); + } + + return services; + } } diff --git a/src/Bicep.Core.UnitTests/Mock/PublicRegistryModuleMetadataClientMock.cs b/src/Bicep.Core.UnitTests/Mock/PublicRegistryModuleMetadataClientMock.cs new file mode 100644 index 00000000000..8cd737b6951 --- /dev/null +++ b/src/Bicep.Core.UnitTests/Mock/PublicRegistryModuleMetadataClientMock.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Immutable; +using Bicep.Core.Registry.PublicRegistry; +using Bicep.Core.UnitTests.Mock; +using Moq; + +namespace Bicep.Core.UnitTests.Mock; + +public static class PublicRegistryModuleMetadataClientMock +{ + // CONSIDER: Mock HttpClient rather than the typed client + + public static Mock Create(IEnumerable metadata) + { + var mock = StrictMock.Of(); + mock + .Setup(client => client.GetModuleMetadata()) + .ReturnsAsync(() => metadata.ToImmutableArray()); + return mock; + } + + public static Mock CreateToThrow(Exception exception) + { + var mock = StrictMock.Of(); + mock + .Setup(client => client.GetModuleMetadata()) + .ThrowsAsync(exception); + return mock; + } +} diff --git a/src/Bicep.Core.UnitTests/Registry/ArtifactDispatcherTests.cs b/src/Bicep.Core.UnitTests/Registry/ArtifactDispatcherTests.cs index 86546067f06..4dac745a42e 100644 --- a/src/Bicep.Core.UnitTests/Registry/ArtifactDispatcherTests.cs +++ b/src/Bicep.Core.UnitTests/Registry/ArtifactDispatcherTests.cs @@ -75,9 +75,13 @@ public async Task MockRegistries_ModuleLifecycle() { var fail = StrictMock.Of(); fail.Setup(m => m.Scheme).Returns("fail"); + fail.Setup(x => x.OnRestoreArtifacts(It.IsAny())) + .Returns(Task.CompletedTask); var mock = StrictMock.Of(); mock.Setup(m => m.Scheme).Returns("mock"); + mock.Setup(x => x.OnRestoreArtifacts(It.IsAny())) + .Returns(Task.CompletedTask); ErrorBuilderDelegate? @null = null; var configuration = BicepTestConstants.BuiltInConfiguration; @@ -174,6 +178,8 @@ public async Task GetModuleRestoreStatus_ConfigurationChanges_ReturnsCachedStatu }); registryMock.Setup(x => x.IsArtifactRestoreRequired(badReference)) .Returns(true); + registryMock.Setup(x => x.OnRestoreArtifacts(It.IsAny())) + .Returns(Task.CompletedTask); var configManagerMock = StrictMock.Of(); configManagerMock.SetupSequence(m => m.GetConfiguration(badReferenceUri)) diff --git a/src/Bicep.LangServer.UnitTests/Completions/PublicRegistryModuleMetadataProviderTests.cs b/src/Bicep.Core.UnitTests/Registry/PublicRegistry/PublicRegistryModuleMetadataProviderTests.cs similarity index 99% rename from src/Bicep.LangServer.UnitTests/Completions/PublicRegistryModuleMetadataProviderTests.cs rename to src/Bicep.Core.UnitTests/Registry/PublicRegistry/PublicRegistryModuleMetadataProviderTests.cs index dd3a25bdc43..1f301770370 100644 --- a/src/Bicep.LangServer.UnitTests/Completions/PublicRegistryModuleMetadataProviderTests.cs +++ b/src/Bicep.Core.UnitTests/Registry/PublicRegistry/PublicRegistryModuleMetadataProviderTests.cs @@ -4,6 +4,7 @@ using System.Text; using System.Text.Json; using Bicep.Core.Extensions; +using Bicep.Core.Registry.PublicRegistry; using Bicep.Core.UnitTests; using Bicep.LanguageServer.Providers; using FluentAssertions; @@ -11,7 +12,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using RichardSzalay.MockHttp; -namespace Bicep.LangServer.UnitTests.Completions +namespace Bicep.Core.UnitTests.Registry.PublicRegistry { [TestClass] public class PublicRegistryModuleMetadataProviderTests @@ -1173,7 +1174,7 @@ public async Task GetModules_Count_SanityCheck() { PublicRegistryModuleMetadataProvider provider = new(GetServiceProvider()); (await provider.TryUpdateCacheAsync()).Should().BeTrue(); - var modules = await provider.GetModules(); + var modules = provider.GetCachedModules(); modules.Should().HaveCount(50); } diff --git a/src/Bicep.Core.UnitTests/Utils/OciRegistryHelper.cs b/src/Bicep.Core.UnitTests/Utils/OciRegistryHelper.cs index 6a22d092ec8..81f76cf5280 100644 --- a/src/Bicep.Core.UnitTests/Utils/OciRegistryHelper.cs +++ b/src/Bicep.Core.UnitTests/Utils/OciRegistryHelper.cs @@ -7,6 +7,7 @@ using Bicep.Core.Modules; using Bicep.Core.Registry; using Bicep.Core.Registry.Oci; +using Bicep.Core.Registry.PublicRegistry; using Bicep.Core.UnitTests.Mock; using Bicep.Core.UnitTests.Registry; using FluentAssertions; @@ -89,6 +90,7 @@ public static (OciArtifactRegistry, MockRegistryBlobClient) CreateModuleRegistry clientFactory.Object, featureProvider, BicepTestConstants.BuiltInConfiguration, + StrictMock.Of().Object, parentModuleUri); return (registry, blobClient); diff --git a/src/Bicep.Core.UnitTests/Utils/RegistryHelper.cs b/src/Bicep.Core.UnitTests/Utils/RegistryHelper.cs index 1c11c756b3d..e44afcc81dd 100644 --- a/src/Bicep.Core.UnitTests/Utils/RegistryHelper.cs +++ b/src/Bicep.Core.UnitTests/Utils/RegistryHelper.cs @@ -33,7 +33,6 @@ public static (IContainerRegistryClientFactory factoryMock, ImmutableDictionary< foreach (var (registryHost, repository) in clients) { containerRegistryFactoryBuilder.RegisterMockRepositoryBlobClient(registryHost, repository); - } return containerRegistryFactoryBuilder.Build(); diff --git a/src/Bicep.Core.UnitTests/Utils/ServiceBuilderExtensions.cs b/src/Bicep.Core.UnitTests/Utils/ServiceBuilderExtensions.cs index ed70bdfaf51..768ad203555 100644 --- a/src/Bicep.Core.UnitTests/Utils/ServiceBuilderExtensions.cs +++ b/src/Bicep.Core.UnitTests/Utils/ServiceBuilderExtensions.cs @@ -7,6 +7,7 @@ using Bicep.Core.Features; using Bicep.Core.FileSystem; using Bicep.Core.Registry; +using Bicep.Core.Registry.PublicRegistry; using Bicep.Core.Semantics; using Bicep.Core.Semantics.Namespaces; using Bicep.Core.TypeSystem.Providers; diff --git a/src/Bicep.Core.UnitTests/packages.lock.json b/src/Bicep.Core.UnitTests/packages.lock.json index 6e0b932bfe8..9701fc63f87 100644 --- a/src/Bicep.Core.UnitTests/packages.lock.json +++ b/src/Bicep.Core.UnitTests/packages.lock.json @@ -100,6 +100,12 @@ "Newtonsoft.Json": "13.0.3" } }, + "RichardSzalay.MockHttp": { + "type": "Direct", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "QwnauYiaywp65QKFnP+wvgiQ2D8Pv888qB2dyfd7MSVDF06sIvxqASenk+RxsWybyyt+Hu1Y251wQxpHTv3UYg==" + }, "System.IO.Abstractions.TestingHelpers": { "type": "Direct", "requested": "[21.0.2, )", @@ -875,6 +881,11 @@ "resolved": "4.4.0", "contentHash": "YhEdSQUsTx+C8m8Bw7ar5/VesXvCFMItyZF7G1AUY+OM0VPZUOeAVpJ4Wl6fydBGUYZxojTDR3I6Bj/+BPkJNA==" }, + "Semver": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "4vYo1zqn6pJ1YrhjuhuOSbIIm0CpM47grbpTJ5ABjOlfGt/EhMEM9ed4MRK5Jr6gVnntWDqOUzGeUJp68PZGjw==" + }, "SharpYaml": { "type": "Transitive", "resolved": "2.1.1", @@ -1459,9 +1470,11 @@ "Microsoft.Extensions.Configuration.Binder": "[8.0.1, )", "Microsoft.Extensions.Configuration.Json": "[8.0.0, )", "Microsoft.Extensions.DependencyInjection": "[8.0.0, )", + "Microsoft.Extensions.Http": "[8.0.0, )", "Microsoft.Graph.Bicep.Types": "[0.1.6-preview, )", "Microsoft.PowerPlatform.ResourceStack": "[7.0.0.2007, )", "Newtonsoft.Json": "[13.0.3, )", + "Semver": "[2.3.0, )", "SharpYaml": "[2.1.1, )", "System.IO.Abstractions": "[21.0.2, )", "System.Private.Uri": "[4.3.2, )" diff --git a/src/Bicep.Core/Analyzers/Interfaces/IBicepAnalyzerRule.cs b/src/Bicep.Core/Analyzers/Interfaces/IBicepAnalyzerRule.cs index 126ecad5772..bea8661047e 100644 --- a/src/Bicep.Core/Analyzers/Interfaces/IBicepAnalyzerRule.cs +++ b/src/Bicep.Core/Analyzers/Interfaces/IBicepAnalyzerRule.cs @@ -3,13 +3,14 @@ using Bicep.Core.Diagnostics; using Bicep.Core.Semantics; +using Microsoft.Extensions.DependencyInjection; namespace Bicep.Core.Analyzers.Interfaces { /// /// Implementing IBicepAnalyzer Rule requires /// the implementing class to have a parameterless - /// constructor which can be discoverd through + /// constructor which can be discovered through /// reflection /// /// Do not rename or move this type to a different namespace. @@ -25,9 +26,8 @@ public interface IBicepAnalyzerRule DiagnosticLevel DefaultDiagnosticLevel { get; } DiagnosticStyling DiagnosticStyling { get; } - Uri? Uri { get; } - IEnumerable Analyze(SemanticModel model); + IEnumerable Analyze(SemanticModel model, IServiceProvider serviceProvider); } } diff --git a/src/Bicep.Core/Analyzers/Linter/ILinterRulesProvider.cs b/src/Bicep.Core/Analyzers/Linter/ILinterRulesProvider.cs index 8badb6f1cdf..55ac80fdc04 100644 --- a/src/Bicep.Core/Analyzers/Linter/ILinterRulesProvider.cs +++ b/src/Bicep.Core/Analyzers/Linter/ILinterRulesProvider.cs @@ -2,12 +2,13 @@ // Licensed under the MIT License. using System.Collections.Immutable; +using Bicep.Core.Diagnostics; namespace Bicep.Core.Analyzers.Linter { public interface ILinterRulesProvider { - ImmutableDictionary GetLinterRules(); + ImmutableDictionary GetLinterRules(); IEnumerable GetRuleTypes(); } diff --git a/src/Bicep.Core/Analyzers/Linter/LinterAnalyzer.cs b/src/Bicep.Core/Analyzers/Linter/LinterAnalyzer.cs index ada7538431e..83fb1093007 100644 --- a/src/Bicep.Core/Analyzers/Linter/LinterAnalyzer.cs +++ b/src/Bicep.Core/Analyzers/Linter/LinterAnalyzer.cs @@ -7,7 +7,9 @@ using Bicep.Core.Configuration; using Bicep.Core.Diagnostics; using Bicep.Core.Parsing; +using Bicep.Core.Registry.PublicRegistry; using Bicep.Core.Semantics; +using Microsoft.Extensions.DependencyInjection; namespace Bicep.Core.Analyzers.Linter { @@ -23,10 +25,13 @@ public class LinterAnalyzer : IBicepAnalyzer private readonly ImmutableArray ruleSet; - public LinterAnalyzer() + private readonly IServiceProvider serviceProvider; + + public LinterAnalyzer(IServiceProvider serviceProvider) { this.linterRulesProvider = new LinterRulesProvider(); this.ruleSet = CreateLinterRules(); + this.serviceProvider = serviceProvider; } private bool LinterEnabled(SemanticModel model) => model.Configuration.Analyzers.GetValue(LinterEnabledSetting, false); // defaults to true in base bicepconfig.json file @@ -63,7 +68,7 @@ public IEnumerable Analyze(SemanticModel semanticModel) diagnostics.Add(GetConfigurationDiagnostic(semanticModel)); } - diagnostics.AddRange(ruleSet.SelectMany(r => r.Analyze(semanticModel))); + diagnostics.AddRange(ruleSet.SelectMany(r => r.Analyze(semanticModel, this.serviceProvider))); } else { diff --git a/src/Bicep.Core/Analyzers/Linter/LinterRuleBase.cs b/src/Bicep.Core/Analyzers/Linter/LinterRuleBase.cs index eee9a2433f1..86662151476 100644 --- a/src/Bicep.Core/Analyzers/Linter/LinterRuleBase.cs +++ b/src/Bicep.Core/Analyzers/Linter/LinterRuleBase.cs @@ -66,24 +66,35 @@ public LinterRuleBase( /// /// /// - public string GetMessage(params object[] values) - => (values.Any() ? FormatMessage(values) : this.Description); + public string GetMessage(params object[] values) => FormatMessage(values); - public IEnumerable Analyze(SemanticModel model) + public IEnumerable Analyze(SemanticModel model, IServiceProvider serviceProvider) { if (GetDiagnosticLevel(model) == DiagnosticLevel.Off) { return []; } - return AnalyzeInternal(model, GetDiagnosticLevel(model)); + return AnalyzeInternal(model, serviceProvider, GetDiagnosticLevel(model)); } /// /// Abstract method each rule must implement to provide analyzer /// diagnostics through the Analyze API /// - public abstract IEnumerable AnalyzeInternal(SemanticModel model, DiagnosticLevel diagnosticLevel); + public virtual IEnumerable AnalyzeInternal(SemanticModel model, IServiceProvider serviceProvider, DiagnosticLevel diagnosticLevel) + { + return AnalyzeInternal(model, diagnosticLevel); + } + + /// + /// Abstract method each rule must implement to provide analyzer + /// diagnostics through the Analyze API + /// + public virtual IEnumerable AnalyzeInternal(SemanticModel model, DiagnosticLevel diagnosticLevel) + { + throw new NotImplementedException($"{this.GetType().Name} must implement one of the overloads of {nameof(AnalyzeInternal)}"); + } protected DiagnosticLevel GetDiagnosticLevel(SemanticModel model) => GetDiagnosticLevel(model.Configuration.Analyzers); diff --git a/src/Bicep.Core/Analyzers/Linter/LinterRulesProvider.cs b/src/Bicep.Core/Analyzers/Linter/LinterRulesProvider.cs index 3d446294e83..ae9d0c5988b 100644 --- a/src/Bicep.Core/Analyzers/Linter/LinterRulesProvider.cs +++ b/src/Bicep.Core/Analyzers/Linter/LinterRulesProvider.cs @@ -4,23 +4,24 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using Bicep.Core.Analyzers.Interfaces; +using Bicep.Core.Diagnostics; using Bicep.RoslynAnalyzers; namespace Bicep.Core.Analyzers.Linter { public partial class LinterRulesProvider : ILinterRulesProvider { - private readonly Lazy> linterRulesLazy; + private readonly Lazy> linterRulesLazy; public LinterRulesProvider() { - this.linterRulesLazy = new Lazy>(() => GetLinterRulesInternal().ToImmutableDictionary()); + this.linterRulesLazy = new (() => GetLinterRulesInternal().ToImmutableDictionary()); } [UnconditionalSuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "List of types comes from a source analyzer")] - private Dictionary GetLinterRulesInternal() + private Dictionary GetLinterRulesInternal() { - var rules = new Dictionary(); + var rules = new Dictionary(); var ruleTypes = GetRuleTypes(); foreach (var ruleType in ruleTypes) @@ -28,8 +29,7 @@ private Dictionary GetLinterRulesInternal() IBicepAnalyzerRule? rule = Activator.CreateInstance(ruleType) as IBicepAnalyzerRule; if (rule is not null) { - var code = rule.Code; - rules.Add(code, $"core.rules.{code}.level"); + rules.Add(rule.Code, ($"core.rules.{rule.Code}.level", rule.DefaultDiagnosticLevel)); } } @@ -39,6 +39,6 @@ private Dictionary GetLinterRulesInternal() [LinterRuleTypesGenerator] public partial IEnumerable GetRuleTypes(); - public ImmutableDictionary GetLinterRules() => linterRulesLazy.Value; + public ImmutableDictionary GetLinterRules() => linterRulesLazy.Value; } } diff --git a/src/Bicep.Core/Analyzers/Linter/Rules/NoHardcodedEnvironmentUrlsRule.cs b/src/Bicep.Core/Analyzers/Linter/Rules/NoHardcodedEnvironmentUrlsRule.cs index cce71a5a503..39b183e8ff9 100644 --- a/src/Bicep.Core/Analyzers/Linter/Rules/NoHardcodedEnvironmentUrlsRule.cs +++ b/src/Bicep.Core/Analyzers/Linter/Rules/NoHardcodedEnvironmentUrlsRule.cs @@ -14,6 +14,7 @@ public sealed class NoHardcodedEnvironmentUrlsRule : LinterRuleBase { public new const string Code = "no-hardcoded-env-urls"; + // Configuration keys for bicepconfig.json public readonly string DisallowedHostsKey = "disallowedHosts"; public readonly string ExcludedHostsKey = "excludedHosts"; diff --git a/src/Bicep.Core/Analyzers/Linter/Rules/UseRecentApiVersionRule.cs b/src/Bicep.Core/Analyzers/Linter/Rules/UseRecentApiVersionRule.cs index fd0d8693b76..c2a5552c47b 100644 --- a/src/Bicep.Core/Analyzers/Linter/Rules/UseRecentApiVersionRule.cs +++ b/src/Bicep.Core/Analyzers/Linter/Rules/UseRecentApiVersionRule.cs @@ -74,7 +74,7 @@ override public IEnumerable AnalyzeInternal(SemanticModel model, Di yield return CreateDiagnosticForSpan( diagnosticLevel, new TextSpan(), - $"{UseRecentApiVersionRule.Code}: Configuration value for {MaxAgeInDaysKey} is not valid: {maxAgeInDays}", + $"{Code}: Configuration value for {MaxAgeInDaysKey} is not valid: {maxAgeInDays}", Array.Empty()); maxAgeInDays = DefaultMaxAgeInDays; @@ -475,7 +475,7 @@ public static (AzureResourceApiVersion[] allApiVersions, AzureResourceApiVersion private static TextSpan? GetReplacementSpan(ResourceSymbol resourceSymbol, string apiVersion) { if (resourceSymbol.DeclaringResource.TypeString is StringSyntax typeString && - typeString.StringTokens.First() is Token token) + typeString.StringTokens.FirstOrDefault() is Token token) { int replacementSpanStart = token.Span.Position + token.Text.IndexOf(apiVersion); diff --git a/src/Bicep.Core/Analyzers/Linter/Rules/UseRecentModuleVersionsRule.cs b/src/Bicep.Core/Analyzers/Linter/Rules/UseRecentModuleVersionsRule.cs new file mode 100644 index 00000000000..e4729bb8371 --- /dev/null +++ b/src/Bicep.Core/Analyzers/Linter/Rules/UseRecentModuleVersionsRule.cs @@ -0,0 +1,220 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Data; +using System.Diagnostics; +using System.Web.Services.Description; +using Bicep.Core.Analyzers.Linter.ApiVersions; +using Bicep.Core.CodeAction; +using Bicep.Core.Diagnostics; +using Bicep.Core.Navigation; +using Bicep.Core.Parsing; +using Bicep.Core.Registry.Oci; +using Bicep.Core.Registry.PublicRegistry; +using Bicep.Core.Resources; +using Bicep.Core.Semantics; +using Bicep.Core.Syntax; +using Bicep.Core.Text; +using Bicep.Core.TypeSystem; +using Bicep.Core.Workspaces; +using Json.Patch; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.WindowsAzure.ResourceStack.Common.Extensions; +using Semver; +using Semver.Comparers; + +namespace Bicep.Core.Analyzers.Linter.Rules +{ + public sealed class UseRecentModuleVersionsRule : LinterRuleBase + { + public new const string Code = "use-recent-module-versions"; + + public record Failure( + TextSpan Span, + string Message, + string[] AcceptableVersions, + CodeFix[] Fixes + ); + + public UseRecentModuleVersionsRule() : base( + code: Code, + description: CoreResources.UseRecentModuleVersionsRule_Description, + LinterRuleCategory.BestPractice, + docUri: new Uri($"https://aka.ms/bicep/linter/{Code}"), + overrideCategoryDefaultDiagnosticLevel: DiagnosticLevel.Off // many users prefer this to be off by default due to the noise + ) + { + } + + public override string FormatMessage(params object[] values) + { + var message = (string)values[0]; + var acceptableVersions = (string[])values[1]; + + var acceptableVersionsString = string.Join(", ", acceptableVersions); + return message + + (acceptableVersionsString.Length > 0 + // Currently we only support using the most recent version + ? " " + string.Format(CoreResources.UseRecentModulesVersionRule_MostRecentVersion, acceptableVersionsString) + : ""); + } + + override public IEnumerable AnalyzeInternal(SemanticModel model, IServiceProvider serviceProvider, DiagnosticLevel diagnosticLevel) + { + return GetFailures(model, serviceProvider, diagnosticLevel) + .Select(f => CreateFixableDiagnosticForSpan(diagnosticLevel, f.Span, f.Fixes, f.Message, f.AcceptableVersions)); + } + + private static IEnumerable GetFailures(SemanticModel model, IServiceProvider serviceProvider, DiagnosticLevel diagnosticLevel) + { + var publicRegistryModuleMetadataProvider = serviceProvider.GetRequiredService(); + var hasShownDownloadWarning = false; + + foreach (var (syntax, artifactResolutionInfo) in model.Compilation.SourceFileGrouping.ArtifactLookup + .Where(entry => entry.Value.Origin == model.SourceFile + && entry.Value.Syntax is ModuleDeclarationSyntax moduleSyntax)) + { + if (syntax is ModuleDeclarationSyntax moduleSyntax) + { + var errorSpan = syntax.SourceSyntax.Span; + + if (artifactResolutionInfo.Reference is IOciArtifactReference ociReference + && ociReference.Registry.Equals(LanguageConstants.BicepPublicMcrRegistry, StringComparison.Ordinal) + && ociReference.Tag is string tag) + { + if (TryGetBicepModuleName(ociReference) is not string publicModulePath) + { + continue; + } + + if (publicRegistryModuleMetadataProvider.DownloadError is string downloadError) + { + if (!hasShownDownloadWarning) + { + hasShownDownloadWarning = true; + yield return new Failure(errorSpan, string.Format(CoreResources.UseRecentModulesVersionRule_CouldNotDownload, downloadError), [], []); + } + continue; + } + else if (!publicRegistryModuleMetadataProvider.IsCached) + { + if (!hasShownDownloadWarning) + { + hasShownDownloadWarning = true; + yield return new Failure(errorSpan, CoreResources.UseRecentModulesVersionRule_NotCached, [], []); + } + continue; + } + + foreach (var failure in AnalyzeBicepModule(publicRegistryModuleMetadataProvider, moduleSyntax, errorSpan, tag, publicModulePath)) + { + yield return failure; + } + } + } + } + yield break; + } + + private static IEnumerable AnalyzeBicepModule(IPublicRegistryModuleMetadataProvider publicRegistryModuleMetadataProvider, ModuleDeclarationSyntax moduleSyntax, TextSpan errorSpan, string tag, string publicModulePath) + { + var availableVersions = publicRegistryModuleMetadataProvider.GetCachedModuleVersions(publicModulePath) + .Select(v => v.Version) + .ToArray(); + if (availableVersions.Length == 0) + { + // If the module doesn't exist, we assume the compiler will flag as an error, no need for us to show anything in the linter. Or else + // the cache is not up to date, again we ignore until the cache is updated. + yield break; + } + + var moreRecentVersions = GetMoreRecentModuleVersions(availableVersions, publicModulePath, tag); + if (moreRecentVersions.Length > 0) + { + var mostRecentVersion = moreRecentVersions[0]; + var replacementSpan = TryGetReplacementSpan(moduleSyntax, tag); + if (replacementSpan.HasValue) // if not found, should still fail, just not have a fix + { + yield return CreateFailureWithFix( + errorSpan, + replacementSpan.Value, + string.Format(CoreResources.UseRecentModuleVersionRule_ErrorMessageFormat, publicModulePath), + [mostRecentVersion]); + } + } + } + + private static string? TryGetBicepModuleName(IOciArtifactReference ociReference) + { + // IPublicRegistryModuleMetadataProvider does not return the "bicep/" that prefixes all + // public registry module paths (it's embedded in the default "bicep" alias path), + // so we need to remove it here. + const string bicepPrefix = "bicep/"; + var repoPath = ociReference.Repository; + if (!repoPath.StartsWith("bicep/", StringComparison.Ordinal)) + { + return null; + } + + return repoPath.Substring(bicepPrefix.Length); + } + + public static string[] GetMoreRecentModuleVersions(string[] availableVersions, string modulePath, string referencedVersion) + { + if (!SemVersion.TryParse(referencedVersion, SemVersionStyles.Strict, out SemVersion requestedSemver)) + { + // Invalid semantic version + return []; + } + + if (!availableVersions.Any()) + { + // The module name is not recognized. + return []; + } + + var availableParsedVersions = availableVersions + .Select(v => (version: v, semVersion: SemVersion.Parse(v, SemVersionStyles.Strict))); + + return availableParsedVersions.Where(v => v.semVersion.ComparePrecedenceTo(requestedSemver) > 0) + .OrderByDescending(v => v.semVersion, SemVersion.PrecedenceComparer) + .Select(v => v.version) + .ToArray(); + } + + // Find the portion of the module/path:version string that corresponds to the module version, + // i.e., the span of the text that should be replaced for a fix + private static TextSpan? TryGetReplacementSpan(ModuleDeclarationSyntax syntax, string apiVersion) + { + if (syntax.Path is StringSyntax pathString && + pathString.StringTokens.FirstOrDefault() is Token token) + { + int replacementSpanStart = token.Span.Position + token.Text.IndexOf(apiVersion); + + return new TextSpan(replacementSpanStart, apiVersion.Length); + } + + return null; + } + + private static Failure CreateFailureWithFix(TextSpan errorSpan, TextSpan replacementSpan, string message, string[] acceptableVersionsSorted) + { + CodeFix? fix = null; + + if (!replacementSpan.IsNil) + { + // Choose the most recent for the suggested auto-fix + var preferredVersion = acceptableVersionsSorted[0]; + var codeReplacement = new CodeReplacement(replacementSpan, preferredVersion); + + fix = new CodeFix( //asfdg run resx generator + string.Format(CoreResources.UseRecentModuleVersionRule_Fix_ReplaceWithMostRecent, preferredVersion), + isPreferred: true, + CodeFixKind.QuickFix, + codeReplacement); + } + + return new Failure(errorSpan, message, acceptableVersionsSorted, fix is null ? [] : [fix]); + } + } +} diff --git a/src/Bicep.Core/Bicep.Core.csproj b/src/Bicep.Core/Bicep.Core.csproj index 1218103c646..01ec8601a6e 100644 --- a/src/Bicep.Core/Bicep.Core.csproj +++ b/src/Bicep.Core/Bicep.Core.csproj @@ -37,6 +37,7 @@ + @@ -48,6 +49,7 @@ + diff --git a/src/Bicep.Core/Configuration/AnalyzersConfiguration.cs b/src/Bicep.Core/Configuration/AnalyzersConfiguration.cs index 1b0202342a4..2ecb48f4f40 100644 --- a/src/Bicep.Core/Configuration/AnalyzersConfiguration.cs +++ b/src/Bicep.Core/Configuration/AnalyzersConfiguration.cs @@ -11,6 +11,8 @@ public class AnalyzersConfiguration : ConfigurationSection { public AnalyzersConfiguration(JsonElement data) : base(data) { } + public AnalyzersConfiguration(string json) : base(JsonElementFactory.CreateElement(json)) { } + public static AnalyzersConfiguration Empty => CreateEmptyAnalyzersConfiguration(); public T GetValue(string path, T defaultValue) diff --git a/src/Bicep.Core/Configuration/AnalyzersConfigurationExtensions.cs b/src/Bicep.Core/Configuration/AnalyzersConfigurationExtensions.cs index 2d706128b90..4daa634b060 100644 --- a/src/Bicep.Core/Configuration/AnalyzersConfigurationExtensions.cs +++ b/src/Bicep.Core/Configuration/AnalyzersConfigurationExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Azure.ResourceManager.Resources.Models; using Bicep.Core.Analyzers.Linter; namespace Bicep.Core.Configuration @@ -24,9 +25,12 @@ public static AnalyzersConfiguration WithAnalyzersDisabled(this AnalyzersConfigu public static AnalyzersConfiguration WithAllAnalyzers(this AnalyzersConfiguration analyzersConfiguration) { var config = analyzersConfiguration; - foreach (string code in new LinterAnalyzer().GetRuleSet().Where(r => r.DefaultDiagnosticLevel == Diagnostics.DiagnosticLevel.Off).Select(r => r.Code)) + foreach (var (code, ruleInfo) in new LinterRulesProvider().GetLinterRules()) { - config = config.SetValue($"core.rules.{code}.level", "warning"); + if (ruleInfo.defaultDiagnosticLevel == Diagnostics.DiagnosticLevel.Off) + { + config = config.SetValue($"core.rules.{code}.level", "warning"); + } } return config; diff --git a/src/Bicep.Core/CoreResources.Designer.cs b/src/Bicep.Core/CoreResources.Designer.cs index be6814707b9..4918dc6fb8d 100644 --- a/src/Bicep.Core/CoreResources.Designer.cs +++ b/src/Bicep.Core/CoreResources.Designer.cs @@ -1,6 +1,7 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This code was generated by a tool. +// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -13,10 +14,12 @@ namespace Bicep.Core { /// /// A strongly-typed resource class, for looking up localized strings, etc. - /// This class was generated by MSBuild using the GenerateResource task. - /// To add or remove a member, edit your .resx file then rerun MSBuild. /// - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Build.Tasks.StronglyTypedResourceBuilder", "15.1.0.0")] + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class CoreResources { @@ -848,18 +851,6 @@ internal static string UseParentPropertyRule_CodeFix { return ResourceManager.GetString("UseParentPropertyRule_CodeFix", resourceCulture); } } - - internal static string UseSecureValueForSecureInputsRule_Description { - get { - return ResourceManager.GetString("UseSecureValueForSecureInputsRule_Description", resourceCulture); - } - } - - internal static string UseSecureValueForSecureInputsRule_MessageFormat { - get { - return ResourceManager.GetString("UseSecureValueForSecureInputsRule_MessageFormat", resourceCulture); - } - } /// /// Looks up a localized string similar to Use the parent property instead of formatting child resource names with '/' characters.. @@ -969,6 +960,60 @@ internal static string UseRecentApiVersionRule_UnknownVersion { } } + /// + /// Looks up a localized string similar to Could not download available module versions: {0}. + /// + internal static string UseRecentModulesVersionRule_CouldNotDownload { + get { + return ResourceManager.GetString("UseRecentModulesVersionRule_CouldNotDownload", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The most recent version is {0}.. + /// + internal static string UseRecentModulesVersionRule_MostRecentVersion { + get { + return ResourceManager.GetString("UseRecentModulesVersionRule_MostRecentVersion", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Available module versions have not yet been downloaded. If running from the command line, be sure --no-restore is not specified.. + /// + internal static string UseRecentModulesVersionRule_NotCached { + get { + return ResourceManager.GetString("UseRecentModulesVersionRule_NotCached", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use a more recent version of module '{0}'.. + /// + internal static string UseRecentModuleVersionRule_ErrorMessageFormat { + get { + return ResourceManager.GetString("UseRecentModuleVersionRule_ErrorMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Replace with most recent version '{0}'. + /// + internal static string UseRecentModuleVersionRule_Fix_ReplaceWithMostRecent { + get { + return ResourceManager.GetString("UseRecentModuleVersionRule_Fix_ReplaceWithMostRecent", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use recent module versions. + /// + internal static string UseRecentModuleVersionsRule_Description { + get { + return ResourceManager.GetString("UseRecentModuleVersionsRule_Description", resourceCulture); + } + } + /// /// Looks up a localized string similar to Properties representing a resource ID must be generated appropriately.. /// @@ -1023,6 +1068,24 @@ internal static string UseResourceSymbolReferenceRule_MessageFormat { } } + /// + /// Looks up a localized string similar to Resource properties expecting secure input should be assigned secure values.. + /// + internal static string UseSecureValueForSecureInputsRule_Description { + get { + return ResourceManager.GetString("UseSecureValueForSecureInputsRule_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property path "{0}" for resources of type "{1}" should be assigned a secure value.. + /// + internal static string UseSecureValueForSecureInputsRule_MessageFormat { + get { + return ResourceManager.GetString("UseSecureValueForSecureInputsRule_MessageFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to Resource identifiers should be reproducible outside of their initial deployment context. . /// diff --git a/src/Bicep.Core/CoreResources.resx b/src/Bicep.Core/CoreResources.resx index 3a19b6c9979..67c3c4ebdeb 100644 --- a/src/Bicep.Core/CoreResources.resx +++ b/src/Bicep.Core/CoreResources.resx @@ -396,6 +396,28 @@ Resource identifiers should be reproducible outside of their initial deployment context. Resource {0}'s '{1}' identifier is potentially nondeterministic due to its use of the '{2}' function ({3}). {0} is the symbolic name of the resource. {1} is the name of the identifier property. {2} is the name of the nondeterministic function used. {3} is the symbol dereference path by which the nondeterministic function call is included. + + Use recent module versions + + + The most recent version is {0}. + {0}: version number, e.g. 1.0.0 + + + Could not download available module versions: {0} + {0}: error message + + + Available module versions have not yet been downloaded. If running from the command line, be sure --no-restore is not specified. + + + Use a more recent version of module '{0}'. + {0} = module name (programmatic) + + + Replace with most recent version '{0}' + {0} a programmatic module version, e.g. "1.2.3" + Use recent API versions diff --git a/src/Bicep.Core/Registry/ArtifactRegistry.cs b/src/Bicep.Core/Registry/ArtifactRegistry.cs index 70b6f206401..b90b5060118 100644 --- a/src/Bicep.Core/Registry/ArtifactRegistry.cs +++ b/src/Bicep.Core/Registry/ArtifactRegistry.cs @@ -24,6 +24,11 @@ public RegistryCapabilities GetCapabilities(ArtifactType artifactType, ArtifactR public abstract Task PublishProvider(T reference, ProviderPackage provider); + public virtual Task OnRestoreArtifacts(bool forceRestore) + { + return Task.CompletedTask; + } + public abstract Task> RestoreArtifacts(IEnumerable references); public abstract Task> InvalidateArtifactsCache(IEnumerable references); diff --git a/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs b/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs index e52eabf26d8..2205ab81353 100644 --- a/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs +++ b/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs @@ -6,6 +6,7 @@ using Bicep.Core.Configuration; using Bicep.Core.Features; using Bicep.Core.FileSystem; +using Bicep.Core.Registry.PublicRegistry; using Microsoft.Extensions.DependencyInjection; namespace Bicep.Core.Registry @@ -45,7 +46,7 @@ public ImmutableArray Registries(Uri templateUri) // Using IServiceProvider instead of constructor injection due to a dependency cycle builder.Add(new LocalModuleRegistry(fileResolver, fileSystem, features, templateUri)); - builder.Add(new OciArtifactRegistry(this.fileResolver, this.fileSystem, this.clientFactory, features, configuration, templateUri)); + builder.Add(new OciArtifactRegistry(this.fileResolver, this.fileSystem, this.clientFactory, features, configuration, serviceProvider.GetRequiredService(), templateUri)); builder.Add(new TemplateSpecModuleRegistry(this.fileResolver, this.fileSystem, this.templateSpecRepositoryFactory, features, configuration, templateUri)); return builder.ToImmutableArray(); diff --git a/src/Bicep.Core/Registry/IArtifactRegistry.cs b/src/Bicep.Core/Registry/IArtifactRegistry.cs index 594036d800d..88765f80f57 100644 --- a/src/Bicep.Core/Registry/IArtifactRegistry.cs +++ b/src/Bicep.Core/Registry/IArtifactRegistry.cs @@ -58,6 +58,12 @@ public interface IArtifactRegistry /// module references Task> RestoreArtifacts(IEnumerable references); + /// + /// Called when time to restore artifacts, even if all artifacts are already restored. Allows the registry provider + /// an opportunity to deal with registry tasks that are not specific to a specific artifact or version. + /// + Task OnRestoreArtifacts(bool forceRestore); + /// /// Invalidate the specified cached modules from the registry. /// Returns a mapping of module references to error builders for modules that failed to be invalidated. diff --git a/src/Bicep.Core/Registry/ModuleDispatcher.cs b/src/Bicep.Core/Registry/ModuleDispatcher.cs index c3ba87a4496..6064453510a 100644 --- a/src/Bicep.Core/Registry/ModuleDispatcher.cs +++ b/src/Bicep.Core/Registry/ModuleDispatcher.cs @@ -5,15 +5,18 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; using Bicep.Core.Configuration; using Bicep.Core.Diagnostics; using Bicep.Core.Modules; using Bicep.Core.Navigation; +using Bicep.Core.Registry.PublicRegistry; using Bicep.Core.Semantics; using Bicep.Core.SourceCode; using Bicep.Core.Syntax; using Bicep.Core.TypeSystem.Providers; using Bicep.Core.Utils; +using Microsoft.Extensions.DependencyInjection; namespace Bicep.Core.Registry { @@ -183,6 +186,21 @@ public async Task RestoreArtifacts(IEnumerable referenc // many module declarations can point to the same module var uniqueReferences = references.Distinct().ToArray(); + // Call OnRestoreArtifacts on each registry provider. Can (currently at least) be done in parallel to restore. + var allRegistries = registryProvider.Registries(new Uri("file:///no-parent-file-is-available.bicep")); + var onRestoreArtifactsTasks = new List(); + foreach (var registry in allRegistries) + { + try + { + onRestoreArtifactsTasks.Add(registry.OnRestoreArtifacts(forceRestore)); + } + catch (Exception ex) + { + Trace.WriteLine($"{nameof(IArtifactRegistry.OnRestoreArtifacts)} failed: {ex.Message}"); + } + } + if (!forceRestore && uniqueReferences.All(module => this.GetArtifactRestoreStatus(module, out _) == ArtifactRestoreStatus.Succeeded)) { @@ -215,6 +233,15 @@ public async Task RestoreArtifacts(IEnumerable referenc { this.SetRestoreFailure(failedReference, configurationManager.GetConfiguration(failedReference.ParentModuleUri), failureBuilder); } + + try + { + await Task.WhenAll(onRestoreArtifactsTasks); + } + catch (Exception ex) + { + Trace.WriteLine($"{nameof(IArtifactRegistry.OnRestoreArtifacts)} failed: {ex.Message}"); + } } return true; diff --git a/src/Bicep.Core/Registry/OciArtifactRegistry.cs b/src/Bicep.Core/Registry/OciArtifactRegistry.cs index a375e4e1bf7..49510eef5bf 100644 --- a/src/Bicep.Core/Registry/OciArtifactRegistry.cs +++ b/src/Bicep.Core/Registry/OciArtifactRegistry.cs @@ -13,6 +13,7 @@ using Bicep.Core.FileSystem; using Bicep.Core.Modules; using Bicep.Core.Registry.Oci; +using Bicep.Core.Registry.PublicRegistry; using Bicep.Core.Semantics; using Bicep.Core.SourceCode; using Bicep.Core.Tracing; @@ -33,12 +34,15 @@ public sealed class OciArtifactRegistry : ExternalArtifactRegistry ArtifactReferenceSchemes.Oci; @@ -210,11 +215,19 @@ private OciManifest GetCachedManifest(OciArtifactReference ociArtifactModuleRefe } } + public override async Task OnRestoreArtifacts(bool forceRestore) + { + await publicRegistryModuleMetadataProvider.TryAwaitCache(forceRestore); + } + public override async Task> RestoreArtifacts(IEnumerable references) { var failures = new Dictionary(); - foreach (var reference in references) + var referencesEvaluated = references.ToArray(); + + // CONSIDER: Run these in parallel + foreach (var reference in referencesEvaluated) { using var timer = new ExecutionTimer($"Restore module {reference.FullyQualifiedReference} to {GetArtifactDirectoryPath(reference)}"); var (result, errorMessage) = await this.TryRestoreArtifactAsync(configuration, reference); diff --git a/src/Bicep.Core/Registry/PublicRegistry/IPublicRegistryModuleMetadataClient.cs b/src/Bicep.Core/Registry/PublicRegistry/IPublicRegistryModuleMetadataClient.cs new file mode 100644 index 00000000000..fba9a25b545 --- /dev/null +++ b/src/Bicep.Core/Registry/PublicRegistry/IPublicRegistryModuleMetadataClient.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Immutable; +using Bicep.Core.Registry.Oci; + +namespace Bicep.Core.Registry.PublicRegistry; + +public record BicepModuleTagPropertiesEntry(string Description, string DocumentationUri); + +public record BicepModuleMetadata( + string ModuleName, // e.g. "avm/app/dapr-containerapp" + List Tags, // e.g. "1.0.0" (not guaranteed to be in that format, although it currently is for public modules) + ImmutableDictionary Properties // Module properties per tag +); + +public interface IPublicRegistryModuleMetadataClient +{ + Task> GetModuleMetadata(); +} diff --git a/src/Bicep.Core/Registry/PublicRegistry/IPublicRegistryModuleMetadataProvider.cs b/src/Bicep.Core/Registry/PublicRegistry/IPublicRegistryModuleMetadataProvider.cs new file mode 100644 index 00000000000..9db456685a4 --- /dev/null +++ b/src/Bicep.Core/Registry/PublicRegistry/IPublicRegistryModuleMetadataProvider.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Bicep.Core.Registry.PublicRegistry; + +public record RegistryModule( + string Name, // e.g. "avm/app/dapr-containerapp" (note: the actual repo name has "bicep/" at the beginning, but the "public" alias takes care of that) + string? Description, + string? DocumentationUri); + +public record RegistryModuleVersion( + string Version, + string? Description, + string? DocumentationUri); + +public interface IPublicRegistryModuleMetadataProvider +{ + public bool IsCached { get; } + public string? DownloadError { get; } + + public Task TryAwaitCache(bool forceUpdate = false); + public void StartUpdateCache(bool forceUpdate = false); + + RegistryModule[] GetCachedModules(); + RegistryModuleVersion[] GetCachedModuleVersions(string modulePath); +} diff --git a/src/Bicep.LangServer/Completions/PublicRegistryModuleMetadataClient.cs b/src/Bicep.Core/Registry/PublicRegistry/PublicRegistryModuleMetadataClient.cs similarity index 79% rename from src/Bicep.LangServer/Completions/PublicRegistryModuleMetadataClient.cs rename to src/Bicep.Core/Registry/PublicRegistry/PublicRegistryModuleMetadataClient.cs index 9a186da6775..7fc2f426dd3 100644 --- a/src/Bicep.LangServer/Completions/PublicRegistryModuleMetadataClient.cs +++ b/src/Bicep.Core/Registry/PublicRegistry/PublicRegistryModuleMetadataClient.cs @@ -4,11 +4,13 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Net; using System.Net.Http.Json; using System.Text.Json; using Bicep.Core.Extensions; +using Microsoft.Extensions.DependencyInjection; -namespace Bicep.LanguageServer.Providers; +namespace Bicep.Core.Registry.PublicRegistry; /// /// Typed http client to get modules metadata that we store at a public endpoint (currently https://github.com/Azure/bicep-registry-modules) @@ -23,18 +25,18 @@ public class PublicRegistryModuleMetadataClient(HttpClient httpClient) : IPublic }; [SuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Relying on references to required properties of the generic type elsewhere in the codebase.")] - public async Task> GetModuleMetadata() + public async Task> GetModuleMetadata() { Trace.WriteLine($"{nameof(PublicRegistryModuleMetadataClient)}: Retrieving list of public registry modules..."); try { - var metadata = await httpClient.GetFromJsonAsync(LiveDataEndpoint, JsonSerializerOptions); + var metadata = await httpClient.GetFromJsonAsync(LiveDataEndpoint, JsonSerializerOptions); if (metadata is not null) { - Trace.WriteLine($"{nameof(PublicRegistryModuleMetadataProvider)}: Retrieved info on {metadata.Length} public registry modules."); - return [.. metadata]; + Trace.WriteLine($"{nameof(PublicRegistryModuleMetadataClient)}: Retrieved info on {metadata.Length} public registry modules."); + return [..metadata]; } else { diff --git a/src/Bicep.Core/Registry/PublicRegistry/PublicRegistryModuleMetadataProvider.cs b/src/Bicep.Core/Registry/PublicRegistry/PublicRegistryModuleMetadataProvider.cs new file mode 100644 index 00000000000..a33bb1418be --- /dev/null +++ b/src/Bicep.Core/Registry/PublicRegistry/PublicRegistryModuleMetadataProvider.cs @@ -0,0 +1,278 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Immutable; +using System.Diagnostics; +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; +using Bicep.Core.Extensions; +using Microsoft.Extensions.DependencyInjection; + +namespace Bicep.Core.Registry.PublicRegistry; + +public static class PublicRegistryModuleMetadataProviderExtensions +{ + public static IServiceCollection AddPublicRegistryModuleMetadataProviderServices(this IServiceCollection services) + { + services.AddSingleton(); + + // using type based registration for Http clients so dependencies can be injected automatically + // without manually constructing up the graph, see https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory#typed-clients + services + .AddHttpClient() + .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler + { + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate + }); + + return services; + } +} + + +/// +/// Provider to get modules metadata that we store at a public endpoint. +/// +public class PublicRegistryModuleMetadataProvider : IPublicRegistryModuleMetadataProvider +{ + private readonly IServiceProvider serviceProvider; + + private readonly TimeSpan CacheValidFor = TimeSpan.FromHours(1); + private readonly TimeSpan InitialThrottleDelay = TimeSpan.FromSeconds(5); + private readonly TimeSpan MaxThrottleDelay = TimeSpan.FromMinutes(2); + + private readonly object queryingLiveSyncObject = new(); + private Task? queryLiveDataTask; + private DateTime? lastSuccessfulQuery; + private int consecutiveFailures = 0; + + private ImmutableArray cachedModules = []; + private string? lastDownloadError = null; + + public bool IsCached => cachedModules.Length > 0; + + public string? DownloadError => IsCached ? null : lastDownloadError; + + public PublicRegistryModuleMetadataProvider(IServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; + } + + public Task TryAwaitCache(bool forceUpdate) + { + return UpdateCacheIfNeeded(forceUpdate: forceUpdate, initialDelay: false); + } + + public void StartUpdateCache(bool forceUpdate) { + _ = TryAwaitCache(forceUpdate); + } + public async Task TryUpdateCacheAsync() + { + if (await TryGetModulesLive() is { } modules) + { + this.cachedModules = modules; + this.lastSuccessfulQuery = DateTime.Now; + return true; + } + else + { + return false; + } + } + + // If cache has not yet successfully been updated, returns empty + public RegistryModule[] GetCachedModules() + { + StartCacheUpdateInBackgroundIfNeeded(); + + var modules = this.cachedModules.ToArray(); + return modules.Select(metadata => + new RegistryModule(metadata.ModuleName, GetDescription(metadata), GetDocumentationUri(metadata))) + .ToArray(); + } + + public RegistryModuleVersion[] GetCachedModuleVersions(string modulePath) + { + StartCacheUpdateInBackgroundIfNeeded(); + + var modules = this.cachedModules.ToArray(); + BicepModuleMetadata? metadata = modules.FirstOrDefault(x => x.ModuleName.Equals(modulePath, StringComparison.Ordinal)); + if (metadata == null) + { + return []; + } + + var versions = metadata.Tags.OrderDescending().ToArray() ?? Enumerable.Empty(); + return versions.Select(v => + new RegistryModuleVersion(v, GetDescription(metadata, v), GetDocumentationUri(metadata, v))) + .ToArray(); + } + + private void StartCacheUpdateInBackgroundIfNeeded(bool initialDelay = false) + { + _ = UpdateCacheIfNeeded(forceUpdate: false, initialDelay); + } + + private Task UpdateCacheIfNeeded(bool forceUpdate, bool initialDelay) + { + if (!this.cachedModules.Any()) + { + Trace.WriteLineIf(IsCacheExpired(), $"{nameof(PublicRegistryModuleMetadataProvider)}: First data retrieval..."); + } + else if (forceUpdate) + { + Trace.WriteLine($"{nameof(PublicRegistryModuleMetadataProvider)}: Force updating cache..."); + } + else if (IsCacheExpired()) + { + Trace.WriteLineIf(IsCacheExpired(), $"{nameof(PublicRegistryModuleMetadataProvider)}: Cache expired, updating..."); + } + else + { + return Task.CompletedTask; + } + + lock (this.queryingLiveSyncObject) + { + if (this.queryLiveDataTask is { }) + { + return this.queryLiveDataTask; + } + + return this.queryLiveDataTask = QueryData(initialDelay); + } + + Task QueryData(bool initialDelay) + { + return Task.Run(async () => + { + try + { + int delay = 0; + if (initialDelay) + { + // Allow language server to start up a bit before first hit + delay = InitialThrottleDelay.Milliseconds; + } + if (consecutiveFailures > 0) + { + // Throttle requests to avoid spamming the endpoint with unsuccessful requests + delay = int.Max(delay, GetExponentialDelay(InitialThrottleDelay, this.consecutiveFailures, MaxThrottleDelay).Milliseconds); // make second try fast + } + + if (delay > 0) + { + Trace.WriteLine($"{nameof(PublicRegistryModuleMetadataProvider)}: Delaying {delay} before retry..."); + await Task.Delay(delay); + } + + if (await TryUpdateCacheAsync()) + { + this.consecutiveFailures = 0; + } + else + { + this.consecutiveFailures++; + } + } + finally + { + lock (this.queryingLiveSyncObject) + { + Trace.Assert(this.queryLiveDataTask is { }, $"{nameof(PublicRegistryModuleMetadataProvider)}: should be querying live data"); + this.queryLiveDataTask = null; + } + } + }); + } + } + + private bool IsCacheExpired() + { + var expired = this.lastSuccessfulQuery.HasValue && this.lastSuccessfulQuery.Value + this.CacheValidFor < DateTime.Now; + if (expired) + { + Trace.TraceInformation($"{nameof(PublicRegistryModuleMetadataProvider)}: Public modules cache is expired."); + } + + return expired; + } + + private async Task?> TryGetModulesLive() + { + try + { + var client = serviceProvider.GetRequiredService(); + var modules = await client.GetModuleMetadata(); + + return modules; + } + catch (Exception ex) + { + this.lastDownloadError = ex.Message; + return null; + } + } + + // Modules paths are, e.g. "app/dapr-containerapp" + public Task> GetModules() + { + StartCacheUpdateInBackgroundIfNeeded(); + var modules = this.cachedModules.ToArray(); + return Task.FromResult( + modules.Select(metadata => + new RegistryModule(metadata.ModuleName, GetDescription(metadata), GetDocumentationUri(metadata)))); + } + + public Task> GetVersions(string modulePath) + { + StartCacheUpdateInBackgroundIfNeeded(); + var modules = this.cachedModules.ToArray(); + BicepModuleMetadata? metadata = modules.FirstOrDefault(x => x.ModuleName.Equals(modulePath, StringComparison.Ordinal)); + if (metadata == null) + { + return Task.FromResult(Enumerable.Empty()); + } + + var versions = metadata.Tags.OrderDescending().ToArray() ?? Enumerable.Empty(); + return Task.FromResult( + versions.Select(v => + new RegistryModuleVersion(v, GetDescription(metadata, v), GetDocumentationUri(metadata, v)))); + } + + private static string? GetDescription(BicepModuleMetadata moduleMetadata, string? version = null) + { + if (moduleMetadata.Properties is null) + { + return null; + } + + if (version is null) + { + // Get description for most recent version with a description + return moduleMetadata.Tags.Select(tag => moduleMetadata.Properties.TryGetValue(tag, out var propertiesEntry) ? propertiesEntry.Description : null) + .WhereNotNull(). + LastOrDefault(); + } + else + { + return moduleMetadata.Properties.TryGetValue(version, out var propertiesEntry) ? propertiesEntry.Description : null; + } + } + + private static string? GetDocumentationUri(BicepModuleMetadata moduleMetadata, string? version = null) + { + version ??= moduleMetadata.Tags.OrderDescending().FirstOrDefault(); + return version is null ? null : moduleMetadata.Properties.TryGetValue(version)?.DocumentationUri; + } + + public static TimeSpan GetExponentialDelay(TimeSpan initialDelay, int consecutiveFailures, TimeSpan maxDelay) + { + var maxFailuresToConsider = (int)Math.Ceiling(Math.Log(maxDelay.TotalSeconds, 2)); // Avoid overflow on Math.Pow() + var secondsDelay = initialDelay.TotalSeconds * Math.Pow(2, Math.Min(consecutiveFailures, maxFailuresToConsider)); + var delay = TimeSpan.FromSeconds(secondsDelay); + + return delay > maxDelay ? maxDelay : delay; + } +} diff --git a/src/Bicep.Core/packages.lock.json b/src/Bicep.Core/packages.lock.json index 952ec83c932..4d6099dc147 100644 --- a/src/Bicep.Core/packages.lock.json +++ b/src/Bicep.Core/packages.lock.json @@ -162,6 +162,20 @@ "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" } }, + "Microsoft.Extensions.Http": { + "type": "Direct", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "cWz4caHwvx0emoYe7NkHPxII/KkTI8R/LC9qdqJqnKv2poTJ4e2qqPGQqvRoQ5kaSA4FU5IV3qFAuLuOhoqULQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Diagnostics": "8.0.0", + "Microsoft.Extensions.Logging": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0" + } + }, "Microsoft.Graph.Bicep.Types": { "type": "Direct", "requested": "[0.1.6-preview, )", @@ -216,6 +230,12 @@ "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, + "Semver": { + "type": "Direct", + "requested": "[2.3.0, )", + "resolved": "2.3.0", + "contentHash": "4vYo1zqn6pJ1YrhjuhuOSbIIm0CpM47grbpTJ5ABjOlfGt/EhMEM9ed4MRK5Jr6gVnntWDqOUzGeUJp68PZGjw==" + }, "SharpYaml": { "type": "Direct", "requested": "[2.1.1, )", @@ -357,6 +377,26 @@ "resolved": "8.0.0", "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==" }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "8.0.0", + "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "System.Diagnostics.DiagnosticSource": "8.0.0" + } + }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", "resolved": "8.0.0", @@ -380,11 +420,50 @@ "resolved": "8.0.0", "contentHash": "OK+670i7esqlQrPjdIKRbsyMCe9g5kSLpRRQGSr4Q58AOYEe/hCnfLZprh7viNisSUUQZmMrbbuDaIrP+V1ebQ==" }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "tvRkov9tAJ3xP51LCv3FJ2zINmv1P8Hi8lhhtcKGqM+ImiTCC84uOPEI4z8Cdq2C3o9e+Aa0Gw0rmrsJD77W+w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + } + }, "Microsoft.Extensions.ObjectPool": { "type": "Transitive", "resolved": "5.0.10", "contentHash": "pp9tbGqIhdEXL6Q1yJl+zevAJSq4BsxqhS1GXzBvEsEz9DDNu9GLNzgUy2xyFc4YjB4m4Ff2YEWTnvQvVYdkvQ==" }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.Configuration.Binder": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "8.0.0", @@ -626,11 +705,8 @@ }, "System.Diagnostics.DiagnosticSource": { "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "KiLYDu2k2J82Q9BJpWiuQqCkFjRBWVq4jDzKKWawVi9KWzyD0XG3cmfX0vqTQlL14Wi9EufJrbL0+KCLTbqWiQ==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } + "resolved": "8.0.0", + "contentHash": "c9xLpVz6PL9lp/djOWtk5KPDZq3cSYpmXoJQY524EOtuFl5z9ZtsotpsyrDW40U1DRnQSYvcPKEUV0X//u6gkQ==" }, "System.Diagnostics.EventLog": { "type": "Transitive", diff --git a/src/Bicep.Decompiler.IntegrationTests/packages.lock.json b/src/Bicep.Decompiler.IntegrationTests/packages.lock.json index f26c3a4195c..30fca8119a5 100644 --- a/src/Bicep.Decompiler.IntegrationTests/packages.lock.json +++ b/src/Bicep.Decompiler.IntegrationTests/packages.lock.json @@ -853,6 +853,11 @@ "OmniSharp.Extensions.LanguageProtocol": "0.19.9" } }, + "RichardSzalay.MockHttp": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "QwnauYiaywp65QKFnP+wvgiQ2D8Pv888qB2dyfd7MSVDF06sIvxqASenk+RxsWybyyt+Hu1Y251wQxpHTv3UYg==" + }, "runtime.linux-arm.runtime.native.System.IO.Ports": { "type": "Transitive", "resolved": "6.0.0", @@ -915,6 +920,11 @@ "resolved": "4.4.0", "contentHash": "YhEdSQUsTx+C8m8Bw7ar5/VesXvCFMItyZF7G1AUY+OM0VPZUOeAVpJ4Wl6fydBGUYZxojTDR3I6Bj/+BPkJNA==" }, + "Semver": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "4vYo1zqn6pJ1YrhjuhuOSbIIm0CpM47grbpTJ5ABjOlfGt/EhMEM9ed4MRK5Jr6gVnntWDqOUzGeUJp68PZGjw==" + }, "SharpYaml": { "type": "Transitive", "resolved": "2.1.1", @@ -1507,9 +1517,11 @@ "Microsoft.Extensions.Configuration.Binder": "[8.0.1, )", "Microsoft.Extensions.Configuration.Json": "[8.0.0, )", "Microsoft.Extensions.DependencyInjection": "[8.0.0, )", + "Microsoft.Extensions.Http": "[8.0.0, )", "Microsoft.Graph.Bicep.Types": "[0.1.6-preview, )", "Microsoft.PowerPlatform.ResourceStack": "[7.0.0.2007, )", "Newtonsoft.Json": "[13.0.3, )", + "Semver": "[2.3.0, )", "SharpYaml": "[2.1.1, )", "System.IO.Abstractions": "[21.0.2, )", "System.Private.Uri": "[4.3.2, )" @@ -1551,6 +1563,7 @@ "Microsoft.NET.Test.Sdk": "[17.10.0, )", "Moq": "[4.20.70, )", "Newtonsoft.Json.Schema": "[3.0.16, )", + "RichardSzalay.MockHttp": "[7.0.0, )", "System.IO.Abstractions.TestingHelpers": "[21.0.2, )" } }, diff --git a/src/Bicep.Decompiler.UnitTests/packages.lock.json b/src/Bicep.Decompiler.UnitTests/packages.lock.json index f26c3a4195c..30fca8119a5 100644 --- a/src/Bicep.Decompiler.UnitTests/packages.lock.json +++ b/src/Bicep.Decompiler.UnitTests/packages.lock.json @@ -853,6 +853,11 @@ "OmniSharp.Extensions.LanguageProtocol": "0.19.9" } }, + "RichardSzalay.MockHttp": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "QwnauYiaywp65QKFnP+wvgiQ2D8Pv888qB2dyfd7MSVDF06sIvxqASenk+RxsWybyyt+Hu1Y251wQxpHTv3UYg==" + }, "runtime.linux-arm.runtime.native.System.IO.Ports": { "type": "Transitive", "resolved": "6.0.0", @@ -915,6 +920,11 @@ "resolved": "4.4.0", "contentHash": "YhEdSQUsTx+C8m8Bw7ar5/VesXvCFMItyZF7G1AUY+OM0VPZUOeAVpJ4Wl6fydBGUYZxojTDR3I6Bj/+BPkJNA==" }, + "Semver": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "4vYo1zqn6pJ1YrhjuhuOSbIIm0CpM47grbpTJ5ABjOlfGt/EhMEM9ed4MRK5Jr6gVnntWDqOUzGeUJp68PZGjw==" + }, "SharpYaml": { "type": "Transitive", "resolved": "2.1.1", @@ -1507,9 +1517,11 @@ "Microsoft.Extensions.Configuration.Binder": "[8.0.1, )", "Microsoft.Extensions.Configuration.Json": "[8.0.0, )", "Microsoft.Extensions.DependencyInjection": "[8.0.0, )", + "Microsoft.Extensions.Http": "[8.0.0, )", "Microsoft.Graph.Bicep.Types": "[0.1.6-preview, )", "Microsoft.PowerPlatform.ResourceStack": "[7.0.0.2007, )", "Newtonsoft.Json": "[13.0.3, )", + "Semver": "[2.3.0, )", "SharpYaml": "[2.1.1, )", "System.IO.Abstractions": "[21.0.2, )", "System.Private.Uri": "[4.3.2, )" @@ -1551,6 +1563,7 @@ "Microsoft.NET.Test.Sdk": "[17.10.0, )", "Moq": "[4.20.70, )", "Newtonsoft.Json.Schema": "[3.0.16, )", + "RichardSzalay.MockHttp": "[7.0.0, )", "System.IO.Abstractions.TestingHelpers": "[21.0.2, )" } }, diff --git a/src/Bicep.Decompiler/packages.lock.json b/src/Bicep.Decompiler/packages.lock.json index cd3e38415af..5f6303a38ae 100644 --- a/src/Bicep.Decompiler/packages.lock.json +++ b/src/Bicep.Decompiler/packages.lock.json @@ -280,6 +280,26 @@ "resolved": "8.0.0", "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==" }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "8.0.0", + "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "System.Diagnostics.DiagnosticSource": "8.0.0" + } + }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", "resolved": "8.0.0", @@ -303,11 +323,63 @@ "resolved": "8.0.0", "contentHash": "OK+670i7esqlQrPjdIKRbsyMCe9g5kSLpRRQGSr4Q58AOYEe/hCnfLZprh7viNisSUUQZmMrbbuDaIrP+V1ebQ==" }, + "Microsoft.Extensions.Http": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "cWz4caHwvx0emoYe7NkHPxII/KkTI8R/LC9qdqJqnKv2poTJ4e2qqPGQqvRoQ5kaSA4FU5IV3qFAuLuOhoqULQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Diagnostics": "8.0.0", + "Microsoft.Extensions.Logging": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "tvRkov9tAJ3xP51LCv3FJ2zINmv1P8Hi8lhhtcKGqM+ImiTCC84uOPEI4z8Cdq2C3o9e+Aa0Gw0rmrsJD77W+w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + } + }, "Microsoft.Extensions.ObjectPool": { "type": "Transitive", "resolved": "5.0.10", "contentHash": "pp9tbGqIhdEXL6Q1yJl+zevAJSq4BsxqhS1GXzBvEsEz9DDNu9GLNzgUy2xyFc4YjB4m4Ff2YEWTnvQvVYdkvQ==" }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.Configuration.Binder": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "8.0.0", @@ -501,6 +573,11 @@ "resolved": "4.4.0", "contentHash": "YhEdSQUsTx+C8m8Bw7ar5/VesXvCFMItyZF7G1AUY+OM0VPZUOeAVpJ4Wl6fydBGUYZxojTDR3I6Bj/+BPkJNA==" }, + "Semver": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "4vYo1zqn6pJ1YrhjuhuOSbIIm0CpM47grbpTJ5ABjOlfGt/EhMEM9ed4MRK5Jr6gVnntWDqOUzGeUJp68PZGjw==" + }, "SharpYaml": { "type": "Transitive", "resolved": "2.1.1", @@ -577,11 +654,8 @@ }, "System.Diagnostics.DiagnosticSource": { "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "KiLYDu2k2J82Q9BJpWiuQqCkFjRBWVq4jDzKKWawVi9KWzyD0XG3cmfX0vqTQlL14Wi9EufJrbL0+KCLTbqWiQ==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } + "resolved": "8.0.0", + "contentHash": "c9xLpVz6PL9lp/djOWtk5KPDZq3cSYpmXoJQY524EOtuFl5z9ZtsotpsyrDW40U1DRnQSYvcPKEUV0X//u6gkQ==" }, "System.Diagnostics.EventLog": { "type": "Transitive", @@ -912,9 +986,11 @@ "Microsoft.Extensions.Configuration.Binder": "[8.0.1, )", "Microsoft.Extensions.Configuration.Json": "[8.0.0, )", "Microsoft.Extensions.DependencyInjection": "[8.0.0, )", + "Microsoft.Extensions.Http": "[8.0.0, )", "Microsoft.Graph.Bicep.Types": "[0.1.6-preview, )", "Microsoft.PowerPlatform.ResourceStack": "[7.0.0.2007, )", "Newtonsoft.Json": "[13.0.3, )", + "Semver": "[2.3.0, )", "SharpYaml": "[2.1.1, )", "System.IO.Abstractions": "[21.0.2, )", "System.Private.Uri": "[4.3.2, )" diff --git a/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs b/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs index f339e0297db..511db8ef211 100644 --- a/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs +++ b/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs @@ -8,6 +8,7 @@ using Bicep.Core.Extensions; using Bicep.Core.FileSystem; using Bicep.Core.Parsing; +using Bicep.Core.Registry.PublicRegistry; using Bicep.Core.Samples; using Bicep.Core.Text; using Bicep.Core.UnitTests; @@ -4141,7 +4142,7 @@ public async Task ModuleRegistryReferenceCompletions_GetPathCompletions(string i settingsProvider.Setup(x => x.GetSetting(LangServerConstants.GetAllAzureContainerRegistriesForCompletionsSetting)).Returns(false); var publicRegistryModuleMetadataProvider = StrictMock.Of(); - publicRegistryModuleMetadataProvider.Setup(x => x.GetModules()).ReturnsAsync(new List { new("app/dapr-containerapp", "d1", "contoso.com/help1"), new("app/dapr-containerapp-env", "d2", "contoso.com/help2") }); + publicRegistryModuleMetadataProvider.Setup(x => x.GetCachedModules()).Returns([new("app/dapr-containerapp", "d1", "contoso.com/help1"), new("app/dapr-containerapp-env", "d2", "contoso.com/help2")]); using var helper = await MultiFileLanguageServerHelper.StartLanguageServer( TestContext, @@ -4185,7 +4186,7 @@ public async Task ModuleRegistryReferenceCompletions_GetVersionCompletions(strin settingsProvider.Setup(x => x.GetSetting(LangServerConstants.GetAllAzureContainerRegistriesForCompletionsSetting)).Returns(false); var publicRegistryModuleMetadataProvider = StrictMock.Of(); - publicRegistryModuleMetadataProvider.Setup(x => x.GetVersions("app/dapr-containerapp")).ReturnsAsync(new List { new("1.0.2", "d1", "contoso.com/help1"), new("1.0.1", null, null) }); + publicRegistryModuleMetadataProvider.Setup(x => x.GetCachedModuleVersions("app/dapr-containerapp")).Returns([new("1.0.2", "d1", "contoso.com/help1"), new("1.0.1", null, null)]); using var helper = await MultiFileLanguageServerHelper.StartLanguageServer( TestContext, diff --git a/src/Bicep.LangServer.IntegrationTests/Registry/ModuleRestoreSchedulerTests.cs b/src/Bicep.LangServer.IntegrationTests/Registry/ModuleRestoreSchedulerTests.cs index 02574d78360..4d97649a6a4 100644 --- a/src/Bicep.LangServer.IntegrationTests/Registry/ModuleRestoreSchedulerTests.cs +++ b/src/Bicep.LangServer.IntegrationTests/Registry/ModuleRestoreSchedulerTests.cs @@ -205,6 +205,7 @@ public ResultWithDiagnostic TryParseArtifactReference(Artifac public ResultWithException TryGetSource(ArtifactReference artifactReference) => new(new SourceNotAvailableException()); public Uri? TryGetProviderBinary(ArtifactReference reference) => null; + public Task OnRestoreArtifacts(bool forceRestore) => Task.CompletedTask; } private class MockArtifactRef : ArtifactReference diff --git a/src/Bicep.LangServer.IntegrationTests/packages.lock.json b/src/Bicep.LangServer.IntegrationTests/packages.lock.json index 726fc1220bd..26bfe2c899b 100644 --- a/src/Bicep.LangServer.IntegrationTests/packages.lock.json +++ b/src/Bicep.LangServer.IntegrationTests/packages.lock.json @@ -811,6 +811,11 @@ "OmniSharp.Extensions.LanguageProtocol": "0.19.9" } }, + "RichardSzalay.MockHttp": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "QwnauYiaywp65QKFnP+wvgiQ2D8Pv888qB2dyfd7MSVDF06sIvxqASenk+RxsWybyyt+Hu1Y251wQxpHTv3UYg==" + }, "runtime.linux-arm.runtime.native.System.IO.Ports": { "type": "Transitive", "resolved": "6.0.0", @@ -873,6 +878,11 @@ "resolved": "4.4.0", "contentHash": "YhEdSQUsTx+C8m8Bw7ar5/VesXvCFMItyZF7G1AUY+OM0VPZUOeAVpJ4Wl6fydBGUYZxojTDR3I6Bj/+BPkJNA==" }, + "Semver": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "4vYo1zqn6pJ1YrhjuhuOSbIIm0CpM47grbpTJ5ABjOlfGt/EhMEM9ed4MRK5Jr6gVnntWDqOUzGeUJp68PZGjw==" + }, "SharpYaml": { "type": "Transitive", "resolved": "2.1.1", @@ -1465,9 +1475,11 @@ "Microsoft.Extensions.Configuration.Binder": "[8.0.1, )", "Microsoft.Extensions.Configuration.Json": "[8.0.0, )", "Microsoft.Extensions.DependencyInjection": "[8.0.0, )", + "Microsoft.Extensions.Http": "[8.0.0, )", "Microsoft.Graph.Bicep.Types": "[0.1.6-preview, )", "Microsoft.PowerPlatform.ResourceStack": "[7.0.0.2007, )", "Newtonsoft.Json": "[13.0.3, )", + "Semver": "[2.3.0, )", "SharpYaml": "[2.1.1, )", "System.IO.Abstractions": "[21.0.2, )", "System.Private.Uri": "[4.3.2, )" @@ -1520,6 +1532,7 @@ "Microsoft.NET.Test.Sdk": "[17.10.0, )", "Moq": "[4.20.70, )", "Newtonsoft.Json.Schema": "[3.0.16, )", + "RichardSzalay.MockHttp": "[7.0.0, )", "System.IO.Abstractions.TestingHelpers": "[21.0.2, )" } }, diff --git a/src/Bicep.LangServer.UnitTests/BicepCompilationManagerHelper.cs b/src/Bicep.LangServer.UnitTests/BicepCompilationManagerHelper.cs index f6f8707a7e7..583a4ef6d7f 100644 --- a/src/Bicep.LangServer.UnitTests/BicepCompilationManagerHelper.cs +++ b/src/Bicep.LangServer.UnitTests/BicepCompilationManagerHelper.cs @@ -91,7 +91,7 @@ public static ICompilationProvider CreateEmptyCompilationProvider(IConfiguration { var helper = ServiceBuilder.Create(services => services .AddSingleton(TestTypeHelper.CreateEmptyResourceTypeLoader()) - .AddSingletonIfNonNull(configurationManager) + .AddSingletonIfNotNull(configurationManager) .AddSingleton()); return helper.Construct(); diff --git a/src/Bicep.LangServer.UnitTests/BicepCompletionProviderTests.cs b/src/Bicep.LangServer.UnitTests/BicepCompletionProviderTests.cs index 5372e40d40b..7bfc0340474 100644 --- a/src/Bicep.LangServer.UnitTests/BicepCompletionProviderTests.cs +++ b/src/Bicep.LangServer.UnitTests/BicepCompletionProviderTests.cs @@ -4,6 +4,7 @@ using Bicep.Core; using Bicep.Core.Extensions; using Bicep.Core.Features; +using Bicep.Core.Registry.PublicRegistry; using Bicep.Core.Syntax; using Bicep.Core.TypeSystem; using Bicep.Core.UnitTests; diff --git a/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs b/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs index 57e1e211a48..2cb1fcebfff 100644 --- a/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs +++ b/src/Bicep.LangServer.UnitTests/Completions/ModuleReferenceCompletionProviderTests.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using Bicep.Core.Registry.PublicRegistry; using Bicep.Core.UnitTests; using Bicep.Core.UnitTests.Mock; using Bicep.Core.UnitTests.Utils; @@ -209,7 +210,7 @@ public async Task GetFilteredCompletions_WithInvalidTextInCompletionContext_Retu public async Task GetFilteredCompletions_WithInvalidCompletionContext_ReturnsEmptyList(string inputWithCursors) { var publicRegistryModuleMetadataProvider = StrictMock.Of(); - publicRegistryModuleMetadataProvider.Setup(x => x.GetVersions("app/dapr-containerapp")).ReturnsAsync(new List { new("1.0.1", null, null), new("1.0.2", null, null) }); + publicRegistryModuleMetadataProvider.Setup(x => x.GetCachedModuleVersions("app/dapr-containerapp")).Returns([new("1.0.1", null, null), new("1.0.2", null, null)]); var completionContext = GetBicepCompletionContext(inputWithCursors, null, out DocumentUri documentUri); var moduleReferenceCompletionProvider = new ModuleReferenceCompletionProvider( @@ -389,7 +390,7 @@ public async Task GetFilteredCompletions_WithACRCompletionsSettingSetToTrue_Retu settingsProviderMock.Setup(x => x.GetSetting(LangServerConstants.GetAllAzureContainerRegistriesForCompletionsSetting)).Returns(true); var azureContainerRegistriesProvider = StrictMock.Of(); - azureContainerRegistriesProvider.Setup(x => x.GetRegistryUris(documentUri.ToUriEncoded(), CancellationToken.None)).Returns(new List { "testacr3.azurecr.io", "testacr4.azurecr.io" }.ToAsyncEnumerable()); + azureContainerRegistriesProvider.Setup(x => x.GetRegistryUrisAccessibleFromAzure(documentUri.ToUriEncoded(), CancellationToken.None)).Returns(new List { "testacr3.azurecr.io", "testacr4.azurecr.io" }.ToAsyncEnumerable()); var moduleReferenceCompletionProvider = new ModuleReferenceCompletionProvider( azureContainerRegistriesProvider.Object, @@ -441,7 +442,7 @@ public async Task GetFilteredCompletions_WithACRCompletionsSettingSetToTrue_AndN settingsProviderMock.Setup(x => x.GetSetting(LangServerConstants.GetAllAzureContainerRegistriesForCompletionsSetting)).Returns(true); var azureContainerRegistriesProvider = StrictMock.Of(); - azureContainerRegistriesProvider.Setup(x => x.GetRegistryUris(documentUri.ToUriEncoded(), CancellationToken.None)).Returns(new List().ToAsyncEnumerable()); + azureContainerRegistriesProvider.Setup(x => x.GetRegistryUrisAccessibleFromAzure(documentUri.ToUriEncoded(), CancellationToken.None)).Returns(new List().ToAsyncEnumerable()); var moduleReferenceCompletionProvider = new ModuleReferenceCompletionProvider( azureContainerRegistriesProvider.Object, @@ -477,7 +478,7 @@ public async Task GetFilteredCompletions_WithPublicMcrModuleRegistryCompletionCo int expectedEnd) { var publicRegistryModuleMetadataProvider = StrictMock.Of(); - publicRegistryModuleMetadataProvider.Setup(x => x.GetModules()).ReturnsAsync(new List { new("app/dapr-cntrapp1", null, null), new("app/dapr-cntrapp2", "description2", "contoso.com/help2") }); + publicRegistryModuleMetadataProvider.Setup(x => x.GetCachedModules()).Returns([new("app/dapr-cntrapp1", null, null), new("app/dapr-cntrapp2", "description2", "contoso.com/help2")]); var completionContext = GetBicepCompletionContext(inputWithCursors, null, out DocumentUri documentUri); var moduleReferenceCompletionProvider = new ModuleReferenceCompletionProvider( @@ -595,7 +596,7 @@ public async Task GetFilteredCompletions_WithMcrVersionCompletionContext_Returns } }"; var publicRegistryModuleMetadataProvider = StrictMock.Of(); - publicRegistryModuleMetadataProvider.Setup(x => x.GetVersions("app/dapr-containerapp")).ReturnsAsync(new List { new("1.0.2", null, null), new("1.0.1", "d2", "contoso.com/help%20page.html") }); + publicRegistryModuleMetadataProvider.Setup(x => x.GetCachedModuleVersions("app/dapr-containerapp")).Returns([new("1.0.2", null, null), new("1.0.1", "d2", "contoso.com/help%20page.html")]); var completionContext = GetBicepCompletionContext(inputWithCursors, bicepConfigFileContents, out DocumentUri documentUri); var moduleReferenceCompletionProvider = new ModuleReferenceCompletionProvider( @@ -637,7 +638,7 @@ public async Task GetFilteredCompletions_WithMcrVersionCompletionContext_Returns public async Task GetFilteredCompletions_WithMcrVersionCompletionContext_AndNoMatchingModuleName_ReturnsEmptyListOfCompletionItems() { var publicRegistryModuleMetadataProvider = StrictMock.Of(); - publicRegistryModuleMetadataProvider.Setup(x => x.GetVersions("app/dapr-containerappapp")).ReturnsAsync(new List()); + publicRegistryModuleMetadataProvider.Setup(x => x.GetCachedModuleVersions("app/dapr-containerappapp")).Returns([]); var completionContext = GetBicepCompletionContext("module test 'br/public:app/dapr-containerappapp:|'", null, out DocumentUri documentUri); var moduleReferenceCompletionProvider = new ModuleReferenceCompletionProvider( @@ -724,7 +725,7 @@ public async Task GetFilteredCompletions_WithAliasForMCRInBicepConfigAndModulePa } }"; var publicRegistryModuleMetadataProvider = StrictMock.Of(); - publicRegistryModuleMetadataProvider.Setup(x => x.GetModules()).ReturnsAsync(new List { new("app/dapr-containerappapp", "dapr description", "contoso.com/help") }); + publicRegistryModuleMetadataProvider.Setup(x => x.GetCachedModules()).Returns([new("app/dapr-containerappapp", "dapr description", "contoso.com/help")]); var completionContext = GetBicepCompletionContext(inputWithCursors, bicepConfigFileContents, out DocumentUri documentUri); var moduleReferenceCompletionProvider = new ModuleReferenceCompletionProvider( @@ -783,7 +784,7 @@ public async Task VerifyTelemetryEventIsPostedOnModuleRegistryPathCompletion(str var completionContext = GetBicepCompletionContext(inputWithCursors, bicepConfigFileContents, out DocumentUri documentUri); var publicRegistryModuleMetadataProvider = StrictMock.Of(); - publicRegistryModuleMetadataProvider.Setup(x => x.GetModules()).ReturnsAsync(new List { new("app/dapr-cntrapp1", "description1", null), new("app/dapr-cntrapp2", null, "contoso.com/help2") }); + publicRegistryModuleMetadataProvider.Setup(x => x.GetCachedModules()).Returns([new("app/dapr-cntrapp1", "description1", null), new("app/dapr-cntrapp2", null, "contoso.com/help2")]); var telemetryProvider = StrictMock.Of(); telemetryProvider.Setup(x => x.PostEvent(It.IsAny())); @@ -836,7 +837,7 @@ async IAsyncEnumerable GetUris([EnumeratorCancellation] CancellationToke secondItemReturned = true; yield return "testacr4.azurecr.io"; } - azureContainerRegistriesProvider.Setup(x => x.GetRegistryUris(documentUri.ToUriEncoded(), It.IsAny())) + azureContainerRegistriesProvider.Setup(x => x.GetRegistryUrisAccessibleFromAzure(documentUri.ToUriEncoded(), It.IsAny())) .Returns((Uri uri, CancellationToken ct) => GetUris(ct)); var moduleReferenceCompletionProvider = new ModuleReferenceCompletionProvider( diff --git a/src/Bicep.LangServer.UnitTests/packages.lock.json b/src/Bicep.LangServer.UnitTests/packages.lock.json index e16db6f4ff9..720e8769d66 100644 --- a/src/Bicep.LangServer.UnitTests/packages.lock.json +++ b/src/Bicep.LangServer.UnitTests/packages.lock.json @@ -940,6 +940,11 @@ "resolved": "4.4.0", "contentHash": "YhEdSQUsTx+C8m8Bw7ar5/VesXvCFMItyZF7G1AUY+OM0VPZUOeAVpJ4Wl6fydBGUYZxojTDR3I6Bj/+BPkJNA==" }, + "Semver": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "4vYo1zqn6pJ1YrhjuhuOSbIIm0CpM47grbpTJ5ABjOlfGt/EhMEM9ed4MRK5Jr6gVnntWDqOUzGeUJp68PZGjw==" + }, "SharpYaml": { "type": "Transitive", "resolved": "2.1.1", @@ -1532,9 +1537,11 @@ "Microsoft.Extensions.Configuration.Binder": "[8.0.1, )", "Microsoft.Extensions.Configuration.Json": "[8.0.0, )", "Microsoft.Extensions.DependencyInjection": "[8.0.0, )", + "Microsoft.Extensions.Http": "[8.0.0, )", "Microsoft.Graph.Bicep.Types": "[0.1.6-preview, )", "Microsoft.PowerPlatform.ResourceStack": "[7.0.0.2007, )", "Newtonsoft.Json": "[13.0.3, )", + "Semver": "[2.3.0, )", "SharpYaml": "[2.1.1, )", "System.IO.Abstractions": "[21.0.2, )", "System.Private.Uri": "[4.3.2, )" @@ -1587,6 +1594,7 @@ "Microsoft.NET.Test.Sdk": "[17.10.0, )", "Moq": "[4.20.70, )", "Newtonsoft.Json.Schema": "[3.0.16, )", + "RichardSzalay.MockHttp": "[7.0.0, )", "System.IO.Abstractions.TestingHelpers": "[21.0.2, )" } }, diff --git a/src/Bicep.LangServer/BicepCompilationManager.cs b/src/Bicep.LangServer/BicepCompilationManager.cs index 18e0a2bde25..224fdd591cc 100644 --- a/src/Bicep.LangServer/BicepCompilationManager.cs +++ b/src/Bicep.LangServer/BicepCompilationManager.cs @@ -16,6 +16,7 @@ using Bicep.LanguageServer.Providers; using Bicep.LanguageServer.Registry; using Bicep.LanguageServer.Telemetry; +using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using OmniSharp.Extensions.LanguageServer.Protocol; using OmniSharp.Extensions.LanguageServer.Protocol.Document; @@ -581,7 +582,7 @@ public BicepTelemetryEvent GetLinterStateTelemetryOnBicepFileOpen(RootConfigurat { foreach (var kvp in LinterRulesProvider.GetLinterRules()) { - string linterRuleDiagnosticLevelValue = configuration.Analyzers.GetValue(kvp.Value, "warning"); + string linterRuleDiagnosticLevelValue = configuration.Analyzers.GetValue(kvp.Value.diagnosticLevelConfigProperty, "warning"); properties.Add(kvp.Key, linterRuleDiagnosticLevelValue); } diff --git a/src/Bicep.LangServer/Completions/IPublicRegistryModuleMetadataClient.cs b/src/Bicep.LangServer/Completions/IPublicRegistryModuleMetadataClient.cs deleted file mode 100644 index ad57bc64198..00000000000 --- a/src/Bicep.LangServer/Completions/IPublicRegistryModuleMetadataClient.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections.Immutable; -using Bicep.Core.Registry.Oci; - -namespace Bicep.LanguageServer.Providers; - -public record ModuleTagPropertiesEntry(string Description, string DocumentationUri); - -public record ModuleMetadata( - string ModuleName, // e.g. "avm/app/dapr-containerapp" - List Tags, // e.g. "1.0.0.0" (not guaranteed in that format) - ImmutableDictionary Properties // Module properties per tag -); - -public interface IPublicRegistryModuleMetadataClient -{ - Task> GetModuleMetadata(); -} diff --git a/src/Bicep.LangServer/Completions/IPublicRegistryModuleMetadataProvider.cs b/src/Bicep.LangServer/Completions/IPublicRegistryModuleMetadataProvider.cs deleted file mode 100644 index a3e211bdd2a..00000000000 --- a/src/Bicep.LangServer/Completions/IPublicRegistryModuleMetadataProvider.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Bicep.LanguageServer.Providers; - -public record RegistryModule( - string Name, // e.g. "avm/app/dapr-containerapp" - string? Description, - string? DocumentationUri); - -public record RegistryModuleVersion( - string Version, - string? Description, - string? DocumentationUri); - -public interface IPublicRegistryModuleMetadataProvider -{ - Task> GetModules(); - - Task> GetVersions(string modulePath); -} diff --git a/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs b/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs index 60d716f7804..f6d049c242a 100644 --- a/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs +++ b/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs @@ -9,6 +9,7 @@ using Bicep.Core.Configuration; using Bicep.Core.Parsing; using Bicep.Core.Registry.Oci; +using Bicep.Core.Registry.PublicRegistry; using Bicep.Core.Syntax; using Bicep.LanguageServer.Providers; using Bicep.LanguageServer.Settings; @@ -72,8 +73,8 @@ public async Task> GetFilteredCompletions(Uri source } return GetTopLevelCompletions(context, replacementText, sourceFileUri) - .Concat(await GetOciModulePathCompletions(context, replacementText, sourceFileUri)) - .Concat(await GetMCRModuleRegistryVersionCompletions(context, replacementText, sourceFileUri)) + .Concat(GetOciModulePathCompletions(context, replacementText, sourceFileUri)) + .Concat(GetMCRModuleRegistryVersionCompletions(context, replacementText, sourceFileUri)) .Concat(await GetAllRegistryNameAndAliasCompletions(context, replacementText, sourceFileUri, cancellationToken)); } @@ -176,7 +177,7 @@ private bool IsOciArtifactRegistryReference(string replacementText) // br:mcr.microsoft/bicep/module/name: // // etc - private async Task> GetMCRModuleRegistryVersionCompletions(BicepCompletionContext context, string replacementText, Uri sourceFileUri) + private IEnumerable GetMCRModuleRegistryVersionCompletions(BicepCompletionContext context, string replacementText, Uri sourceFileUri) { if (!IsOciArtifactRegistryReference(replacementText)) { @@ -208,7 +209,7 @@ private async Task> GetMCRModuleRegistryVersionCompl List completions = new(); replacementText = replacementText.TrimEnd('\''); - var versionInfos = (await publicRegistryModuleMetadataProvider.GetVersions(modulePath)).ToArray(); + var versionInfos = publicRegistryModuleMetadataProvider.GetCachedModuleVersions(modulePath).ToArray(); for (int i = versionInfos.Count() - 1; i >= 0; i--) { var (version, description, documentationUri) = versionInfos[i]; @@ -288,7 +289,7 @@ private ImmutableSortedDictionary GetOciArtifact } // Handles remote (OCI) path completions, e.g. br: and br/ - private async Task> GetOciModulePathCompletions(BicepCompletionContext context, string replacementText, Uri sourceFileUri) + private IEnumerable GetOciModulePathCompletions(BicepCompletionContext context, string replacementText, Uri sourceFileUri) { if (!IsOciArtifactRegistryReference(replacementText)) { @@ -300,14 +301,14 @@ private async Task> GetOciModulePathCompletions(Bice replacementText == "'br/public:" || replacementText == $"'br:{PublicMCRRegistry}/bicep/") { - return await GetPublicMCRPathCompletions(replacementText, context, sourceFileUri); + return GetPublicModuleCompletions(replacementText, context, sourceFileUri); } else { List completions = new(); completions.AddRange(GetACRPartialPathCompletionsFromBicepConfig(replacementText, context, sourceFileUri)); - completions.AddRange(await GetMCRPathCompletionFromBicepConfig(replacementText, context, sourceFileUri)); + completions.AddRange(GetMCRPathCompletionFromBicepConfig(replacementText, context, sourceFileUri)); return completions; } @@ -315,7 +316,7 @@ private async Task> GetOciModulePathCompletions(Bice // Handles path completions for case where user has specified an alias in bicepconfig.json with registry set to "mcr.microsoft.com". - private async Task> GetMCRPathCompletionFromBicepConfig(string replacementText, BicepCompletionContext context, Uri sourceFileUri) + private IEnumerable GetMCRPathCompletionFromBicepConfig(string replacementText, BicepCompletionContext context, Uri sourceFileUri) { List completions = new(); @@ -359,7 +360,7 @@ private async Task> GetMCRPathCompletionFromBicepCon if (replacementTextWithTrimmedEnd.Equals($"'br/{kvp.Key}:", StringComparison.Ordinal)) { - var modules = await publicRegistryModuleMetadataProvider.GetModules(); + var modules = publicRegistryModuleMetadataProvider.GetCachedModules(); foreach (var (moduleName, description, documentationUri) in modules) { var label = $"bicep/{moduleName}"; @@ -397,8 +398,8 @@ private async Task> GetMCRPathCompletionFromBicepCon } // Completions are e.g. br/[alias]/[module] - var modulePathWithoutBicepKeyword = modulePath.Substring("bicep/".Length); - var modules = await publicRegistryModuleMetadataProvider.GetModules(); + var modulePathWithoutBicepKeyword = TrimStart(modulePath, "bicep/"); + var modules = publicRegistryModuleMetadataProvider.GetCachedModules(); var matchingModules = modules.Where(x => x.Name.StartsWith($"{modulePathWithoutBicepKeyword}/")); @@ -436,6 +437,8 @@ private async Task> GetMCRPathCompletionFromBicepCon return completions; } + private string TrimStart(string text, string prefixToTrim) => text.StartsWith(prefixToTrim) ? text.Substring(prefixToTrim.Length) : text; + /// /// True if a direct reference to a private ACR registry (i.e. not pointing to the Microsoft public bicep registry) /// @@ -495,17 +498,17 @@ private IEnumerable GetACRPartialPathCompletionsFromBicepConfig( return completions; } - // Handles path completions for MCR: + // Handles module path completions for MCR: // br/public: // or // br:mcr.microsoft.com/bicep/: - private async Task> GetPublicMCRPathCompletions(string replacementText, BicepCompletionContext context, Uri sourceUri) + private IEnumerable GetPublicModuleCompletions(string replacementText, BicepCompletionContext context, Uri sourceUri) { List completions = new(); var replacementTextWithTrimmedEnd = replacementText.TrimEnd('\''); - var modules = await publicRegistryModuleMetadataProvider.GetModules(); + var modules = publicRegistryModuleMetadataProvider.GetCachedModules(); foreach (var (moduleName, description, documentationUri) in modules) { var insertText = $"{replacementTextWithTrimmedEnd}{moduleName}:$0'"; @@ -582,7 +585,7 @@ private async Task> GetACRModuleRegistriesCompletion { if (settingsProvider.GetSetting(LangServerConstants.GetAllAzureContainerRegistriesForCompletionsSetting)) { - return await GetACRModuleRegistriesCompletionsFromGraphClient(replacementText, context, sourceFileUri, cancellationToken); + return await GetACRModuleRegistriesCompletionsFromAzure(replacementText, context, sourceFileUri, cancellationToken); } else { @@ -595,13 +598,13 @@ private async Task> GetACRModuleRegistriesCompletion // This returns all registries that the user has access to via Azure (whether or not they contain bicep modules, and whether // or not they're registered in the bicepconfig.json file) // This is for completions after typing "'br:" - private async Task> GetACRModuleRegistriesCompletionsFromGraphClient(string replacementText, BicepCompletionContext context, Uri sourceFileUri, CancellationToken cancellationToken) + private async Task> GetACRModuleRegistriesCompletionsFromAzure(string replacementText, BicepCompletionContext context, Uri sourceFileUri, CancellationToken cancellationToken) { List completions = new(); try { - await foreach (string registryName in azureContainerRegistriesProvider.GetRegistryUris(sourceFileUri, cancellationToken) + await foreach (string registryName in azureContainerRegistriesProvider.GetRegistryUrisAccessibleFromAzure(sourceFileUri, cancellationToken) .WithCancellation(cancellationToken)) { var replacementTextWithTrimmedEnd = replacementText.Trim('\''); @@ -625,7 +628,7 @@ private async Task> GetACRModuleRegistriesCompletion } } - // Handles private ACR registry name completions only for registries that are configured in the bicepconfig.json file + // Handles private ACR registry name completions only for registries that are configured via aliases in the bicepconfig.json file private IEnumerable GetACRModuleRegistriesCompletionsFromBicepConfig(string replacementText, BicepCompletionContext context, Uri sourceFileUri) { List completions = new(); diff --git a/src/Bicep.LangServer/Completions/PublicRegistryModuleMetadataProvider.cs b/src/Bicep.LangServer/Completions/PublicRegistryModuleMetadataProvider.cs deleted file mode 100644 index b012cdcb218..00000000000 --- a/src/Bicep.LangServer/Completions/PublicRegistryModuleMetadataProvider.cs +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections.Immutable; -using System.Diagnostics; -using System.Net.Http.Json; -using System.Text.Json; -using Bicep.Core.Extensions; -using Microsoft.Extensions.DependencyInjection; - -namespace Bicep.LanguageServer.Providers -{ - /// - /// Provider to get modules metadata that we store at a public endpoint. - /// - public class PublicRegistryModuleMetadataProvider : IPublicRegistryModuleMetadataProvider - { - private readonly TimeSpan CacheValidFor = TimeSpan.FromHours(1); - - private readonly TimeSpan InitialThrottleDelay = TimeSpan.FromSeconds(5); - - private readonly TimeSpan MaxThrottleDelay = TimeSpan.FromMinutes(2); - - private readonly object queryingLiveSyncObject = new(); - - private ImmutableArray cachedModules = []; - - private bool isQueryingLiveData = false; - - private DateTime? lastSuccessfulQuery; - - private int consecutiveFailures = 0; - - private readonly IServiceProvider serviceProvider; - - public PublicRegistryModuleMetadataProvider(IServiceProvider serviceProvider) - { - this.serviceProvider = serviceProvider; - - this.TryUpdateCacheInBackground(true); - } - - public async Task TryUpdateCacheAsync() - { - if (await TryGetModulesLive() is { } modules) - { - this.cachedModules = modules; - this.lastSuccessfulQuery = DateTime.Now; - return true; - } - else - { - return false; - } - } - - private void TryUpdateCacheInBackground(bool initialDelay = false) - { - if (!IsCacheExpired() && this.cachedModules.Any()) - { - return; - } - Trace.WriteLineIf(IsCacheExpired(), $"{nameof(PublicRegistryModuleMetadataProvider)}: Cache expired"); - - lock (this.queryingLiveSyncObject) - { - if (this.isQueryingLiveData) - { - return; - } - - this.isQueryingLiveData = true; - } - - _ = Task.Run(async () => - { - try - { - if (initialDelay) - { - // Allow language server to start up a bit before first hit - await Task.Delay(InitialThrottleDelay); - } - - if (await TryUpdateCacheAsync()) - { - this.consecutiveFailures = 0; - } - else - { - this.consecutiveFailures++; - } - } - finally - { - // Throttle requests to avoid spamming the endpoint with unsuccessful requests - var delay = GetExponentialDelay(InitialThrottleDelay, this.consecutiveFailures, MaxThrottleDelay); - Trace.WriteLine($"{nameof(PublicRegistryModuleMetadataProvider)}: Delaying {delay}..."); - await Task.Delay(delay); - - lock (this.queryingLiveSyncObject) - { - Trace.Assert(this.isQueryingLiveData, "this.isQueryingLiveData should be true"); - this.isQueryingLiveData = false; - } - } - }); - } - - private bool IsCacheExpired() - { - var expired = this.lastSuccessfulQuery.HasValue && this.lastSuccessfulQuery.Value + this.CacheValidFor < DateTime.Now; - if (expired) - { - Trace.TraceInformation($"{nameof(PublicRegistryModuleMetadataProvider)}: Public modules cache is expired."); - } - - return expired; - } - - private async Task?> TryGetModulesLive() - { - try - { - var client = serviceProvider.GetRequiredService(); - return await client.GetModuleMetadata(); - } - catch - { - return null; - } - } - - // Modules paths are, e.g. "app/dapr-containerapp" - public Task> GetModules() - { - TryUpdateCacheInBackground(); - var modules = this.cachedModules.ToArray(); - return Task.FromResult( - modules.Select(metadata => - new RegistryModule(metadata.ModuleName, GetDescription(metadata), GetDocumentationUri(metadata)))); - } - - public Task> GetVersions(string modulePath) - { - TryUpdateCacheInBackground(); - var modules = this.cachedModules.ToArray(); - ModuleMetadata? metadata = modules.FirstOrDefault(x => x.ModuleName.Equals(modulePath, StringComparison.Ordinal)); - if (metadata == null) - { - return Task.FromResult(Enumerable.Empty()); - } - - var versions = metadata.Tags.OrderDescending().ToArray() ?? Enumerable.Empty(); - return Task.FromResult( - versions.Select(v => - new RegistryModuleVersion(v, GetDescription(metadata, v), GetDocumentationUri(metadata, v)))); - } - - private static string? GetDescription(ModuleMetadata moduleMetadata, string? version = null) - { - if (moduleMetadata.Properties is null) - { - return null; - } - - if (version is null) - { - // Get description for most recent version with a description - return moduleMetadata.Tags.Select(tag => moduleMetadata.Properties.TryGetValue(tag, out var propertiesEntry) ? propertiesEntry.Description : null) - .WhereNotNull(). - LastOrDefault(); - } - else - { - return moduleMetadata.Properties.TryGetValue(version, out var propertiesEntry) ? propertiesEntry.Description : null; - } - } - - private static string? GetDocumentationUri(ModuleMetadata moduleMetadata, string? version = null) - { - version ??= moduleMetadata.Tags.OrderDescending().FirstOrDefault(); - return version is null ? null : moduleMetadata.Properties[version].DocumentationUri; - } - - public static TimeSpan GetExponentialDelay(TimeSpan initialDelay, int consecutiveFailures, TimeSpan maxDelay) - { - var maxFailuresToConsider = (int)Math.Ceiling(Math.Log(maxDelay.TotalSeconds, 2)); // Avoid overflow on Math.Pow() - var secondsDelay = initialDelay.TotalSeconds * Math.Pow(2, Math.Min(consecutiveFailures, maxFailuresToConsider)); - var delay = TimeSpan.FromSeconds(secondsDelay); - - return delay > maxDelay ? maxDelay : delay; - } - } -} diff --git a/src/Bicep.LangServer/IServiceCollectionExtensions.cs b/src/Bicep.LangServer/IServiceCollectionExtensions.cs index a7a40509e8e..c896d312781 100644 --- a/src/Bicep.LangServer/IServiceCollectionExtensions.cs +++ b/src/Bicep.LangServer/IServiceCollectionExtensions.cs @@ -9,6 +9,7 @@ using Bicep.Core.FileSystem; using Bicep.Core.Registry; using Bicep.Core.Registry.Auth; +using Bicep.Core.Registry.PublicRegistry; using Bicep.Core.Semantics.Namespaces; using Bicep.Core.TypeSystem.Providers; using Bicep.Core.Utils; @@ -45,6 +46,7 @@ public static IServiceCollection AddBicepCore(this IServiceCollection services) .AddSingleton() .AddSingleton() .AddSingleton() + .AddPublicRegistryModuleMetadataProviderServices() .AddSingleton(); public static IServiceCollection AddBicepDecompiler(this IServiceCollection services) => services @@ -72,7 +74,5 @@ public static IServiceCollection AddServerDependencies(this IServiceCollection s .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() - .AddSingleton() - ; + .AddSingleton(); } diff --git a/src/Bicep.LangServer/Providers/AzureContainerRegistriesProvider.cs b/src/Bicep.LangServer/Providers/AzureContainerRegistriesProvider.cs index 49cb2751f97..52295ef2fd2 100644 --- a/src/Bicep.LangServer/Providers/AzureContainerRegistriesProvider.cs +++ b/src/Bicep.LangServer/Providers/AzureContainerRegistriesProvider.cs @@ -33,7 +33,7 @@ public AzureContainerRegistriesProvider(IConfigurationManager configurationManag } // Used for completions after typing "'br:" - public async IAsyncEnumerable GetRegistryUris(Uri templateUri, [EnumeratorCancellation] CancellationToken cancellationToken) + public async IAsyncEnumerable GetRegistryUrisAccessibleFromAzure(Uri templateUri, [EnumeratorCancellation] CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/Bicep.LangServer/Providers/BicepCompilationProvider.cs b/src/Bicep.LangServer/Providers/BicepCompilationProvider.cs index 3da2027d6da..fcc972a6028 100644 --- a/src/Bicep.LangServer/Providers/BicepCompilationProvider.cs +++ b/src/Bicep.LangServer/Providers/BicepCompilationProvider.cs @@ -11,6 +11,7 @@ using Bicep.Core.Utils; using Bicep.Core.Workspaces; using Bicep.LanguageServer.CompilationManager; +using Microsoft.Extensions.DependencyInjection; using OmniSharp.Extensions.LanguageServer.Protocol; namespace Bicep.LanguageServer.Providers diff --git a/src/Bicep.LangServer/Providers/IAzureContainerRegistriesProvider.cs b/src/Bicep.LangServer/Providers/IAzureContainerRegistriesProvider.cs index af16c68e259..c348580d449 100644 --- a/src/Bicep.LangServer/Providers/IAzureContainerRegistriesProvider.cs +++ b/src/Bicep.LangServer/Providers/IAzureContainerRegistriesProvider.cs @@ -6,6 +6,6 @@ namespace Bicep.LanguageServer.Providers public interface IAzureContainerRegistriesProvider { // Returns login server URIs, e.g. "contoso.azurecr.io" - IAsyncEnumerable GetRegistryUris(Uri templateUri, CancellationToken cancellation); + IAsyncEnumerable GetRegistryUrisAccessibleFromAzure(Uri templateUri, CancellationToken cancellation); } } diff --git a/src/Bicep.LangServer/Server.cs b/src/Bicep.LangServer/Server.cs index 17a19e966fe..1814bac604a 100644 --- a/src/Bicep.LangServer/Server.cs +++ b/src/Bicep.LangServer/Server.cs @@ -3,7 +3,9 @@ using System.Diagnostics; using System.Net; +using System.ServiceProcess; using Bicep.Core.Features; +using Bicep.Core.Registry.PublicRegistry; using Bicep.Core.Tracing; using Bicep.LanguageServer.Handlers; using Bicep.LanguageServer.Providers; @@ -93,20 +95,14 @@ public async Task RunAsync(CancellationToken cancellationToken) await server.WaitForExit; #pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks } + + var moduleMetadataProvider = server.GetRequiredService(); + moduleMetadataProvider.StartUpdateCache(); } private static void RegisterServices(IServiceCollection services) { - // using type based registration for Http clients so dependencies can be injected automatically - // without manually constructing up the graph, see https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory#typed-clients services.AddServerDependencies(); - - services - .AddHttpClient() - .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler - { - AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate - }); } public void Dispose() diff --git a/src/Bicep.LangServer/Telemetry/TelemetryHelper.cs b/src/Bicep.LangServer/Telemetry/TelemetryHelper.cs index b29fcf02e57..987f99ba399 100644 --- a/src/Bicep.LangServer/Telemetry/TelemetryHelper.cs +++ b/src/Bicep.LangServer/Telemetry/TelemetryHelper.cs @@ -41,8 +41,8 @@ public static IEnumerable GetTelemetryEventsForBicepConfigC { foreach (var kvp in linterRulesProvider.GetLinterRules()) { - string prevLinterRuleDiagnosticLevelValue = prevConfiguration.Analyzers.GetValue(kvp.Value, "warning"); - string curLinterRuleDiagnosticLevelValue = curConfiguration.Analyzers.GetValue(kvp.Value, "warning"); + string prevLinterRuleDiagnosticLevelValue = prevConfiguration.Analyzers.GetValue(kvp.Value.diagnosticLevelConfigProperty, "warning"); + string curLinterRuleDiagnosticLevelValue = curConfiguration.Analyzers.GetValue(kvp.Value.diagnosticLevelConfigProperty, "warning"); if (prevLinterRuleDiagnosticLevelValue != curLinterRuleDiagnosticLevelValue) { diff --git a/src/Bicep.LangServer/packages.lock.json b/src/Bicep.LangServer/packages.lock.json index 5a0db38de00..6e309ec372a 100644 --- a/src/Bicep.LangServer/packages.lock.json +++ b/src/Bicep.LangServer/packages.lock.json @@ -790,6 +790,11 @@ "resolved": "4.4.0", "contentHash": "YhEdSQUsTx+C8m8Bw7ar5/VesXvCFMItyZF7G1AUY+OM0VPZUOeAVpJ4Wl6fydBGUYZxojTDR3I6Bj/+BPkJNA==" }, + "Semver": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "4vYo1zqn6pJ1YrhjuhuOSbIIm0CpM47grbpTJ5ABjOlfGt/EhMEM9ed4MRK5Jr6gVnntWDqOUzGeUJp68PZGjw==" + }, "Sprache.StrongNamed": { "type": "Transitive", "resolved": "2.3.2", @@ -1355,9 +1360,11 @@ "Microsoft.Extensions.Configuration.Binder": "[8.0.1, )", "Microsoft.Extensions.Configuration.Json": "[8.0.0, )", "Microsoft.Extensions.DependencyInjection": "[8.0.0, )", + "Microsoft.Extensions.Http": "[8.0.0, )", "Microsoft.Graph.Bicep.Types": "[0.1.6-preview, )", "Microsoft.PowerPlatform.ResourceStack": "[7.0.0.2007, )", "Newtonsoft.Json": "[13.0.3, )", + "Semver": "[2.3.0, )", "SharpYaml": "[2.1.1, )", "System.IO.Abstractions": "[21.0.2, )", "System.Private.Uri": "[4.3.2, )" diff --git a/src/Bicep.Local.Deploy.IntegrationTests/EndToEndDeploymentTests.cs b/src/Bicep.Local.Deploy.IntegrationTests/EndToEndDeploymentTests.cs index edc8084d501..a4c4d619ff0 100644 --- a/src/Bicep.Local.Deploy.IntegrationTests/EndToEndDeploymentTests.cs +++ b/src/Bicep.Local.Deploy.IntegrationTests/EndToEndDeploymentTests.cs @@ -4,6 +4,10 @@ using System.IO.Abstractions; using Azure.Deployments.Core.Definitions; using Azure.Deployments.Extensibility.Messages; +using Bicep.Core.Configuration; +using Bicep.Core.Features; +using Bicep.Core.FileSystem; +using Bicep.Core.Registry; using Bicep.Core.UnitTests; using Bicep.Core.UnitTests.Assertions; using Bicep.Core.UnitTests.Features; @@ -127,7 +131,8 @@ param coords { return Task.FromResult(new(req.Resource, null, null)); }); - await using LocalExtensibilityHandler extensibilityHandler = new(BicepTestConstants.ModuleDispatcher, uri => Task.FromResult(providerMock.Object)); + var dispatcher = BicepTestConstants.CreateModuleDispatcher(services.Build().Construct()); + await using LocalExtensibilityHandler extensibilityHandler = new(dispatcher, uri => Task.FromResult(providerMock.Object)); await extensibilityHandler.InitializeProviders(result.Compilation); var localDeployResult = await LocalDeployment.Deploy(extensibilityHandler, templateFile, parametersFile, TestContext.CancellationTokenSource.Token); diff --git a/src/Bicep.Local.Deploy.IntegrationTests/packages.lock.json b/src/Bicep.Local.Deploy.IntegrationTests/packages.lock.json index 3d6f448f8f3..dbf770d7442 100644 --- a/src/Bicep.Local.Deploy.IntegrationTests/packages.lock.json +++ b/src/Bicep.Local.Deploy.IntegrationTests/packages.lock.json @@ -863,6 +863,11 @@ "OmniSharp.Extensions.LanguageProtocol": "0.19.9" } }, + "RichardSzalay.MockHttp": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "QwnauYiaywp65QKFnP+wvgiQ2D8Pv888qB2dyfd7MSVDF06sIvxqASenk+RxsWybyyt+Hu1Y251wQxpHTv3UYg==" + }, "runtime.linux-arm.runtime.native.System.IO.Ports": { "type": "Transitive", "resolved": "6.0.0", @@ -925,6 +930,11 @@ "resolved": "4.4.0", "contentHash": "YhEdSQUsTx+C8m8Bw7ar5/VesXvCFMItyZF7G1AUY+OM0VPZUOeAVpJ4Wl6fydBGUYZxojTDR3I6Bj/+BPkJNA==" }, + "Semver": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "4vYo1zqn6pJ1YrhjuhuOSbIIm0CpM47grbpTJ5ABjOlfGt/EhMEM9ed4MRK5Jr6gVnntWDqOUzGeUJp68PZGjw==" + }, "SharpYaml": { "type": "Transitive", "resolved": "2.1.1", @@ -1517,9 +1527,11 @@ "Microsoft.Extensions.Configuration.Binder": "[8.0.1, )", "Microsoft.Extensions.Configuration.Json": "[8.0.0, )", "Microsoft.Extensions.DependencyInjection": "[8.0.0, )", + "Microsoft.Extensions.Http": "[8.0.0, )", "Microsoft.Graph.Bicep.Types": "[0.1.6-preview, )", "Microsoft.PowerPlatform.ResourceStack": "[7.0.0.2007, )", "Newtonsoft.Json": "[13.0.3, )", + "Semver": "[2.3.0, )", "SharpYaml": "[2.1.1, )", "System.IO.Abstractions": "[21.0.2, )", "System.Private.Uri": "[4.3.2, )" @@ -1561,6 +1573,7 @@ "Microsoft.NET.Test.Sdk": "[17.10.0, )", "Moq": "[4.20.70, )", "Newtonsoft.Json.Schema": "[3.0.16, )", + "RichardSzalay.MockHttp": "[7.0.0, )", "System.IO.Abstractions.TestingHelpers": "[21.0.2, )" } }, diff --git a/src/Bicep.Local.Deploy/packages.lock.json b/src/Bicep.Local.Deploy/packages.lock.json index 78b32b99917..ff3d4a2e8a2 100644 --- a/src/Bicep.Local.Deploy/packages.lock.json +++ b/src/Bicep.Local.Deploy/packages.lock.json @@ -391,6 +391,26 @@ "resolved": "8.0.0", "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==" }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "8.0.0", + "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "System.Diagnostics.DiagnosticSource": "8.0.0" + } + }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", "resolved": "8.0.0", @@ -414,16 +434,63 @@ "resolved": "8.0.0", "contentHash": "OK+670i7esqlQrPjdIKRbsyMCe9g5kSLpRRQGSr4Q58AOYEe/hCnfLZprh7viNisSUUQZmMrbbuDaIrP+V1ebQ==" }, + "Microsoft.Extensions.Http": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "cWz4caHwvx0emoYe7NkHPxII/KkTI8R/LC9qdqJqnKv2poTJ4e2qqPGQqvRoQ5kaSA4FU5IV3qFAuLuOhoqULQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Diagnostics": "8.0.0", + "Microsoft.Extensions.Logging": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "tvRkov9tAJ3xP51LCv3FJ2zINmv1P8Hi8lhhtcKGqM+ImiTCC84uOPEI4z8Cdq2C3o9e+Aa0Gw0rmrsJD77W+w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0" + } + }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "/HggWBbTwy8TgebGSX5DBZ24ndhzi93sHUBDvP1IxbZD7FDokYzdAr6+vbWGjw2XAfR2EJ1sfKUotpjHnFWPxA==" + "resolved": "8.0.0", + "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + } }, "Microsoft.Extensions.ObjectPool": { "type": "Transitive", "resolved": "5.0.10", "contentHash": "pp9tbGqIhdEXL6Q1yJl+zevAJSq4BsxqhS1GXzBvEsEz9DDNu9GLNzgUy2xyFc4YjB4m4Ff2YEWTnvQvVYdkvQ==" }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.Configuration.Binder": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "8.0.0", @@ -625,6 +692,11 @@ "resolved": "4.4.0", "contentHash": "YhEdSQUsTx+C8m8Bw7ar5/VesXvCFMItyZF7G1AUY+OM0VPZUOeAVpJ4Wl6fydBGUYZxojTDR3I6Bj/+BPkJNA==" }, + "Semver": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "4vYo1zqn6pJ1YrhjuhuOSbIIm0CpM47grbpTJ5ABjOlfGt/EhMEM9ed4MRK5Jr6gVnntWDqOUzGeUJp68PZGjw==" + }, "SharpYaml": { "type": "Transitive", "resolved": "2.1.1", @@ -733,11 +805,8 @@ }, "System.Diagnostics.DiagnosticSource": { "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "KiLYDu2k2J82Q9BJpWiuQqCkFjRBWVq4jDzKKWawVi9KWzyD0XG3cmfX0vqTQlL14Wi9EufJrbL0+KCLTbqWiQ==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } + "resolved": "8.0.0", + "contentHash": "c9xLpVz6PL9lp/djOWtk5KPDZq3cSYpmXoJQY524EOtuFl5z9ZtsotpsyrDW40U1DRnQSYvcPKEUV0X//u6gkQ==" }, "System.Diagnostics.EventLog": { "type": "Transitive", @@ -1183,9 +1252,11 @@ "Microsoft.Extensions.Configuration.Binder": "[8.0.1, )", "Microsoft.Extensions.Configuration.Json": "[8.0.0, )", "Microsoft.Extensions.DependencyInjection": "[8.0.0, )", + "Microsoft.Extensions.Http": "[8.0.0, )", "Microsoft.Graph.Bicep.Types": "[0.1.6-preview, )", "Microsoft.PowerPlatform.ResourceStack": "[7.0.0.2007, )", "Newtonsoft.Json": "[13.0.3, )", + "Semver": "[2.3.0, )", "SharpYaml": "[2.1.1, )", "System.IO.Abstractions": "[21.0.2, )", "System.Private.Uri": "[4.3.2, )" diff --git a/src/Bicep.Local.Extension.Mock/packages.lock.json b/src/Bicep.Local.Extension.Mock/packages.lock.json index 0acdadd3c2b..ed1ab880828 100644 --- a/src/Bicep.Local.Extension.Mock/packages.lock.json +++ b/src/Bicep.Local.Extension.Mock/packages.lock.json @@ -348,6 +348,26 @@ "resolved": "8.0.0", "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==" }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "8.0.0", + "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "System.Diagnostics.DiagnosticSource": "8.0.0" + } + }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", "resolved": "8.0.0", @@ -373,31 +393,34 @@ }, "Microsoft.Extensions.Http": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "15+pa2G0bAMHbHewaQIdr/y6ag2H3yh4rd9hTXavtWDzQBkvpe2RMqFg8BxDpcQWssmjmBApGPcw93QRz6YcMg==", + "resolved": "8.0.0", + "contentHash": "cWz4caHwvx0emoYe7NkHPxII/KkTI8R/LC9qdqJqnKv2poTJ4e2qqPGQqvRoQ5kaSA4FU5IV3qFAuLuOhoqULQ==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging": "6.0.0", - "Microsoft.Extensions.Logging.Abstractions": "6.0.0", - "Microsoft.Extensions.Options": "6.0.0" + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Diagnostics": "8.0.0", + "Microsoft.Extensions.Logging": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0" } }, "Microsoft.Extensions.Logging": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "eIbyj40QDg1NDz0HBW0S5f3wrLVnKWnDJ/JtZ+yJDFnDj90VoPuoPmFkeaXrtu+0cKm5GRAwoDf+dBWXK0TUdg==", + "resolved": "8.0.0", + "contentHash": "tvRkov9tAJ3xP51LCv3FJ2zINmv1P8Hi8lhhtcKGqM+ImiTCC84uOPEI4z8Cdq2C3o9e+Aa0Gw0rmrsJD77W+w==", "dependencies": { - "Microsoft.Extensions.DependencyInjection": "6.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging.Abstractions": "6.0.0", - "Microsoft.Extensions.Options": "6.0.0", - "System.Diagnostics.DiagnosticSource": "6.0.0" + "Microsoft.Extensions.DependencyInjection": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0" } }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "/HggWBbTwy8TgebGSX5DBZ24ndhzi93sHUBDvP1IxbZD7FDokYzdAr6+vbWGjw2XAfR2EJ1sfKUotpjHnFWPxA==" + "resolved": "8.0.0", + "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + } }, "Microsoft.Extensions.ObjectPool": { "type": "Transitive", @@ -406,11 +429,23 @@ }, "Microsoft.Extensions.Options": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "dzXN0+V1AyjOe2xcJ86Qbo233KHuLEY0njf/P2Kw8SfJU+d45HNS2ctJdnEnrWbM9Ye2eFgaC5Mj9otRMU6IsQ==", + "resolved": "8.0.0", + "contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Primitives": "6.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.Configuration.Binder": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Primitives": { @@ -606,6 +641,11 @@ "resolved": "4.4.0", "contentHash": "YhEdSQUsTx+C8m8Bw7ar5/VesXvCFMItyZF7G1AUY+OM0VPZUOeAVpJ4Wl6fydBGUYZxojTDR3I6Bj/+BPkJNA==" }, + "Semver": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "4vYo1zqn6pJ1YrhjuhuOSbIIm0CpM47grbpTJ5ABjOlfGt/EhMEM9ed4MRK5Jr6gVnntWDqOUzGeUJp68PZGjw==" + }, "SharpYaml": { "type": "Transitive", "resolved": "2.1.1", @@ -682,11 +722,8 @@ }, "System.Diagnostics.DiagnosticSource": { "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "KiLYDu2k2J82Q9BJpWiuQqCkFjRBWVq4jDzKKWawVi9KWzyD0XG3cmfX0vqTQlL14Wi9EufJrbL0+KCLTbqWiQ==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } + "resolved": "8.0.0", + "contentHash": "c9xLpVz6PL9lp/djOWtk5KPDZq3cSYpmXoJQY524EOtuFl5z9ZtsotpsyrDW40U1DRnQSYvcPKEUV0X//u6gkQ==" }, "System.Diagnostics.EventLog": { "type": "Transitive", @@ -1017,9 +1054,11 @@ "Microsoft.Extensions.Configuration.Binder": "[8.0.1, )", "Microsoft.Extensions.Configuration.Json": "[8.0.0, )", "Microsoft.Extensions.DependencyInjection": "[8.0.0, )", + "Microsoft.Extensions.Http": "[8.0.0, )", "Microsoft.Graph.Bicep.Types": "[0.1.6-preview, )", "Microsoft.PowerPlatform.ResourceStack": "[7.0.0.2007, )", "Newtonsoft.Json": "[13.0.3, )", + "Semver": "[2.3.0, )", "SharpYaml": "[2.1.1, )", "System.IO.Abstractions": "[21.0.2, )", "System.Private.Uri": "[4.3.2, )" diff --git a/src/Bicep.RegistryModuleTool.IntegrationTests/packages.lock.json b/src/Bicep.RegistryModuleTool.IntegrationTests/packages.lock.json index 8b8304a5ec7..a0ce182d2ea 100644 --- a/src/Bicep.RegistryModuleTool.IntegrationTests/packages.lock.json +++ b/src/Bicep.RegistryModuleTool.IntegrationTests/packages.lock.json @@ -1012,6 +1012,11 @@ "OmniSharp.Extensions.LanguageProtocol": "0.19.9" } }, + "RichardSzalay.MockHttp": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "QwnauYiaywp65QKFnP+wvgiQ2D8Pv888qB2dyfd7MSVDF06sIvxqASenk+RxsWybyyt+Hu1Y251wQxpHTv3UYg==" + }, "runtime.linux-arm.runtime.native.System.IO.Ports": { "type": "Transitive", "resolved": "6.0.0", @@ -1074,6 +1079,11 @@ "resolved": "4.4.0", "contentHash": "YhEdSQUsTx+C8m8Bw7ar5/VesXvCFMItyZF7G1AUY+OM0VPZUOeAVpJ4Wl6fydBGUYZxojTDR3I6Bj/+BPkJNA==" }, + "Semver": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "4vYo1zqn6pJ1YrhjuhuOSbIIm0CpM47grbpTJ5ABjOlfGt/EhMEM9ed4MRK5Jr6gVnntWDqOUzGeUJp68PZGjw==" + }, "Serilog": { "type": "Transitive", "resolved": "3.1.1", @@ -1733,9 +1743,11 @@ "Microsoft.Extensions.Configuration.Binder": "[8.0.1, )", "Microsoft.Extensions.Configuration.Json": "[8.0.0, )", "Microsoft.Extensions.DependencyInjection": "[8.0.0, )", + "Microsoft.Extensions.Http": "[8.0.0, )", "Microsoft.Graph.Bicep.Types": "[0.1.6-preview, )", "Microsoft.PowerPlatform.ResourceStack": "[7.0.0.2007, )", "Newtonsoft.Json": "[13.0.3, )", + "Semver": "[2.3.0, )", "SharpYaml": "[2.1.1, )", "System.IO.Abstractions": "[21.0.2, )", "System.Private.Uri": "[4.3.2, )" @@ -1795,6 +1807,7 @@ "Microsoft.NET.Test.Sdk": "[17.10.0, )", "Moq": "[4.20.70, )", "Newtonsoft.Json.Schema": "[3.0.16, )", + "RichardSzalay.MockHttp": "[7.0.0, )", "System.IO.Abstractions.TestingHelpers": "[21.0.2, )" } }, diff --git a/src/Bicep.RegistryModuleTool.TestFixtures/packages.lock.json b/src/Bicep.RegistryModuleTool.TestFixtures/packages.lock.json index a8fa1a32066..e30f2c45de2 100644 --- a/src/Bicep.RegistryModuleTool.TestFixtures/packages.lock.json +++ b/src/Bicep.RegistryModuleTool.TestFixtures/packages.lock.json @@ -953,6 +953,11 @@ "OmniSharp.Extensions.LanguageProtocol": "0.19.9" } }, + "RichardSzalay.MockHttp": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "QwnauYiaywp65QKFnP+wvgiQ2D8Pv888qB2dyfd7MSVDF06sIvxqASenk+RxsWybyyt+Hu1Y251wQxpHTv3UYg==" + }, "runtime.linux-arm.runtime.native.System.IO.Ports": { "type": "Transitive", "resolved": "6.0.0", @@ -1015,6 +1020,11 @@ "resolved": "4.4.0", "contentHash": "YhEdSQUsTx+C8m8Bw7ar5/VesXvCFMItyZF7G1AUY+OM0VPZUOeAVpJ4Wl6fydBGUYZxojTDR3I6Bj/+BPkJNA==" }, + "Semver": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "4vYo1zqn6pJ1YrhjuhuOSbIIm0CpM47grbpTJ5ABjOlfGt/EhMEM9ed4MRK5Jr6gVnntWDqOUzGeUJp68PZGjw==" + }, "Serilog": { "type": "Transitive", "resolved": "3.1.1", @@ -1674,9 +1684,11 @@ "Microsoft.Extensions.Configuration.Binder": "[8.0.1, )", "Microsoft.Extensions.Configuration.Json": "[8.0.0, )", "Microsoft.Extensions.DependencyInjection": "[8.0.0, )", + "Microsoft.Extensions.Http": "[8.0.0, )", "Microsoft.Graph.Bicep.Types": "[0.1.6-preview, )", "Microsoft.PowerPlatform.ResourceStack": "[7.0.0.2007, )", "Newtonsoft.Json": "[13.0.3, )", + "Semver": "[2.3.0, )", "SharpYaml": "[2.1.1, )", "System.IO.Abstractions": "[21.0.2, )", "System.Private.Uri": "[4.3.2, )" @@ -1736,6 +1748,7 @@ "Microsoft.NET.Test.Sdk": "[17.10.0, )", "Moq": "[4.20.70, )", "Newtonsoft.Json.Schema": "[3.0.16, )", + "RichardSzalay.MockHttp": "[7.0.0, )", "System.IO.Abstractions.TestingHelpers": "[21.0.2, )" } }, diff --git a/src/Bicep.RegistryModuleTool.UnitTests/packages.lock.json b/src/Bicep.RegistryModuleTool.UnitTests/packages.lock.json index 8b8304a5ec7..a0ce182d2ea 100644 --- a/src/Bicep.RegistryModuleTool.UnitTests/packages.lock.json +++ b/src/Bicep.RegistryModuleTool.UnitTests/packages.lock.json @@ -1012,6 +1012,11 @@ "OmniSharp.Extensions.LanguageProtocol": "0.19.9" } }, + "RichardSzalay.MockHttp": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "QwnauYiaywp65QKFnP+wvgiQ2D8Pv888qB2dyfd7MSVDF06sIvxqASenk+RxsWybyyt+Hu1Y251wQxpHTv3UYg==" + }, "runtime.linux-arm.runtime.native.System.IO.Ports": { "type": "Transitive", "resolved": "6.0.0", @@ -1074,6 +1079,11 @@ "resolved": "4.4.0", "contentHash": "YhEdSQUsTx+C8m8Bw7ar5/VesXvCFMItyZF7G1AUY+OM0VPZUOeAVpJ4Wl6fydBGUYZxojTDR3I6Bj/+BPkJNA==" }, + "Semver": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "4vYo1zqn6pJ1YrhjuhuOSbIIm0CpM47grbpTJ5ABjOlfGt/EhMEM9ed4MRK5Jr6gVnntWDqOUzGeUJp68PZGjw==" + }, "Serilog": { "type": "Transitive", "resolved": "3.1.1", @@ -1733,9 +1743,11 @@ "Microsoft.Extensions.Configuration.Binder": "[8.0.1, )", "Microsoft.Extensions.Configuration.Json": "[8.0.0, )", "Microsoft.Extensions.DependencyInjection": "[8.0.0, )", + "Microsoft.Extensions.Http": "[8.0.0, )", "Microsoft.Graph.Bicep.Types": "[0.1.6-preview, )", "Microsoft.PowerPlatform.ResourceStack": "[7.0.0.2007, )", "Newtonsoft.Json": "[13.0.3, )", + "Semver": "[2.3.0, )", "SharpYaml": "[2.1.1, )", "System.IO.Abstractions": "[21.0.2, )", "System.Private.Uri": "[4.3.2, )" @@ -1795,6 +1807,7 @@ "Microsoft.NET.Test.Sdk": "[17.10.0, )", "Moq": "[4.20.70, )", "Newtonsoft.Json.Schema": "[3.0.16, )", + "RichardSzalay.MockHttp": "[7.0.0, )", "System.IO.Abstractions.TestingHelpers": "[21.0.2, )" } }, diff --git a/src/Bicep.RegistryModuleTool/Extensions/IServiceCollectionExtensions.cs b/src/Bicep.RegistryModuleTool/Extensions/IServiceCollectionExtensions.cs index 1bab295ebc5..3c24021bb22 100644 --- a/src/Bicep.RegistryModuleTool/Extensions/IServiceCollectionExtensions.cs +++ b/src/Bicep.RegistryModuleTool/Extensions/IServiceCollectionExtensions.cs @@ -10,6 +10,7 @@ using Bicep.Core.FileSystem; using Bicep.Core.Registry; using Bicep.Core.Registry.Auth; +using Bicep.Core.Registry.PublicRegistry; using Bicep.Core.Semantics.Namespaces; using Bicep.Core.TypeSystem.Providers; using Bicep.Core.Utils; @@ -35,6 +36,7 @@ public static IServiceCollection AddBicepCompiler(this IServiceCollection servic .AddSingleton() .AddSingleton() .AddSingleton() + .AddPublicRegistryModuleMetadataProviderServices() .AddSingleton(); } } diff --git a/src/Bicep.RegistryModuleTool/packages.lock.json b/src/Bicep.RegistryModuleTool/packages.lock.json index 76845e2d087..51c4f9c56a3 100644 --- a/src/Bicep.RegistryModuleTool/packages.lock.json +++ b/src/Bicep.RegistryModuleTool/packages.lock.json @@ -413,6 +413,16 @@ "resolved": "8.0.0", "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==" }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "8.0.0", + "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" + } + }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", "resolved": "8.0.0", @@ -486,6 +496,19 @@ "Microsoft.Extensions.Logging.Abstractions": "8.0.0" } }, + "Microsoft.Extensions.Http": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "cWz4caHwvx0emoYe7NkHPxII/KkTI8R/LC9qdqJqnKv2poTJ4e2qqPGQqvRoQ5kaSA4FU5IV3qFAuLuOhoqULQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Diagnostics": "8.0.0", + "Microsoft.Extensions.Logging": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0" + } + }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", "resolved": "8.0.0", @@ -574,14 +597,14 @@ }, "Microsoft.Extensions.Options.ConfigurationExtensions": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "bXWINbTn0vC0FYc9GaQTISbxhQLAMrvtbuvD9N6JelEaIS/Pr62wUCinrq5bf1WRBGczt1v4wDhxFtVFNcMdUQ==", + "resolved": "8.0.0", + "contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", - "Microsoft.Extensions.Configuration.Binder": "6.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Options": "6.0.0", - "Microsoft.Extensions.Primitives": "6.0.0" + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.Configuration.Binder": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Primitives": { @@ -777,6 +800,11 @@ "resolved": "4.4.0", "contentHash": "YhEdSQUsTx+C8m8Bw7ar5/VesXvCFMItyZF7G1AUY+OM0VPZUOeAVpJ4Wl6fydBGUYZxojTDR3I6Bj/+BPkJNA==" }, + "Semver": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "4vYo1zqn6pJ1YrhjuhuOSbIIm0CpM47grbpTJ5ABjOlfGt/EhMEM9ed4MRK5Jr6gVnntWDqOUzGeUJp68PZGjw==" + }, "Serilog": { "type": "Transitive", "resolved": "3.1.1", @@ -1231,9 +1259,11 @@ "Microsoft.Extensions.Configuration.Binder": "[8.0.1, )", "Microsoft.Extensions.Configuration.Json": "[8.0.0, )", "Microsoft.Extensions.DependencyInjection": "[8.0.0, )", + "Microsoft.Extensions.Http": "[8.0.0, )", "Microsoft.Graph.Bicep.Types": "[0.1.6-preview, )", "Microsoft.PowerPlatform.ResourceStack": "[7.0.0.2007, )", "Newtonsoft.Json": "[13.0.3, )", + "Semver": "[2.3.0, )", "SharpYaml": "[2.1.1, )", "System.IO.Abstractions": "[21.0.2, )", "System.Private.Uri": "[4.3.2, )" diff --git a/src/Bicep.Tools.Benchmark/packages.lock.json b/src/Bicep.Tools.Benchmark/packages.lock.json index c4363f54886..28884f2b31e 100644 --- a/src/Bicep.Tools.Benchmark/packages.lock.json +++ b/src/Bicep.Tools.Benchmark/packages.lock.json @@ -898,6 +898,11 @@ "System.Memory": "4.5.3" } }, + "RichardSzalay.MockHttp": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "QwnauYiaywp65QKFnP+wvgiQ2D8Pv888qB2dyfd7MSVDF06sIvxqASenk+RxsWybyyt+Hu1Y251wQxpHTv3UYg==" + }, "runtime.linux-arm.runtime.native.System.IO.Ports": { "type": "Transitive", "resolved": "6.0.0", @@ -960,6 +965,11 @@ "resolved": "4.4.0", "contentHash": "YhEdSQUsTx+C8m8Bw7ar5/VesXvCFMItyZF7G1AUY+OM0VPZUOeAVpJ4Wl6fydBGUYZxojTDR3I6Bj/+BPkJNA==" }, + "Semver": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "4vYo1zqn6pJ1YrhjuhuOSbIIm0CpM47grbpTJ5ABjOlfGt/EhMEM9ed4MRK5Jr6gVnntWDqOUzGeUJp68PZGjw==" + }, "SharpYaml": { "type": "Transitive", "resolved": "2.1.1", @@ -1552,9 +1562,11 @@ "Microsoft.Extensions.Configuration.Binder": "[8.0.1, )", "Microsoft.Extensions.Configuration.Json": "[8.0.0, )", "Microsoft.Extensions.DependencyInjection": "[8.0.0, )", + "Microsoft.Extensions.Http": "[8.0.0, )", "Microsoft.Graph.Bicep.Types": "[0.1.6-preview, )", "Microsoft.PowerPlatform.ResourceStack": "[7.0.0.2007, )", "Newtonsoft.Json": "[13.0.3, )", + "Semver": "[2.3.0, )", "SharpYaml": "[2.1.1, )", "System.IO.Abstractions": "[21.0.2, )", "System.Private.Uri": "[4.3.2, )" @@ -1607,6 +1619,7 @@ "Microsoft.NET.Test.Sdk": "[17.10.0, )", "Moq": "[4.20.70, )", "Newtonsoft.Json.Schema": "[3.0.16, )", + "RichardSzalay.MockHttp": "[7.0.0, )", "System.IO.Abstractions.TestingHelpers": "[21.0.2, )" } }, diff --git a/src/Bicep.Wasm/IServiceCollectionExtensions.cs b/src/Bicep.Wasm/IServiceCollectionExtensions.cs index 9aadf189f17..bb2f408aa98 100644 --- a/src/Bicep.Wasm/IServiceCollectionExtensions.cs +++ b/src/Bicep.Wasm/IServiceCollectionExtensions.cs @@ -10,6 +10,7 @@ using Bicep.Core.FileSystem; using Bicep.Core.Registry; using Bicep.Core.Registry.Auth; +using Bicep.Core.Registry.PublicRegistry; using Bicep.Core.Semantics.Namespaces; using Bicep.Core.TypeSystem.Providers; using Bicep.Core.Utils; @@ -34,6 +35,7 @@ public static IServiceCollection AddBicepCore(this IServiceCollection services) .AddSingleton() .AddSingleton() .AddSingleton() + .AddPublicRegistryModuleMetadataProviderServices() .AddSingleton(); public static IServiceCollection AddBicepDecompiler(this IServiceCollection services) => services diff --git a/src/Bicep.Wasm/packages.lock.json b/src/Bicep.Wasm/packages.lock.json index b87802e7a26..f4054e372b8 100644 --- a/src/Bicep.Wasm/packages.lock.json +++ b/src/Bicep.Wasm/packages.lock.json @@ -352,6 +352,26 @@ "resolved": "8.0.1", "contentHash": "fGLiCRLMYd00JYpClraLjJTNKLmMJPnqxMaiRzEBIIvevlzxz33mXy39Lkd48hu1G+N21S7QpaO5ZzKsI6FRuA==" }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "8.0.0", + "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "System.Diagnostics.DiagnosticSource": "8.0.0" + } + }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", "resolved": "8.0.0", @@ -375,6 +395,19 @@ "resolved": "8.0.0", "contentHash": "OK+670i7esqlQrPjdIKRbsyMCe9g5kSLpRRQGSr4Q58AOYEe/hCnfLZprh7viNisSUUQZmMrbbuDaIrP+V1ebQ==" }, + "Microsoft.Extensions.Http": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "cWz4caHwvx0emoYe7NkHPxII/KkTI8R/LC9qdqJqnKv2poTJ4e2qqPGQqvRoQ5kaSA4FU5IV3qFAuLuOhoqULQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Diagnostics": "8.0.0", + "Microsoft.Extensions.Logging": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0" + } + }, "Microsoft.Extensions.Logging": { "type": "Transitive", "resolved": "8.0.0", @@ -407,6 +440,18 @@ "Microsoft.Extensions.Primitives": "8.0.0" } }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.Configuration.Binder": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "8.0.0", @@ -613,6 +658,11 @@ "resolved": "4.4.0", "contentHash": "YhEdSQUsTx+C8m8Bw7ar5/VesXvCFMItyZF7G1AUY+OM0VPZUOeAVpJ4Wl6fydBGUYZxojTDR3I6Bj/+BPkJNA==" }, + "Semver": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "4vYo1zqn6pJ1YrhjuhuOSbIIm0CpM47grbpTJ5ABjOlfGt/EhMEM9ed4MRK5Jr6gVnntWDqOUzGeUJp68PZGjw==" + }, "SharpYaml": { "type": "Transitive", "resolved": "2.1.1", @@ -689,11 +739,8 @@ }, "System.Diagnostics.DiagnosticSource": { "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "KiLYDu2k2J82Q9BJpWiuQqCkFjRBWVq4jDzKKWawVi9KWzyD0XG3cmfX0vqTQlL14Wi9EufJrbL0+KCLTbqWiQ==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } + "resolved": "8.0.0", + "contentHash": "c9xLpVz6PL9lp/djOWtk5KPDZq3cSYpmXoJQY524EOtuFl5z9ZtsotpsyrDW40U1DRnQSYvcPKEUV0X//u6gkQ==" }, "System.Diagnostics.EventLog": { "type": "Transitive", @@ -1038,9 +1085,11 @@ "Microsoft.Extensions.Configuration.Binder": "[8.0.1, )", "Microsoft.Extensions.Configuration.Json": "[8.0.0, )", "Microsoft.Extensions.DependencyInjection": "[8.0.0, )", + "Microsoft.Extensions.Http": "[8.0.0, )", "Microsoft.Graph.Bicep.Types": "[0.1.6-preview, )", "Microsoft.PowerPlatform.ResourceStack": "[7.0.0.2007, )", "Newtonsoft.Json": "[13.0.3, )", + "Semver": "[2.3.0, )", "SharpYaml": "[2.1.1, )", "System.IO.Abstractions": "[21.0.2, )", "System.Private.Uri": "[4.3.2, )" diff --git a/src/vscode-bicep/schemas/bicepconfig.schema.json b/src/vscode-bicep/schemas/bicepconfig.schema.json index 95497036ea0..3abd54a3602 100644 --- a/src/vscode-bicep/schemas/bicepconfig.schema.json +++ b/src/vscode-bicep/schemas/bicepconfig.schema.json @@ -729,6 +729,17 @@ } ] }, + "use-recent-module-versions": { + "allOf": [ + { + "description": "Use recent module versions. Defaults to 'Off'. See https://aka.ms/bicep/linter/use-recent-module-versions", + "type": "object" + }, + { + "$ref": "#/definitions/rule-def-level-off" + } + ] + }, "use-resource-id-functions": { "allOf": [ { @@ -879,4 +890,4 @@ } } } -} \ No newline at end of file +}