diff --git a/src/Bicep.Core.IntegrationTests/RegistryTests.cs b/src/Bicep.Core.IntegrationTests/RegistryTests.cs index 466897dec6c..3bc5a7719a5 100644 --- a/src/Bicep.Core.IntegrationTests/RegistryTests.cs +++ b/src/Bicep.Core.IntegrationTests/RegistryTests.cs @@ -64,9 +64,9 @@ public async Task InvalidRootCachePathShouldProduceReasonableErrors() var compilation = await compiler.CreateCompilation(fileUri.ToIOUri()); var diagnostics = compilation.GetAllDiagnosticsByBicepFile(); - diagnostics.Should().HaveCount(1); + diagnostics.Should().HaveCount(2); var expectedErrorMessage = "Unable to restore the artifact with reference \"{0}\": Unable to create the local artifact directory \""; - diagnostics.Single().Value.ExcludingLinterDiagnostics().Should().SatisfyRespectively( + diagnostics[compilation.SourceFileGrouping.EntryPoint].ExcludingLinterDiagnostics().Should().SatisfyRespectively( x => { x.Level.Should().Be(DiagnosticLevel.Error); diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/bicepconfig.json b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/bicepconfig.json index 047ab63222d..161bd860fd4 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/bicepconfig.json +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/bicepconfig.json @@ -13,6 +13,9 @@ "demo-two": { "registry": "mock-registry-two.invalid", "modulePath": "demo" + }, + "mock-registry-emulated": { + "fileSystem": "Publish" } } } diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.bicep index 6480bc5dcba..db4d3503e0f 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.bicep @@ -21,6 +21,14 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { } } +module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { + name: 'planDeploy3' + scope: rg + params: { + namePrefix: 'hello' + } +} + var websites = [ { name: 'fancy' @@ -130,4 +138,4 @@ module ipv6port 'br:[::1]:5000/passthrough/ipv6port:v1' = { params: { ipv6port: 'test' } -} \ No newline at end of file +} diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.diagnostics.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.diagnostics.bicep index b104fe240fe..7d4f17d9542 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.diagnostics.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.diagnostics.bicep @@ -21,6 +21,14 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { } } +module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { + name: 'planDeploy3' + scope: rg + params: { + namePrefix: 'hello' + } +} + var websites = [ { name: 'fancy' @@ -132,3 +140,4 @@ module ipv6port 'br:[::1]:5000/passthrough/ipv6port:v1' = { ipv6port: 'test' } } + diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.formatted.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.formatted.bicep index 5f20b35edaf..c7a28bb80ad 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.formatted.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.formatted.bicep @@ -21,6 +21,14 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { } } +module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { + name: 'planDeploy3' + scope: rg + params: { + namePrefix: 'hello' + } +} + var websites = [ { name: 'fancy' diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.ir.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.ir.bicep index 8d0b57400a4..fcd302c7f41 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.ir.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.ir.bicep @@ -1,5 +1,7 @@ targetScope = 'subscription' -//@[000:2463) ProgramExpression +//@[000:2603) ProgramExpression +//@[000:0000) | └─ResourceDependencyExpression [UNPARENTED] +//@[000:0000) | └─ResourceReferenceExpression [UNPARENTED] //@[000:0000) | └─ResourceDependencyExpression [UNPARENTED] //@[000:0000) | └─ResourceReferenceExpression [UNPARENTED] //@[000:0000) | └─ResourceDependencyExpression [UNPARENTED] @@ -74,6 +76,23 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { } } +module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { +//@[000:0137) ├─DeclaredModuleExpression +//@[060:0137) | ├─ObjectExpression + name: 'planDeploy3' +//@[002:0021) | | └─ObjectPropertyExpression +//@[002:0006) | | ├─StringLiteralExpression { Value = name } +//@[008:0021) | | └─StringLiteralExpression { Value = planDeploy3 } + scope: rg + params: { +//@[010:0039) | ├─ObjectExpression + namePrefix: 'hello' +//@[004:0023) | | └─ObjectPropertyExpression +//@[004:0014) | | ├─StringLiteralExpression { Value = namePrefix } +//@[016:0023) | | └─StringLiteralExpression { Value = hello } + } +} + var websites = [ //@[000:0110) ├─DeclaredVariableExpression { Name = websites } //@[015:0110) | └─ArrayExpression @@ -374,3 +393,4 @@ module ipv6port 'br:[::1]:5000/passthrough/ipv6port:v1' = { //@[014:0020) | | └─StringLiteralExpression { Value = test } } } + diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.json b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.json index 703ac380df8..710dcd5ed2b 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.json +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "dev", - "templateHash": "16097344762357511977" + "templateHash": "11340138247578714789" } }, "variables": { @@ -159,6 +159,67 @@ "[subscriptionResourceId('Microsoft.Resources/resourceGroups', 'adotfrank-rg')]" ] }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "planDeploy3", + "resourceGroup": "adotfrank-rg", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "namePrefix": { + "value": "hello" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "dev", + "templateHash": "15019246960605065046" + } + }, + "parameters": { + "namePrefix": { + "type": "string" + }, + "sku": { + "type": "string", + "defaultValue": "B1" + } + }, + "resources": [ + { + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2020-06-01", + "name": "[format('{0}appPlan', parameters('namePrefix'))]", + "location": "[resourceGroup().location]", + "kind": "linux", + "sku": { + "name": "[parameters('sku')]" + }, + "properties": { + "reserved": true + } + } + ], + "outputs": { + "planId": { + "type": "string", + "value": "[resourceId('Microsoft.Web/serverfarms', format('{0}appPlan', parameters('namePrefix')))]" + } + } + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', 'adotfrank-rg')]" + ] + }, { "copy": { "name": "siteDeploy", diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.sourcemap.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.sourcemap.bicep index f286a23b89d..529e8a6ea67 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.sourcemap.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.sourcemap.bicep @@ -149,6 +149,75 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { } } +module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { +//@ { +//@ "type": "Microsoft.Resources/deployments", +//@ "apiVersion": "2025-04-01", +//@ "resourceGroup": "adotfrank-rg", +//@ "properties": { +//@ "expressionEvaluationOptions": { +//@ "scope": "inner" +//@ }, +//@ "mode": "Incremental", +//@ "template": { +//@ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", +//@ "contentVersion": "1.0.0.0", +//@ "metadata": { +//@ "_generator": { +//@ "name": "bicep", +//@ "version": "dev", +//@ "templateHash": "15019246960605065046" +//@ } +//@ }, +//@ "parameters": { +//@ "namePrefix": { +//@ "type": "string" +//@ }, +//@ "sku": { +//@ "type": "string", +//@ "defaultValue": "B1" +//@ } +//@ }, +//@ "resources": [ +//@ { +//@ "type": "Microsoft.Web/serverfarms", +//@ "apiVersion": "2020-06-01", +//@ "name": "[format('{0}appPlan', parameters('namePrefix'))]", +//@ "location": "[resourceGroup().location]", +//@ "kind": "linux", +//@ "sku": { +//@ "name": "[parameters('sku')]" +//@ }, +//@ "properties": { +//@ "reserved": true +//@ } +//@ } +//@ ], +//@ "outputs": { +//@ "planId": { +//@ "type": "string", +//@ "value": "[resourceId('Microsoft.Web/serverfarms', format('{0}appPlan', parameters('namePrefix')))]" +//@ } +//@ } +//@ } +//@ }, +//@ "dependsOn": [ +//@ "[subscriptionResourceId('Microsoft.Resources/resourceGroups', 'adotfrank-rg')]" +//@ ] +//@ }, + name: 'planDeploy3' +//@ "name": "planDeploy3", + scope: rg + params: { +//@ "parameters": { +//@ }, + namePrefix: 'hello' +//@ "namePrefix": { +//@ "value": "hello" +//@ } + } +} + var websites = [ //@ "websites": [ //@ ], @@ -780,3 +849,4 @@ module ipv6port 'br:[::1]:5000/passthrough/ipv6port:v1' = { //@ } } } + diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbolicnames.json b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbolicnames.json index f90b2d774a0..57e16934904 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbolicnames.json +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbolicnames.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "dev", - "templateHash": "3717084932040700305" + "templateHash": "17302964314594708830" } }, "variables": { @@ -162,6 +162,68 @@ "rg" ] }, + "appPlanDeploy3": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "planDeploy3", + "resourceGroup": "adotfrank-rg", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "namePrefix": { + "value": "hello" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "dev", + "templateHash": "13508561622047952911" + } + }, + "parameters": { + "namePrefix": { + "type": "string" + }, + "sku": { + "type": "string", + "defaultValue": "B1" + } + }, + "resources": { + "appPlan": { + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2020-06-01", + "name": "[format('{0}appPlan', parameters('namePrefix'))]", + "location": "[resourceGroup().location]", + "kind": "linux", + "sku": { + "name": "[parameters('sku')]" + }, + "properties": { + "reserved": true + } + } + }, + "outputs": { + "planId": { + "type": "string", + "value": "[resourceId('Microsoft.Web/serverfarms', format('{0}appPlan', parameters('namePrefix')))]" + } + } + } + }, + "dependsOn": [ + "rg" + ] + }, "siteDeploy": { "copy": { "name": "siteDeploy", diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbols.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbols.bicep index 1bfca75ea18..fea26bf29d8 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbols.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbols.bicep @@ -24,6 +24,15 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { } } +module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { +//@[07:21) Module appPlanDeploy3. Type: module. Declaration start char: 0, length: 137 + name: 'planDeploy3' + scope: rg + params: { + namePrefix: 'hello' + } +} + var websites = [ //@[04:12) Variable websites. Type: [object, object]. Declaration start char: 0, length: 110 { @@ -152,3 +161,4 @@ module ipv6port 'br:[::1]:5000/passthrough/ipv6port:v1' = { ipv6port: 'test' } } + diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.syntax.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.syntax.bicep index 1f5a364ea01..55f64164bc5 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.syntax.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.syntax.bicep @@ -1,5 +1,5 @@ targetScope = 'subscription' -//@[000:2463) ProgramSyntax +//@[000:2603) ProgramSyntax //@[000:0028) ├─TargetScopeSyntax //@[000:0011) | ├─Token(Identifier) |targetScope| //@[012:0013) | ├─Token(Assignment) |=| @@ -147,6 +147,57 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { //@[000:0001) | └─Token(RightBrace) |}| //@[001:0003) ├─Token(NewLine) |\n\n| +module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { +//@[000:0137) ├─ModuleDeclarationSyntax +//@[000:0006) | ├─Token(Identifier) |module| +//@[007:0021) | ├─IdentifierSyntax +//@[007:0021) | | └─Token(Identifier) |appPlanDeploy3| +//@[022:0057) | ├─StringSyntax +//@[022:0057) | | └─Token(StringComplete) |'br/mock-registry-emulated:plan:v2'| +//@[058:0059) | ├─Token(Assignment) |=| +//@[060:0137) | └─ObjectSyntax +//@[060:0061) | ├─Token(LeftBrace) |{| +//@[061:0062) | ├─Token(NewLine) |\n| + name: 'planDeploy3' +//@[002:0021) | ├─ObjectPropertySyntax +//@[002:0006) | | ├─IdentifierSyntax +//@[002:0006) | | | └─Token(Identifier) |name| +//@[006:0007) | | ├─Token(Colon) |:| +//@[008:0021) | | └─StringSyntax +//@[008:0021) | | └─Token(StringComplete) |'planDeploy3'| +//@[021:0022) | ├─Token(NewLine) |\n| + scope: rg +//@[002:0011) | ├─ObjectPropertySyntax +//@[002:0007) | | ├─IdentifierSyntax +//@[002:0007) | | | └─Token(Identifier) |scope| +//@[007:0008) | | ├─Token(Colon) |:| +//@[009:0011) | | └─VariableAccessSyntax +//@[009:0011) | | └─IdentifierSyntax +//@[009:0011) | | └─Token(Identifier) |rg| +//@[011:0012) | ├─Token(NewLine) |\n| + params: { +//@[002:0039) | ├─ObjectPropertySyntax +//@[002:0008) | | ├─IdentifierSyntax +//@[002:0008) | | | └─Token(Identifier) |params| +//@[008:0009) | | ├─Token(Colon) |:| +//@[010:0039) | | └─ObjectSyntax +//@[010:0011) | | ├─Token(LeftBrace) |{| +//@[011:0012) | | ├─Token(NewLine) |\n| + namePrefix: 'hello' +//@[004:0023) | | ├─ObjectPropertySyntax +//@[004:0014) | | | ├─IdentifierSyntax +//@[004:0014) | | | | └─Token(Identifier) |namePrefix| +//@[014:0015) | | | ├─Token(Colon) |:| +//@[016:0023) | | | └─StringSyntax +//@[016:0023) | | | └─Token(StringComplete) |'hello'| +//@[023:0024) | | ├─Token(NewLine) |\n| + } +//@[002:0003) | | └─Token(RightBrace) |}| +//@[003:0004) | ├─Token(NewLine) |\n| +} +//@[000:0001) | └─Token(RightBrace) |}| +//@[001:0003) ├─Token(NewLine) |\n\n| + var websites = [ //@[000:0110) ├─VariableDeclarationSyntax //@[000:0003) | ├─Token(Identifier) |var| @@ -988,4 +1039,6 @@ module ipv6port 'br:[::1]:5000/passthrough/ipv6port:v1' = { //@[003:0004) | ├─Token(NewLine) |\n| } //@[000:0001) | └─Token(RightBrace) |}| -//@[001:0001) └─Token(EndOfFile) || +//@[001:0002) ├─Token(NewLine) |\n| + +//@[000:0000) └─Token(EndOfFile) || diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.tokens.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.tokens.bicep index 43d953768e3..ff81a47d607 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.tokens.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.tokens.bicep @@ -97,6 +97,40 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { //@[000:001) RightBrace |}| //@[001:003) NewLine |\n\n| +module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { +//@[000:006) Identifier |module| +//@[007:021) Identifier |appPlanDeploy3| +//@[022:057) StringComplete |'br/mock-registry-emulated:plan:v2'| +//@[058:059) Assignment |=| +//@[060:061) LeftBrace |{| +//@[061:062) NewLine |\n| + name: 'planDeploy3' +//@[002:006) Identifier |name| +//@[006:007) Colon |:| +//@[008:021) StringComplete |'planDeploy3'| +//@[021:022) NewLine |\n| + scope: rg +//@[002:007) Identifier |scope| +//@[007:008) Colon |:| +//@[009:011) Identifier |rg| +//@[011:012) NewLine |\n| + params: { +//@[002:008) Identifier |params| +//@[008:009) Colon |:| +//@[010:011) LeftBrace |{| +//@[011:012) NewLine |\n| + namePrefix: 'hello' +//@[004:014) Identifier |namePrefix| +//@[014:015) Colon |:| +//@[016:023) StringComplete |'hello'| +//@[023:024) NewLine |\n| + } +//@[002:003) RightBrace |}| +//@[003:004) NewLine |\n| +} +//@[000:001) RightBrace |}| +//@[001:003) NewLine |\n\n| + var websites = [ //@[000:003) Identifier |var| //@[004:012) Identifier |websites| @@ -633,4 +667,6 @@ module ipv6port 'br:[::1]:5000/passthrough/ipv6port:v1' = { //@[003:004) NewLine |\n| } //@[000:001) RightBrace |}| -//@[001:001) EndOfFile || +//@[001:002) NewLine |\n| + +//@[000:000) EndOfFile || diff --git a/src/Bicep.Core.UnitTests/BicepTestConstants.cs b/src/Bicep.Core.UnitTests/BicepTestConstants.cs index 3c58c17288c..2d1e439f46c 100644 --- a/src/Bicep.Core.UnitTests/BicepTestConstants.cs +++ b/src/Bicep.Core.UnitTests/BicepTestConstants.cs @@ -84,7 +84,7 @@ public static class BicepTestConstants public static readonly IServiceProvider EmptyServiceProvider = new Mock(MockBehavior.Loose).Object; public static IArtifactRegistryProvider CreateRegistryProvider(IServiceProvider services) => - new DefaultArtifactRegistryProvider(TestRegistryConfiguration, services.GetRequiredService(), ClientFactory, TemplateSpecRepositoryFactory); + new DefaultArtifactRegistryProvider(TestRegistryConfiguration, services.GetRequiredService(), ClientFactory, TemplateSpecRepositoryFactory, FileExplorer); public static readonly RegistryConfiguration TestRegistryConfiguration = new(PermitUntrustedRegistries: true); diff --git a/src/Bicep.Core.UnitTests/Modules/OciArtifactModuleReferenceTests.cs b/src/Bicep.Core.UnitTests/Modules/OciArtifactModuleReferenceTests.cs index 579ba0b7d47..a915de36860 100644 --- a/src/Bicep.Core.UnitTests/Modules/OciArtifactModuleReferenceTests.cs +++ b/src/Bicep.Core.UnitTests/Modules/OciArtifactModuleReferenceTests.cs @@ -277,7 +277,7 @@ private static IEnumerable GetInvalidAliasData() ["moduleAliases.br.myModulePath.modulePath"] = "path", }), "BCP216", - "The OCI artifact module alias \"myModulePath\" in the built-in Bicep configuration is invalid. The \"registry\" property cannot be null or undefined.", + "The OCI artifact module alias \"myModulePath\" in the built-in Bicep configuration is invalid. Either the \"registry\" or \"fileSystem\" property must be specified.", }; yield return new object[] @@ -291,7 +291,7 @@ private static IEnumerable GetInvalidAliasData() }, "/bicepconfig.json"), "BCP216", - "The OCI artifact module alias \"myModulePath2\" in the Bicep configuration \"/bicepconfig.json\" is invalid. The \"registry\" property cannot be null or undefined.", + "The OCI artifact module alias \"myModulePath2\" in the Bicep configuration \"/bicepconfig.json\" is invalid. Either the \"registry\" or \"fileSystem\" property must be specified.", }; } diff --git a/src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs b/src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs new file mode 100644 index 00000000000..ae942c39e5e --- /dev/null +++ b/src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs @@ -0,0 +1,269 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO.Abstractions.TestingHelpers; +using Bicep.Core.Configuration; +using Bicep.Core.Diagnostics; +using Bicep.Core.Registry; +using Bicep.Core.Registry.Oci; +using Bicep.Core.SourceGraph; +using Bicep.Core.UnitTests.Assertions; +using Bicep.IO.Abstraction; +using Bicep.IO.FileSystem; +using FluentAssertions; +using FluentAssertions.Execution; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Bicep.Core.UnitTests.Registry.Oci +{ + [TestClass] + public class OciArtifactEmulatedReferenceTests + { + [TestMethod] + [DataRow("keyvault:1.0.0", "keyvault")] + [DataRow("storage/queue:v2.0", "storage/queue")] + [DataRow("keyvault@sha256:e207a69d02b3de40d48ede9fd208d80441a9e590a83a0bc915d46244c03310d4", "keyvault")] + [DataRow("keyvault", "keyvault")] + [DataRow("a/b/c:latest", "a/b/c")] + [DataRow("mymodule:v1", "mymodule")] + [DataRow("", "")] + [DataRow(":", "")] + [DataRow("@sha256:abc", "")] + public void ExtractModulePath_ShouldExtractCorrectPath(string input, string expected) + { + OciArtifactEmulatedReference.ExtractModulePath(input).Should().Be(expected); + } + + [TestMethod] + public void TryParse_EmptyModulePath_ShouldFail() + { + var fileExplorer = new FileSystemFileExplorer(new MockFileSystem()); + var referencingFile = BicepTestConstants.CreateDummyBicepFile(); + var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); + + var result = OciArtifactEmulatedReference.TryParse( + referencingFile, + "./modules", + configFileUri, + ":1.0.0", + fileExplorer); + + result.IsSuccess(out _, out var failureBuilder).Should().BeFalse(); + var diagnostic = failureBuilder!(DiagnosticBuilder.ForDocumentStart()); + diagnostic.Code.Should().Be("BCP090"); + } + + [TestMethod] + public void TryParse_ValidModulePath_ShouldSucceed() + { + var fileExplorer = new FileSystemFileExplorer(new MockFileSystem()); + var referencingFile = BicepTestConstants.CreateDummyBicepFile(); + var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); + + var result = OciArtifactEmulatedReference.TryParse( + referencingFile, + "../bicepModules", + configFileUri, + "keyvault:1.0.0", + fileExplorer); + + result.IsSuccess(out var reference, out _).Should().BeTrue(); + reference!.UnqualifiedReference.Should().Be("keyvault"); + reference!.FullyQualifiedReference.Should().Be("br:keyvault"); + reference!.IsExternal.Should().BeFalse(); + reference.Scheme.Should().Be("br-fs"); + } + + [TestMethod] + public void TryParse_MultiSegmentModulePath_ShouldSucceed() + { + var fileExplorer = new FileSystemFileExplorer(new MockFileSystem()); + var referencingFile = BicepTestConstants.CreateDummyBicepFile(); + var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); + + var result = OciArtifactEmulatedReference.TryParse( + referencingFile, + "./modules", + configFileUri, + "storage/queue:v2.0", + fileExplorer); + + result.IsSuccess(out var reference, out _).Should().BeTrue(); + reference!.UnqualifiedReference.Should().Be("storage/queue"); + reference!.FullyQualifiedReference.Should().Be("br:storage/queue"); + } + + [TestMethod] + public void TryParse_DigestReference_ShouldIgnoreDigestAndSucceed() + { + var fileExplorer = new FileSystemFileExplorer(new MockFileSystem()); + var referencingFile = BicepTestConstants.CreateDummyBicepFile(); + var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); + + var result = OciArtifactEmulatedReference.TryParse( + referencingFile, + "./modules", + configFileUri, + "keyvault@sha256:e207a69d02b3de40d48ede9fd208d80441a9e590a83a0bc915d46244c03310d4", + fileExplorer); + + result.IsSuccess(out var reference, out _).Should().BeTrue(); + reference!.UnqualifiedReference.Should().Be("keyvault"); + reference!.FullyQualifiedReference.Should().Be("br:keyvault"); + } + + [TestMethod] + public void TryGetEntryPointFileHandle_ShouldReturnFileHandle() + { + var fileExplorer = new FileSystemFileExplorer(new MockFileSystem()); + var referencingFile = BicepTestConstants.CreateDummyBicepFile(); + var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); + + var parseResult = OciArtifactEmulatedReference.TryParse( + referencingFile, + "../bicepModules", + configFileUri, + "keyvault:1.0.0", + fileExplorer); + + parseResult.IsSuccess(out var reference, out _).Should().BeTrue(); + + var entryPointResult = reference!.TryGetEntryPointFileHandle(); + + entryPointResult.IsSuccess(out var fileHandle, out _).Should().BeTrue(); + fileHandle.Should().NotBeNull(); + fileHandle!.Uri.Path.Should().Contain("keyvault.bicep"); + } + + [TestMethod] + public void Equals_SameReferences_ShouldBeEqual() + { + var fileExplorer = new FileSystemFileExplorer(new MockFileSystem()); + var referencingFile = BicepTestConstants.CreateDummyBicepFile(); + var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); + + var result1 = OciArtifactEmulatedReference.TryParse( + referencingFile, "../bicepModules", configFileUri, "keyvault:1.0.0", fileExplorer); + var result2 = OciArtifactEmulatedReference.TryParse( + referencingFile, "../bicepModules", configFileUri, "keyvault:2.0.0", fileExplorer); + + result1.IsSuccess(out var ref1, out _).Should().BeTrue(); + result2.IsSuccess(out var ref2, out _).Should().BeTrue(); + + ref1!.Equals(ref2).Should().BeTrue(); + ref1.GetHashCode().Should().Be(ref2!.GetHashCode()); + } + + [TestMethod] + public void Equals_DifferentModulePaths_ShouldNotBeEqual() + { + var fileExplorer = new FileSystemFileExplorer(new MockFileSystem()); + var referencingFile = BicepTestConstants.CreateDummyBicepFile(); + var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); + + var result1 = OciArtifactEmulatedReference.TryParse( + referencingFile, "../bicepModules", configFileUri, "keyvault:1.0.0", fileExplorer); + var result2 = OciArtifactEmulatedReference.TryParse( + referencingFile, "../bicepModules", configFileUri, "storage:1.0.0", fileExplorer); + + result1.IsSuccess(out var ref1, out _).Should().BeTrue(); + result2.IsSuccess(out var ref2, out _).Should().BeTrue(); + + ref1!.Equals(ref2).Should().BeFalse(); + } + + [TestMethod] + public void TryGetOciArtifactModuleAlias_BothRegistryAndFileSystemSet_ShouldFail() + { + var configuration = BicepTestConstants.CreateMockConfiguration( + new() + { + ["moduleAliases.br.myAlias.registry"] = "example.azurecr.io", + ["moduleAliases.br.myAlias.fileSystem"] = "../bicepModules", + }); + + var result = configuration.ModuleAliases.TryGetOciArtifactModuleAlias("myAlias"); + + result.IsSuccess(out _, out var failureBuilder).Should().BeFalse(); + var diagnostic = failureBuilder!(DiagnosticBuilder.ForDocumentStart()); + diagnostic.Code.Should().Be("BCP447"); + diagnostic.Message.Should().Contain("mutually exclusive"); + } + + [TestMethod] + public void TryGetOciArtifactModuleAlias_NeitherRegistryNorFileSystemSet_ShouldFail() + { + var configuration = BicepTestConstants.CreateMockConfiguration( + new() + { + ["moduleAliases.br.myAlias.modulePath"] = "path", + }); + + var result = configuration.ModuleAliases.TryGetOciArtifactModuleAlias("myAlias"); + + result.IsSuccess(out _, out var failureBuilder).Should().BeFalse(); + var diagnostic = failureBuilder!(DiagnosticBuilder.ForDocumentStart()); + diagnostic.Code.Should().Be("BCP216"); + diagnostic.Message.Should().Contain("fileSystem"); + } + + [TestMethod] + public void TryGetOciArtifactModuleAlias_OnlyFileSystemSet_ShouldSucceed() + { + var configuration = BicepTestConstants.CreateMockConfiguration( + new() + { + ["moduleAliases.br.myAlias.fileSystem"] = "../bicepModules", + }); + + var result = configuration.ModuleAliases.TryGetOciArtifactModuleAlias("myAlias"); + + result.IsSuccess(out var alias, out _).Should().BeTrue(); + alias!.FileSystem.Should().Be("../bicepModules"); + alias.Registry.Should().BeNull(); + } + + [TestMethod] + public void TryGetOciArtifactModuleAlias_OnlyRegistrySet_ShouldSucceed() + { + var configuration = BicepTestConstants.CreateMockConfiguration( + new() + { + ["moduleAliases.br.myAlias.registry"] = "example.azurecr.io", + }); + + var result = configuration.ModuleAliases.TryGetOciArtifactModuleAlias("myAlias"); + + result.IsSuccess(out var alias, out _).Should().BeTrue(); + alias!.Registry.Should().Be("example.azurecr.io"); + alias.FileSystem.Should().BeNull(); + } + + [TestMethod] + [DataRow("../escape:1.0.0", "..")] + [DataRow("valid/../escape:1.0.0", "..")] + [DataRow(".:1.0.0", ".")] + [DataRow("UPPERCASE:1.0.0", "UPPERCASE")] + [DataRow("has spaces/module:1.0.0", "has spaces")] + [DataRow("valid/bad!segment:1.0.0", "bad!segment")] + public void TryParse_InvalidPathSegment_ShouldFail(string unqualifiedReference, string expectedBadSegment) + { + var fileExplorer = new FileSystemFileExplorer(new MockFileSystem()); + var referencingFile = BicepTestConstants.CreateDummyBicepFile(); + var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); + + var result = OciArtifactEmulatedReference.TryParse( + referencingFile, + "./modules", + configFileUri, + unqualifiedReference, + fileExplorer, + "myAlias"); + + result.IsSuccess(out _, out var failureBuilder).Should().BeFalse(); + var diagnostic = failureBuilder!(DiagnosticBuilder.ForDocumentStart()); + diagnostic.Code.Should().Be("BCP195"); + diagnostic.Message.Should().Contain(expectedBadSegment); + } + } +} diff --git a/src/Bicep.Core.UnitTests/Utils/OciRegistryHelper.cs b/src/Bicep.Core.UnitTests/Utils/OciRegistryHelper.cs index 580910a42d6..e6272cb5ce4 100644 --- a/src/Bicep.Core.UnitTests/Utils/OciRegistryHelper.cs +++ b/src/Bicep.Core.UnitTests/Utils/OciRegistryHelper.cs @@ -77,7 +77,7 @@ public static (OciArtifactRegistry, FakeRegistryBlobClient) CreateModuleRegistry .Setup(m => m.CreateAuthenticatedBlobClient(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(blobClient); - var registry = new OciArtifactRegistry(BicepTestConstants.TestRegistryConfiguration, clientFactory.Object, StrictMock.Of().Object); + var registry = new OciArtifactRegistry(BicepTestConstants.TestRegistryConfiguration, clientFactory.Object, StrictMock.Of().Object, new FileSystemFileExplorer(new MockFileSystem())); return (registry, blobClient); } diff --git a/src/Bicep.Core/Configuration/ModuleAliasesConfiguration.cs b/src/Bicep.Core/Configuration/ModuleAliasesConfiguration.cs index 1e3f30ba77a..53c35c7c37b 100644 --- a/src/Bicep.Core/Configuration/ModuleAliasesConfiguration.cs +++ b/src/Bicep.Core/Configuration/ModuleAliasesConfiguration.cs @@ -37,9 +37,13 @@ public record OciArtifactModuleAlias public string? ModulePath { get; init; } - public override string ToString() => this.ModulePath is not null - ? $"{Registry}/{ModulePath}" - : $"{Registry}"; + public string? FileSystem { get; init; } + + public override string ToString() => this.FileSystem is not null + ? $"{FileSystem}" + : this.ModulePath is not null + ? $"{Registry}/{ModulePath}" + : $"{Registry}"; } public partial class ModuleAliasesConfiguration : ConfigurationSection @@ -101,7 +105,12 @@ public ResultWithDiagnosticBuilder TryGetOciArtifactModu return new(x => x.OciArtifactModuleAliasNameDoesNotExistInConfiguration(aliasName, configFileUri)); } - if (alias.Registry is null) + if (alias.Registry is not null && alias.FileSystem is not null) + { + return new(x => x.InvalidOciArtifactModuleAliasRegistryAndFileSystemSetTogether(aliasName, configFileUri)); + } + + if (alias.Registry is null && alias.FileSystem is null) { return new(x => x.InvalidOciArtifactModuleAliasRegistryNullOrUndefined(aliasName, configFileUri)); } diff --git a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs index 03a2acf80d9..61eee9c1e26 100644 --- a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs +++ b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs @@ -991,7 +991,7 @@ public Diagnostic ReferencedArmTemplateHasErrors() => CoreError( public Diagnostic UnknownModuleReferenceScheme(string badScheme, ImmutableArray allowedSchemes) { - string FormatSchemes() => ToQuotedString(allowedSchemes.Where(scheme => !string.Equals(scheme, ArtifactReferenceSchemes.Local))); + string FormatSchemes() => ToQuotedString(allowedSchemes.Where(scheme => !string.Equals(scheme, ArtifactReferenceSchemes.Local) && scheme != ArtifactReferenceSchemes.OciEmulated)); return CoreError( "BCP189", @@ -1111,7 +1111,7 @@ public Diagnostic InvalidTemplateSpecAliasResourceGroupNullOrUndefined(string al public Diagnostic InvalidOciArtifactModuleAliasRegistryNullOrUndefined(string aliasName, IOUri? configFileUri) => CoreError( "BCP216", - $"The OCI artifact module alias \"{aliasName}\" in the {BuildBicepConfigurationClause(configFileUri)} is invalid. The \"registry\" property cannot be null or undefined."); + $"The OCI artifact module alias \"{aliasName}\" in the {BuildBicepConfigurationClause(configFileUri)} is invalid. Either the \"registry\" or \"fileSystem\" property must be specified."); public Diagnostic InvalidTemplateSpecReferenceInvalidSubscriptionId(string? aliasName, string subscriptionId, string referenceValue) => CoreError( "BCP217", @@ -2031,6 +2031,26 @@ public Diagnostic ArtifactRestoreBlockedByRegistry(string registryHostname) => C "BCP446", $"Restore from registry \"{registryHostname}\" is blocked because it is not in the trusted registries list. " + $"See https://aka.ms/bicep/registry-trust for details."); + + public Diagnostic InvalidOciArtifactModuleAliasRegistryAndFileSystemSetTogether(string aliasName, IOUri? configFileUri) => CoreError( + "BCP447", + $"The OCI artifact module alias \"{aliasName}\" in the {BuildBicepConfigurationClause(configFileUri)} is invalid. The \"registry\" and \"fileSystem\" properties are mutually exclusive."); + + public Diagnostic OciArtifactModuleAliasFileSystemOnlySupportsModules(string aliasName) => CoreError( + "BCP448", + $"The OCI artifact module alias \"{aliasName}\" has a \"fileSystem\" property which is only supported for modules, not extensions."); + + public Diagnostic ModuleReferenceSchemeBrFsNotSupported() => CoreError( + "BCP449", + "The 'br-fs' module reference scheme is for internal use only. Use a 'br/:' reference with a configured 'fileSystem' alias instead."); + + public Diagnostic ConfigurationFileNotFound(string featureName) => CoreError( + "BCP450", + $"Configuration file is not found. Feature \"{featureName}\" requires a configuration file."); + + public Diagnostic InvalidOciArtifactModuleAliasFileSystemPath(string? aliasName, string path, string reason) => CoreError( + "BCP451", + $"The OCI artifact module alias{(aliasName is not null ? $" \"{aliasName}\"" : "")} has an invalid \"fileSystem\" path \"{path}\": {reason}"); } public static DiagnosticBuilderInternal ForPosition(TextSpan span) diff --git a/src/Bicep.Core/Modules/ModuleReferenceSchemes.cs b/src/Bicep.Core/Modules/ModuleReferenceSchemes.cs index 75d3f813f1a..b399a229042 100644 --- a/src/Bicep.Core/Modules/ModuleReferenceSchemes.cs +++ b/src/Bicep.Core/Modules/ModuleReferenceSchemes.cs @@ -11,6 +11,8 @@ public static class ArtifactReferenceSchemes public const string Oci = OciArtifactReferenceFacts.Scheme; + public const string OciEmulated = OciArtifactReferenceFacts.EmulatedScheme; + public const string TemplateSpecs = "ts"; } } diff --git a/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs b/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs index 8747e7300d3..5fc90b74551 100644 --- a/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs +++ b/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs @@ -3,17 +3,18 @@ using Bicep.Core.Configuration; using Bicep.Core.Registry.Catalog; -using Microsoft.Extensions.DependencyInjection; +using Bicep.IO.Abstraction; namespace Bicep.Core.Registry { public class DefaultArtifactRegistryProvider : ArtifactRegistryProvider { - public DefaultArtifactRegistryProvider(RegistryConfiguration registryConfiguration, IPublicModuleMetadataProvider publicModuleMetadataProvider, IContainerRegistryClientFactory clientFactory, ITemplateSpecRepositoryFactory templateSpecRepositoryFactory) + public DefaultArtifactRegistryProvider(RegistryConfiguration registryConfiguration, IPublicModuleMetadataProvider publicModuleMetadataProvider, IContainerRegistryClientFactory clientFactory, ITemplateSpecRepositoryFactory templateSpecRepositoryFactory, IFileExplorer fileExplorer) : base(new IArtifactRegistry[] { new LocalModuleRegistry(), - new OciArtifactRegistry(registryConfiguration, clientFactory, publicModuleMetadataProvider), + new OciArtifactRegistry(registryConfiguration, clientFactory, publicModuleMetadataProvider, fileExplorer), + new OciArtifactEmulatedRegistry(), new TemplateSpecModuleRegistry(templateSpecRepositoryFactory), }) { diff --git a/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs b/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs new file mode 100644 index 00000000000..fa1b6dcd008 --- /dev/null +++ b/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Bicep.Core.Diagnostics; +using Bicep.Core.SourceGraph; +using Bicep.IO.Abstraction; + +namespace Bicep.Core.Registry.Oci +{ + /// + /// Represents an OCI module reference that is emulated via a local filesystem path. + /// When a module alias specifies a "fileSystem" property instead of "registry", + /// module references are resolved to local .bicep files instead of pulling from a container registry. + /// + public class OciArtifactEmulatedReference : ArtifactReference + { + private readonly IFileHandle fileHandle; + private readonly string modulePath; + private readonly string? fullyQualifiedReference; + + public OciArtifactEmulatedReference(BicepSourceFile referencingFile, string modulePath, IFileHandle fileHandle, string? fullyQualifiedReference = null) + : base(referencingFile.Features, referencingFile.Configuration, OciArtifactReferenceFacts.EmulatedScheme) + { + this.modulePath = modulePath; + this.fileHandle = fileHandle; + this.fullyQualifiedReference = fullyQualifiedReference; + } + + // Override FullyQualifiedReference so user-facing diagnostics shows "br:..." + public override string FullyQualifiedReference => fullyQualifiedReference ?? $"{OciArtifactReferenceFacts.Scheme}:{UnqualifiedReference}"; + + public override string UnqualifiedReference => modulePath; + + public override bool IsExternal => false; + + public override ResultWithDiagnosticBuilder TryGetEntryPointFileHandle() + { + return new(fileHandle); + } + + // Extracts the module path from an unqualified reference string by removing any tag or digest suffix + public static string ExtractModulePath(string unqualifiedReference) + { + // Check for digest separator (@) + var digestIndex = unqualifiedReference.IndexOf('@'); + if (digestIndex >= 0) + { + return unqualifiedReference[..digestIndex]; + } + + // Check for tag separator (:) + var tagIndex = unqualifiedReference.LastIndexOf(':'); + if (tagIndex >= 0) + { + return unqualifiedReference[..tagIndex]; + } + + // No tag or digest — use the whole reference as the module path + return unqualifiedReference; + } + + // referencingFile is the Bicep source file containing the module reference + // fileSystemPath is the filesystem path from the alias configuration + // configFileUri is the URI of the bicepconfig.json file, used to resolve relative paths + // unqualifiedReference is the unqualified reference string (e.g., "keyvault:1.0.0") + // fileExplorer is the file explorer used to create file handles + // aliasName is the name of the module alias, used in diagnostics + public static ResultWithDiagnosticBuilder TryParse( + BicepSourceFile referencingFile, + string fileSystemPath, + IOUri configFileUri, + string unqualifiedReference, + IFileExplorer fileExplorer, + string? aliasName = null) + { + var modulePath = ExtractModulePath(unqualifiedReference); + + if (string.IsNullOrEmpty(modulePath)) + { + return new(x => x.ModulePathHasNotBeenSpecified()); + } + + var segments = modulePath.Split('/'); + foreach (var segment in segments) + { + if (!OciArtifactReferenceFacts.IsOciNamespaceSegment(segment)) + { + return new(x => x.InvalidOciArtifactReferenceInvalidPathSegment(aliasName, unqualifiedReference, segment)); + } + } + + IOUri baseUri; + // Ensure the fileSystem path ends with '/' so it's treated as a directory. + var directoryPath = fileSystemPath.EndsWith('/') || fileSystemPath.EndsWith('\\') + ? fileSystemPath + : fileSystemPath + "/"; + + if (IOUri.IsAbsoluteFilePath(fileSystemPath)) + { + try + { + baseUri = IOUri.FromFilePath(directoryPath); + } + catch (IOException ex) + { + return new(x => x.InvalidOciArtifactModuleAliasFileSystemPath(aliasName, fileSystemPath, ex.Message)); + } + } + else + { + baseUri = configFileUri.Resolve(directoryPath); + } + + // Construct the file URI by appending the module path with a .bicep extension. + var moduleFileName = modulePath + ".bicep"; + var moduleFileUri = baseUri.Resolve(moduleFileName); + + var fileHandle = fileExplorer.GetFile(moduleFileUri); + + return new(new OciArtifactEmulatedReference(referencingFile, modulePath, fileHandle)); + } + + public override bool Equals(object? obj) + { + if (obj is not OciArtifactEmulatedReference other) + { + return false; + } + + return StringComparer.Ordinal.Equals(modulePath, other.modulePath) && + fileHandle.Equals(other.fileHandle); + } + + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(modulePath, StringComparer.Ordinal); + hash.Add(fileHandle); + + return hash.ToHashCode(); + } + } +} diff --git a/src/Bicep.Core/Registry/Oci/OciArtifactReferenceFacts.cs b/src/Bicep.Core/Registry/Oci/OciArtifactReferenceFacts.cs index 7e9faecd9a7..8a52a0ee355 100644 --- a/src/Bicep.Core/Registry/Oci/OciArtifactReferenceFacts.cs +++ b/src/Bicep.Core/Registry/Oci/OciArtifactReferenceFacts.cs @@ -8,6 +8,9 @@ namespace Bicep.Core.Registry.Oci public static partial class OciArtifactReferenceFacts { public const string Scheme = "br"; + + public const string EmulatedScheme = "br-fs"; + public const string SchemeWithColon = Scheme + ":"; public const int MaxRegistryLength = 255; diff --git a/src/Bicep.Core/Registry/OciArtifactEmulatedRegistry.cs b/src/Bicep.Core/Registry/OciArtifactEmulatedRegistry.cs new file mode 100644 index 00000000000..8f0edf86763 --- /dev/null +++ b/src/Bicep.Core/Registry/OciArtifactEmulatedRegistry.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Bicep.Core.Diagnostics; +using Bicep.Core.Modules; +using Bicep.Core.Registry.Oci; +using Bicep.Core.Semantics; +using Bicep.Core.SourceGraph; +using Bicep.Core.SourceLink; + +namespace Bicep.Core.Registry +{ + public class OciArtifactEmulatedRegistry : ArtifactRegistry + { + public override string Scheme => ArtifactReferenceSchemes.OciEmulated; + + public override RegistryCapabilities GetCapabilities(ArtifactType artifactType, OciArtifactEmulatedReference reference) + => RegistryCapabilities.Default; + + public override ResultWithDiagnosticBuilder TryParseArtifactReference(BicepSourceFile referencingFile, ArtifactType artifactType, string? aliasName, string reference) + => new(x => x.ModuleReferenceSchemeBrFsNotSupported()); + + public override bool IsArtifactRestoreRequired(OciArtifactEmulatedReference reference) => false; + + public override Task CheckArtifactExists(ArtifactType artifactType, OciArtifactEmulatedReference reference) + => Task.FromResult(reference.TryGetEntryPointFileHandle().IsSuccess(out var fileHandle, out _) && fileHandle.Exists()); + + public override Task> RestoreArtifacts(IEnumerable references) + => Task.FromResult>( + new Dictionary()); + + public override Task> InvalidateArtifactsCache(IEnumerable references) + => Task.FromResult>( + new Dictionary()); + + public override Task PublishModule(OciArtifactEmulatedReference reference, BinaryData compiled, BinaryData? bicepSources, string? documentationUri, string? description) + => throw new NotSupportedException("Publishing is not supported for filesystem-based module aliases."); + + public override Task PublishExtension(OciArtifactEmulatedReference reference, ExtensionPackage package) + => throw new NotSupportedException("Publishing is not supported for filesystem-based module aliases."); + + public override string? TryGetDocumentationUri(OciArtifactEmulatedReference reference) => null; + + public override Task TryGetModuleDescription(ModuleSymbol module, OciArtifactEmulatedReference reference) + => Task.FromResult(null); + } +} diff --git a/src/Bicep.Core/Registry/OciArtifactRegistry.cs b/src/Bicep.Core/Registry/OciArtifactRegistry.cs index 76eb5c07575..ceb06e6d8d0 100644 --- a/src/Bicep.Core/Registry/OciArtifactRegistry.cs +++ b/src/Bicep.Core/Registry/OciArtifactRegistry.cs @@ -30,14 +30,18 @@ public sealed class OciArtifactRegistry : ExternalArtifactRegistry ArtifactReferenceSchemes.Oci; @@ -50,6 +54,41 @@ public override RegistryCapabilities GetCapabilities(ArtifactType artifactType, public override ResultWithDiagnosticBuilder TryParseArtifactReference(BicepSourceFile referencingFile, ArtifactType artifactType, string? aliasName, string reference) { + // Check if the alias resolves to a filesystem-based alias. + if (aliasName is not null) + { + if (!referencingFile.Configuration.ModuleAliases.TryGetOciArtifactModuleAlias(aliasName).IsSuccess(out var alias, out var aliasFailureBuilder)) + { + return new(aliasFailureBuilder); + } + + if (alias.FileSystem is not null) + { + if (artifactType != ArtifactType.Module) + { + return new(x => x.OciArtifactModuleAliasFileSystemOnlySupportsModules(aliasName)); + } + + if (referencingFile.Configuration.ConfigFileUri is null) + { + return new(x => x.ConfigurationFileNotFound("OciEmulatedModuleAliases")); + } + + if (!OciArtifactEmulatedReference.TryParse( + referencingFile, + alias.FileSystem, + referencingFile.Configuration.ConfigFileUri, + reference, + this.fileExplorer, + aliasName).IsSuccess(out var emulatedRef, out var emulatedFailureBuilder)) + { + return new(emulatedFailureBuilder); + } + + return new(emulatedRef); + } + } + if (!OciArtifactReference.TryParse(referencingFile.Features, referencingFile.Configuration, artifactType, aliasName, reference).IsSuccess(out var @ref, out var failureBuilder)) { return new(failureBuilder); diff --git a/src/Bicep.IO/Abstraction/IOUri.cs b/src/Bicep.IO/Abstraction/IOUri.cs index 49e33f7f67b..e056da52e1b 100644 --- a/src/Bicep.IO/Abstraction/IOUri.cs +++ b/src/Bicep.IO/Abstraction/IOUri.cs @@ -71,6 +71,9 @@ public IOUri(IOUriScheme scheme, string? authority, string path, string query = public static implicit operator string(IOUri uri) => uri.ToString(); + public static bool IsAbsoluteFilePath(string path) => + FilePath.IsPathFullyQualified(path) || path.StartsWith('/') || path.StartsWith('\\'); + public static IOUri FromFilePath(string filePath) { if (!FilePath.IsPathFullyQualified(filePath) && !(filePath.StartsWith('/') || filePath.StartsWith('\\'))) diff --git a/src/vscode-bicep/schemas/bicepconfig.schema.json b/src/vscode-bicep/schemas/bicepconfig.schema.json index 76ff978997d..4c29b8b9a2d 100644 --- a/src/vscode-bicep/schemas/bicepconfig.schema.json +++ b/src/vscode-bicep/schemas/bicepconfig.schema.json @@ -136,8 +136,17 @@ "bicepRegistryModuleAlias": { "type": "object", "additionalProperties": false, - "required": [ - "registry" + "oneOf": [ + { + "required": [ + "registry" + ] + }, + { + "required": [ + "fileSystem" + ] + } ], "properties": { "registry": { @@ -147,6 +156,12 @@ "minLength": 1, "maxLength": 255 }, + "fileSystem": { + "title": "File System", + "description": "The path relative to bicepconfig.json used to emulate a registry alias", + "type": "string", + "minLength": 1 + }, "modulePath": { "title": "Module Path", "description": "The module path of the alias",