diff --git a/src/Bicep.Cli.IntegrationTests/BuildCommandTests.cs b/src/Bicep.Cli.IntegrationTests/BuildCommandTests.cs index b1753ead48f..14bce160bd4 100644 --- a/src/Bicep.Cli.IntegrationTests/BuildCommandTests.cs +++ b/src/Bicep.Cli.IntegrationTests/BuildCommandTests.cs @@ -10,11 +10,12 @@ using Bicep.Core; using Bicep.Core.Configuration; using Bicep.Core.FileSystem; +using Bicep.Core.Modules; using Bicep.Core.Registry; +using Bicep.Core.Registry.Oci; using Bicep.Core.Samples; using Bicep.Core.UnitTests; using Bicep.Core.UnitTests.Assertions; -using Bicep.Core.UnitTests.Features; using Bicep.Core.UnitTests.Mock; using Bicep.Core.UnitTests.Registry; using Bicep.Core.UnitTests.Utils; @@ -102,6 +103,73 @@ public async Task Build_Valid_SingleFile_WithTemplateSpecReference_ShouldSucceed actualLocation: compiledFilePath); } + [TestMethod] + public async Task Provider_Artifacts_Restore_From_Registry_ShouldSucceed() + { + // SETUP + // 1. create a mock registry client + var registryUri = new Uri($"https://{LanguageConstants.BicepPublicMcrRegistry}"); + var repository = $"bicep/providers/az"; + var (clientFactory, blobClients) = DataSetsExtensions.CreateMockRegistryClients((registryUri, repository)); + var myClient = blobClients[(registryUri, repository)]; + + // 2. upload a manifest and its blob layer + var manifestStr = $$""" + { + "schemaVersion": 2, + "artifactType": "{{BicepMediaTypes.BicepProviderArtifactType}}", + "config": { + "mediaType": "{{BicepMediaTypes.BicepProviderConfigV1}}", + "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "size": 2 + }, + "layers": [ + { + "mediaType": "{{BicepMediaTypes.BicepProviderArtifactLayerV1TarGzip}}", + "digest": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "size": 0 + } + ], + "annotations": { + "bicep.serialization.format": "v1", + "org.opencontainers.image.created": "2023-05-04T16:40:05Z" + } + } + """; + + await myClient.SetManifestAsync(BinaryData.FromString(manifestStr), "2.0.0"); + await myClient.UploadBlobAsync(new MemoryStream()); + + // 3. create a main.bicep and save it to a output directory + var bicepFile = """ +import 'az@2.0.0' +"""; + var tempDirectory = FileHelper.GetUniqueTestOutputPath(TestContext); + Directory.CreateDirectory(tempDirectory); + var bicepFilePath = Path.Combine(tempDirectory, "main.bicep"); + File.WriteAllText(bicepFilePath, bicepFile); + + // 4. create a settings object with the mock registry client and relevant features enabled + var settings = new InvocationSettings(new(TestContext, RegistryEnabled: true, ExtensibilityEnabled: true, DynamicTypeLoading: true), clientFactory.Object, Repository.Create().Object); + + // TEST + // 5. run bicep build + var (output, error, result) = await Bicep(settings, "build", bicepFilePath); + + // ASSERT + // 6. assert 'bicep build' completed successfully + using (new AssertionScope()) + { + result.Should().Be(0); + output.Should().BeEmpty(); + AssertNoErrors(error); + } + // 7. assert the provider files were restored to the cache directory + Directory.Exists(settings.FeatureOverrides.CacheRootDirectory).Should().BeTrue(); + var providerDir = Path.Combine(settings.FeatureOverrides.CacheRootDirectory!, ModuleReferenceSchemes.Oci, LanguageConstants.BicepPublicMcrRegistry, "bicep$providers$az", "2.0.0$"); + Directory.EnumerateFiles(providerDir).ToList().Select(Path.GetFileName).Should().BeEquivalentTo(new List { "types.tgz", "lock", "manifest", "metadata" }); + } + [DataTestMethod] [DynamicData(nameof(GetValidDataSets), DynamicDataSourceType.Method, DynamicDataDisplayNameDeclaringType = typeof(DataSet), DynamicDataDisplayName = nameof(DataSet.GetDisplayName))] public async Task Build_Valid_SingleFile_WithTemplateSpecReference_ToStdOut_ShouldSucceed(DataSet dataSet) @@ -218,11 +286,11 @@ public async Task Build_Valid_SingleFile_WithDigestReference_ShouldSucceed() string digest = client.Manifests.Single().Key; - var bicep = $@" -module empty 'br:{registry}/{repository}@{digest}' = {{ - name: 'empty' -}} -"; + var bicep = $$""" +module empty 'br:{{registry}}/{{repository}}@{{digest}}' = { + name: 'empty' +} +"""; var bicepFilePath = Path.Combine(tempDirectory, "built.bicep"); File.WriteAllText(bicepFilePath, bicep); @@ -275,9 +343,12 @@ public async Task Build_Invalid_SingleFile_ToStdOut_ShouldFail_WithExpectedError [TestMethod] public async Task Build_WithOutFile_ShouldSucceed() { - var bicepPath = FileHelper.SaveResultFile(TestContext, "input.bicep", @" -output myOutput string = 'hello!' - "); + var bicepPath = FileHelper.SaveResultFile( + TestContext, + "input.bicep", + """ + output myOutput string = 'hello!' + """); var outputFilePath = FileHelper.GetResultFilePath(TestContext, "output.json"); @@ -293,9 +364,12 @@ public async Task Build_WithOutFile_ShouldSucceed() [TestMethod] public async Task Build_WithNonExistantOutDir_ShouldFail_WithExpectedErrorMessage() { - var bicepPath = FileHelper.SaveResultFile(TestContext, "input.bicep", @" -output myOutput string = 'hello!' - "); + var bicepPath = FileHelper.SaveResultFile( + TestContext, + "input.bicep", + """ + output myOutput string = 'hello!' + """); var outputFileDir = FileHelper.GetResultFilePath(TestContext, "outputdir"); var (output, error, result) = await Bicep("build", "--outdir", outputFileDir, bicepPath); @@ -305,49 +379,54 @@ public async Task Build_WithNonExistantOutDir_ShouldFail_WithExpectedErrorMessag error.Should().MatchRegex(@"The specified output directory "".*outputdir"" does not exist"); } - [DataRow(new string[] {})] - [DataRow(new[] { "--diagnostics-format", "defAULt"})] - [DataRow(new[] { "--diagnostics-format", "sArif"})] + [DataRow(new string[] { })] + [DataRow(new[] { "--diagnostics-format", "defAULt" })] + [DataRow(new[] { "--diagnostics-format", "sArif" })] [DataTestMethod] public async Task Build_WithOutDir_ShouldSucceed(string[] args) { - var bicepPath = FileHelper.SaveResultFile(TestContext, "input.bicep", @" -output myOutput string = 'hello!' - "); + var bicepPath = FileHelper.SaveResultFile( + TestContext, + "input.bicep", + """ + output myOutput string = 'hello!' + """); var outputFileDir = FileHelper.GetResultFilePath(TestContext, "outputdir"); Directory.CreateDirectory(outputFileDir); var expectedOutputFile = Path.Combine(outputFileDir, "input.json"); File.Exists(expectedOutputFile).Should().BeFalse(); - var (output, error, result) = await Bicep(new[] { "build", "--outdir", outputFileDir, bicepPath}.Concat(args).ToArray()); + var (output, error, result) = await Bicep(new[] { "build", "--outdir", outputFileDir, bicepPath }.Concat(args).ToArray()); File.Exists(expectedOutputFile).Should().BeTrue(); output.Should().BeEmpty(); if (Array.Exists(args, x => x.Equals("sarif", StringComparison.OrdinalIgnoreCase))) { var errorJToken = JToken.Parse(error); - var expectedErrorJToken = JToken.Parse(@"{ - ""$schema"": ""https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.6.json"", - ""version"": ""2.1.0"", - ""runs"": [ - { - ""tool"": { - ""driver"": { - ""name"": ""bicep"" - } - }, - ""results"": [], - ""columnKind"": ""utf16CodeUnits"" - } - ] -}"); + var expectedErrorJToken = JToken.Parse(""" + { + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.6.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "bicep" + } + }, + "results": [], + "columnKind": "utf16CodeUnits" + } + ] + } + """); errorJToken.Should().EqualWithJsonDiffOutput( - TestContext, - expectedErrorJToken, - "", - "", - validateLocation: false); + TestContext, + expectedErrorJToken, + "", + "", + validateLocation: false); } else { @@ -410,15 +489,21 @@ public async Task Build_WithInvalidBicepConfig_ShouldProduceConfigurationError() { string testOutputPath = FileHelper.GetUniqueTestOutputPath(TestContext); var inputFile = FileHelper.SaveResultFile(this.TestContext, "main.bicep", DataSets.Empty.Bicep, testOutputPath); - var configurationPath = FileHelper.SaveResultFile(this.TestContext, "bicepconfig.json", @"{ - ""analyzers"": { - ""core"": { - ""verbose"": false, - ""enabled"": true, - ""rules"": { - ""no-unused-params"": { - ""level"": ""info"" -", testOutputPath); + var configurationPath = FileHelper.SaveResultFile( + this.TestContext, + "bicepconfig.json", + """ + { + "analyzers": { + "core": { + "verbose": false, + "enabled": true, + "rules": { + "no-unused-params": { + "level": "info" + + """, + testOutputPath); var (output, error, result) = await Bicep("build", inputFile); @@ -427,31 +512,37 @@ public async Task Build_WithInvalidBicepConfig_ShouldProduceConfigurationError() error.Should().StartWith($"{inputFile}(1,1) : Error BCP271: Failed to parse the contents of the Bicep configuration file \"{configurationPath}\" as valid JSON: \"Expected depth to be zero at the end of the JSON payload. There is an open JSON object or array that should be closed. LineNumber: 8 | BytePositionInLine: 0.\"."); } - [DataRow(new string[] {})] - [DataRow(new[] { "--diagnostics-format", "defAULt"})] + [DataRow(new string[] { })] + [DataRow(new[] { "--diagnostics-format", "defAULt" })] [DataTestMethod] public async Task Build_WithValidBicepConfig_ShouldProduceOutputFileAndExpectedError(string[] args) { string testOutputPath = FileHelper.GetUniqueTestOutputPath(TestContext); var inputFile = FileHelper.SaveResultFile(this.TestContext, "main.bicep", @"param storageAccountName string = 'test'", testOutputPath); - FileHelper.SaveResultFile(this.TestContext, "bicepconfig.json", @"{ - ""analyzers"": { - ""core"": { - ""verbose"": false, - ""enabled"": true, - ""rules"": { - ""no-unused-params"": { - ""level"": ""warning"" - } - } - } - } -}", testOutputPath); + FileHelper.SaveResultFile( + this.TestContext, + "bicepconfig.json", + """ + { + "analyzers": { + "core": { + "verbose": false, + "enabled": true, + "rules": { + "no-unused-params": { + "level": "warning" + } + } + } + } + } + """, + testOutputPath); var expectedOutputFile = Path.Combine(testOutputPath, "main.json"); File.Exists(expectedOutputFile).Should().BeFalse(); - var (output, error, result) = await Bicep(new[] { "build", "--outdir", testOutputPath, inputFile}.Concat(args).ToArray()); + var (output, error, result) = await Bicep(new[] { "build", "--outdir", testOutputPath, inputFile }.Concat(args).ToArray()); File.Exists(expectedOutputFile).Should().BeTrue(); result.Should().Be(0); @@ -464,19 +555,25 @@ public async Task Build_WithValidBicepConfig_ShouldProduceOutputFileAndExpectedE { string testOutputPath = FileHelper.GetUniqueTestOutputPath(TestContext); var inputFile = FileHelper.SaveResultFile(this.TestContext, "main.bicep", @"param storageAccountName string = 'test'", testOutputPath); - FileHelper.SaveResultFile(this.TestContext, "bicepconfig.json", @"{ - ""analyzers"": { - ""core"": { - ""verbose"": false, - ""enabled"": true, - ""rules"": { - ""no-unused-params"": { - ""level"": ""warning"" - } - } - } - } -}", testOutputPath); + FileHelper.SaveResultFile( + this.TestContext, + "bicepconfig.json", + """ + { + "analyzers":{ + "core":{ + "verbose":false, + "enabled":true, + "rules":{ + "no-unused-params":{ + "level":"warning" + } + } + } + } + } + """, + testOutputPath); var expectedOutputFile = Path.Combine(testOutputPath, "main.json"); @@ -488,52 +585,54 @@ public async Task Build_WithValidBicepConfig_ShouldProduceOutputFileAndExpectedE result.Should().Be(0); output.Should().BeEmpty(); var errorJToken = JToken.Parse(error); - var expectedErrorJToken = JToken.Parse(@"{ - ""$schema"": ""https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.6.json"", - ""version"": ""2.1.0"", - ""runs"": [ - { - ""tool"": { - ""driver"": { - ""name"": ""bicep"" - } - }, - ""results"": [ - { - ""ruleId"": ""no-unused-params"", - ""message"": { - ""text"": ""Parameter \""storageAccountName\"" is declared but never used. [https://aka.ms/bicep/linter/no-unused-params]"" - }, - ""locations"": [ + var expectedErrorJToken = JToken.Parse(""" +{ + "$schema":"https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.6.json", + "version":"2.1.0", + "runs":[ + { + "tool":{ + "driver":{ + "name":"bicep" + } + }, + "results":[ { - ""physicalLocation"": { - ""artifactLocation"": { - ""uri"": ""main.bicep"" - }, - ""region"": { - ""startLine"": 1, - ""charOffset"": 7 - } - } + "ruleId":"no-unused-params", + "message":{ + "text":"Parameter \"storageAccountName\" is declared but never used. [https://aka.ms/bicep/linter/no-unused-params]" + }, + "locations":[ + { + "physicalLocation":{ + "artifactLocation":{ + "uri":"main.bicep" + }, + "region":{ + "startLine":1, + "charOffset":7 + } + } + } + ] } - ] - } - ], - ""columnKind"": ""utf16CodeUnits"" - } - ] -}"); - var selectedPath = errorJToken.SelectToken("$.runs[0].results[0].locations[0].physicalLocation.artifactLocation.uri"); - selectedPath.Should().NotBeNull(); - selectedPath?.Value().Should().Contain("file://"); - selectedPath?.Value().Should().Contain("main.bicep"); - selectedPath?.Replace("main.bicep"); - errorJToken.Should().EqualWithJsonDiffOutput( - TestContext, - expectedErrorJToken, - "", - "", - validateLocation: false); + ], + "columnKind":"utf16CodeUnits" + } + ] +} +"""); + var selectedPath = errorJToken.SelectToken("$.runs[0].results[0].locations[0].physicalLocation.artifactLocation.uri"); + selectedPath.Should().NotBeNull(); + selectedPath?.Value().Should().Contain("file://"); + selectedPath?.Value().Should().Contain("main.bicep"); + selectedPath?.Replace("main.bicep"); + errorJToken.Should().EqualWithJsonDiffOutput( + TestContext, + expectedErrorJToken, + "", + "", + validateLocation: false); } private static IEnumerable GetValidDataSets() => DataSets diff --git a/src/Bicep.Cli.IntegrationTests/RestoreCommandTests.cs b/src/Bicep.Cli.IntegrationTests/RestoreCommandTests.cs index 91822712d87..77059464725 100644 --- a/src/Bicep.Cli.IntegrationTests/RestoreCommandTests.cs +++ b/src/Bicep.Cli.IntegrationTests/RestoreCommandTests.cs @@ -157,14 +157,13 @@ public async Task Restore_ArtifactWithoutArtifactType_ShouldSucceed() using (var compiledStream = new BufferedMemoryStream()) { - OciArtifactModuleReference.TryParse(null, $"{registry}/{repository}:v1", configuration, new Uri("file:///main.bicep"), out var moduleReference, out _).Should().BeTrue(); - + OciModuleReference.TryParse(null, $"{registry}/{repository}:v1", configuration, new Uri("file:///main.bicep"), out var artifactReference, out _).Should().BeTrue(); compiledStream.Write(TemplateEmitter.UTF8EncodingWithoutBom.GetBytes(dataSet.Compiled!)); compiledStream.Position = 0; await containerRegistryManager.PushArtifactAsync( configuration: configuration, - moduleReference: moduleReference!, + artifactReference: artifactReference!, // intentionally setting artifactType to null to simulate a publish done by an older version of Bicep artifactType: null, config: new StreamDescriptor(Stream.Null, BicepMediaTypes.BicepModuleConfigV1), @@ -219,14 +218,14 @@ public async Task Restore_With_Force_Should_Overwrite_Existing_Cache() Directory.CreateDirectory(tempDirectory); var publishedBicepFilePath = Path.Combine(tempDirectory, "module.bicep"); - File.WriteAllText(publishedBicepFilePath,@" + File.WriteAllText(publishedBicepFilePath, @" param p1 string output o1 string = p1"); - var (publishOutput, publishError, publishResult) = await Bicep(settings, "publish", publishedBicepFilePath, "--target", $"br:{registry}/{repository}:v1"); + var (publishOutput, publishError, exitCode) = await Bicep(settings, "publish", publishedBicepFilePath, "--target", $"br:{registry}/{repository}:v1"); using (new AssertionScope()) { - publishResult.Should().Be(0); + exitCode.Should().Be(0); publishOutput.Should().BeEmpty(); publishError.Should().BeEmpty(); } @@ -273,10 +272,10 @@ param p1 string param p2 string output o1 string = '${p1}${p2}'"); - (publishOutput, publishError, publishResult) = await Bicep(settings, "publish", publishedBicepFilePath, "--target", $"br:{registry}/{repository}:v1", "--force"); + (publishOutput, publishError, exitCode) = await Bicep(settings, "publish", publishedBicepFilePath, "--target", $"br:{registry}/{repository}:v1", "--force"); using (new AssertionScope()) { - publishResult.Should().Be(0); + exitCode.Should().Be(0); publishOutput.Should().BeEmpty(); publishError.Should().BeEmpty(); } @@ -386,13 +385,15 @@ public async Task Restore_NonExistentModules_ShouldFail(DataSet dataSet) var settings = new InvocationSettings(new(TestContext, RegistryEnabled: dataSet.HasExternalModules), clientFactory, templateSpecRepositoryFactory); TestContext.WriteLine($"Cache root = {settings.FeatureOverrides.CacheRootDirectory}"); - var (output, error, result) = await Bicep(settings, "restore", bicepFilePath); + var (output, error, exitCode) = await Bicep(settings, "restore", bicepFilePath); using (new AssertionScope()) { - result.Should().Be(1); + exitCode.Should().Be(1); output.Should().BeEmpty(); - error.Should().ContainAll(": Error BCP192: Unable to restore the module with reference ", "The module does not exist in the registry."); + error.Should().ContainAll(": Error BCP192: Unable to restore the module with reference ", "The artifact does not exist in the registry."); + + } } diff --git a/src/Bicep.Cli/Commands/PublishCommand.cs b/src/Bicep.Cli/Commands/PublishCommand.cs index 9ace2409167..b6047864ee3 100644 --- a/src/Bicep.Cli/Commands/PublishCommand.cs +++ b/src/Bicep.Cli/Commands/PublishCommand.cs @@ -9,10 +9,8 @@ using Bicep.Core.Exceptions; using Bicep.Core.FileSystem; using Bicep.Core.Modules; -using Bicep.Core.Parsing; using Bicep.Core.Registry; using System; -using System.Data.Common; using System.IO; using System.IO.Abstractions; using System.Threading.Tasks; diff --git a/src/Bicep.Cli/Services/CompilationService.cs b/src/Bicep.Cli/Services/CompilationService.cs index 10ae2399f5c..cf81bd6398c 100644 --- a/src/Bicep.Cli/Services/CompilationService.cs +++ b/src/Bicep.Cli/Services/CompilationService.cs @@ -6,6 +6,7 @@ using Bicep.Core.Configuration; using Bicep.Core.Diagnostics; using Bicep.Core.Extensions; +using Bicep.Core.Features; using Bicep.Core.FileSystem; using Bicep.Core.Navigation; using Bicep.Core.Registry; @@ -29,6 +30,7 @@ public class CompilationService private readonly IDiagnosticLogger diagnosticLogger; private readonly IModuleDispatcher moduleDispatcher; private readonly IConfigurationManager configurationManager; + private readonly IFeatureProviderFactory featureProviderFactory; private readonly Workspace workspace; public CompilationService( @@ -37,7 +39,8 @@ public CompilationService( BicepparamDecompiler paramDecompiler, IDiagnosticLogger diagnosticLogger, IModuleDispatcher moduleDispatcher, - IConfigurationManager configurationManager) + IConfigurationManager configurationManager, + IFeatureProviderFactory featureProviderFactory) { this.bicepCompiler = bicepCompiler; this.decompiler = decompiler; @@ -46,6 +49,7 @@ public CompilationService( this.moduleDispatcher = moduleDispatcher; this.configurationManager = configurationManager; this.workspace = new Workspace(); + this.featureProviderFactory = featureProviderFactory; } public async Task RestoreAsync(string inputPath, bool forceModulesRestore) @@ -65,7 +69,7 @@ public async Task RestoreAsync(string inputPath, bool forceModulesRestore) await moduleDispatcher.RestoreModules(modulesToRestoreReferences, forceModulesRestore); // update the errors based on restore status - var sourceFileGrouping = SourceFileGroupingBuilder.Rebuild(this.moduleDispatcher, this.workspace, compilation.SourceFileGrouping); + var sourceFileGrouping = SourceFileGroupingBuilder.Rebuild(featureProviderFactory, this.moduleDispatcher, this.workspace, compilation.SourceFileGrouping); LogDiagnostics(GetModuleRestoreDiagnosticsByBicepFile(sourceFileGrouping, originalModulesToRestore, forceModulesRestore)); } @@ -131,16 +135,18 @@ public DecompileResult DecompileParams(string inputPath, string outputPath, stri return decompilation; } - private static ImmutableDictionary> GetModuleRestoreDiagnosticsByBicepFile(SourceFileGrouping sourceFileGrouping, ImmutableHashSet originalModulesToRestore, bool forceModulesRestore) + private static ImmutableDictionary> GetModuleRestoreDiagnosticsByBicepFile(SourceFileGrouping sourceFileGrouping, ImmutableHashSet originalModulesToRestore, bool forceModulesRestore) { - static IDiagnostic? DiagnosticForModule(SourceFileGrouping grouping, IForeignTemplateReference module) + static IDiagnostic? DiagnosticForModule(SourceFileGrouping grouping, IForeignArtifactReference module) => grouping.TryGetErrorDiagnostic(module) is { } errorBuilder ? errorBuilder(DiagnosticBuilder.ForPosition(module.ReferenceSourceSyntax)) : null; - static IEnumerable<(BicepFile, IDiagnostic)> GetDiagnosticsForModulesToRestore(SourceFileGrouping grouping, ImmutableHashSet originalModulesToRestore) + static IEnumerable<(BicepFile, IDiagnostic)> GetDiagnosticsForModulesToRestore(SourceFileGrouping grouping, ImmutableHashSet originalArtifactsToRestore) { + var originalModulesToRestore = originalArtifactsToRestore.OfType(); foreach (var (module, sourceFile) in originalModulesToRestore) { - if (sourceFile is BicepFile bicepFile && DiagnosticForModule(grouping, module) is { } diagnostic) + if (sourceFile is BicepFile bicepFile && + DiagnosticForModule(grouping, module) is { } diagnostic) { yield return (bicepFile, diagnostic); } diff --git a/src/Bicep.Core.IntegrationTests/Emit/TemplateEmitterTests.cs b/src/Bicep.Core.IntegrationTests/Emit/TemplateEmitterTests.cs index 96338cc3659..6e80df1dbad 100644 --- a/src/Bicep.Core.IntegrationTests/Emit/TemplateEmitterTests.cs +++ b/src/Bicep.Core.IntegrationTests/Emit/TemplateEmitterTests.cs @@ -13,22 +13,17 @@ using Bicep.Core.Parsing; using Bicep.Core.Registry; using Bicep.Core.Samples; -using Bicep.Core.Semantics; using Bicep.Core.UnitTests; using Bicep.Core.UnitTests.Assertions; using Bicep.Core.UnitTests.Features; -using Bicep.Core.UnitTests.Mock; using Bicep.Core.UnitTests.Utils; using Bicep.Core.Workspaces; using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using FluentAssertions.Execution; using Bicep.Core.UnitTests.Baselines; -using System; -using System.Reflection; -using System.Text.RegularExpressions; + namespace Bicep.Core.IntegrationTests.Emit { @@ -53,10 +48,10 @@ private async Task GetSourceFileGrouping(DataSet dataSet) var configManager = BicepTestConstants.CreateFilesystemConfigurationManager(); var dispatcher = new ModuleDispatcher(new DefaultModuleRegistryProvider(BicepTestConstants.EmptyServiceProvider, BicepTestConstants.FileResolver, clientFactory, templateSpecRepositoryFactory, BicepTestConstants.CreateFeatureProviderFactory(new(TestContext, RegistryEnabled: dataSet.HasExternalModules), configManager), configManager), configManager); Workspace workspace = new(); - var sourceFileGrouping = SourceFileGroupingBuilder.Build(BicepTestConstants.FileResolver, dispatcher, workspace, PathHelper.FilePathToFileUrl(bicepFilePath)); + var sourceFileGrouping = SourceFileGroupingBuilder.Build(BicepTestConstants.FileResolver, dispatcher, workspace, PathHelper.FilePathToFileUrl(bicepFilePath), BicepTestConstants.FeatureProviderFactory); if (await dispatcher.RestoreModules(dispatcher.GetValidModuleReferences(sourceFileGrouping.GetModulesToRestore()))) { - sourceFileGrouping = SourceFileGroupingBuilder.Rebuild(dispatcher, workspace, sourceFileGrouping); + sourceFileGrouping = SourceFileGroupingBuilder.Rebuild(BicepTestConstants.FeatureProviderFactory, dispatcher, workspace, sourceFileGrouping); } return sourceFileGrouping; @@ -222,7 +217,7 @@ public void InvalidBicep_TemplateEmiterShouldNotProduceAnyTemplate(DataSet dataS // emitting the template should fail var dispatcher = new ModuleDispatcher(BicepTestConstants.RegistryProvider, IConfigurationManager.WithStaticConfiguration(BicepTestConstants.BuiltInConfigurationWithAllAnalyzersDisabled)); - var result = this.EmitTemplate(SourceFileGroupingBuilder.Build(BicepTestConstants.FileResolver, dispatcher, new Workspace(), PathHelper.FilePathToFileUrl(bicepFilePath)), new(), filePath); + var result = this.EmitTemplate(SourceFileGroupingBuilder.Build(BicepTestConstants.FileResolver, dispatcher, new Workspace(), PathHelper.FilePathToFileUrl(bicepFilePath), BicepTestConstants.FeatureProviderFactory), new(), filePath); result.Diagnostics.Should().NotBeEmpty(); result.Status.Should().Be(EmitStatus.Failed); } @@ -235,7 +230,7 @@ public void Valid_bicepparam_TemplateEmiter_should_produce_expected_template(Bas var data = baselineData.GetData(TestContext); data.Compiled.Should().NotBeNull(); - var sourceFileGrouping = SourceFileGroupingBuilder.Build(BicepTestConstants.FileResolver, BicepTestConstants.ModuleDispatcher, new Workspace(), PathHelper.FilePathToFileUrl(data.Parameters.OutputFilePath)); + var sourceFileGrouping = SourceFileGroupingBuilder.Build(BicepTestConstants.FileResolver, BicepTestConstants.ModuleDispatcher, new Workspace(), PathHelper.FilePathToFileUrl(data.Parameters.OutputFilePath), BicepTestConstants.FeatureProviderFactory); var result = this.EmitParam(sourceFileGrouping, data.Compiled!.OutputFilePath); result.Diagnostics.Should().NotHaveErrors(); @@ -251,7 +246,7 @@ public void Invalid_bicepparam_TemplateEmiter_should_not_produce_a_template(Base { var data = baselineData.GetData(TestContext); - var sourceFileGrouping = SourceFileGroupingBuilder.Build(BicepTestConstants.FileResolver, BicepTestConstants.ModuleDispatcher, new Workspace(), PathHelper.FilePathToFileUrl(data.Parameters.OutputFilePath)); + var sourceFileGrouping = SourceFileGroupingBuilder.Build(BicepTestConstants.FileResolver, BicepTestConstants.ModuleDispatcher, new Workspace(), PathHelper.FilePathToFileUrl(data.Parameters.OutputFilePath), BicepTestConstants.FeatureProviderFactory); var result = this.EmitParam(sourceFileGrouping, Path.ChangeExtension(data.Parameters.OutputFilePath, ".json")); result.Diagnostics.Should().NotBeEmpty(); diff --git a/src/Bicep.Core.IntegrationTests/ExamplesTests.cs b/src/Bicep.Core.IntegrationTests/ExamplesTests.cs index f1462b7a419..3fb3d14f74b 100644 --- a/src/Bicep.Core.IntegrationTests/ExamplesTests.cs +++ b/src/Bicep.Core.IntegrationTests/ExamplesTests.cs @@ -41,7 +41,7 @@ private void RunExampleTest(EmbeddedFile embeddedBicep, FeatureProviderOverrides var configManager = IConfigurationManager.WithStaticConfiguration(BicepTestConstants.BuiltInConfigurationWithAllAnalyzersDisabled); var dispatcher = new ModuleDispatcher(BicepTestConstants.RegistryProvider, configManager); - var sourceFileGrouping = SourceFileGroupingBuilder.Build(BicepTestConstants.FileResolver, dispatcher, new Workspace(), PathHelper.FilePathToFileUrl(bicepFile.OutputFilePath)); + var sourceFileGrouping = SourceFileGroupingBuilder.Build(BicepTestConstants.FileResolver, dispatcher, new Workspace(), PathHelper.FilePathToFileUrl(bicepFile.OutputFilePath), BicepTestConstants.FeatureProviderFactory); var compilation = Services.WithFeatureOverrides(features).Build().BuildCompilation(sourceFileGrouping); var emitter = new TemplateEmitter(compilation.GetEntrypointSemanticModel()); diff --git a/src/Bicep.Core.IntegrationTests/ModuleTests.cs b/src/Bicep.Core.IntegrationTests/ModuleTests.cs index 20a459f34c8..db5f45339fc 100644 --- a/src/Bicep.Core.IntegrationTests/ModuleTests.cs +++ b/src/Bicep.Core.IntegrationTests/ModuleTests.cs @@ -209,7 +209,7 @@ public void SourceFileGroupingBuilder_build_should_throw_diagnostic_exception_if var mockDispatcher = Repository.Create(); SetupFileReaderMock(mockFileResolver, fileUri, null, x => x.ErrorOccurredReadingFile("Mock read failure!")); - Action buildAction = () => SourceFileGroupingBuilder.Build(mockFileResolver.Object, mockDispatcher.Object, new Workspace(), fileUri); + Action buildAction = () => SourceFileGroupingBuilder.Build(mockFileResolver.Object, mockDispatcher.Object, new Workspace(), fileUri, BicepTestConstants.FeatureProviderFactory); buildAction.Should().Throw() .And.Diagnostic.Should().HaveCodeAndSeverity("BCP091", DiagnosticLevel.Error).And.HaveMessage("An error occurred reading file. Mock read failure!"); } diff --git a/src/Bicep.Core.IntegrationTests/RegistryTests.cs b/src/Bicep.Core.IntegrationTests/RegistryTests.cs index f67fe760dd3..ebe94eec1f3 100644 --- a/src/Bicep.Core.IntegrationTests/RegistryTests.cs +++ b/src/Bicep.Core.IntegrationTests/RegistryTests.cs @@ -65,10 +65,10 @@ public async Task InvalidRootCachePathShouldProduceReasonableErrors() var dispatcher = new ModuleDispatcher(new DefaultModuleRegistryProvider(EmptyServiceProvider, BicepTestConstants.FileResolver, clientFactory, templateSpecRepositoryFactory, featuresFactory, BicepTestConstants.ConfigurationManager), BicepTestConstants.ConfigurationManager); var workspace = new Workspace(); - var sourceFileGrouping = SourceFileGroupingBuilder.Build(BicepTestConstants.FileResolver, dispatcher, workspace, fileUri); + var sourceFileGrouping = SourceFileGroupingBuilder.Build(BicepTestConstants.FileResolver, dispatcher, workspace, fileUri, featuresFactory); if (await dispatcher.RestoreModules(dispatcher.GetValidModuleReferences(sourceFileGrouping.GetModulesToRestore()))) { - sourceFileGrouping = SourceFileGroupingBuilder.Rebuild(dispatcher, workspace, sourceFileGrouping); + sourceFileGrouping = SourceFileGroupingBuilder.Rebuild(featuresFactory, dispatcher, workspace, sourceFileGrouping); } var compilation = Services.WithFeatureOverrides(featureOverrides).Build().BuildCompilation(sourceFileGrouping); @@ -186,7 +186,7 @@ public async Task ModuleRestoreContentionShouldProduceConsistentState() // initially the cache should be empty foreach (var moduleReference in moduleReferences) { - dispatcher.GetModuleRestoreStatus(moduleReference, out _).Should().Be(ModuleRestoreStatus.Unknown); + dispatcher.GetModuleRestoreStatus(moduleReference, out _).Should().Be(ArtifactRestoreStatus.Unknown); } const int ConcurrentTasks = 50; @@ -202,7 +202,7 @@ public async Task ModuleRestoreContentionShouldProduceConsistentState() // modules should now be in the cache foreach (var moduleReference in moduleReferences) { - dispatcher.GetModuleRestoreStatus(moduleReference, out _).Should().Be(ModuleRestoreStatus.Succeeded); + dispatcher.GetModuleRestoreStatus(moduleReference, out _).Should().Be(ArtifactRestoreStatus.Succeeded); } } @@ -238,7 +238,7 @@ public async Task ModuleRestoreWithStuckFileLockShouldFailAfterTimeout(IEnumerab // initially the cache should be empty foreach (var moduleReference in moduleReferences) { - dispatcher.GetModuleRestoreStatus(moduleReference, out _).Should().Be(ModuleRestoreStatus.Unknown); + dispatcher.GetModuleRestoreStatus(moduleReference, out _).Should().Be(ArtifactRestoreStatus.Unknown); } dispatcher.TryGetLocalModuleEntryPointUri(moduleReferences[0], out var moduleFileUri, out _).Should().BeTrue(); @@ -261,7 +261,7 @@ public async Task ModuleRestoreWithStuckFileLockShouldFailAfterTimeout(IEnumerab } // the first module should have failed due to a timeout - dispatcher.GetModuleRestoreStatus(moduleReferences[0], out var failureBuilder).Should().Be(ModuleRestoreStatus.Failed); + dispatcher.GetModuleRestoreStatus(moduleReferences[0], out var failureBuilder).Should().Be(ArtifactRestoreStatus.Failed); using (new AssertionScope()) { failureBuilder!.Should().HaveCode("BCP192"); @@ -271,7 +271,7 @@ public async Task ModuleRestoreWithStuckFileLockShouldFailAfterTimeout(IEnumerab // all other modules should have succeeded foreach (var moduleReference in moduleReferences.Skip(1)) { - dispatcher.GetModuleRestoreStatus(moduleReference, out _).Should().Be(ModuleRestoreStatus.Succeeded); + dispatcher.GetModuleRestoreStatus(moduleReference, out _).Should().Be(ArtifactRestoreStatus.Succeeded); } } @@ -306,7 +306,7 @@ public async Task ForceModuleRestoreWithStuckFileLockShouldFailAfterTimeout(IEnu // initially the cache should be empty foreach (var moduleReference in moduleReferences) { - dispatcher.GetModuleRestoreStatus(moduleReference, out _).Should().Be(ModuleRestoreStatus.Unknown); + dispatcher.GetModuleRestoreStatus(moduleReference, out _).Should().Be(ArtifactRestoreStatus.Unknown); } dispatcher.TryGetLocalModuleEntryPointUri(moduleReferences[0], out var moduleFileUri, out _).Should().BeTrue(); @@ -333,18 +333,18 @@ public async Task ForceModuleRestoreWithStuckFileLockShouldFailAfterTimeout(IEnu using (new AssertionScope()) { #if WINDOWS_BUILD - dispatcher.GetModuleRestoreStatus(moduleReferences[0], out var failureBuilder).Should().Be(ModuleRestoreStatus.Failed); + dispatcher.GetModuleRestoreStatus(moduleReferences[0], out var failureBuilder).Should().Be(ArtifactRestoreStatus.Failed); failureBuilder!.Should().HaveCode("BCP233"); failureBuilder!.Should().HaveMessageStartWith($"Unable to delete the module with reference \"{moduleReferences[0].FullyQualifiedReference}\" from cache: Exceeded the timeout of \"00:00:05\" for the lock on file \"{lockFileUri}\" to be released."); #else - dispatcher.GetModuleRestoreStatus(moduleReferences[0], out _).Should().Be(ModuleRestoreStatus.Succeeded); + dispatcher.GetModuleRestoreStatus(moduleReferences[0], out _).Should().Be(ArtifactRestoreStatus.Succeeded); #endif // all other modules should have succeeded foreach (var moduleReference in moduleReferences.Skip(1)) { - dispatcher.GetModuleRestoreStatus(moduleReference, out _).Should().Be(ModuleRestoreStatus.Succeeded); + dispatcher.GetModuleRestoreStatus(moduleReference, out _).Should().Be(ArtifactRestoreStatus.Succeeded); } } @@ -382,7 +382,7 @@ public async Task ForceModuleRestoreShouldRestoreAllModules(IEnumerable CreateMockRegistryClients(th { var target = publishInfo.Metadata.Target; - if (!dispatcher.TryGetModuleReference(target, RandomFileUri(), out var @ref, out _) || @ref is not OciArtifactModuleReference targetReference) + if (!dispatcher.TryGetModuleReference(target, RandomFileUri(), out var @ref, out _) || @ref is not OciModuleReference targetReference) { throw new InvalidOperationException($"Module '{moduleName}' has an invalid target reference '{target}'. Specify a reference to an OCI artifact."); } diff --git a/src/Bicep.Core.UnitTests/IServiceCollectionExtensions.cs b/src/Bicep.Core.UnitTests/IServiceCollectionExtensions.cs index b35771b927b..87705802ef0 100644 --- a/src/Bicep.Core.UnitTests/IServiceCollectionExtensions.cs +++ b/src/Bicep.Core.UnitTests/IServiceCollectionExtensions.cs @@ -12,7 +12,6 @@ using Bicep.Core.Registry; using Bicep.Core.Registry.Auth; using Bicep.Core.Semantics.Namespaces; -using Bicep.Core.Syntax; using Bicep.Core.TypeSystem; using Bicep.Core.TypeSystem.Az; using Bicep.Core.UnitTests.Configuration; diff --git a/src/Bicep.Core.UnitTests/Modules/OciArtifactModuleReferenceTests.cs b/src/Bicep.Core.UnitTests/Modules/OciArtifactModuleReferenceTests.cs index 2b821976ed9..916981a07fe 100644 --- a/src/Bicep.Core.UnitTests/Modules/OciArtifactModuleReferenceTests.cs +++ b/src/Bicep.Core.UnitTests/Modules/OciArtifactModuleReferenceTests.cs @@ -52,6 +52,7 @@ public void ValidReferencesShouldParseCorrectly(ValidCase @case) parsed.Tag.Should().Be(@case.ExpectedTag); parsed.Digest.Should().Be(@case.ExpectedDigest); parsed.ArtifactId.Should().Be(@case.Value); + parsed.UnqualifiedReference.Should().Be(@case.Value); } } @@ -102,7 +103,7 @@ public void ValidReferenceShouldBeUriParseable(ValidCase @case) [DataTestMethod] public void InvalidReferencesShouldProduceExpectedError(string value, string expectedCode, string expectedError) { - OciArtifactModuleReference.TryParse(null, value, BicepTestConstants.BuiltInConfigurationWithAllAnalyzersDisabled, RandomFileUri(), out var @ref, out var failureBuilder).Should().BeFalse(); + OciModuleReference.TryParse(null, value, BicepTestConstants.BuiltInConfigurationWithAllAnalyzersDisabled, RandomFileUri(), out var @ref, out var failureBuilder).Should().BeFalse(); @ref.Should().BeNull(); failureBuilder!.Should().NotBeNull(); @@ -144,7 +145,7 @@ public void MismatchedReferencesShouldNotBeEqual(string package1, string package [DataRow("foo bar ÄÄÄ")] public void TryParse_InvalidAliasName_ReturnsFalseAndSetsErrorDiagnostic(string aliasName) { - OciArtifactModuleReference.TryParse(aliasName, "", BicepTestConstants.BuiltInConfiguration, RandomFileUri(), out var reference, out var errorBuilder).Should().BeFalse(); + OciModuleReference.TryParse(aliasName, "", BicepTestConstants.BuiltInConfiguration, RandomFileUri(), out var reference, out var errorBuilder).Should().BeFalse(); reference.Should().BeNull(); errorBuilder!.Should().HaveCode("BCP211"); @@ -158,7 +159,7 @@ public void TryParse_AliasNotInConfiguration_ReturnsFalseAndSetsErrorDiagnostic( { var configuration = BicepTestConstants.CreateMockConfiguration(configurationPath: configurationPath); - OciArtifactModuleReference.TryParse(aliasName, referenceValue, configuration, RandomFileUri(), out var reference, out var errorBuilder).Should().BeFalse(); + OciModuleReference.TryParse(aliasName, referenceValue, configuration, RandomFileUri(), out var reference, out var errorBuilder).Should().BeFalse(); reference.Should().BeNull(); errorBuilder!.Should().NotBeNull(); @@ -170,7 +171,7 @@ public void TryParse_AliasNotInConfiguration_ReturnsFalseAndSetsErrorDiagnostic( [DynamicData(nameof(GetInvalidAliasData), DynamicDataSourceType.Method)] public void TryParse_InvalidAlias_ReturnsFalseAndSetsErrorDiagnostic(string aliasName, string referenceValue, RootConfiguration configuration, string expectedCode, string expectedMessage) { - OciArtifactModuleReference.TryParse(aliasName, referenceValue, configuration, RandomFileUri(), out var reference, out var errorBuilder).Should().BeFalse(); + OciModuleReference.TryParse(aliasName, referenceValue, configuration, RandomFileUri(), out var reference, out var errorBuilder).Should().BeFalse(); reference.Should().BeNull(); errorBuilder!.Should().NotBeNull(); @@ -182,22 +183,22 @@ public void TryParse_InvalidAlias_ReturnsFalseAndSetsErrorDiagnostic(string alia [DynamicData(nameof(GetValidAliasData), DynamicDataSourceType.Method)] public void TryGetModuleReference_ValidAlias_ReplacesReferenceValue(string aliasName, string referenceValue, string fullyQualifiedReferenceValue, RootConfiguration configuration) { - OciArtifactModuleReference.TryParse(aliasName, referenceValue, configuration, RandomFileUri(), out var reference, out var errorBuilder).Should().BeTrue(); + OciModuleReference.TryParse(aliasName, referenceValue, configuration, RandomFileUri(), out var reference, out var errorBuilder).Should().BeTrue(); reference.Should().NotBeNull(); reference!.FullyQualifiedReference.Should().Be(fullyQualifiedReferenceValue); } - private static OciArtifactModuleReference Parse(string package) + private static OciModuleReference Parse(string package) { - OciArtifactModuleReference.TryParse(null, package, BicepTestConstants.BuiltInConfigurationWithAllAnalyzersDisabled, RandomFileUri(), out var parsed, out var failureBuilder).Should().BeTrue(); + OciModuleReference.TryParse(null, package, BicepTestConstants.BuiltInConfigurationWithAllAnalyzersDisabled, RandomFileUri(), out var parsed, out var failureBuilder).Should().BeTrue(); failureBuilder!.Should().BeNull(); parsed.Should().NotBeNull(); return parsed!; } - private static (OciArtifactModuleReference, OciArtifactModuleReference) ParsePair(string first, string second) => (Parse(first), Parse(second)); + private static (OciModuleReference, OciModuleReference) ParsePair(string first, string second) => (Parse(first), Parse(second)); private static IEnumerable GetValidCases() { diff --git a/src/Bicep.Core.UnitTests/Registry/ModuleDispatcherTests.cs b/src/Bicep.Core.UnitTests/Registry/ArtifactDispatcherTests.cs similarity index 96% rename from src/Bicep.Core.UnitTests/Registry/ModuleDispatcherTests.cs rename to src/Bicep.Core.UnitTests/Registry/ArtifactDispatcherTests.cs index 653c4db37dd..1fc02be332a 100644 --- a/src/Bicep.Core.UnitTests/Registry/ModuleDispatcherTests.cs +++ b/src/Bicep.Core.UnitTests/Registry/ArtifactDispatcherTests.cs @@ -142,14 +142,14 @@ public async Task MockRegistries_ModuleLifecycle() badValidationBuilder!.Should().HaveCode("BCPMock"); badValidationBuilder!.Should().HaveMessage("Bad ref error"); - dispatcher.GetModuleRestoreStatus(validRef, out var goodAvailabilityBuilder).Should().Be(ModuleRestoreStatus.Unknown); + dispatcher.GetModuleRestoreStatus(validRef, out var goodAvailabilityBuilder).Should().Be(ArtifactRestoreStatus.Unknown); goodAvailabilityBuilder!.Should().HaveCode("BCP190"); goodAvailabilityBuilder!.Should().HaveMessage("The module with reference \"mock:validRef\" has not been restored."); - dispatcher.GetModuleRestoreStatus(validRef2, out var goodAvailabilityBuilder2).Should().Be(ModuleRestoreStatus.Succeeded); + dispatcher.GetModuleRestoreStatus(validRef2, out var goodAvailabilityBuilder2).Should().Be(ArtifactRestoreStatus.Succeeded); goodAvailabilityBuilder2!.Should().BeNull(); - dispatcher.GetModuleRestoreStatus(validRef3, out var goodAvailabilityBuilder3).Should().Be(ModuleRestoreStatus.Unknown); + dispatcher.GetModuleRestoreStatus(validRef3, out var goodAvailabilityBuilder3).Should().Be(ArtifactRestoreStatus.Unknown); goodAvailabilityBuilder3!.Should().HaveCode("BCP190"); goodAvailabilityBuilder3!.Should().HaveMessage("The module with reference \"mock:validRef3\" has not been restored."); @@ -163,14 +163,14 @@ public async Task MockRegistries_ModuleLifecycle() (await dispatcher.RestoreModules(new[] { validRef, validRef3 })).Should().BeTrue(); - dispatcher.GetModuleRestoreStatus(validRef3, out var goodAvailabilityBuilder3AfterRestore).Should().Be(ModuleRestoreStatus.Failed); + dispatcher.GetModuleRestoreStatus(validRef3, out var goodAvailabilityBuilder3AfterRestore).Should().Be(ArtifactRestoreStatus.Failed); goodAvailabilityBuilder3AfterRestore!.Should().HaveCode("RegFail"); goodAvailabilityBuilder3AfterRestore!.Should().HaveMessage("Failed to restore module"); } [DataTestMethod] [DynamicData(nameof(GetConfigurationData), DynamicDataSourceType.Method)] - public async Task GetModuleRestoreStatus_ConfigurationChanges_ReturnsCachedStatusWhenChangeIsIrrelevant(RootConfiguration changedConfiguration, ModuleRestoreStatus expectedStatus) + public async Task GetModuleRestoreStatus_ConfigurationChanges_ReturnsCachedStatusWhenChangeIsIrrelevant(RootConfiguration changedConfiguration, ArtifactRestoreStatus expectedStatus) { // Arrange. var badReferenceUri = RandomFileUri(); @@ -214,7 +214,7 @@ private static IEnumerable GetConfigurationData() ["cloud.profiles.AzureCloud.resourceManagerEndpoint"] = "HTTPS://EXAMPLE.INVALID", ["cloud.profiles.AzureCloud.activeDirectoryAuthority"] = "https://example.invalid/", }), - ModuleRestoreStatus.Failed + ArtifactRestoreStatus.Failed }; yield return new object[] @@ -226,7 +226,7 @@ private static IEnumerable GetConfigurationData() ["cloud.profiles.MyCloud.resourceManagerEndpoint"] = "HTTPS://EXAMPLE.INVALID", ["cloud.profiles.MyCloud.activeDirectoryAuthority"] = "https://example.invalid/", }), - ModuleRestoreStatus.Failed + ArtifactRestoreStatus.Failed }; yield return new object[] @@ -238,7 +238,7 @@ private static IEnumerable GetConfigurationData() ["cloud.profiles.MyCloud.resourceManagerEndpoint"] = "https://example.invalid", ["cloud.profiles.MyCloud.activeDirectoryAuthority"] = "https://foo.bar.com", }), - ModuleRestoreStatus.Unknown + ArtifactRestoreStatus.Unknown }; yield return new object[] @@ -248,7 +248,7 @@ private static IEnumerable GetConfigurationData() { ["cloud.credentialPrecedence"] = new[] { "VisualStudioCode" }, }), - ModuleRestoreStatus.Unknown + ArtifactRestoreStatus.Unknown }; } diff --git a/src/Bicep.Core.UnitTests/Registry/OciModuleRegistryTests.cs b/src/Bicep.Core.UnitTests/Registry/OciModuleRegistryTests.cs index 2c18d753a5f..805942ca308 100644 --- a/src/Bicep.Core.UnitTests/Registry/OciModuleRegistryTests.cs +++ b/src/Bicep.Core.UnitTests/Registry/OciModuleRegistryTests.cs @@ -1,9 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Threading.Tasks; using Bicep.Core.Features; using Bicep.Core.Modules; @@ -28,7 +26,7 @@ public class OciModuleRegistryTests [DataTestMethod] public void GetDocumentationUri_WithInvalidManifestContents_ShouldReturnNull(string manifestFileContents) { - (OciModuleRegistry ociModuleRegistry, OciArtifactModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( + (OciModuleRegistry ociModuleRegistry, OciModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( "output myOutput string = 'hello!'", manifestFileContents, "test.azurecr.io", @@ -43,7 +41,7 @@ public void GetDocumentationUri_WithInvalidManifestContents_ShouldReturnNull(str [TestMethod] public void GetDocumentationUri_WithNonExistentManifestFile_ShouldReturnNull() { - (OciModuleRegistry ociModuleRegistry, OciArtifactModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( + (OciModuleRegistry ociModuleRegistry, OciModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( "output myOutput string = 'hello!'", "some_manifest_text", "test.azurecr.io", @@ -77,7 +75,7 @@ public void GetDocumentationUri_WithManifestFileAndNoAnnotations_ShouldReturnNul } ] }"; - (OciModuleRegistry ociModuleRegistry, OciArtifactModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( + (OciModuleRegistry ociModuleRegistry, OciModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( "output myOutput string = 'hello!'", manifestFileContents, "test.azurecr.io", @@ -115,7 +113,7 @@ public void GetDocumentationUri_WithAnnotationsInManifestFileAndInvalidDocumenta ""org.opencontainers.image.documentation"": """ + documentationUri + @""" } }"; - (OciModuleRegistry ociModuleRegistry, OciArtifactModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( + (OciModuleRegistry ociModuleRegistry, OciModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( "output myOutput string = 'hello!'", manifestFileContents, "test.azurecr.io", @@ -150,7 +148,7 @@ public async Task GetDocumentationUri_WithAnnotationsInManifestFile_ButEmpty_Sho ""annotations"": { } }"; - (OciModuleRegistry ociModuleRegistry, OciArtifactModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( + (OciModuleRegistry ociModuleRegistry, OciModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( "output myOutput string = 'hello!'", manifestFileContents, "test.azurecr.io", @@ -189,7 +187,7 @@ public async Task GetDocumentationUri_WithAnnotationsInManifestFile_ButOnlyHasOt ""org.opencontainers.image.notdescription"": """ + "description" + @""" } }"; - (OciModuleRegistry ociModuleRegistry, OciArtifactModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( + (OciModuleRegistry ociModuleRegistry, OciModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( "output myOutput string = 'hello!'", manifestFileContents, "test.azurecr.io", @@ -228,7 +226,7 @@ public void GetDocumentationUri_WithValidDocumentationUriInManifestFile_ShouldRe ""org.opencontainers.image.documentation"": """ + documentationUri + @""" } }"; - (OciModuleRegistry ociModuleRegistry, OciArtifactModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( + (OciModuleRegistry ociModuleRegistry, OciModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( "output myOutput string = 'hello!'", manifestFileContents, "test.azurecr.io", @@ -274,7 +272,7 @@ public void GetDocumentationUri_WithMcrModuleReferenceAndNoDocumentationUriInMan ] } }"; - (OciModuleRegistry ociModuleRegistry, OciArtifactModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( + (OciModuleRegistry ociModuleRegistry, OciModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( bicepFileContents, manifestFileContents, "mcr.microsoft.com", @@ -293,7 +291,7 @@ public void GetDocumentationUri_WithMcrModuleReferenceAndNoDocumentationUriInMan [DataTestMethod] public void GetDescription_WithInvalidManifestContents_ShouldReturnNull(string manifestFileContents) { - (OciModuleRegistry ociModuleRegistry, OciArtifactModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( + (OciModuleRegistry ociModuleRegistry, OciModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( "output myOutput string = 'hello!'", manifestFileContents, "test.azurecr.io", @@ -308,7 +306,7 @@ public void GetDescription_WithInvalidManifestContents_ShouldReturnNull(string m [TestMethod] public async Task GetDescription_WithNonExistentManifestFile_ShouldReturnNull() { - (OciModuleRegistry ociModuleRegistry, OciArtifactModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( + (OciModuleRegistry ociModuleRegistry, OciModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( "output myOutput string = 'hello!'", "some_manifest_text", "test.azurecr.io", @@ -342,7 +340,7 @@ public async Task GetDescription_WithManifestFileAndNoAnnotations_ShouldReturnNu } ] }"; - (OciModuleRegistry ociModuleRegistry, OciArtifactModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( + (OciModuleRegistry ociModuleRegistry, OciModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( "output myOutput string = 'hello!'", manifestFileContents, "test.azurecr.io", @@ -377,7 +375,7 @@ public async Task GetDescription_WithManifestFileAndJustDocumentationUri_ShouldR } ] }"; - (OciModuleRegistry ociModuleRegistry, OciArtifactModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( + (OciModuleRegistry ociModuleRegistry, OciModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( "output myOutput string = 'hello!'", manifestFileContents, "test.azurecr.io", @@ -414,7 +412,7 @@ public async Task GetDescription_WithValidDescriptionInManifestFile_ShouldReturn ""org.opencontainers.image.description"": """ + description + @""" } }"; - (OciModuleRegistry ociModuleRegistry, OciArtifactModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( + (OciModuleRegistry ociModuleRegistry, OciModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( "output myOutput string = 'hello!'", manifestFileContents, "test.azurecr.io", @@ -453,7 +451,7 @@ public async Task GetDescription_WithAnnotationsInManifestFileAndInvalidDescript ""org.opencontainers.image.description"": """ + description + @""" } }"; - (OciModuleRegistry ociModuleRegistry, OciArtifactModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( + (OciModuleRegistry ociModuleRegistry, OciModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( "output myOutput string = 'hello!'", manifestFileContents, "test.azurecr.io", @@ -492,7 +490,7 @@ public async Task GetDescription_WithValidDescriptionAndDocumentationUriInManife ""org.opencontainers.image.description"": """ + description + @""" } }"; - (OciModuleRegistry ociModuleRegistry, OciArtifactModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( + (OciModuleRegistry ociModuleRegistry, OciModuleReference ociArtifactModuleReference) = GetOciModuleRegistryAndOciArtifactModuleReference( "output myOutput string = 'hello!'", manifestFileContents, "test.azurecr.io", @@ -510,7 +508,7 @@ public async Task GetDescription_WithValidDescriptionAndDocumentationUriInManife actualDescription.Should().BeEquivalentTo(description.Replace("\\", "")); // unencode json } - private (OciModuleRegistry, OciArtifactModuleReference) GetOciModuleRegistryAndOciArtifactModuleReference( + private (OciModuleRegistry, OciModuleReference) GetOciModuleRegistryAndOciArtifactModuleReference( string bicepFileContents, string manifestFileContents, string registory, diff --git a/src/Bicep.Core.UnitTests/Registry/TagEncoderTests.cs b/src/Bicep.Core.UnitTests/Registry/TagEncoderTests.cs index 3357d1cc24c..81f19463fa1 100644 --- a/src/Bicep.Core.UnitTests/Registry/TagEncoderTests.cs +++ b/src/Bicep.Core.UnitTests/Registry/TagEncoderTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Bicep.Core.Modules; using Bicep.Core.Registry.Oci; using Bicep.Core.UnitTests.Modules; using FluentAssertions; @@ -44,7 +43,7 @@ public void EncoderShouldThrowWhenMaxTagLengthIsExceeded() => [TestMethod] public void EncodingFullyCapitalizedStringOfMaxLengthShouldNotExceedMaxLinuxFileNameLength() { - var fullyCapitalizedTag = new string('A', OciArtifactModuleReference.MaxTagLength); + var fullyCapitalizedTag = new string('A', OciArtifactReferenceFacts.MaxTagLength); var encoded = TagEncoder.Encode(fullyCapitalizedTag); encoded.Length.Should().BeLessOrEqualTo(255); diff --git a/src/Bicep.Core.UnitTests/ServiceBuilder.cs b/src/Bicep.Core.UnitTests/ServiceBuilder.cs index ed9396c97b1..2a020ed2df8 100644 --- a/src/Bicep.Core.UnitTests/ServiceBuilder.cs +++ b/src/Bicep.Core.UnitTests/ServiceBuilder.cs @@ -39,6 +39,7 @@ public static SourceFileGrouping BuildSourceFileGrouping(this IDependencyHelper helper.Construct(), helper.Construct(), entryFileUri, + helper.Construct(), forceModulesRestore); public static BicepCompiler GetCompiler(this IDependencyHelper helper) diff --git a/src/Bicep.Core.UnitTests/Utils/OciArtifactModuleReferenceHelper.cs b/src/Bicep.Core.UnitTests/Utils/OciArtifactModuleReferenceHelper.cs index 85c2b2be511..7d3ce72f6eb 100644 --- a/src/Bicep.Core.UnitTests/Utils/OciArtifactModuleReferenceHelper.cs +++ b/src/Bicep.Core.UnitTests/Utils/OciArtifactModuleReferenceHelper.cs @@ -4,13 +4,15 @@ using System; using System.IO; using Bicep.Core.Modules; +using Bicep.Core.Registry.Oci; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Bicep.Core.UnitTests.Mock; namespace Bicep.Core.UnitTests.Utils { public static class OciArtifactModuleReferenceHelper { - public static OciArtifactModuleReference GetModuleReferenceAndSaveManifestFile( + public static OciModuleReference GetModuleReferenceAndSaveManifestFile( TestContext testContext, string registry, string repository, @@ -36,7 +38,14 @@ public static OciArtifactModuleReference GetModuleReferenceAndSaveManifestFile( FileHelper.SaveResultFile(testContext, "manifest", manifestFileContents, manifestFilePath); } - return new OciArtifactModuleReference(registry, repository, tag, digest, parentModuleUri); + var artifactReferenceMock = StrictMock.Of(); + artifactReferenceMock.SetupGet(m => m.Registry).Returns(registry); + artifactReferenceMock.SetupGet(m => m.Repository).Returns(repository); + artifactReferenceMock.SetupGet(m => m.Digest).Returns(digest); + artifactReferenceMock.SetupGet(m => m.Tag).Returns(tag); + artifactReferenceMock.SetupGet(m => m.ArtifactId).Returns($"{registry}/{repository}:{tag ?? digest}"); + + return new OciModuleReference(artifactReferenceMock.Object, parentModuleUri); } } } diff --git a/src/Bicep.Core/BicepCompiler.cs b/src/Bicep.Core/BicepCompiler.cs index 397d7f1762a..08e5d948e14 100644 --- a/src/Bicep.Core/BicepCompiler.cs +++ b/src/Bicep.Core/BicepCompiler.cs @@ -41,7 +41,7 @@ public BicepCompiler( public async Task CreateCompilation(Uri bicepUri, IReadOnlyWorkspace? workspace = null, bool skipRestore = false, bool forceModulesRestore = false) { workspace ??= new Workspace(); - var sourceFileGrouping = SourceFileGroupingBuilder.Build(fileResolver, moduleDispatcher, workspace, bicepUri, forceModulesRestore); + var sourceFileGrouping = SourceFileGroupingBuilder.Build(fileResolver, moduleDispatcher, workspace, bicepUri, featureProviderFactory, forceModulesRestore); if (!skipRestore) { @@ -52,7 +52,7 @@ public async Task CreateCompilation(Uri bicepUri, IReadOnlyWorkspac if (await moduleDispatcher.RestoreModules(moduleDispatcher.GetValidModuleReferences(sourceFileGrouping.GetModulesToRestore()))) { // modules had to be restored - recompile - sourceFileGrouping = SourceFileGroupingBuilder.Rebuild(moduleDispatcher, workspace, sourceFileGrouping); + sourceFileGrouping = SourceFileGroupingBuilder.Rebuild(featureProviderFactory, moduleDispatcher, workspace, sourceFileGrouping); } //TODO(asilverman): I want to inject here the logic that restores the providers } diff --git a/src/Bicep.Core/Modules/ModuleReference.cs b/src/Bicep.Core/Modules/ModuleReference.cs index e7d480c818a..7b029267575 100644 --- a/src/Bicep.Core/Modules/ModuleReference.cs +++ b/src/Bicep.Core/Modules/ModuleReference.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System; -using System.Text.RegularExpressions; namespace Bicep.Core.Modules { @@ -17,8 +16,6 @@ protected ModuleReference(string scheme, Uri parentModuleUri) this.ParentModuleUri = parentModuleUri; } - protected static Regex ModuleAliasNameRegex { get; } = new(@"[\w-]"); - public string Scheme { get; } /// diff --git a/src/Bicep.Core/Modules/ModuleReferenceSchemes.cs b/src/Bicep.Core/Modules/ModuleReferenceSchemes.cs index b661ece1b6e..8cd01ba1f2c 100644 --- a/src/Bicep.Core/Modules/ModuleReferenceSchemes.cs +++ b/src/Bicep.Core/Modules/ModuleReferenceSchemes.cs @@ -1,13 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Bicep.Core.Registry.Oci; + namespace Bicep.Core.Modules { public static class ModuleReferenceSchemes { public const string Local = ""; - public const string Oci = "br"; + public const string Oci = OciArtifactReferenceFacts.Scheme; public const string TemplateSpecs = "ts"; } diff --git a/src/Bicep.Core/Modules/OciModuleReference.cs b/src/Bicep.Core/Modules/OciModuleReference.cs new file mode 100644 index 00000000000..2b59a727224 --- /dev/null +++ b/src/Bicep.Core/Modules/OciModuleReference.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Bicep.Core.Configuration; +using Bicep.Core.Diagnostics; +using Bicep.Core.Registry.Oci; +using System; +using System.Diagnostics.CodeAnalysis; + + +namespace Bicep.Core.Modules +{ + /// + /// Represents a reference to an artifact in an OCI registry. + /// + public class OciModuleReference : ModuleReference, IOciArtifactReference + { + private readonly IOciArtifactReference ociArtifactRef; + public OciModuleReference(IOciArtifactReference ociArtifactReference, Uri parentModuleUri) + : base(ModuleReferenceSchemes.Oci, parentModuleUri) + { + this.ociArtifactRef = ociArtifactReference; + } + public override string UnqualifiedReference => this.ArtifactId; + public override bool IsExternal => true; + public override bool Equals(object? obj) => obj is OciModuleReference other && this.ociArtifactRef.Equals(other.ociArtifactRef); + public override int GetHashCode() => this.ociArtifactRef.GetHashCode(); + public string Registry => this.ociArtifactRef.Registry; + public string Repository => this.ociArtifactRef.Repository; + public string? Tag => this.ociArtifactRef.Tag; + public string? Digest => this.ociArtifactRef.Digest; + public string ArtifactId => this.ociArtifactRef.ArtifactId; + + public static bool TryParse( + string? aliasName, + string rawValue, + RootConfiguration configuration, + Uri parentModuleUri, + [NotNullWhen(true)] out OciModuleReference? moduleReference, + [NotNullWhen(false)] out DiagnosticBuilder.ErrorBuilderDelegate? failureBuilder) + { + if (OciArtifactReference.TryParse(aliasName, rawValue, configuration, out var artifactReference, out failureBuilder)) + { + moduleReference = new OciModuleReference(artifactReference, parentModuleUri); + return true; + } + moduleReference = null; + return false; + } + } +} diff --git a/src/Bicep.Core/Navigation/IForeignTemplateReference.cs b/src/Bicep.Core/Navigation/IForeignTemplateReference.cs index a32726e4ab9..82d8e7a1197 100644 --- a/src/Bicep.Core/Navigation/IForeignTemplateReference.cs +++ b/src/Bicep.Core/Navigation/IForeignTemplateReference.cs @@ -4,7 +4,7 @@ namespace Bicep.Core.Navigation; -public interface IForeignTemplateReference +public interface IForeignArtifactReference { public SyntaxBase ReferenceSourceSyntax { get; } diff --git a/src/Bicep.Core/Registry/ModuleRestoreStatus.cs b/src/Bicep.Core/Registry/ArtifactRestoreStatus.cs similarity index 93% rename from src/Bicep.Core/Registry/ModuleRestoreStatus.cs rename to src/Bicep.Core/Registry/ArtifactRestoreStatus.cs index 38e9e9cd459..2c8df67d05b 100644 --- a/src/Bicep.Core/Registry/ModuleRestoreStatus.cs +++ b/src/Bicep.Core/Registry/ArtifactRestoreStatus.cs @@ -6,7 +6,7 @@ namespace Bicep.Core.Registry /// /// Represents the restore status of a module /// - public enum ModuleRestoreStatus + public enum ArtifactRestoreStatus { /// /// We have not yet attempted to restore the module. diff --git a/src/Bicep.Core/Registry/AzureContainerRegistryManager.cs b/src/Bicep.Core/Registry/AzureContainerRegistryManager.cs index 6eedc45710d..3a6c7b59549 100644 --- a/src/Bicep.Core/Registry/AzureContainerRegistryManager.cs +++ b/src/Bicep.Core/Registry/AzureContainerRegistryManager.cs @@ -2,12 +2,12 @@ // Licensed under the MIT License. using System; -using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.IO; using System.Linq; -using System.Reflection.Metadata.Ecma335; +using System.Threading; using System.Threading.Tasks; using Azure; using Azure.Containers.ContainerRegistry; @@ -23,7 +23,6 @@ namespace Bicep.Core.Registry public class AzureContainerRegistryManager { // media types are case-insensitive (they are lowercase by convention only) - private const StringComparison MediaTypeComparison = StringComparison.OrdinalIgnoreCase; private const StringComparison DigestComparison = StringComparison.Ordinal; private readonly IContainerRegistryClientFactory clientFactory; @@ -33,51 +32,50 @@ public AzureContainerRegistryManager(IContainerRegistryClientFactory clientFacto this.clientFactory = clientFactory; } - public async Task PullArtifactAsync(RootConfiguration configuration, OciArtifactModuleReference moduleReference) + public async Task PullArtifactAsync( + RootConfiguration configuration, + IOciArtifactReference artifactReference) { - ContainerRegistryContentClient client; - OciManifest manifest; - Stream manifestStream; - string manifestDigest; - - async Task<(ContainerRegistryContentClient, OciManifest, Stream, string)> DownloadManifestInternalAsync(bool anonymousAccess) + async Task DownloadManifestInternalAsync(bool anonymousAccess) { - var client = this.CreateBlobClient(configuration, moduleReference, anonymousAccess); - var (manifest, manifestStream, manifestDigest) = await DownloadManifestAsync(moduleReference, client); - return (client, manifest, manifestStream, manifestDigest); + var client = CreateBlobClient(configuration, artifactReference, anonymousAccess); + return await DownloadManifestAsync(artifactReference, client); } try { // Try authenticated client first. - Trace.WriteLine($"Authenticated attempt to pull artifact for module {moduleReference.FullyQualifiedReference}."); - (client, manifest, manifestStream, manifestDigest) = await DownloadManifestInternalAsync(anonymousAccess: false); + Trace.WriteLine($"Authenticated attempt to pull artifact for module {artifactReference.FullyQualifiedReference}."); + return await DownloadManifestInternalAsync(anonymousAccess: false); } catch (RequestFailedException exception) when (exception.Status == 401 || exception.Status == 403) { // Fall back to anonymous client. - Trace.WriteLine($"Authenticated attempt to pull artifact for module {moduleReference.FullyQualifiedReference} failed, received code {exception.Status}. Fallback to anonymous pull."); - (client, manifest, manifestStream, manifestDigest) = await DownloadManifestInternalAsync(anonymousAccess: true); + Trace.WriteLine($"Authenticated attempt to pull artifact for module {artifactReference.FullyQualifiedReference} failed, received code {exception.Status}. Fallback to anonymous pull."); + return await DownloadManifestInternalAsync(anonymousAccess: true); } catch (CredentialUnavailableException) { // Fall back to anonymous client. - Trace.WriteLine($"Authenticated attempt to pull artifact for module {moduleReference.FullyQualifiedReference} failed due to missing login step. Fallback to anonymous pull."); - (client, manifest, manifestStream, manifestDigest) = await DownloadManifestInternalAsync(anonymousAccess: true); + Trace.WriteLine($"Authenticated attempt to pull artifact for module {artifactReference.FullyQualifiedReference} failed due to missing login step. Fallback to anonymous pull."); + return await DownloadManifestInternalAsync(anonymousAccess: true); } - - var moduleStream = await ProcessManifest(client, manifest); - - return new OciArtifactResult(manifestDigest, manifest, manifestStream, moduleStream); } - public async Task PushArtifactAsync(RootConfiguration configuration, OciArtifactModuleReference moduleReference, string? artifactType, StreamDescriptor config, string? documentationUri = null, string? description = null, params StreamDescriptor[] layers) + public async Task PushArtifactAsync( + RootConfiguration configuration, + IOciArtifactReference artifactReference, + string? artifactType, + StreamDescriptor config, + string? documentationUri = null, + string? description = null, + params StreamDescriptor[] layers) { // TODO: How do we choose this? Does it ever change? var algorithmIdentifier = DescriptorFactory.AlgorithmIdentifierSha256; // push is not supported anonymously - var blobClient = this.CreateBlobClient(configuration, moduleReference, anonymousAccess: false); + var blobClient = this.CreateBlobClient(configuration, artifactReference, anonymousAccess: false); config.ResetStream(); var configDescriptor = DescriptorFactory.CreateDescriptor(algorithmIdentifier, config); @@ -115,86 +113,71 @@ public async Task PushArtifactAsync(RootConfiguration configuration, OciArtifact manifestStream.Position = 0; var manifestBinaryData = await BinaryData.FromStreamAsync(manifestStream); - var manifestUploadResult = await blobClient.SetManifestAsync(manifestBinaryData, moduleReference.Tag, mediaType: ManifestMediaType.OciImageManifest); + var manifestUploadResult = await blobClient.SetManifestAsync(manifestBinaryData, artifactReference.Tag, mediaType: ManifestMediaType.OciImageManifest); } - private static Uri GetRegistryUri(OciArtifactModuleReference moduleReference) => new($"https://{moduleReference.Registry}"); + private static Uri GetRegistryUri(IOciArtifactReference artifactReference) => new($"https://{artifactReference.Registry}"); - private ContainerRegistryContentClient CreateBlobClient(RootConfiguration configuration, OciArtifactModuleReference moduleReference, bool anonymousAccess) => anonymousAccess - ? this.clientFactory.CreateAnonymousBlobClient(configuration, GetRegistryUri(moduleReference), moduleReference.Repository) - : this.clientFactory.CreateAuthenticatedBlobClient(configuration, GetRegistryUri(moduleReference), moduleReference.Repository); + private ContainerRegistryContentClient CreateBlobClient( + RootConfiguration configuration, + IOciArtifactReference artifactReference, + bool anonymousAccess) => anonymousAccess + ? this.clientFactory.CreateAnonymousBlobClient(configuration, GetRegistryUri(artifactReference), artifactReference.Repository) + : this.clientFactory.CreateAuthenticatedBlobClient(configuration, GetRegistryUri(artifactReference), artifactReference.Repository); - private static async Task<(OciManifest, Stream, string)> DownloadManifestAsync(OciArtifactModuleReference moduleReference, ContainerRegistryContentClient client) + private static async Task DownloadManifestAsync(IOciArtifactReference artifactReference, ContainerRegistryContentClient client) { Response manifestResponse; try { // either Tag or Digest is null (enforced by reference parser) - var tagOrDigest = moduleReference.Tag - ?? moduleReference.Digest - ?? throw new ArgumentNullException(nameof(moduleReference), $"The specified module reference has both {nameof(moduleReference.Tag)} and {nameof(moduleReference.Digest)} set to null."); + var tagOrDigest = artifactReference.Tag + ?? artifactReference.Digest + ?? throw new ArgumentNullException(nameof(artifactReference), $"The specified artifact reference has both {nameof(artifactReference.Tag)} and {nameof(artifactReference.Digest)} set to null."); manifestResponse = await client.GetManifestAsync(tagOrDigest); } catch (RequestFailedException exception) when (exception.Status == 404) { // manifest does not exist - throw new OciModuleRegistryException("The module does not exist in the registry.", exception); + throw new OciModuleRegistryException("The artifact does not exist in the registry.", exception); } // the Value is disposable, but we are not calling it because we need to pass the stream outside of this scope - var stream = manifestResponse.Value.Manifest.ToStream(); + using var stream = manifestResponse.Value.Manifest.ToStream(); // BUG: The SDK internally consumed the stream for validation purposes and left position at the end stream.Position = 0; ValidateManifestResponse(manifestResponse); - // the SDK doesn't expose all the manifest properties we need - // so we need to deserialize the manifest ourselves to get everything - stream.Position = 0; - var deserialized = DeserializeManifest(stream); - stream.Position = 0; - - return (deserialized, stream, manifestResponse.Value.Digest); - } - - private static void ValidateManifestResponse(Response manifestResponse) - { - var digestFromRegistry = manifestResponse.Value.Digest; - var stream = manifestResponse.Value.Manifest.ToStream(); - - // TODO: The registry may use a different digest algorithm - we need to handle that - string digestFromContent = DescriptorFactory.ComputeDigest(DescriptorFactory.AlgorithmIdentifierSha256, stream); + var deserializedManifest = OciManifest.FromBinaryData(manifestResponse.Value.Manifest) ?? throw new InvalidOperationException("the manifest is not a valid OCI manifest"); + var layers = deserializedManifest.Layers + .Select(layer => new KeyValuePair(layer.Digest, PullLayerAsync(client, layer).Result)) + .ToImmutableDictionary(pair => pair.Key, pair => pair.Value); - if (!string.Equals(digestFromRegistry, digestFromContent, DigestComparison)) - { - throw new OciModuleRegistryException($"There is a mismatch in the manifest digests. Received content digest = {digestFromContent}, Digest in registry response = {digestFromRegistry}"); - } + return new(manifestResponse.Value.Manifest, manifestResponse.Value.Digest, layers); } - private static async Task ProcessManifest(ContainerRegistryContentClient client, OciManifest manifest) + private static async Task PullLayerAsync(ContainerRegistryContentClient client, OciDescriptor layer, CancellationToken cancellationToken = default) { - // Bicep versions before 0.14 used to publish modules without the artifactType field set in the OCI manifest, - // so we must allow null here - if (manifest.ArtifactType is not null && !string.Equals(manifest.ArtifactType, BicepMediaTypes.BicepModuleArtifactType, MediaTypeComparison)) + Response blobResult; + try { - throw new InvalidModuleException($"Expected OCI artifact to have the artifactType field set to either null or '{BicepMediaTypes.BicepModuleArtifactType}' but found '{manifest.ArtifactType}'.", InvalidModuleExceptionKind.WrongArtifactType); + blobResult = await client.DownloadBlobContentAsync(layer.Digest, cancellationToken); } - - ProcessConfig(manifest.Config); - if (manifest.Layers.Length != 1) + catch (RequestFailedException exception) when (exception.Status == 404) { - throw new InvalidModuleException("Expected a single layer in the OCI artifact."); + throw new InvalidModuleException($"Module manifest refers to a non-existent blob with digest \"{layer.Digest}\".", exception); } - var layer = manifest.Layers.Single(); + ValidateBlobResponse(blobResult, layer); - return await ProcessLayer(client, layer); + return blobResult.Value.Content; } private static void ValidateBlobResponse(Response blobResponse, OciDescriptor descriptor) { - var stream = blobResponse.Value.Content.ToStream(); + using var stream = blobResponse.Value.Content.ToStream(); if (descriptor.Size != stream.Length) { @@ -205,57 +188,23 @@ private static void ValidateBlobResponse(Response bl string digestFromContents = DescriptorFactory.ComputeDigest(DescriptorFactory.AlgorithmIdentifierSha256, stream); stream.Position = 0; - if (!string.Equals(descriptor.Digest, digestFromContents, DigestComparison)) + if (!string.Equals(descriptor.Digest, digestFromContents, StringComparison.Ordinal)) { throw new InvalidModuleException($"There is a mismatch in the layer digests. Received content digest = {digestFromContents}, Requested digest = {descriptor.Digest}"); } } - private static async Task ProcessLayer(ContainerRegistryContentClient client, OciDescriptor layer) - { - if (!string.Equals(layer.MediaType, BicepMediaTypes.BicepModuleLayerV1Json, MediaTypeComparison)) - { - throw new InvalidModuleException($"Did not expect layer media type \"{layer.MediaType}\".", InvalidModuleExceptionKind.WrongModuleLayerMediaType); - } - - Response blobResult; - try - { - blobResult = await client.DownloadBlobContentAsync(layer.Digest); - } - catch (RequestFailedException exception) when (exception.Status == 404) - { - throw new InvalidModuleException($"Module manifest refers to a non-existent blob with digest \"{layer.Digest}\".", exception); - } - - ValidateBlobResponse(blobResult, layer); - - return blobResult.Value.Content.ToStream(); - } - - private static void ProcessConfig(OciDescriptor config) + private static void ValidateManifestResponse(Response manifestResponse) { - // media types are case insensitive - if (!string.Equals(config.MediaType, BicepMediaTypes.BicepModuleConfigV1, MediaTypeComparison)) - { - throw new InvalidModuleException($"Did not expect config media type \"{config.MediaType}\"."); - } + var digestFromRegistry = manifestResponse.Value.Digest; + var stream = manifestResponse.Value.Manifest.ToStream(); - if (config.Size != 0) - { - throw new InvalidModuleException("Expected an empty config blob."); - } - } + // TODO: The registry may use a different digest algorithm - we need to handle that + string digestFromContent = DescriptorFactory.ComputeDigest(DescriptorFactory.AlgorithmIdentifierSha256, stream); - private static OciManifest DeserializeManifest(Stream stream) - { - try - { - return OciSerialization.Deserialize(stream); - } - catch (Exception exception) + if (!string.Equals(digestFromRegistry, digestFromContent, DigestComparison)) { - throw new InvalidModuleException("Unable to deserialize the module manifest.", exception); + throw new OciModuleRegistryException($"There is a mismatch in the manifest digests. Received content digest = {digestFromContent}, Digest in registry response = {digestFromRegistry}"); } } } diff --git a/src/Bicep.Core/Registry/IModuleDispatcher.cs b/src/Bicep.Core/Registry/IArtifactDispatcher.cs similarity index 86% rename from src/Bicep.Core/Registry/IModuleDispatcher.cs rename to src/Bicep.Core/Registry/IArtifactDispatcher.cs index eba50273e12..16e54dc653d 100644 --- a/src/Bicep.Core/Registry/IModuleDispatcher.cs +++ b/src/Bicep.Core/Registry/IArtifactDispatcher.cs @@ -15,7 +15,7 @@ public interface IModuleDispatcher : IModuleReferenceFactory { RegistryCapabilities GetRegistryCapabilities(ModuleReference moduleReference); - ModuleRestoreStatus GetModuleRestoreStatus(ModuleReference moduleReference, out DiagnosticBuilder.ErrorBuilderDelegate? errorDetailBuilder); + ArtifactRestoreStatus GetModuleRestoreStatus(ModuleReference moduleReference, out DiagnosticBuilder.ErrorBuilderDelegate? errorDetailBuilder); bool TryGetLocalModuleEntryPointUri(ModuleReference moduleReference, [NotNullWhen(true)] out Uri? localUri, [NotNullWhen(false)] out DiagnosticBuilder.ErrorBuilderDelegate? failureBuilder); diff --git a/src/Bicep.Core/Registry/IModuleReferenceFactory.cs b/src/Bicep.Core/Registry/IModuleReferenceFactory.cs index 6bd65ea0a55..f53f7d6ef42 100644 --- a/src/Bicep.Core/Registry/IModuleReferenceFactory.cs +++ b/src/Bicep.Core/Registry/IModuleReferenceFactory.cs @@ -16,5 +16,5 @@ public interface IModuleReferenceFactory bool TryGetModuleReference(string reference, Uri parentModuleUri, [NotNullWhen(true)] out ModuleReference? moduleReference, [NotNullWhen(false)] out DiagnosticBuilder.ErrorBuilderDelegate? failureBuilder); - bool TryGetModuleReference(IForeignTemplateReference module, Uri parentModuleUri, [NotNullWhen(true)] out ModuleReference? moduleReference, [NotNullWhen(false)] out DiagnosticBuilder.ErrorBuilderDelegate? failureBuilder); + bool TryGetModuleReference(IForeignArtifactReference module, Uri parentModuleUri, [NotNullWhen(true)] out ModuleReference? moduleReference, [NotNullWhen(false)] out DiagnosticBuilder.ErrorBuilderDelegate? failureBuilder); } diff --git a/src/Bicep.Core/Registry/ModuleDispatcher.cs b/src/Bicep.Core/Registry/ModuleDispatcher.cs index cdafcb5d427..c641e61eee0 100644 --- a/src/Bicep.Core/Registry/ModuleDispatcher.cs +++ b/src/Bicep.Core/Registry/ModuleDispatcher.cs @@ -90,7 +90,7 @@ public bool TryGetModuleReference(string reference, Uri parentModuleUri, [NotNul } } - public bool TryGetModuleReference(IForeignTemplateReference module, Uri parentModuleUri, [NotNullWhen(true)] out ModuleReference? moduleReference, [NotNullWhen(false)] out DiagnosticBuilder.ErrorBuilderDelegate? failureBuilder) + public bool TryGetModuleReference(IForeignArtifactReference module, Uri parentModuleUri, [NotNullWhen(true)] out ModuleReference? moduleReference, [NotNullWhen(false)] out DiagnosticBuilder.ErrorBuilderDelegate? failureBuilder) { if (!SyntaxHelper.TryGetForeignTemplatePath(module, out var moduleReferenceString, out failureBuilder)) { @@ -107,7 +107,9 @@ public RegistryCapabilities GetRegistryCapabilities(ModuleReference moduleRefere return registry.GetCapabilities(moduleReference); } - public ModuleRestoreStatus GetModuleRestoreStatus(ModuleReference moduleReference, out DiagnosticBuilder.ErrorBuilderDelegate? failureBuilder) + public ArtifactRestoreStatus GetModuleRestoreStatus( + ModuleReference moduleReference, + out DiagnosticBuilder.ErrorBuilderDelegate? failureBuilder) { var registry = this.GetRegistry(moduleReference); var configuration = configurationManager.GetConfiguration(moduleReference.ParentModuleUri); @@ -116,18 +118,18 @@ public ModuleRestoreStatus GetModuleRestoreStatus(ModuleReference moduleReferenc if (this.HasRestoreFailed(moduleReference, configuration, out var restoreFailureBuilder)) { failureBuilder = restoreFailureBuilder; - return ModuleRestoreStatus.Failed; + return ArtifactRestoreStatus.Failed; } if (registry.IsModuleRestoreRequired(moduleReference)) { // module is not present on the local file system failureBuilder = x => x.ModuleRequiresRestore(moduleReference.FullyQualifiedReference); - return ModuleRestoreStatus.Unknown; + return ArtifactRestoreStatus.Unknown; } failureBuilder = null; - return ModuleRestoreStatus.Succeeded; + return ArtifactRestoreStatus.Succeeded; } public bool TryGetLocalModuleEntryPointUri(ModuleReference moduleReference, [NotNullWhen(true)] out Uri? localUri, [NotNullWhen(false)] out DiagnosticBuilder.ErrorBuilderDelegate? failureBuilder) @@ -149,7 +151,8 @@ public async Task RestoreModules(IEnumerable moduleRefere { // WARNING: The various operations on ModuleReference objects here rely on the custom Equals() implementation and NOT on object identity - if (!forceModulesRestore && moduleReferences.All(module => this.GetModuleRestoreStatus(module, out _) == ModuleRestoreStatus.Succeeded)) + if (!forceModulesRestore && + moduleReferences.All(module => this.GetModuleRestoreStatus(module, out _) == ArtifactRestoreStatus.Succeeded)) { // all the modules have already been restored - no need to do anything return false; diff --git a/src/Bicep.Core/Registry/ModuleDispatcherExtensions.cs b/src/Bicep.Core/Registry/ModuleDispatcherExtensions.cs index cf3aa85eab2..8e7162c2996 100644 --- a/src/Bicep.Core/Registry/ModuleDispatcherExtensions.cs +++ b/src/Bicep.Core/Registry/ModuleDispatcherExtensions.cs @@ -3,7 +3,9 @@ using Bicep.Core.Extensions; using Bicep.Core.Modules; +using Bicep.Core.Syntax; using Bicep.Core.Workspaces; +using Microsoft.WindowsAzure.ResourceStack.Common.Extensions; using System.Collections.Generic; using System.Linq; @@ -11,9 +13,9 @@ namespace Bicep.Core.Registry { public static class ModuleDispatcherExtensions { - public static IEnumerable GetValidModuleReferences(this IModuleDispatcher moduleDispatcher, IEnumerable modules) => - modules - .Select(t => moduleDispatcher.TryGetModuleReference(t.ForeignTemplateReference, t.ParentTemplateFile.FileUri, out var moduleRef, out _) ? moduleRef : null) + public static IEnumerable GetValidModuleReferences(this IModuleDispatcher moduleDispatcher, IEnumerable artifacts) + => artifacts + .Select(t => moduleDispatcher.TryGetModuleReference(t.DeclarationSyntax, t.ParentTemplateFile.FileUri, out var moduleRef, out _) ? moduleRef : null) .WhereNotNull(); } } diff --git a/src/Bicep.Core/Registry/Oci/BicepMediaTypes.cs b/src/Bicep.Core/Registry/Oci/BicepMediaTypes.cs index 7c2c4b138ec..4cce7c1d880 100644 --- a/src/Bicep.Core/Registry/Oci/BicepMediaTypes.cs +++ b/src/Bicep.Core/Registry/Oci/BicepMediaTypes.cs @@ -5,10 +5,20 @@ namespace Bicep.Core.Registry.Oci { public static class BicepMediaTypes { + // Module Media Types + + public const string BicepModuleArtifactType = "application/vnd.ms.bicep.module.artifact"; + public const string BicepModuleConfigV1 = "application/vnd.ms.bicep.module.config.v1+json"; public const string BicepModuleLayerV1Json = "application/vnd.ms.bicep.module.layer.v1+json"; - public const string BicepModuleArtifactType = "application/vnd.ms.bicep.module.artifact"; + // Provider Media Types + + public const string BicepProviderArtifactType = "application/vnd.ms.bicep.provider.artifact"; + + public const string BicepProviderConfigV1 = "application/vnd.ms.bicep.provider.config.v1+json"; + + public const string BicepProviderArtifactLayerV1TarGzip = "application/vnd.ms.bicep.provider.layer.v1.tar+gzip"; } } diff --git a/src/Bicep.Core/Registry/Oci/IOciArtifactReference.cs b/src/Bicep.Core/Registry/Oci/IOciArtifactReference.cs new file mode 100644 index 00000000000..868779e6bfe --- /dev/null +++ b/src/Bicep.Core/Registry/Oci/IOciArtifactReference.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace Bicep.Core.Registry.Oci +{ + public interface IOciArtifactReference + { + + /// + /// Gets the registry URI. + /// + string Registry { get; } + + /// + /// Gets the repository name. The repository name is the path to an artifact in the registry without the tag. + /// + string Repository { get; } + + /// + /// Gets the tag. Either tag or digest is set but not both. + /// + string? Tag { get; } + + /// + /// Gets the digest. Either tag or digest is set but not both. + /// + string? Digest { get; } + + /// + /// Gets the artifact ID. + /// + string ArtifactId { get; } + + string FullyQualifiedReference { get; } + } +} \ No newline at end of file diff --git a/src/Bicep.Core/Modules/OciArtifactModuleReference.cs b/src/Bicep.Core/Registry/Oci/OciArtifactReference.cs similarity index 54% rename from src/Bicep.Core/Modules/OciArtifactModuleReference.cs rename to src/Bicep.Core/Registry/Oci/OciArtifactReference.cs index abf0e376990..9a4b9e10167 100644 --- a/src/Bicep.Core/Modules/OciArtifactModuleReference.cs +++ b/src/Bicep.Core/Registry/Oci/OciArtifactReference.cs @@ -6,46 +6,16 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Text; using System.Text.RegularExpressions; +using System.Text; using System.Web; -namespace Bicep.Core.Modules +namespace Bicep.Core.Registry.Oci { - /// - /// Represents a reference to an artifact in an OCI registry. - /// - public class OciArtifactModuleReference : ModuleReference - { - public const int MaxRegistryLength = 255; - - // must be kept in sync with the tag name regex - public const int MaxTagLength = 128; - - public const int MaxRepositoryLength = 255; - - // obtained from https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pull - private static readonly Regex ModulePathSegmentRegex = new(@"^[a-z0-9]+([._-][a-z0-9]+)*$", RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant); - - // must be kept in sync with the tag max length - private static readonly Regex TagRegex = new(@"^[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}$", RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant); - private static readonly Regex DigestRegex = new(@"^sha256:[a-f0-9]{64}$", RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant); - - // the registry component is equivalent to a host in a URI, which are case-insensitive - public static readonly IEqualityComparer RegistryComparer = StringComparer.OrdinalIgnoreCase; - - // repository component is case-sensitive (although regex blocks upper case) - public static readonly IEqualityComparer RepositoryComparer = StringComparer.Ordinal; - - // tags are case-sensitive and may contain upper and lowercase characters - public static readonly IEqualityComparer TagComparer = StringComparer.Ordinal; - - // digests are case sensitive - public static readonly IEqualityComparer DigestComparer = StringComparer.Ordinal; - - public OciArtifactModuleReference(string registry, string repository, string? tag, string? digest, Uri parentModuleUri) - : base(ModuleReferenceSchemes.Oci, parentModuleUri) + public class OciArtifactReference : IOciArtifactReference + { + private OciArtifactReference(string registry, string repository, string? tag, string? digest) { switch (tag, digest) { @@ -60,7 +30,6 @@ public OciArtifactModuleReference(string registry, string repository, string? ta this.Tag = tag; this.Digest = digest; } - /// /// Gets the registry URI. /// @@ -88,70 +57,50 @@ public OciArtifactModuleReference(string registry, string repository, string? ta ? $"{this.Registry}/{this.Repository}:{this.Tag}" : $"{this.Registry}/{this.Repository}@{this.Digest}"; - public override string UnqualifiedReference => this.ArtifactId; + public string FullyQualifiedReference => ArtifactId; - public override bool IsExternal => true; - - public override bool Equals(object? obj) + public static bool TryParse( + string? aliasName, + string rawValue, + RootConfiguration configuration, + [NotNullWhen(true)] out OciArtifactReference? artifactReference, + [NotNullWhen(false)] out DiagnosticBuilder.ErrorBuilderDelegate? failureBuilder) { - if (obj is not OciArtifactModuleReference other) - { - return false; - } - - return - // TODO: Are all of these case-sensitive? - RegistryComparer.Equals(this.Registry, other.Registry) && - RepositoryComparer.Equals(this.Repository, other.Repository) && - TagComparer.Equals(this.Tag, other.Tag) && - DigestComparer.Equals(this.Digest, other.Digest); - } + static string GetBadReference(string referenceValue) => $"{OciArtifactReferenceFacts.Scheme}:{referenceValue}"; - public override int GetHashCode() - { - var hash = new HashCode(); - hash.Add(this.Registry, RegistryComparer); - hash.Add(this.Repository, RepositoryComparer); - hash.Add(this.Tag, TagComparer); - hash.Add(this.Digest, DigestComparer); - - return hash.ToHashCode(); - } - - public static bool TryParse(string? aliasName, string rawValue, RootConfiguration configuration, Uri parentModuleUri, [NotNullWhen(true)] out OciArtifactModuleReference? moduleReference, [NotNullWhen(false)] out DiagnosticBuilder.ErrorBuilderDelegate? failureBuilder) - { - static string GetBadReference(string referenceValue) => $"{ModuleReferenceSchemes.Oci}:{referenceValue}"; - - static string UnescapeSegment(string segment) => HttpUtility.UrlDecode(segment); + static string DecodeSegment(string segment) => HttpUtility.UrlDecode(segment); if (aliasName is not null) { if (!configuration.ModuleAliases.TryGetOciArtifactModuleAlias(aliasName, out var alias, out failureBuilder)) { - moduleReference = null; + artifactReference = null; return false; } rawValue = $"{alias}/{rawValue}"; } - // the set of valid OCI artifact refs is a subset of the set of valid URIs if you remove the scheme portion from each URI // manually prepending any valid URI scheme allows to get free validation via the built-in URI parser - if (!Uri.TryCreate($"{ModuleReferenceSchemes.Oci}://{rawValue}", UriKind.Absolute, out var artifactUri) || + if (!Uri.TryCreate($"{OciArtifactReferenceFacts.Scheme}://{rawValue}", UriKind.Absolute, out var artifactUri) || artifactUri.Segments.Length <= 1 || !string.Equals(artifactUri.Segments[0], "/", StringComparison.Ordinal)) { failureBuilder = x => x.InvalidOciArtifactReference(aliasName, GetBadReference(rawValue)); - moduleReference = null; + artifactReference = null; return false; } string registry = artifactUri.Authority; - if (registry.Length > MaxRegistryLength) + if (registry.Length > OciArtifactReferenceFacts.MaxRegistryLength) { - failureBuilder = x => x.InvalidOciArtifactReferenceRegistryTooLong(aliasName, GetBadReference(rawValue), registry, MaxRegistryLength); - moduleReference = null; + failureBuilder = x => x.InvalidOciArtifactReferenceRegistryTooLong( + aliasName, + GetBadReference(rawValue), + registry, + OciArtifactReferenceFacts.MaxRegistryLength); + artifactReference = null; return false; } @@ -161,19 +110,19 @@ public static bool TryParse(string? aliasName, string rawValue, RootConfiguratio for (int i = 1; i < artifactUri.Segments.Length - 1; i++) { // don't try to match the last character, which is always '/' - string current = artifactUri.Segments[i]; - var pathMatch = ModulePathSegmentRegex.Match(current, 0, current.Length - 1); - if (!pathMatch.Success) + string segment = artifactUri.Segments[i]; + var segmentWithoutTrailingSlash = segment[..^1]; + if (!OciArtifactReferenceFacts.IsOciNamespaceSegment(segmentWithoutTrailingSlash)) { - var invalidSegment = UnescapeSegment(current[0..^1]); + var invalidSegment = DecodeSegment(segmentWithoutTrailingSlash); failureBuilder = x => x.InvalidOciArtifactReferenceInvalidPathSegment(aliasName, GetBadReference(rawValue), invalidSegment); - moduleReference = null; + artifactReference = null; return false; } // even though chars that require URL-escaping are not part of the allowed regexes // users can still type them in, so error messages should contain the original text rather than an escaped version - repoBuilder.Append(UnescapeSegment(current)); + repoBuilder.Append(DecodeSegment(segment)); } // on a valid ref it would look something like "bar:v1" or "bar@sha256:e207a69d02b3de40d48ede9fd208d80441a9e590a83a0bc915d46244c03310d4" @@ -181,37 +130,35 @@ public static bool TryParse(string? aliasName, string rawValue, RootConfiguratio static (int index, char? delimiter) FindLastSegmentDelimiter(string lastSegment) { - for (int i = 0; i < lastSegment.Length; i++) - { - var current = lastSegment[i]; - if (current == ':' || current == '@') - { - return (i, current); - } - } + char[] delimiters = { ':', '@' }; + int index = lastSegment.IndexOfAny(delimiters); - return (-1, null); + return (index, index == -1 ? null : lastSegment[index]); } var (indexOfLastSegmentDelimiter, delimiter) = FindLastSegmentDelimiter(lastSegment); // users will type references from left to right, so we should validate the last component of the module path // before we complain about the missing tag, which is the last part of the module ref - var name = UnescapeSegment(!delimiter.HasValue ? lastSegment : lastSegment.Substring(0, indexOfLastSegmentDelimiter)); - if (!ModulePathSegmentRegex.IsMatch(name)) + var name = DecodeSegment(!delimiter.HasValue ? lastSegment : lastSegment[..indexOfLastSegmentDelimiter]); + if (!OciArtifactReferenceFacts.IsOciNamespaceSegment(name)) { failureBuilder = x => x.InvalidOciArtifactReferenceInvalidPathSegment(aliasName, GetBadReference(rawValue), name); - moduleReference = null; + artifactReference = null; return false; } repoBuilder.Append(name); string repository = repoBuilder.ToString(); - if (repository.Length > MaxRepositoryLength) + if (repository.Length > OciArtifactReferenceFacts.MaxRepositoryLength) { - failureBuilder = x => x.InvalidOciArtifactReferenceRepositoryTooLong(aliasName, GetBadReference(rawValue), repository, MaxRepositoryLength); - moduleReference = null; + failureBuilder = x => x.InvalidOciArtifactReferenceRepositoryTooLong( + aliasName, + GetBadReference(rawValue), + repository, + OciArtifactReferenceFacts.MaxRepositoryLength); + artifactReference = null; return false; } @@ -219,15 +166,15 @@ public static bool TryParse(string? aliasName, string rawValue, RootConfiguratio if (!delimiter.HasValue) { failureBuilder = x => x.InvalidOciArtifactReferenceMissingTagOrDigest(aliasName, GetBadReference(rawValue)); - moduleReference = null; + artifactReference = null; return false; } - var tagOrDigest = UnescapeSegment(lastSegment[(indexOfLastSegmentDelimiter + 1)..]); + var tagOrDigest = DecodeSegment(lastSegment.Substring(indexOfLastSegmentDelimiter + 1)); if (string.IsNullOrEmpty(tagOrDigest)) { failureBuilder = x => x.InvalidOciArtifactReferenceMissingTagOrDigest(aliasName, GetBadReference(rawValue)); - moduleReference = null; + artifactReference = null; return false; } @@ -235,40 +182,74 @@ public static bool TryParse(string? aliasName, string rawValue, RootConfiguratio { case ':': var tag = tagOrDigest; - if (tag.Length > MaxTagLength) + if (tag.Length > OciArtifactReferenceFacts.MaxTagLength) { - failureBuilder = x => x.InvalidOciArtifactReferenceTagTooLong(aliasName, GetBadReference(rawValue), tag, MaxTagLength); - moduleReference = null; + failureBuilder = x => x.InvalidOciArtifactReferenceTagTooLong( + aliasName, + GetBadReference(rawValue), + tag, + OciArtifactReferenceFacts.MaxTagLength); + artifactReference = null; return false; } - if (!TagRegex.IsMatch(tag)) + if (!OciArtifactReferenceFacts.IsOciTag(tag)) { - failureBuilder = x => x.InvalidOciArtifactReferenceInvalidTag(aliasName, GetBadReference(rawValue), tag); - moduleReference = null; + failureBuilder = x => x.InvalidOciArtifactReferenceInvalidTag( + aliasName, + GetBadReference(rawValue), + tag); + artifactReference = null; return false; } failureBuilder = null; - moduleReference = new OciArtifactModuleReference(registry, repository, tag: tag, digest: null, parentModuleUri); + artifactReference = new OciArtifactReference(registry, repository, tag: tag, digest: null); return true; case '@': var digest = tagOrDigest; - if (!DigestRegex.IsMatch(digest)) + if (!OciArtifactReferenceFacts.IsOciDigest(digest)) { failureBuilder = x => x.InvalidOciArtifactReferenceInvalidDigest(aliasName, GetBadReference(rawValue), digest); - moduleReference = null; + artifactReference = null; return false; } failureBuilder = null; - moduleReference = new OciArtifactModuleReference(registry, repository, tag: null, digest: digest, parentModuleUri); + artifactReference = new OciArtifactReference(registry, repository, tag: null, digest: digest); return true; default: throw new NotImplementedException($"Unexpected last segment delimiter character '{delimiter.Value}'."); } + + } + + public override bool Equals(object? obj) + { + if (obj is not OciArtifactReference other) + { + return false; + } + + return + // TODO: Are all of these case-sensitive? + OciArtifactReferenceFacts.RegistryComparer.Equals(this.Registry, other.Registry) && + OciArtifactReferenceFacts.RepositoryComparer.Equals(this.Repository, other.Repository) && + OciArtifactReferenceFacts.TagComparer.Equals(this.Tag, other.Tag) && + OciArtifactReferenceFacts.DigestComparer.Equals(this.Digest, other.Digest); + } + + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(this.Registry, OciArtifactReferenceFacts.RegistryComparer); + hash.Add(this.Repository, OciArtifactReferenceFacts.RepositoryComparer); + hash.Add(this.Tag, OciArtifactReferenceFacts.TagComparer); + hash.Add(this.Digest, OciArtifactReferenceFacts.DigestComparer); + + return hash.ToHashCode(); } } } diff --git a/src/Bicep.Core/Registry/Oci/OciArtifactReferenceFacts.cs b/src/Bicep.Core/Registry/Oci/OciArtifactReferenceFacts.cs new file mode 100644 index 00000000000..c3511a39c8b --- /dev/null +++ b/src/Bicep.Core/Registry/Oci/OciArtifactReferenceFacts.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace Bicep.Core.Registry.Oci +{ + public static partial class OciArtifactReferenceFacts + { + public const string Scheme = "br"; + + public const int MaxRegistryLength = 255; + + // must be kept in sync with the tag name regex + public const int MaxTagLength = 128; + + public const int MaxRepositoryLength = 255; + + // the registry component is equivalent to a host in a URI, which are case-insensitive + public static readonly IEqualityComparer RegistryComparer = StringComparer.OrdinalIgnoreCase; + + // repository component is case-sensitive (although regex blocks upper case) + public static readonly IEqualityComparer RepositoryComparer = StringComparer.Ordinal; + + // tags are case-sensitive and may contain upper and lowercase characters + public static readonly IEqualityComparer TagComparer = StringComparer.Ordinal; + + // digests are case sensitive + public static readonly IEqualityComparer DigestComparer = StringComparer.Ordinal; + + // must be kept in sync with the tag max length + private static readonly Regex TagRegex = new(@$"^[a-zA-Z0-9_][a-zA-Z0-9._-]{{0,{MaxTagLength - 1}}}$", RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant); + + public static bool IsOciTag(string value) => TagRegex.IsMatch(value); + + public static bool IsOciNamespaceSegment(string value) => OciNamespaceSegmentRegex().IsMatch(value); + + public static bool IsOciDigest(string value) => DigestRegex().IsMatch(value); + + // obtained from https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pull + [GeneratedRegex("^[a-z0-9]+([._-][a-z0-9]+)*$", RegexOptions.ExplicitCapture | RegexOptions.Compiled | RegexOptions.CultureInvariant)] + public static partial Regex OciNamespaceSegmentRegex(); + [GeneratedRegex("^sha256:[a-f0-9]{64}$", RegexOptions.ExplicitCapture | RegexOptions.Compiled | RegexOptions.CultureInvariant)] + public static partial Regex DigestRegex(); + } +} diff --git a/src/Bicep.Core/Registry/Oci/OciArtifactResult.cs b/src/Bicep.Core/Registry/Oci/OciArtifactResult.cs new file mode 100644 index 00000000000..18491e1025a --- /dev/null +++ b/src/Bicep.Core/Registry/Oci/OciArtifactResult.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +using Azure.Containers.ContainerRegistry; +using System; +using System.Collections.Immutable; +using System.Threading.Tasks; +using System.Threading; +using Azure; +using System.IO; +using System.Linq; +using System.Collections.Generic; + +namespace Bicep.Core.Registry.Oci +{ + public class OciArtifactResult + { + public OciArtifactResult(BinaryData manifestBits, string manifestDigest, ImmutableDictionary layers) + { + this.manifest = manifestBits; + this.serializedManifest = OciManifest.FromBinaryData(manifestBits) ?? throw new InvalidOperationException("the manifest is not a valid OCI manifest"); + this.manifestDigest = manifestDigest; + this.layers = layers; + } + + private readonly BinaryData manifest; + private readonly string manifestDigest; + private readonly OciManifest serializedManifest; + private readonly ImmutableDictionary layers; + + public Stream ToStream() => manifest.ToStream(); + + public OciManifest Manifest => serializedManifest; + + public string ManifestDigest => manifestDigest; + + public IEnumerable Layers => layers.Values; + } +} \ No newline at end of file diff --git a/src/Bicep.Core/Registry/Oci/OciDescriptor.cs b/src/Bicep.Core/Registry/Oci/OciDescriptor.cs index 7a061443476..01229a9e3d2 100644 --- a/src/Bicep.Core/Registry/Oci/OciDescriptor.cs +++ b/src/Bicep.Core/Registry/Oci/OciDescriptor.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Immutable; +using System.Text.Json.Serialization; namespace Bicep.Core.Registry.Oci { @@ -15,14 +16,17 @@ public OciDescriptor(string mediaType, string digest, long size, IDictionary.Empty; } - + [JsonPropertyName("mediaType")] public string MediaType { get; } + [JsonPropertyName("digest")] public string Digest { get; } + [JsonPropertyName("size")] public long Size { get; } // TODO: Skip serialization for empty annotations + [JsonPropertyName("annotations")] public ImmutableDictionary Annotations { get; } } } diff --git a/src/Bicep.Core/Registry/Oci/OciManifest.cs b/src/Bicep.Core/Registry/Oci/OciManifest.cs index 74d1c989134..95763dbca9a 100644 --- a/src/Bicep.Core/Registry/Oci/OciManifest.cs +++ b/src/Bicep.Core/Registry/Oci/OciManifest.cs @@ -1,14 +1,22 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.IO; +using Newtonsoft.Json; namespace Bicep.Core.Registry.Oci { public class OciManifest { - public OciManifest(int schemaVersion, string? artifactType, OciDescriptor config, IEnumerable layers, IDictionary? annotations = null) + public OciManifest( + int schemaVersion, + string? artifactType, + OciDescriptor config, + IEnumerable layers, + IDictionary? annotations = null) { this.Annotations = annotations?.ToImmutableDictionary() ?? ImmutableDictionary.Empty; this.SchemaVersion = schemaVersion; @@ -29,5 +37,10 @@ public OciManifest(int schemaVersion, string? artifactType, OciDescriptor config /// Additional information provided through arbitrary metadata. /// public ImmutableDictionary Annotations { get; } + + internal static OciManifest? FromBinaryData(BinaryData data) + { + return JsonConvert.DeserializeObject(data.ToString()); + } } } diff --git a/src/Bicep.Core/Registry/Oci/TagEncoder.cs b/src/Bicep.Core/Registry/Oci/TagEncoder.cs index 4b11be549f3..14ea7934500 100644 --- a/src/Bicep.Core/Registry/Oci/TagEncoder.cs +++ b/src/Bicep.Core/Registry/Oci/TagEncoder.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Bicep.Core.Modules; using System; using System.Text; @@ -17,8 +16,8 @@ public static class TagEncoder private const int BitsInHexDigit = 4; - // this should be equivalent to ceil(OciArtifactModuleReference.MaxTagLength / 64.0) but without floating point conversion - private const int MaskComponents = (OciArtifactModuleReference.MaxTagLength + BitsInUnsignedLong - 1) / BitsInUnsignedLong; + // this should be equivalent to ceil(OciArtifactReference.MaxTagLength / 64.0) but without floating point conversion + private const int MaskComponents = (OciArtifactReferenceFacts.MaxTagLength + BitsInUnsignedLong - 1) / BitsInUnsignedLong; private const string HexFormat = "x"; @@ -31,9 +30,9 @@ public static class TagEncoder /// The tag value. The tag should be validated to match the OCI spec before calling this function. public static string Encode(string tag) { - if (tag.Length > OciArtifactModuleReference.MaxTagLength) + if (tag.Length > OciArtifactReferenceFacts.MaxTagLength) { - throw new ArgumentException($"The specified tag '{tag}' exceeds max length of {OciArtifactModuleReference.MaxTagLength}."); + throw new ArgumentException($"The specified tag '{tag}' exceeds max length of {OciArtifactReferenceFacts.MaxTagLength}."); } var mask = new ulong[MaskComponents]; diff --git a/src/Bicep.Core/Registry/OciArtifactResult.cs b/src/Bicep.Core/Registry/OciArtifactResult.cs deleted file mode 100644 index 1e5aad31138..00000000000 --- a/src/Bicep.Core/Registry/OciArtifactResult.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Bicep.Core.Registry.Oci; -using System.IO; - -namespace Bicep.Core.Registry -{ - public class OciArtifactResult - { - public OciArtifactResult(string manifestDigest, OciManifest manifest, Stream manifestStream, Stream moduleStream) - { - this.ManifestDigest = manifestDigest; - this.Manifest = manifest; - this.ManifestStream = manifestStream; - this.ModuleStream = moduleStream; - } - - public string ManifestDigest { get; } - - /// - /// Gets the deserialized manifest. This is useful for accessing various properties inside the manifest. - /// - public OciManifest Manifest { get; } - - /// - /// Gets the original manifest bytes. This is useful for persisting the exact copy of the manifest that is agnostic of (de)serialization settings. - /// - public Stream ManifestStream { get; } - - /// - /// Gets the stream containing the module contents. - /// - public Stream ModuleStream { get; } - } -} diff --git a/src/Bicep.Core/Registry/OciModuleRegistry.cs b/src/Bicep.Core/Registry/OciModuleRegistry.cs index 6b6706022d9..9af8eb4bbb8 100644 --- a/src/Bicep.Core/Registry/OciModuleRegistry.cs +++ b/src/Bicep.Core/Registry/OciModuleRegistry.cs @@ -22,7 +22,7 @@ namespace Bicep.Core.Registry { - public sealed class OciModuleRegistry : ExternalModuleRegistry + public sealed class OciModuleRegistry : ExternalModuleRegistry { private readonly AzureContainerRegistryManager client; @@ -30,7 +30,7 @@ public sealed class OciModuleRegistry : ExternalModuleRegistry ModuleReferenceSchemes.Oci; - public override RegistryCapabilities GetCapabilities(OciArtifactModuleReference reference) + public override RegistryCapabilities GetCapabilities(OciModuleReference reference) { // cannot publish without tag return reference.Tag is null ? RegistryCapabilities.Default : RegistryCapabilities.Publish; @@ -51,7 +51,7 @@ public override RegistryCapabilities GetCapabilities(OciArtifactModuleReference public override bool TryParseModuleReference(string? aliasName, string reference, [NotNullWhen(true)] out ModuleReference? moduleReference, [NotNullWhen(false)] out DiagnosticBuilder.ErrorBuilderDelegate? failureBuilder) { - if (OciArtifactModuleReference.TryParse(aliasName, reference, configuration, parentModuleUri, out var @ref, out failureBuilder)) + if (OciModuleReference.TryParse(aliasName, reference, configuration, parentModuleUri, out var @ref, out failureBuilder)) { moduleReference = @ref; return true; @@ -61,7 +61,7 @@ public override bool TryParseModuleReference(string? aliasName, string reference return false; } - public override bool IsModuleRestoreRequired(OciArtifactModuleReference reference) + public override bool IsModuleRestoreRequired(OciModuleReference reference) { /* * this should be kept in sync with the WriteModuleContent() implementation @@ -78,7 +78,7 @@ public override bool IsModuleRestoreRequired(OciArtifactModuleReference referenc !this.FileResolver.FileExists(this.GetModuleFileUri(reference, ModuleFileType.Metadata)); } - public override async Task CheckModuleExists(OciArtifactModuleReference reference) + public override async Task CheckModuleExists(OciModuleReference reference) { try { @@ -113,14 +113,61 @@ public override async Task CheckModuleExists(OciArtifactModuleReference re return true; } - public override bool TryGetLocalModuleEntryPointUri(OciArtifactModuleReference reference, [NotNullWhen(true)] out Uri? localUri, [NotNullWhen(false)] out DiagnosticBuilder.ErrorBuilderDelegate? failureBuilder) + private readonly ImmutableArray allowedArtifactMediaTypes = ImmutableArray.Create( + BicepMediaTypes.BicepModuleArtifactType, + BicepMediaTypes.BicepProviderArtifactType); + + private readonly ImmutableArray allowedConfigMediaTypes = ImmutableArray.Create( + BicepMediaTypes.BicepModuleConfigV1, + BicepMediaTypes.BicepProviderConfigV1); + + private readonly ImmutableArray allowedLayerMediaTypes = ImmutableArray.Create( + BicepMediaTypes.BicepModuleLayerV1Json, + BicepMediaTypes.BicepProviderArtifactLayerV1TarGzip); + private void ValidateModule(OciArtifactResult artifactResult) + { + var manifest = artifactResult.Manifest; + var artifactType = manifest.ArtifactType; + if (artifactType is not null && + !allowedArtifactMediaTypes.Contains(artifactType, MediaTypeComparer)) + { + throw new InvalidModuleException( + $"Expected OCI artifact to have the artifactType field set to either null or '{BicepMediaTypes.BicepModuleArtifactType}' but found '{artifactType}'.", + InvalidModuleExceptionKind.WrongArtifactType); + } + var config = manifest.Config; + var configMediaType = config.MediaType; + if (configMediaType is not null && + !allowedConfigMediaTypes.Contains(configMediaType, MediaTypeComparer)) + { + throw new InvalidModuleException($"Did not expect config media type \"{configMediaType}\"."); + } + + if (config.Size > 2) + { + throw new InvalidModuleException("Expected an empty config blob."); + } + + if (manifest.Layers.Length != 1) + { + throw new InvalidModuleException("Expected a single layer in the OCI artifact."); + } + + var layer = manifest.Layers.Single(); + if (!allowedLayerMediaTypes.Contains(layer.MediaType, MediaTypeComparer)) + { + throw new InvalidModuleException($"Did not expect layer media type \"{layer.MediaType}\".", InvalidModuleExceptionKind.WrongModuleLayerMediaType); + } + } + + public override bool TryGetLocalModuleEntryPointUri(OciModuleReference reference, [NotNullWhen(true)] out Uri? localUri, [NotNullWhen(false)] out DiagnosticBuilder.ErrorBuilderDelegate? failureBuilder) { failureBuilder = null; localUri = this.GetModuleFileUri(reference, ModuleFileType.ModuleMain); return true; } - public override string? TryGetDocumentationUri(OciArtifactModuleReference ociArtifactModuleReference) + public override string? TryGetDocumentationUri(OciModuleReference ociArtifactModuleReference) { var ociAnnotations = TryGetOciAnnotations(ociArtifactModuleReference); if (ociAnnotations is null || @@ -151,13 +198,43 @@ public static string GetPublicBicepModuleDocumentationUri(string publicModuleNam return $"https://github.com/Azure/bicep-registry-modules/tree/{publicModuleName}/{tag}/modules/{publicModuleName}/README.md"; } - public override Task TryGetDescription(OciArtifactModuleReference ociArtifactModuleReference) + public override Task TryGetDescription(OciModuleReference ociArtifactModuleReference) { var ociAnnotations = TryGetOciAnnotations(ociArtifactModuleReference); return Task.FromResult(DescriptionHelper.TryGetFromOciManifestAnnotations(ociAnnotations)); } - private ImmutableDictionary? TryGetOciAnnotations(OciArtifactModuleReference ociArtifactModuleReference) + private string? TryGetModuleLayerMediaType(OciModuleReference ociArtifactReference) + { + try + { + string manifestFilePath = this.GetModuleFilePath(ociArtifactReference, ModuleFileType.Manifest); + if (!File.Exists(manifestFilePath)) + { + return null; + } + + string manifestFileContents = File.ReadAllText(manifestFilePath); + if (string.IsNullOrWhiteSpace(manifestFileContents)) + { + return null; + } + + OciManifest? ociManifest = JsonConvert.DeserializeObject(manifestFileContents); + if (ociManifest is null) + { + return null; + } + + return ociManifest.Layers.Single().MediaType; + } + catch + { + return null; + } + } + + private ImmutableDictionary? TryGetOciAnnotations(OciModuleReference ociArtifactModuleReference) { try { @@ -187,7 +264,7 @@ public static string GetPublicBicepModuleDocumentationUri(string publicModuleNam } } - public override async Task> RestoreModules(IEnumerable references) + public override async Task> RestoreModules(IEnumerable references) { var statuses = new Dictionary(); @@ -214,12 +291,12 @@ public static string GetPublicBicepModuleDocumentationUri(string publicModuleNam return statuses; } - public override async Task> InvalidateModulesCache(IEnumerable references) + public override async Task> InvalidateModulesCache(IEnumerable references) { return await base.InvalidateModulesCacheInternal(references); } - public override async Task PublishModule(OciArtifactModuleReference moduleReference, Stream compiled, string? documentationUri, string? description) + public override async Task PublishModule(OciModuleReference moduleReference, Stream compiled, string? documentationUri, string? description) { var config = new StreamDescriptor(Stream.Null, BicepMediaTypes.BicepModuleConfigV1); var layer = new StreamDescriptor(compiled, BicepMediaTypes.BicepModuleLayerV1Json); @@ -240,19 +317,50 @@ public override async Task PublishModule(OciArtifactModuleReference moduleRefere } } - protected override void WriteModuleContent(OciArtifactModuleReference reference, OciArtifactResult result) + // media types are case-insensitive (they are lowercase by convention only) + public static readonly IEqualityComparer MediaTypeComparer = StringComparer.OrdinalIgnoreCase; + + protected override void WriteModuleContent(OciModuleReference reference, OciArtifactResult result) { /* * this should be kept in sync with the IsModuleRestoreRequired() implementation */ - // write main.bicep - this.FileResolver.Write(this.GetModuleFileUri(reference, ModuleFileType.ModuleMain), result.ModuleStream); // write manifest // it's important to write the original stream here rather than serialize the manifest object // this way we guarantee the manifest hash will match - this.FileResolver.Write(this.GetModuleFileUri(reference, ModuleFileType.Manifest), result.ManifestStream); + var manifestFileUri = this.GetModuleFileUri(reference, ModuleFileType.Manifest); + using var manifestStream = result.ToStream(); + this.FileResolver.Write(manifestFileUri, manifestStream); + + var mediaType = TryGetModuleLayerMediaType(reference); + if (mediaType is not null) + { + switch (mediaType) + { + // NOTE(asilverman): currently the only difference in the processing is the filename written to disk + // but this may change in the future if we chose to publish providers in multiple layers. + case BicepMediaTypes.BicepModuleLayerV1Json: + { + // write module.json + var moduleData = result.Layers.Single(); + using var moduleStream = moduleData!.ToStream(); + this.FileResolver.Write(this.GetModuleFileUri(reference, ModuleFileType.ModuleMain), moduleStream); + break; + } + case BicepMediaTypes.BicepProviderArtifactLayerV1TarGzip: + { + // write provider.tar.gz + var providerData = result.Layers.Single(); + using var providerStream = providerData!.ToStream(); + this.FileResolver.Write(this.GetModuleFileUri(reference, ModuleFileType.Provider), providerStream); + break; + } + default: + break; + } + } // write metadata var metadata = new ModuleMetadata(result.ManifestDigest); @@ -262,7 +370,7 @@ protected override void WriteModuleContent(OciArtifactModuleReference reference, this.FileResolver.Write(this.GetModuleFileUri(reference, ModuleFileType.Metadata), metadataStream); } - protected override string GetModuleDirectoryPath(OciArtifactModuleReference reference) + protected override string GetModuleDirectoryPath(OciModuleReference reference) { // cachePath is already set to %userprofile%\.bicep\br or ~/.bicep/br by default depending on OS // we need to split each component of the reference into a sub directory to fit within the max file name length limit on linux and mac @@ -303,13 +411,14 @@ protected override string GetModuleDirectoryPath(OciArtifactModuleReference refe return Path.Combine(this.cachePath, registry, repository, tagOrDigest); } - protected override Uri GetModuleLockFileUri(OciArtifactModuleReference reference) => this.GetModuleFileUri(reference, ModuleFileType.Lock); + protected override Uri GetModuleLockFileUri(OciModuleReference reference) => this.GetModuleFileUri(reference, ModuleFileType.Lock); - private async Task<(OciArtifactResult?, string? errorMessage)> TryPullArtifactAsync(RootConfiguration configuration, OciArtifactModuleReference reference) + private async Task<(OciArtifactResult?, string? errorMessage)> TryPullArtifactAsync(RootConfiguration configuration, OciModuleReference reference) { try { var result = await this.client.PullArtifactAsync(configuration, reference); + ValidateModule(result); await this.TryWriteModuleContentAsync(reference, result); @@ -340,7 +449,7 @@ protected override string GetModuleDirectoryPath(OciArtifactModuleReference refe private static bool CheckAllInnerExceptionsAreRequestFailures(AggregateException exception) => exception.InnerExceptions.All(inner => inner is RequestFailedException); - private Uri GetModuleFileUri(OciArtifactModuleReference reference, ModuleFileType fileType) + private Uri GetModuleFileUri(OciModuleReference reference, ModuleFileType fileType) { string localFilePath = this.GetModuleFilePath(reference, fileType); if (Uri.TryCreate(localFilePath, UriKind.Absolute, out var uri)) @@ -351,7 +460,7 @@ private Uri GetModuleFileUri(OciArtifactModuleReference reference, ModuleFileTyp throw new NotImplementedException($"Local module file path is malformed: \"{localFilePath}\""); } - private string GetModuleFilePath(OciArtifactModuleReference reference, ModuleFileType fileType) + private string GetModuleFilePath(OciModuleReference reference, ModuleFileType fileType) { var fileName = fileType switch { @@ -359,6 +468,7 @@ private string GetModuleFilePath(OciArtifactModuleReference reference, ModuleFil ModuleFileType.Lock => "lock", ModuleFileType.Manifest => "manifest", ModuleFileType.Metadata => "metadata", + ModuleFileType.Provider => "types.tgz", _ => throw new NotImplementedException($"Unexpected module file type '{fileType}'.") }; @@ -370,7 +480,8 @@ private enum ModuleFileType ModuleMain, Manifest, Lock, - Metadata + Metadata, + Provider }; } } diff --git a/src/Bicep.Core/Semantics/SemanticModelHelper.cs b/src/Bicep.Core/Semantics/SemanticModelHelper.cs index 5dab08f7550..e6049caa473 100644 --- a/src/Bicep.Core/Semantics/SemanticModelHelper.cs +++ b/src/Bicep.Core/Semantics/SemanticModelHelper.cs @@ -45,7 +45,7 @@ function.DeclaringObject is NamespaceType namespaceType && } public static bool TryGetSemanticModelForForeignTemplateReference(ISourceFileLookup sourceFileLookup, - IForeignTemplateReference reference, + IForeignArtifactReference reference, DiagnosticBuilder.ErrorBuilderDelegate onInvalidSourceFileType, ISemanticModelLookup semanticModelLookup, [NotNullWhen(true)] out ISemanticModel? semanticModel, diff --git a/src/Bicep.Core/Syntax/CompileTimeImportDeclarationSyntax.cs b/src/Bicep.Core/Syntax/CompileTimeImportDeclarationSyntax.cs index 99c96421ff8..ae776ec30f3 100644 --- a/src/Bicep.Core/Syntax/CompileTimeImportDeclarationSyntax.cs +++ b/src/Bicep.Core/Syntax/CompileTimeImportDeclarationSyntax.cs @@ -7,7 +7,7 @@ namespace Bicep.Core.Syntax; -public class CompileTimeImportDeclarationSyntax : StatementSyntax, ITopLevelDeclarationSyntax, IForeignTemplateReference +public class CompileTimeImportDeclarationSyntax : StatementSyntax, ITopLevelDeclarationSyntax, IForeignArtifactReference { public CompileTimeImportDeclarationSyntax(IEnumerable leadingNodes, Token keyword, SyntaxBase importExpression, SyntaxBase fromClause) : base(leadingNodes) @@ -27,7 +27,7 @@ public CompileTimeImportDeclarationSyntax(IEnumerable leadingNodes, public SyntaxBase FromClause { get; } - SyntaxBase IForeignTemplateReference.ReferenceSourceSyntax + SyntaxBase IForeignArtifactReference.ReferenceSourceSyntax => FromClause is CompileTimeImportFromClauseSyntax fromClauseSyntax ? fromClauseSyntax.Path : FromClause; public override void Accept(ISyntaxVisitor visitor) => visitor.VisitCompileTimeImportDeclarationSyntax(this); diff --git a/src/Bicep.Core/Syntax/ModuleDeclarationSyntax.cs b/src/Bicep.Core/Syntax/ModuleDeclarationSyntax.cs index ea901fd563a..8508267127f 100644 --- a/src/Bicep.Core/Syntax/ModuleDeclarationSyntax.cs +++ b/src/Bicep.Core/Syntax/ModuleDeclarationSyntax.cs @@ -9,7 +9,7 @@ namespace Bicep.Core.Syntax { - public class ModuleDeclarationSyntax : StatementSyntax, ITopLevelNamedDeclarationSyntax, IForeignTemplateReference + public class ModuleDeclarationSyntax : StatementSyntax, ITopLevelNamedDeclarationSyntax, IForeignArtifactReference { public ModuleDeclarationSyntax(IEnumerable leadingNodes, Token keyword, IdentifierSyntax name, SyntaxBase path, SyntaxBase assignment, ImmutableArray newlines, SyntaxBase value) : base(leadingNodes) @@ -48,7 +48,7 @@ public ModuleDeclarationSyntax(IEnumerable leadingNodes, Token keywo public StringSyntax? TryGetPath() => Path as StringSyntax; - SyntaxBase IForeignTemplateReference.ReferenceSourceSyntax => Path; + SyntaxBase IForeignArtifactReference.ReferenceSourceSyntax => Path; public ObjectSyntax? TryGetBody() => this.Value switch diff --git a/src/Bicep.Core/Syntax/ProviderDeclarationSyntax.cs b/src/Bicep.Core/Syntax/ProviderDeclarationSyntax.cs index b21f7de957d..c300a15d888 100644 --- a/src/Bicep.Core/Syntax/ProviderDeclarationSyntax.cs +++ b/src/Bicep.Core/Syntax/ProviderDeclarationSyntax.cs @@ -3,12 +3,13 @@ using Bicep.Core.Navigation; using Bicep.Core.Parsing; +using Bicep.Core.Registry.Oci; using System; using System.Collections.Generic; namespace Bicep.Core.Syntax { - public class ProviderDeclarationSyntax : StatementSyntax, ITopLevelDeclarationSyntax + public class ProviderDeclarationSyntax : StatementSyntax, ITopLevelDeclarationSyntax, IForeignArtifactReference { private readonly Lazy lazySpecification; @@ -40,8 +41,14 @@ public ProviderDeclarationSyntax(IEnumerable leadingNodes, Token key public IdentifierSyntax? Alias => (this.AsClause as AliasAsClauseSyntax)?.Alias; + private StringSyntax ProviderPath => SyntaxFactory.CreateStringLiteral($@"{OciArtifactReferenceFacts.Scheme}:{LanguageConstants.BicepPublicMcrRegistry}/bicep/providers/{this.Specification.Name}:{this.Specification.Version}"); + public override TextSpan Span => TextSpan.Between(this.Keyword, TextSpan.LastNonNull(this.SpecificationString, this.WithClause, this.AsClause)); + SyntaxBase IForeignArtifactReference.ReferenceSourceSyntax => ProviderPath; + public override void Accept(ISyntaxVisitor visitor) => visitor.VisitProviderDeclarationSyntax(this); + + public StringSyntax? TryGetPath() => ProviderPath; } } diff --git a/src/Bicep.Core/Syntax/SyntaxHelper.cs b/src/Bicep.Core/Syntax/SyntaxHelper.cs index b6d795ea258..833ac60137e 100644 --- a/src/Bicep.Core/Syntax/SyntaxHelper.cs +++ b/src/Bicep.Core/Syntax/SyntaxHelper.cs @@ -32,7 +32,7 @@ public static class SyntaxHelper return null; } - public static bool TryGetForeignTemplatePath(IForeignTemplateReference foreignTemplateReference, + public static bool TryGetForeignTemplatePath(IForeignArtifactReference foreignTemplateReference, [NotNullWhen(true)] out string? path, [NotNullWhen(false)] out DiagnosticBuilder.ErrorBuilderDelegate? failureBuilder) { @@ -55,7 +55,7 @@ public static bool TryGetForeignTemplatePath(IForeignTemplateReference foreignTe return true; } - private static DiagnosticBuilder.ErrorBuilderDelegate OnMissingPathSyntaxErrorBuilder(IForeignTemplateReference syntax) => syntax switch + private static DiagnosticBuilder.ErrorBuilderDelegate OnMissingPathSyntaxErrorBuilder(IForeignArtifactReference syntax) => syntax switch { ModuleDeclarationSyntax => x => x.ModulePathHasNotBeenSpecified(), UsingDeclarationSyntax => x => x.UsingPathHasNotBeenSpecified(), diff --git a/src/Bicep.Core/Syntax/TestDeclarationSyntax.cs b/src/Bicep.Core/Syntax/TestDeclarationSyntax.cs index 75b5d65d2e2..d1ba9674038 100644 --- a/src/Bicep.Core/Syntax/TestDeclarationSyntax.cs +++ b/src/Bicep.Core/Syntax/TestDeclarationSyntax.cs @@ -8,7 +8,7 @@ namespace Bicep.Core.Syntax { - public class TestDeclarationSyntax : StatementSyntax, ITopLevelNamedDeclarationSyntax, IForeignTemplateReference + public class TestDeclarationSyntax : StatementSyntax, ITopLevelNamedDeclarationSyntax, IForeignArtifactReference { public TestDeclarationSyntax(IEnumerable leadingNodes, Token keyword, IdentifierSyntax name, SyntaxBase path, SyntaxBase assignment, SyntaxBase value) : base(leadingNodes) diff --git a/src/Bicep.Core/Syntax/UsingDeclarationSyntax.cs b/src/Bicep.Core/Syntax/UsingDeclarationSyntax.cs index 868514a3660..29741be0f98 100644 --- a/src/Bicep.Core/Syntax/UsingDeclarationSyntax.cs +++ b/src/Bicep.Core/Syntax/UsingDeclarationSyntax.cs @@ -6,7 +6,7 @@ namespace Bicep.Core.Syntax { - public class UsingDeclarationSyntax : StatementSyntax, ITopLevelDeclarationSyntax, IForeignTemplateReference + public class UsingDeclarationSyntax : StatementSyntax, ITopLevelDeclarationSyntax, IForeignArtifactReference { public UsingDeclarationSyntax(Token keyword, SyntaxBase path) : base(Enumerable.Empty()) @@ -29,6 +29,6 @@ public UsingDeclarationSyntax(Token keyword, SyntaxBase path) public StringSyntax? TryGetPath() => Path as StringSyntax; - SyntaxBase IForeignTemplateReference.ReferenceSourceSyntax => Path; + SyntaxBase IForeignArtifactReference.ReferenceSourceSyntax => Path; } } diff --git a/src/Bicep.Core/TypeSystem/Az/AzResourceTypeLoaderFactory.cs b/src/Bicep.Core/TypeSystem/Az/AzResourceTypeLoaderFactory.cs index 78874480889..fb7c3e1a411 100644 --- a/src/Bicep.Core/TypeSystem/Az/AzResourceTypeLoaderFactory.cs +++ b/src/Bicep.Core/TypeSystem/Az/AzResourceTypeLoaderFactory.cs @@ -6,6 +6,7 @@ using Bicep.Core.Registry.Oci; using Newtonsoft.Json; using Bicep.Core.Features; +using Bicep.Core.Modules; namespace Bicep.Core.TypeSystem.Az { @@ -31,15 +32,19 @@ public IAzResourceTypeLoader GetBuiltInTypeLoader() return resourceTypeLoaders[BuiltInLoaderKey]; } - public IAzResourceTypeLoader? GetResourceTypeLoader(string? version, IFeatureProvider features) + public IAzResourceTypeLoader? GetResourceTypeLoader(string? providerVersion, IFeatureProvider features) { - if (!features.DynamicTypeLoadingEnabled || version is null) + if (!features.DynamicTypeLoadingEnabled || providerVersion is null) { return resourceTypeLoaders[BuiltInLoaderKey]; } - //TODO(asilverman): The magic strings below are temporary and will be changed to use variables fetched at restore time - var azProviderDir = Path.Combine(features.CacheRootDirectory, "br", "mcr.microsoft.com", @"bicep$providers$az", version); + // compose the path to the OCI manifest based on the cache root directory and provider version + var azProviderDir = Path.Combine( + features.CacheRootDirectory, + ModuleReferenceSchemes.Oci, + LanguageConstants.BicepPublicMcrRegistry, + $"bicep$providers$az{providerVersion}$"); var ociManifestPath = Path.Combine(azProviderDir, "manifest"); if (!File.Exists(ociManifestPath)) { diff --git a/src/Bicep.Core/Workspaces/ISourceFileLookup.cs b/src/Bicep.Core/Workspaces/ISourceFileLookup.cs index 1fc04ce9006..680327edf5b 100644 --- a/src/Bicep.Core/Workspaces/ISourceFileLookup.cs +++ b/src/Bicep.Core/Workspaces/ISourceFileLookup.cs @@ -8,7 +8,7 @@ namespace Bicep.Core.Workspaces; public interface ISourceFileLookup { - public DiagnosticBuilder.ErrorBuilderDelegate? TryGetErrorDiagnostic(IForeignTemplateReference foreignTemplateReference); + public DiagnosticBuilder.ErrorBuilderDelegate? TryGetErrorDiagnostic(IForeignArtifactReference foreignTemplateReference); - public ISourceFile? TryGetSourceFile(IForeignTemplateReference foreignTemplateReference); + public ISourceFile? TryGetSourceFile(IForeignArtifactReference foreignTemplateReference); } diff --git a/src/Bicep.Core/Workspaces/SourceFileGrouping.cs b/src/Bicep.Core/Workspaces/SourceFileGrouping.cs index 8188eca3bad..2fd287519a9 100644 --- a/src/Bicep.Core/Workspaces/SourceFileGrouping.cs +++ b/src/Bicep.Core/Workspaces/SourceFileGrouping.cs @@ -7,10 +7,16 @@ using Bicep.Core.Extensions; using Bicep.Core.FileSystem; using Bicep.Core.Navigation; +using Bicep.Core.Syntax; using static Bicep.Core.Diagnostics.DiagnosticBuilder; namespace Bicep.Core.Workspaces { + + public record ArtifactResolutionInfo( + IForeignArtifactReference DeclarationSyntax, + ISourceFile ParentTemplateFile); + public record FileResolutionResult( Uri FileUri, ErrorBuilderDelegate? ErrorBuilder, @@ -21,28 +27,24 @@ public record UriResolutionResult( bool RequiresRestore, ErrorBuilderDelegate? ErrorBuilder); - public record ModuleSourceResolutionInfo( - IForeignTemplateReference ForeignTemplateReference, - ISourceFile ParentTemplateFile); - public record SourceFileGrouping( IFileResolver FileResolver, Uri EntryFileUri, ImmutableDictionary FileResultByUri, - ImmutableDictionary> UriResultByModule, + ImmutableDictionary> UriResultByModule, ImmutableDictionary> SourceFileParentLookup) : ISourceFileLookup { - public IEnumerable GetModulesToRestore() + public IEnumerable GetModulesToRestore() => UriResultByModule.SelectMany( kvp => kvp.Value .Where(entry => entry.Value.RequiresRestore) - .Select(entry => new ModuleSourceResolutionInfo(entry.Key, kvp.Key))); + .Select(entry => new ArtifactResolutionInfo(entry.Key, kvp.Key))); public BicepSourceFile EntryPoint => (FileResultByUri[EntryFileUri].File as BicepSourceFile)!; public IEnumerable SourceFiles => FileResultByUri.Values.Select(x => x.File).WhereNotNull(); - public ErrorBuilderDelegate? TryGetErrorDiagnostic(IForeignTemplateReference foreignTemplateReference) + public ErrorBuilderDelegate? TryGetErrorDiagnostic(IForeignArtifactReference foreignTemplateReference) { var uriResult = UriResultByModule.Values.Select(d => d.TryGetValue(foreignTemplateReference, out var result) ? result : null).WhereNotNull().First(); if (uriResult.ErrorBuilder is not null) @@ -54,7 +56,7 @@ public IEnumerable GetModulesToRestore() return fileResult.ErrorBuilder; } - public ISourceFile? TryGetSourceFile(IForeignTemplateReference foreignTemplateReference) + public ISourceFile? TryGetSourceFile(IForeignArtifactReference foreignTemplateReference) { var uriResult = UriResultByModule.Values.Select(d => d.TryGetValue(foreignTemplateReference, out var result) ? result : null).WhereNotNull().First(); if (uriResult.FileUri is null) diff --git a/src/Bicep.Core/Workspaces/SourceFileGroupingBuilder.cs b/src/Bicep.Core/Workspaces/SourceFileGroupingBuilder.cs index 07d1c34445a..e97a5063c0d 100644 --- a/src/Bicep.Core/Workspaces/SourceFileGroupingBuilder.cs +++ b/src/Bicep.Core/Workspaces/SourceFileGroupingBuilder.cs @@ -13,6 +13,8 @@ using Bicep.Core.Registry; using Bicep.Core.Syntax; using Bicep.Core.Utils; +using Bicep.Core.Features; +using Bicep.Core.Semantics.Namespaces; using static Bicep.Core.Diagnostics.DiagnosticBuilder; namespace Bicep.Core.Workspaces @@ -24,11 +26,15 @@ public class SourceFileGroupingBuilder private readonly IReadOnlyWorkspace workspace; private readonly Dictionary fileResultByUri; - private readonly ConcurrentDictionary> uriResultByModule; + private readonly ConcurrentDictionary> uriResultByModule; private readonly bool forceModulesRestore; - private SourceFileGroupingBuilder(IFileResolver fileResolver, IModuleDispatcher moduleDispatcher, IReadOnlyWorkspace workspace, bool forceModulesRestore = false) + private SourceFileGroupingBuilder( + IFileResolver fileResolver, + IModuleDispatcher moduleDispatcher, + IReadOnlyWorkspace workspace, + bool forceModulesRestore = false) { this.fileResolver = fileResolver; this.moduleDispatcher = moduleDispatcher; @@ -38,7 +44,12 @@ private SourceFileGroupingBuilder(IFileResolver fileResolver, IModuleDispatcher this.forceModulesRestore = forceModulesRestore; } - private SourceFileGroupingBuilder(IFileResolver fileResolver, IModuleDispatcher moduleDispatcher, IReadOnlyWorkspace workspace, SourceFileGrouping current, bool forceforceModulesRestore = false) + private SourceFileGroupingBuilder( + IFileResolver fileResolver, + IModuleDispatcher moduleDispatcher, + IReadOnlyWorkspace workspace, + SourceFileGrouping current, + bool forceforceModulesRestore = false) { this.fileResolver = fileResolver; this.moduleDispatcher = moduleDispatcher; @@ -48,14 +59,14 @@ private SourceFileGroupingBuilder(IFileResolver fileResolver, IModuleDispatcher this.forceModulesRestore = forceforceModulesRestore; } - public static SourceFileGrouping Build(IFileResolver fileResolver, IModuleDispatcher moduleDispatcher, IReadOnlyWorkspace workspace, Uri entryFileUri, bool forceModulesRestore = false) + public static SourceFileGrouping Build(IFileResolver fileResolver, IModuleDispatcher moduleDispatcher, IReadOnlyWorkspace workspace, Uri entryFileUri, IFeatureProviderFactory featuresFactory, bool forceModulesRestore = false) { var builder = new SourceFileGroupingBuilder(fileResolver, moduleDispatcher, workspace, forceModulesRestore); - return builder.Build(entryFileUri); + return builder.Build(entryFileUri, featuresFactory); } - public static SourceFileGrouping Rebuild(IModuleDispatcher moduleDispatcher, IReadOnlyWorkspace workspace, SourceFileGrouping current) + public static SourceFileGrouping Rebuild(IFeatureProviderFactory featuresFactory, IModuleDispatcher moduleDispatcher, IReadOnlyWorkspace workspace, SourceFileGrouping current) { var builder = new SourceFileGroupingBuilder(current.FileResolver, moduleDispatcher, workspace, current); var isParamsFile = current.FileResultByUri[current.EntryFileUri].File is BicepParamFile; @@ -68,17 +79,20 @@ public static SourceFileGrouping Rebuild(IModuleDispatcher moduleDispatcher, IRe // Rebuild source files that contain external module references restored during the inital build. var sourceFilesToRebuild = current.SourceFiles - .Where(sourceFile => GetModuleDeclarations(sourceFile).Any(moduleDeclaration => modulesToRestore.Contains(new(moduleDeclaration, sourceFile)))) + .Where(sourceFile + => GetModuleDeclarations(sourceFile) + .Any(moduleDeclaration + => modulesToRestore.Contains(new ArtifactResolutionInfo(moduleDeclaration, sourceFile)))) .ToImmutableHashSet() .SelectMany(sourceFile => current.GetFilesDependingOn(sourceFile)) .ToImmutableHashSet(); - return builder.Build(current.EntryPoint.FileUri, sourceFilesToRebuild); + return builder.Build(current.EntryPoint.FileUri, featuresFactory, sourceFilesToRebuild); } - private SourceFileGrouping Build(Uri entryFileUri, ImmutableHashSet? sourceFilesToRebuild = null) + private SourceFileGrouping Build(Uri entryFileUri, IFeatureProviderFactory featuresFactory, ImmutableHashSet? sourceFilesToRebuild = null) { - var fileResult = this.PopulateRecursive(entryFileUri, null, sourceFilesToRebuild); + var fileResult = this.PopulateRecursive(entryFileUri, null, sourceFilesToRebuild, featuresFactory); if (fileResult.File is null) { @@ -128,69 +142,77 @@ private FileResolutionResult GetFileResolutionResultWithCaching(Uri fileUri, Mod return resolutionResult; } - private FileResolutionResult PopulateRecursive(Uri fileUri, ModuleReference? moduleReference, ImmutableHashSet? sourceFileToRebuild) + private FileResolutionResult PopulateRecursive(Uri fileUri, ModuleReference? moduleReference, ImmutableHashSet? sourceFileToRebuild, IFeatureProviderFactory featuresFactory) { var fileResult = GetFileResolutionResultWithCaching(fileUri, moduleReference); - + var features = featuresFactory.GetFeatureProvider(fileUri); switch (fileResult.File) { case BicepFile bicepFile: - { - foreach (var restorable in bicepFile.ProgramSyntax.Children.OfType()) { - var (childModuleReference, uriResult) = GetModuleRestoreResult(fileUri, restorable); - - uriResultByModule.GetOrAdd(bicepFile, f => new())[restorable] = uriResult; - if (uriResult.FileUri is null) + foreach (var restorable in bicepFile.ProgramSyntax.Children.OfType()) { - continue; - } + // NOTE(asilverman): The below check is ugly but temporary until we have a better way to + // handle dynamic type loading in a way that is decoupled from modules. + if (restorable is ProviderDeclarationSyntax providerImport && + (providerImport.Specification.Name != AzNamespaceType.BuiltInName || !features.DynamicTypeLoadingEnabled)) + { + continue; + } + var (childModuleReference, uriResult) = GetModuleRestoreResult(fileUri, restorable); - if (!fileResultByUri.TryGetValue(uriResult.FileUri, out var childResult) || - (childResult.File is not null && sourceFileToRebuild is not null && sourceFileToRebuild.Contains(childResult.File))) - { - // only recurse if we've not seen this file before - to avoid infinite loops - childResult = PopulateRecursive(uriResult.FileUri, childModuleReference, sourceFileToRebuild); - } + uriResultByModule.GetOrAdd(bicepFile, f => new())[restorable] = uriResult; - fileResultByUri[uriResult.FileUri] = childResult; + if (uriResult.FileUri is null) + { + continue; + } + + if (!fileResultByUri.TryGetValue(uriResult.FileUri, out var childResult) || + (childResult.File is not null && sourceFileToRebuild is not null && sourceFileToRebuild.Contains(childResult.File))) + { + // only recurse if we've not seen this file before - to avoid infinite loops + childResult = PopulateRecursive(uriResult.FileUri, childModuleReference, sourceFileToRebuild, featuresFactory); + } + + fileResultByUri[uriResult.FileUri] = childResult; + } + break; } - break; - } case BicepParamFile paramsFile: - { - foreach (var usingDeclaration in paramsFile.ProgramSyntax.Children.OfType()) { - if (!SyntaxHelper.TryGetForeignTemplatePath(usingDeclaration, out var usingFilePath, out var errorBuilder)) + foreach (var usingDeclaration in paramsFile.ProgramSyntax.Children.OfType()) { - uriResultByModule.GetOrAdd(paramsFile, f => new())[usingDeclaration] = new(null, false, errorBuilder); - continue; - } + if (!SyntaxHelper.TryGetForeignTemplatePath(usingDeclaration, out var usingFilePath, out var errorBuilder)) + { + uriResultByModule.GetOrAdd(paramsFile, f => new())[usingDeclaration] = new(null, false, errorBuilder); + continue; + } - if (fileResolver.TryResolveFilePath(fileUri, usingFilePath) is not {} usingFileUri) - { - uriResultByModule.GetOrAdd(paramsFile, f => new())[usingDeclaration] = new(null, false, x => x.FilePathCouldNotBeResolved(usingFilePath, fileUri.LocalPath)); - continue; - } + if (fileResolver.TryResolveFilePath(fileUri, usingFilePath) is not { } usingFileUri) + { + uriResultByModule.GetOrAdd(paramsFile, f => new())[usingDeclaration] = new(null, false, x => x.FilePathCouldNotBeResolved(usingFilePath, fileUri.LocalPath)); + continue; + } - uriResultByModule.GetOrAdd(paramsFile, f => new())[usingDeclaration] = new(usingFileUri, false, null); + uriResultByModule.GetOrAdd(paramsFile, f => new())[usingDeclaration] = new(usingFileUri, false, null); - if (!fileResultByUri.TryGetValue(usingFileUri, out var childResult)) - { - // only recurse if we've not seen this file before - to avoid infinite loops - childResult = PopulateRecursive(usingFileUri, null, sourceFileToRebuild); - } + if (!fileResultByUri.TryGetValue(usingFileUri, out var childResult)) + { + // only recurse if we've not seen this file before - to avoid infinite loops + childResult = PopulateRecursive(usingFileUri, null, sourceFileToRebuild, featuresFactory); + } - fileResultByUri[usingFileUri] = childResult; + fileResultByUri[usingFileUri] = childResult; + } + break; } - break; - } } return fileResult; } - private (ModuleReference? reference, UriResolutionResult result) GetModuleRestoreResult(Uri parentFileUri, IForeignTemplateReference foreignTemplateReference) + private (ModuleReference? reference, UriResolutionResult result) GetModuleRestoreResult(Uri parentFileUri, IForeignArtifactReference foreignTemplateReference) { if (!moduleDispatcher.TryGetModuleReference(foreignTemplateReference, parentFileUri, out var moduleReference, out var referenceResolutionError)) { @@ -212,10 +234,10 @@ private FileResolutionResult PopulateRecursive(Uri fileUri, ModuleReference? mod var restoreStatus = moduleDispatcher.GetModuleRestoreStatus(moduleReference, out var restoreErrorBuilder); switch (restoreStatus) { - case ModuleRestoreStatus.Unknown: + case ArtifactRestoreStatus.Unknown: // we have not yet attempted to restore the module, so let's do it return (moduleReference, new(moduleFileUri, true, x => x.ModuleRequiresRestore(moduleReference.FullyQualifiedReference))); - case ModuleRestoreStatus.Failed: + case ArtifactRestoreStatus.Failed: // the module has not yet been restored or restore failed // in either case, set the error return (moduleReference, new(null, false, restoreErrorBuilder)); @@ -245,7 +267,7 @@ private ILookup ReportFailuresForCycles() foreach (var (statement, urlResult) in uriResultByModuleForFile) { if (urlResult.FileUri is not null && - fileResultByUri[urlResult.FileUri].File is {} sourceFile && + fileResultByUri[urlResult.FileUri].File is { } sourceFile && cycles.TryGetValue(sourceFile, out var cycle)) { if (cycle.Length == 1) @@ -272,11 +294,11 @@ fileResultByUri[urlResult.FileUri].File is {} sourceFile && /// This method only looks at top-level statements. If nested syntax nodes can be foreign template references at any point in the future, /// a SyntaxAggregator will need to be used in place of the sourceFile.ProgramSyntax.Children expressions. /// - private static IEnumerable GetReferenceSourceNodes(ISourceFile sourceFile) => sourceFile switch + private static IEnumerable GetReferenceSourceNodes(ISourceFile sourceFile) => sourceFile switch { - BicepFile bicepFile => bicepFile.ProgramSyntax.Children.OfType(), - BicepParamFile paramsFile => paramsFile.ProgramSyntax.Children.OfType(), - _ => Enumerable.Empty(), + BicepFile bicepFile => bicepFile.ProgramSyntax.Children.OfType(), + BicepParamFile paramsFile => paramsFile.ProgramSyntax.Children.OfType(), + _ => Enumerable.Empty(), }; private static IEnumerable GetModuleDeclarations(ISourceFile sourceFile) => sourceFile is BicepFile bicepFile diff --git a/src/Bicep.LangServer.IntegrationTests/HoverTests.cs b/src/Bicep.LangServer.IntegrationTests/HoverTests.cs index 470f50191b6..f22678f7198 100644 --- a/src/Bicep.LangServer.IntegrationTests/HoverTests.cs +++ b/src/Bicep.LangServer.IntegrationTests/HoverTests.cs @@ -995,7 +995,13 @@ private async Task GetLanguageClientAsync( tag); SharedLanguageHelperManager sharedLanguageHelperManager = new(); - sharedLanguageHelperManager.Initialize(async () => await MultiFileLanguageServerHelper.StartLanguageServer(TestContext, services => services.WithFeatureProviderFactory(featureProviderFactory).WithModuleDispatcher(moduleDispatcher).WithCompilationManager(compilationManager))); + sharedLanguageHelperManager.Initialize( + async () => await MultiFileLanguageServerHelper.StartLanguageServer( + TestContext, + services => services + .WithFeatureProviderFactory(featureProviderFactory) + .WithModuleDispatcher(moduleDispatcher) + .WithCompilationManager(compilationManager))); var multiFileLanguageServerHelper = await sharedLanguageHelperManager.GetAsync(); return multiFileLanguageServerHelper.Client; diff --git a/src/Bicep.LangServer.IntegrationTests/Registry/ModuleRestoreSchedulerTests.cs b/src/Bicep.LangServer.IntegrationTests/Registry/ModuleRestoreSchedulerTests.cs index ae3250860d2..7d859d5227c 100644 --- a/src/Bicep.LangServer.IntegrationTests/Registry/ModuleRestoreSchedulerTests.cs +++ b/src/Bicep.LangServer.IntegrationTests/Registry/ModuleRestoreSchedulerTests.cs @@ -39,11 +39,9 @@ public class ModuleRestoreSchedulerTests public async Task DisposeAfterCreateShouldNotThrow() { var moduleDispatcher = Repository.Create(); - await using (var scheduler = new ModuleRestoreScheduler(moduleDispatcher.Object)) - { - // intentional extra call to dispose() - await scheduler.DisposeAsync(); - } + await using var scheduler = new ModuleRestoreScheduler(moduleDispatcher.Object); + // intentional extra call to dispose() + await scheduler.DisposeAsync(); } [TestMethod] @@ -77,7 +75,7 @@ public async Task PublicMethodsShouldThrowAfterDispose() Action startFail = () => scheduler.Start(); startFail.Should().Throw(); - Action requestFail = () => scheduler.RequestModuleRestore(Repository.Create().Object, DocumentUri.From("untitled://one"), Enumerable.Empty()); + Action requestFail = () => scheduler.RequestModuleRestore(Repository.Create().Object, DocumentUri.From("untitled://one"), Enumerable.Empty()); requestFail.Should().Throw(); } @@ -158,7 +156,7 @@ public async Task RestoreShouldBeScheduledAsRequested() } } - private static ImmutableArray CreateModules(params string[] references) + private static ImmutableArray CreateModules(params string[] references) { var buffer = new StringBuilder(); foreach (var reference in references) @@ -167,7 +165,7 @@ private static ImmutableArray CreateModules(params s } var file = SourceFileFactory.CreateBicepFile(new System.Uri("untitled://hello"), buffer.ToString()); - return file.ProgramSyntax.Declarations.OfType().Select(mds => new ModuleSourceResolutionInfo(mds, file as ISourceFile)).ToImmutableArray(); + return file.ProgramSyntax.Declarations.OfType().Select(mds => new ArtifactResolutionInfo(mds, file as ISourceFile)).ToImmutableArray(); } private class MockRegistry : IModuleRegistry diff --git a/src/Bicep.LangServer.UnitTests/BicepCompilationManagerHelper.cs b/src/Bicep.LangServer.UnitTests/BicepCompilationManagerHelper.cs index 2e62150bbaa..37028634738 100644 --- a/src/Bicep.LangServer.UnitTests/BicepCompilationManagerHelper.cs +++ b/src/Bicep.LangServer.UnitTests/BicepCompilationManagerHelper.cs @@ -100,7 +100,7 @@ public static ICompilationProvider CreateEmptyCompilationProvider(IConfiguration public static Mock CreateMockScheduler() { var scheduler = Repository.Create(); - scheduler.Setup(m => m.RequestModuleRestore(It.IsAny(), It.IsAny(), It.IsAny>())); + scheduler.Setup(m => m.RequestModuleRestore(It.IsAny(), It.IsAny(), It.IsAny>())); return scheduler; } diff --git a/src/Bicep.LangServer.UnitTests/BicepRegistryCacheRequestHandlerTests.cs b/src/Bicep.LangServer.UnitTests/BicepRegistryCacheRequestHandlerTests.cs index 79d4eab5246..be9c433a4a4 100644 --- a/src/Bicep.LangServer.UnitTests/BicepRegistryCacheRequestHandlerTests.cs +++ b/src/Bicep.LangServer.UnitTests/BicepRegistryCacheRequestHandlerTests.cs @@ -89,12 +89,12 @@ public async Task ExternalModuleNotInCacheShouldThrow() var configuration = IConfigurationManager.GetBuiltInConfiguration(); var parentModuleLocalPath = "/foo/main.bicep"; var parentModuleUri = new Uri($"file://{parentModuleLocalPath}"); - OciArtifactModuleReference.TryParse(null, UnqualifiedModuleRefStr, configuration, parentModuleUri, out var moduleReference, out _).Should().BeTrue(); + OciModuleReference.TryParse(null, UnqualifiedModuleRefStr, configuration, parentModuleUri, out var moduleReference, out _).Should().BeTrue(); moduleReference.Should().NotBeNull(); ModuleReference? outRef = moduleReference; dispatcher.Setup(m => m.TryGetModuleReference(ModuleRefStr, parentModuleUri, out outRef, out failureBuilder)).Returns(true); - dispatcher.Setup(m => m.GetModuleRestoreStatus(moduleReference!, out failureBuilder)).Returns(ModuleRestoreStatus.Unknown); + dispatcher.Setup(m => m.GetModuleRestoreStatus(moduleReference!, out failureBuilder)).Returns(ArtifactRestoreStatus.Unknown); var resolver = StrictMock.Of(); @@ -119,12 +119,12 @@ public async Task ExternalModuleFailedEntryPointShouldThrow() var configuration = IConfigurationManager.GetBuiltInConfiguration(); var parentModuleLocalPath = "/main.bicep"; var parentModuleUri = new Uri($"file://{parentModuleLocalPath}"); - OciArtifactModuleReference.TryParse(null, UnqualifiedModuleRefStr, configuration, parentModuleUri, out var moduleReference, out _).Should().BeTrue(); + OciModuleReference.TryParse(null, UnqualifiedModuleRefStr, configuration, parentModuleUri, out var moduleReference, out _).Should().BeTrue(); moduleReference.Should().NotBeNull(); ModuleReference? outRef = moduleReference; dispatcher.Setup(m => m.TryGetModuleReference(ModuleRefStr, parentModuleUri, out outRef, out failureBuilder)).Returns(true); - dispatcher.Setup(m => m.GetModuleRestoreStatus(moduleReference!, out failureBuilder)).Returns(ModuleRestoreStatus.Succeeded); + dispatcher.Setup(m => m.GetModuleRestoreStatus(moduleReference!, out failureBuilder)).Returns(ArtifactRestoreStatus.Succeeded); Uri? @null = null; dispatcher.Setup(m => m.TryGetLocalModuleEntryPointUri(moduleReference!, out @null, out failureBuilder)).Returns(false); @@ -155,12 +155,12 @@ public async Task FailureToReadEntryPointShouldThrow() var fileUri = new Uri("file:///main.bicep"); var configuration = IConfigurationManager.GetBuiltInConfiguration(); - OciArtifactModuleReference.TryParse(null, UnqualifiedModuleRefStr, configuration, fileUri, out var moduleReference, out _).Should().BeTrue(); + OciModuleReference.TryParse(null, UnqualifiedModuleRefStr, configuration, fileUri, out var moduleReference, out _).Should().BeTrue(); moduleReference.Should().NotBeNull(); ModuleReference? outRef = moduleReference; dispatcher.Setup(m => m.TryGetModuleReference(ModuleRefStr, It.IsAny(), out outRef, out nullBuilder)).Returns(true); - dispatcher.Setup(m => m.GetModuleRestoreStatus(moduleReference!, out nullBuilder)).Returns(ModuleRestoreStatus.Succeeded); + dispatcher.Setup(m => m.GetModuleRestoreStatus(moduleReference!, out nullBuilder)).Returns(ArtifactRestoreStatus.Succeeded); dispatcher.Setup(m => m.TryGetLocalModuleEntryPointUri(moduleReference!, out fileUri, out nullBuilder)).Returns(true); var resolver = StrictMock.Of(); @@ -192,12 +192,12 @@ public async Task RestoredValidModuleShouldReturnSuccessfully() var fileUri = new Uri("file:///foo/bar/main.bicep"); var configuration = ConfigurationManager.GetConfiguration(fileUri); - OciArtifactModuleReference.TryParse(null, UnqualifiedModuleRefStr, configuration, fileUri, out var moduleReference, out _).Should().BeTrue(); + OciModuleReference.TryParse(null, UnqualifiedModuleRefStr, configuration, fileUri, out var moduleReference, out _).Should().BeTrue(); moduleReference.Should().NotBeNull(); ModuleReference? outRef = moduleReference; dispatcher.Setup(m => m.TryGetModuleReference(ModuleRefStr, It.IsAny(), out outRef, out nullBuilder)).Returns(true); - dispatcher.Setup(m => m.GetModuleRestoreStatus(moduleReference!, out nullBuilder)).Returns(ModuleRestoreStatus.Succeeded); + dispatcher.Setup(m => m.GetModuleRestoreStatus(moduleReference!, out nullBuilder)).Returns(ArtifactRestoreStatus.Succeeded); dispatcher.Setup(m => m.TryGetLocalModuleEntryPointUri(moduleReference!, out fileUri, out nullBuilder)).Returns(true); var resolver = StrictMock.Of(); diff --git a/src/Bicep.LangServer/BicepCompilationManager.cs b/src/Bicep.LangServer/BicepCompilationManager.cs index c7814859957..f758859ac4e 100644 --- a/src/Bicep.LangServer/BicepCompilationManager.cs +++ b/src/Bicep.LangServer/BicepCompilationManager.cs @@ -329,7 +329,7 @@ private CompilationContextBase CreateCompilationContext(IWorkspace workspace, Do } // this completes immediately - this.scheduler.RequestModuleRestore(this, documentUri, context.Compilation.SourceFileGrouping.GetModulesToRestore()); + this.scheduler.RequestModuleRestore(this, documentUri, context.Compilation.SourceFileGrouping.GetModulesToRestore().OfType()); var sourceFiles = context.Compilation.SourceFileGrouping.SourceFiles; var output = workspace.UpsertSourceFiles(sourceFiles); diff --git a/src/Bicep.LangServer/Handlers/BicepDefinitionHandler.cs b/src/Bicep.LangServer/Handlers/BicepDefinitionHandler.cs index fd280acbd76..7c5b194074f 100644 --- a/src/Bicep.LangServer/Handlers/BicepDefinitionHandler.cs +++ b/src/Bicep.LangServer/Handlers/BicepDefinitionHandler.cs @@ -398,7 +398,7 @@ semanticModel is SemanticModel bicepModel && }; } - private static (Template?, Uri?) GetArmSourceTemplateInfo(CompilationContext context, IForeignTemplateReference foreignTemplateReference) + private static (Template?, Uri?) GetArmSourceTemplateInfo(CompilationContext context, IForeignArtifactReference foreignTemplateReference) => context.Compilation.SourceFileGrouping.TryGetSourceFile(foreignTemplateReference) switch { TemplateSpecFile templateSpecFile => (templateSpecFile.MainTemplateFile.Template, templateSpecFile.FileUri), diff --git a/src/Bicep.LangServer/Handlers/BicepForceModulesRestoreCommandHandler.cs b/src/Bicep.LangServer/Handlers/BicepForceModulesRestoreCommandHandler.cs index 3c6b691010c..63fb5808df8 100644 --- a/src/Bicep.LangServer/Handlers/BicepForceModulesRestoreCommandHandler.cs +++ b/src/Bicep.LangServer/Handlers/BicepForceModulesRestoreCommandHandler.cs @@ -13,6 +13,7 @@ using OmniSharp.Extensions.LanguageServer.Protocol.Workspace; using System.Text; using Bicep.Core.Syntax; +using Bicep.Core.Features; namespace Bicep.LanguageServer.Handlers { @@ -23,13 +24,20 @@ public class BicepForceModulesRestoreCommandHandler : ExecuteTypedResponseComman private readonly IFileResolver fileResolver; private readonly IModuleDispatcher moduleDispatcher; private readonly IWorkspace workspace; + private readonly IFeatureProviderFactory featureProviderFactory; - public BicepForceModulesRestoreCommandHandler(ISerializer serializer, IFileResolver fileResolver, IModuleDispatcher moduleDispatcher, IWorkspace workspace) + public BicepForceModulesRestoreCommandHandler( + ISerializer serializer, + IFileResolver fileResolver, + IModuleDispatcher moduleDispatcher, + IWorkspace workspace, + IFeatureProviderFactory featureProviderFactory) : base(LangServerConstants.ForceModulesRestoreCommand, serializer) { this.fileResolver = fileResolver; this.moduleDispatcher = moduleDispatcher; this.workspace = workspace; + this.featureProviderFactory = featureProviderFactory; } public override Task Handle(string bicepFilePath, CancellationToken cancellationToken) @@ -49,11 +57,16 @@ private async Task ForceModulesRestoreAndGenerateOutputMessage(DocumentU { var fileUri = documentUri.ToUri(); - SourceFileGrouping sourceFileGrouping = SourceFileGroupingBuilder.Build(this.fileResolver, this.moduleDispatcher, workspace, fileUri); + SourceFileGrouping sourceFileGrouping = SourceFileGroupingBuilder.Build( + this.fileResolver, + this.moduleDispatcher, + workspace, + fileUri, + featureProviderFactory); // Ignore modules to restore logic, include all modules to be restored var modulesToRestore = sourceFileGrouping.UriResultByModule - .SelectMany(kvp => kvp.Value.Keys.OfType().Select(mds => new ModuleSourceResolutionInfo(mds, kvp.Key))); + .SelectMany(kvp => kvp.Value.Keys.OfType().Select(mds => new ArtifactResolutionInfo(mds, kvp.Key))); // RestoreModules() does a distinct but we'll do it also to prevent duplicates in outputs and logging var modulesToRestoreReferences = this.moduleDispatcher.GetValidModuleReferences(modulesToRestore) diff --git a/src/Bicep.LangServer/Handlers/BicepHoverHandler.cs b/src/Bicep.LangServer/Handlers/BicepHoverHandler.cs index 3780790baae..cbb2269ddf8 100644 --- a/src/Bicep.LangServer/Handlers/BicepHoverHandler.cs +++ b/src/Bicep.LangServer/Handlers/BicepHoverHandler.cs @@ -65,7 +65,9 @@ public BicepHoverHandler( private static string? TryGetDescriptionMarkdown(SymbolResolutionResult result, DeclaredSymbol symbol) { if (symbol.DeclaringSyntax is DecorableSyntax decorableSyntax && - DescriptionHelper.TryGetFromDecorator(result.Context.Compilation.GetEntrypointSemanticModel(), decorableSyntax) is { } description) + DescriptionHelper.TryGetFromDecorator( + result.Context.Compilation.GetEntrypointSemanticModel(), + decorableSyntax) is { } description) { return description; } @@ -170,7 +172,12 @@ public BicepHoverHandler( } } - private static async Task GetModuleMarkdown(HoverParams request, SymbolResolutionResult result, IModuleDispatcher moduleDispatcher, IModuleRegistryProvider moduleRegistryProvider, ModuleSymbol module) + private static async Task GetModuleMarkdown( + HoverParams request, + SymbolResolutionResult result, + IModuleDispatcher moduleDispatcher, + IModuleRegistryProvider moduleRegistryProvider, + ModuleSymbol module) { if (!SyntaxHelper.TryGetForeignTemplatePath(module.DeclaringModule, out var filePath, out _)) { diff --git a/src/Bicep.LangServer/Handlers/BicepRegistryCacheRequestHandler.cs b/src/Bicep.LangServer/Handlers/BicepRegistryCacheRequestHandler.cs index 092a074306c..587bae1152f 100644 --- a/src/Bicep.LangServer/Handlers/BicepRegistryCacheRequestHandler.cs +++ b/src/Bicep.LangServer/Handlers/BicepRegistryCacheRequestHandler.cs @@ -3,7 +3,6 @@ using Bicep.Core.Diagnostics; using Bicep.Core.FileSystem; -using Bicep.Core.Parsing; using Bicep.Core.Registry; using MediatR; using OmniSharp.Extensions.JsonRpc; @@ -45,22 +44,26 @@ public Task Handle(BicepRegistryCacheParams request, if (!moduleDispatcher.TryGetModuleReference(request.Target, request.TextDocument.Uri.ToUri(), out var moduleReference, out _)) { - throw new InvalidOperationException($"The client specified an invalid module reference '{request.Target}'."); + throw new InvalidOperationException( + $"The client specified an invalid module reference '{request.Target}'."); } if (!moduleReference.IsExternal) { - throw new InvalidOperationException($"The specified module reference '{request.Target}' refers to a local module which is not supported by {BicepCacheLspMethod} requests."); + throw new InvalidOperationException( + $"The specified module reference '{request.Target}' refers to a local module which is not supported by {BicepCacheLspMethod} requests."); } - if (this.moduleDispatcher.GetModuleRestoreStatus(moduleReference, out _) != ModuleRestoreStatus.Succeeded) + if (this.moduleDispatcher.GetModuleRestoreStatus(moduleReference, out _) != ArtifactRestoreStatus.Succeeded) { - throw new InvalidOperationException($"The module '{moduleReference.FullyQualifiedReference}' has not yet been successfully restored."); + throw new InvalidOperationException( + $"The module '{moduleReference.FullyQualifiedReference}' has not yet been successfully restored."); } if (!moduleDispatcher.TryGetLocalModuleEntryPointUri(moduleReference, out var uri, out _)) { - throw new InvalidOperationException($"Unable to obtain the entry point URI for module '{moduleReference.FullyQualifiedReference}'."); + throw new InvalidOperationException( + $"Unable to obtain the entry point URI for module '{moduleReference.FullyQualifiedReference}'."); } if (!this.fileResolver.TryRead(uri, out var contents, out var failureBuilder)) diff --git a/src/Bicep.LangServer/Providers/BicepCompilationProvider.cs b/src/Bicep.LangServer/Providers/BicepCompilationProvider.cs index 14d65c60c0b..aef97074699 100644 --- a/src/Bicep.LangServer/Providers/BicepCompilationProvider.cs +++ b/src/Bicep.LangServer/Providers/BicepCompilationProvider.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System.Collections.Immutable; using Bicep.Core.Analyzers.Interfaces; -using Bicep.Core.Analyzers.Linter.ApiVersions; using Bicep.Core.Configuration; using Bicep.Core.Features; using Bicep.Core.FileSystem; @@ -28,7 +27,13 @@ public class BicepCompilationProvider : ICompilationProvider private readonly IFileResolver fileResolver; private readonly IModuleDispatcher moduleDispatcher; - public BicepCompilationProvider(IFeatureProviderFactory featureProviderFactory, INamespaceProvider namespaceProvider, IFileResolver fileResolver, IModuleDispatcher moduleDispatcher, IConfigurationManager configurationManager, IBicepAnalyzer bicepAnalyzer) + public BicepCompilationProvider( + IFeatureProviderFactory featureProviderFactory, + INamespaceProvider namespaceProvider, + IFileResolver fileResolver, + IModuleDispatcher moduleDispatcher, + IConfigurationManager configurationManager, + IBicepAnalyzer bicepAnalyzer) { this.featureProviderFactory = featureProviderFactory; this.namespaceProvider = namespaceProvider; @@ -38,21 +43,45 @@ public BicepCompilationProvider(IFeatureProviderFactory featureProviderFactory, this.bicepAnalyzer = bicepAnalyzer; } - public CompilationContext Create(IReadOnlyWorkspace workspace, DocumentUri documentUri, ImmutableDictionary modelLookup) + public CompilationContext Create( + IReadOnlyWorkspace workspace, + DocumentUri documentUri, + ImmutableDictionary modelLookup) { - var sourceFileGrouping = SourceFileGroupingBuilder.Build(fileResolver, moduleDispatcher, workspace, documentUri.ToUri()); + var sourceFileGrouping = SourceFileGroupingBuilder.Build( + fileResolver, + moduleDispatcher, + workspace, + documentUri.ToUri(), + featureProviderFactory); return this.CreateContext(sourceFileGrouping, modelLookup); } - public CompilationContext Update(IReadOnlyWorkspace workspace, CompilationContext current, ImmutableDictionary modelLookup) + public CompilationContext Update( + IReadOnlyWorkspace workspace, + CompilationContext current, + ImmutableDictionary modelLookup) { - var sourceFileGrouping = SourceFileGroupingBuilder.Rebuild(moduleDispatcher, workspace, current.Compilation.SourceFileGrouping); + var sourceFileGrouping = SourceFileGroupingBuilder.Rebuild( + featureProviderFactory, + moduleDispatcher, + workspace, + current.Compilation.SourceFileGrouping); return this.CreateContext(sourceFileGrouping, modelLookup); } - private CompilationContext CreateContext(SourceFileGrouping syntaxTreeGrouping, ImmutableDictionary modelLookup) + private CompilationContext CreateContext( + SourceFileGrouping syntaxTreeGrouping, + ImmutableDictionary modelLookup) { - var compilation = new Compilation(featureProviderFactory, namespaceProvider, syntaxTreeGrouping, configurationManager, bicepAnalyzer, moduleDispatcher, modelLookup); + var compilation = new Compilation( + featureProviderFactory, + namespaceProvider, + syntaxTreeGrouping, + configurationManager, + bicepAnalyzer, + moduleDispatcher, + modelLookup); return new CompilationContext(compilation); } } diff --git a/src/Bicep.LangServer/Registry/IModuleRestoreScheduler.cs b/src/Bicep.LangServer/Registry/IModuleRestoreScheduler.cs index f4e41877c8f..39b96c7ceb9 100644 --- a/src/Bicep.LangServer/Registry/IModuleRestoreScheduler.cs +++ b/src/Bicep.LangServer/Registry/IModuleRestoreScheduler.cs @@ -12,6 +12,6 @@ public interface IModuleRestoreScheduler { void Start(); - void RequestModuleRestore(ICompilationManager compilationManager, DocumentUri documentUri, IEnumerable references); + void RequestModuleRestore(ICompilationManager compilationManager, DocumentUri documentUri, IEnumerable references); } } diff --git a/src/Bicep.LangServer/Registry/ModuleRestoreScheduler.cs b/src/Bicep.LangServer/Registry/ModuleRestoreScheduler.cs index 844f2a53803..ceeb4c54461 100644 --- a/src/Bicep.LangServer/Registry/ModuleRestoreScheduler.cs +++ b/src/Bicep.LangServer/Registry/ModuleRestoreScheduler.cs @@ -3,7 +3,6 @@ using Bicep.Core.Modules; using Bicep.Core.Registry; -using Bicep.Core.Syntax; using Bicep.Core.Workspaces; using Bicep.LanguageServer.CompilationManager; using OmniSharp.Extensions.LanguageServer.Protocol; @@ -47,7 +46,7 @@ public ModuleRestoreScheduler(IModuleDispatcher moduleDispatcher) /// /// The module references /// The document URI that needs to be recompiled once restore completes asynchronously - public void RequestModuleRestore(ICompilationManager compilationManager, DocumentUri documentUri, IEnumerable modules) + public void RequestModuleRestore(ICompilationManager compilationManager, DocumentUri documentUri, IEnumerable modules) { this.CheckDisposed();