From a67c321f4dbb9ee38e62ee087298e9d0d70c1083 Mon Sep 17 00:00:00 2001 From: Ariel Silverman Date: Wed, 16 Aug 2023 10:33:51 -0700 Subject: [PATCH] Provider restore for resource type providers (#11458) ## Description This change enables the restoration of provider artifacts from the bicep registry. In its current (temporary) form the provider artifact is disguised as a module given that the handling of OCI Artifacts by the ModuleRegistry is not decoupled from the handling of modules. Refactoring (in separate PRs) is needed to decouple the concerns and have a simpler handling of the provider artifacts. ###### Microsoft Reviewers: codeflow:open?pullrequest=https://github.com/Azure/bicep/pull/11458&drop=dogfoodAlpha --------- Co-authored-by: Ariel Silverman --- .../BuildCommandTests.cs | 341 +++++++++++------- .../RestoreCommandTests.cs | 23 +- src/Bicep.Cli/Commands/PublishCommand.cs | 2 - src/Bicep.Cli/Services/CompilationService.cs | 18 +- .../Emit/TemplateEmitterTests.cs | 17 +- .../ExamplesTests.cs | 2 +- .../ModuleTests.cs | 2 +- .../RegistryTests.cs | 26 +- src/Bicep.Core.Samples/DataSetsExtensions.cs | 9 +- .../IServiceCollectionExtensions.cs | 1 - .../OciArtifactModuleReferenceTests.cs | 17 +- ...herTests.cs => ArtifactDispatcherTests.cs} | 18 +- .../Registry/OciModuleRegistryTests.cs | 34 +- .../Registry/TagEncoderTests.cs | 3 +- src/Bicep.Core.UnitTests/ServiceBuilder.cs | 1 + .../Utils/OciArtifactModuleReferenceHelper.cs | 13 +- src/Bicep.Core/BicepCompiler.cs | 4 +- src/Bicep.Core/Modules/ModuleReference.cs | 3 - .../Modules/ModuleReferenceSchemes.cs | 4 +- src/Bicep.Core/Modules/OciModuleReference.cs | 51 +++ .../Navigation/IForeignTemplateReference.cs | 2 +- ...toreStatus.cs => ArtifactRestoreStatus.cs} | 2 +- .../Registry/AzureContainerRegistryManager.cs | 169 +++------ ...leDispatcher.cs => IArtifactDispatcher.cs} | 2 +- .../Registry/IModuleReferenceFactory.cs | 2 +- src/Bicep.Core/Registry/ModuleDispatcher.cs | 15 +- .../Registry/ModuleDispatcherExtensions.cs | 8 +- .../Registry/Oci/BicepMediaTypes.cs | 12 +- .../Registry/Oci/IOciArtifactReference.cs | 40 ++ .../Oci/OciArtifactReference.cs} | 199 +++++----- .../Registry/Oci/OciArtifactReferenceFacts.cs | 48 +++ .../Registry/Oci/OciArtifactResult.cs | 38 ++ src/Bicep.Core/Registry/Oci/OciDescriptor.cs | 6 +- src/Bicep.Core/Registry/Oci/OciManifest.cs | 15 +- src/Bicep.Core/Registry/Oci/TagEncoder.cs | 9 +- src/Bicep.Core/Registry/OciArtifactResult.cs | 36 -- src/Bicep.Core/Registry/OciModuleRegistry.cs | 157 ++++++-- .../Semantics/SemanticModelHelper.cs | 2 +- .../CompileTimeImportDeclarationSyntax.cs | 4 +- .../Syntax/ModuleDeclarationSyntax.cs | 4 +- .../Syntax/ProviderDeclarationSyntax.cs | 9 +- src/Bicep.Core/Syntax/SyntaxHelper.cs | 4 +- .../Syntax/TestDeclarationSyntax.cs | 2 +- .../Syntax/UsingDeclarationSyntax.cs | 4 +- .../Az/AzResourceTypeLoaderFactory.cs | 13 +- .../Workspaces/ISourceFileLookup.cs | 4 +- .../Workspaces/SourceFileGrouping.cs | 20 +- .../Workspaces/SourceFileGroupingBuilder.cs | 136 ++++--- .../HoverTests.cs | 8 +- .../Registry/ModuleRestoreSchedulerTests.cs | 14 +- .../BicepCompilationManagerHelper.cs | 2 +- .../BicepRegistryCacheRequestHandlerTests.cs | 16 +- .../BicepCompilationManager.cs | 2 +- .../Handlers/BicepDefinitionHandler.cs | 2 +- .../BicepForceModulesRestoreCommandHandler.cs | 19 +- .../Handlers/BicepHoverHandler.cs | 11 +- .../BicepRegistryCacheRequestHandler.cs | 15 +- .../Providers/BicepCompilationProvider.cs | 45 ++- .../Registry/IModuleRestoreScheduler.cs | 2 +- .../Registry/ModuleRestoreScheduler.cs | 3 +- 60 files changed, 1046 insertions(+), 644 deletions(-) rename src/Bicep.Core.UnitTests/Registry/{ModuleDispatcherTests.cs => ArtifactDispatcherTests.cs} (96%) create mode 100644 src/Bicep.Core/Modules/OciModuleReference.cs rename src/Bicep.Core/Registry/{ModuleRestoreStatus.cs => ArtifactRestoreStatus.cs} (93%) rename src/Bicep.Core/Registry/{IModuleDispatcher.cs => IArtifactDispatcher.cs} (86%) create mode 100644 src/Bicep.Core/Registry/Oci/IOciArtifactReference.cs rename src/Bicep.Core/{Modules/OciArtifactModuleReference.cs => Registry/Oci/OciArtifactReference.cs} (54%) create mode 100644 src/Bicep.Core/Registry/Oci/OciArtifactReferenceFacts.cs create mode 100644 src/Bicep.Core/Registry/Oci/OciArtifactResult.cs delete mode 100644 src/Bicep.Core/Registry/OciArtifactResult.cs 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 19c957dc959..86811fe6506 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();