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..91a04d13158 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,16 @@ "demo-two": { "registry": "mock-registry-two.invalid", "modulePath": "demo" + }, + "mock-registry-mocked": { + "registry": "mock-registry-three.invalid" + } + } + }, + "moduleAliasesMock": { + "br": { + "mock-registry-mocked": { + "mapToFilePath": "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..f7f10f261e0 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-mocked: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..d246828887f 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-mocked: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..8348128e843 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-mocked: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..8cd83b71f5c 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:2601) 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-mocked:plan:v2' = { +//@[000:0135) ├─DeclaredModuleExpression +//@[058:0135) | ├─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..64c762a0aeb 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-mocked: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 2fd95972edc..1325272636f 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 @@ -25,6 +25,15 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { } } +module appPlanDeploy3 'br/mock-registry-mocked:plan:v2' = { +//@[07:21) Module appPlanDeploy3. Type: module. Declaration start char: 0, length: 135 + name: 'planDeploy3' + scope: rg + params: { + namePrefix: 'hello' + } +} + var websites = [ //@[04:12) Variable websites. Type: [object, object]. Declaration start char: 0, length: 110 { @@ -153,3 +162,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..07c5cf46cf1 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:2601) 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-mocked:plan:v2' = { +//@[000:0135) ├─ModuleDeclarationSyntax +//@[000:0006) | ├─Token(Identifier) |module| +//@[007:0021) | ├─IdentifierSyntax +//@[007:0021) | | └─Token(Identifier) |appPlanDeploy3| +//@[022:0055) | ├─StringSyntax +//@[022:0055) | | └─Token(StringComplete) |'br/mock-registry-mocked:plan:v2'| +//@[056:0057) | ├─Token(Assignment) |=| +//@[058:0135) | └─ObjectSyntax +//@[058:0059) | ├─Token(LeftBrace) |{| +//@[059:0060) | ├─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..69e9b140343 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-mocked:plan:v2' = { +//@[000:006) Identifier |module| +//@[007:021) Identifier |appPlanDeploy3| +//@[022:055) StringComplete |'br/mock-registry-mocked:plan:v2'| +//@[056:057) Assignment |=| +//@[058:059) LeftBrace |{| +//@[059:060) 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..7c5d8bcd65c 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); @@ -112,6 +112,7 @@ public static RootConfiguration CreateMockConfiguration(Dictionary(), + ["moduleAliasesMock"] = new Dictionary(), ["extensions"] = new Dictionary(), ["implicitExtensions"] = new[] { "az" }, ["analyzers"] = new Dictionary(), diff --git a/src/Bicep.Core.UnitTests/Configuration/ConfigurationManagerTests.cs b/src/Bicep.Core.UnitTests/Configuration/ConfigurationManagerTests.cs index a01a74a7ce9..981f6a48981 100644 --- a/src/Bicep.Core.UnitTests/Configuration/ConfigurationManagerTests.cs +++ b/src/Bicep.Core.UnitTests/Configuration/ConfigurationManagerTests.cs @@ -64,6 +64,9 @@ public void GetBuiltInConfiguration_NoParameter_ReturnsBuiltInConfigurationWithA } } }, + "moduleAliasesMock": { + "br": {} + }, "extensions": { "az": "builtin:", "kubernetes": "builtin:" @@ -170,6 +173,9 @@ public void GetBuiltInConfiguration_DisableAllAnalyzers_ReturnsBuiltInConfigurat } } }, + "moduleAliasesMock": { + "br": {} + }, "extensions": { "az": "builtin:", "kubernetes": "builtin:" @@ -243,6 +249,9 @@ public void GetBuiltInConfiguration_DisableAnalyzers_ReturnsBuiltInConfiguration } } }, + "moduleAliasesMock": { + "br": {} + }, "extensions": { "az": "builtin:", "kubernetes": "builtin:" @@ -412,6 +421,9 @@ public void GetBuiltInConfiguration_EnableExperimentalFeature_ReturnsBuiltInConf } } }, + "moduleAliasesMock": { + "br": {} + }, "extensions": { "kubernetes": "builtin:", "az": "builtin:" @@ -766,6 +778,9 @@ public void GetConfiguration_ValidCustomConfiguration_OverridesBuiltInConfigurat } } }, + "moduleAliasesMock": { + "br": {} + }, "extensions": { "az": "builtin:", "kubernetes": "builtin:" @@ -855,5 +870,66 @@ public void Bicepconfig_resolution_is_a_merge_between_closest_bicepconfig_file_a configuration.ModuleAliases.TryGetOciArtifactModuleAlias("public").IsSuccess(out var moduleAlias).Should().BeTrue(); moduleAlias!.Registry.Should().Be("mcr.microsoft.com"); } + + [TestMethod] + public void GetConfiguration_ModuleAliasesMock_SupersedesModuleAliasesForSameAlias() + { + // Arrange. + var fileSet = InMemoryTestFileSet.Create(("bicepconfig.json", """ + { + "moduleAliases": { + "br": { + "myAlias": { "registry": "real.azurecr.io", "modulePath": "real/path" } + } + }, + "moduleAliasesMock": { + "br": { + "myAlias": { "mapToFilePath": "mock/path" } + } + } + } + """)); + var sut = new ConfigurationManager(fileSet.FileExplorer); + + // Act. + var configuration = sut.GetConfiguration(fileSet.GetUri("main.bicep")); + + // Assert. + configuration.ModuleAliasesMock.TryGetOciArtifactModuleAliasMock("myAlias").IsSuccess(out var mockAlias).Should().BeTrue(); + mockAlias!.MapToFilePath.Should().Be("mock/path"); + + // The merged view should expose the mock definition without throwing on duplicate keys. + var mocks = configuration.ModuleAliasesMock.GetOciArtifactModuleAliasesMock(); + mocks.Should().ContainKey("myAlias"); + mocks["myAlias"].MapToFilePath.Should().Be("mock/path"); + } + + [TestMethod] + public void GetConfiguration_ModuleAliasesMock_FallsBackToModuleAliasesWhenAliasNotInMock() + { + // Arrange. + var fileSet = InMemoryTestFileSet.Create(("bicepconfig.json", """ + { + "moduleAliases": { + "br": { + "myAlias": { "registry": "real.azurecr.io" } + } + }, + "moduleAliasesMock": { + "br": { + "otherAlias": { "mapToFilePath": "mock/path" } + } + } + } + """)); + var sut = new ConfigurationManager(fileSet.FileExplorer); + + // Act. + var configuration = sut.GetConfiguration(fileSet.GetUri("main.bicep")); + + // Assert. + configuration.ModuleAliases.TryGetOciArtifactModuleAlias("myAlias").IsSuccess(out var alias).Should().BeTrue(); + alias!.Registry.Should().Be("real.azurecr.io"); + } } } diff --git a/src/Bicep.Core.UnitTests/Configuration/RootConfigurationTests.cs b/src/Bicep.Core.UnitTests/Configuration/RootConfigurationTests.cs index 7a1e3301831..394087fceb7 100644 --- a/src/Bicep.Core.UnitTests/Configuration/RootConfigurationTests.cs +++ b/src/Bicep.Core.UnitTests/Configuration/RootConfigurationTests.cs @@ -17,6 +17,7 @@ public void RootConfiguration_LeadingTildeInCacheRootDirectory_ExpandPath(string var configuration = new RootConfiguration( BicepTestConstants.BuiltInConfiguration.Cloud, BicepTestConstants.BuiltInConfiguration.ModuleAliases, + BicepTestConstants.BuiltInConfiguration.ModuleAliasesMock, BicepTestConstants.BuiltInConfiguration.Extensions, BicepTestConstants.BuiltInConfiguration.ImplicitExtensions, BicepTestConstants.BuiltInConfiguration.Analyzers, diff --git a/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/UseRecentApiVersionRuleTests.cs b/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/UseRecentApiVersionRuleTests.cs index 09e66dc48aa..e816359b147 100644 --- a/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/UseRecentApiVersionRuleTests.cs +++ b/src/Bicep.Core.UnitTests/Diagnostics/LinterRuleTests/UseRecentApiVersionRuleTests.cs @@ -105,6 +105,7 @@ private static RootConfiguration CreateConfigurationWithFakeToday(RootConfigurat return new RootConfiguration( original.Cloud, original.ModuleAliases, + original.ModuleAliasesMock, original.Extensions, original.ImplicitExtensions, new AnalyzersConfiguration( diff --git a/src/Bicep.Core.UnitTests/Modules/OciArtifactModuleReferenceTests.cs b/src/Bicep.Core.UnitTests/Modules/OciArtifactModuleReferenceTests.cs index 579ba0b7d47..68e4e8c345c 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. The \"registry\" 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. The \"registry\" property must be specified.", }; } diff --git a/src/Bicep.Core.UnitTests/Registry/OciArtifactMockedReferenceTests.cs b/src/Bicep.Core.UnitTests/Registry/OciArtifactMockedReferenceTests.cs new file mode 100644 index 00000000000..a2238dd5c5b --- /dev/null +++ b/src/Bicep.Core.UnitTests/Registry/OciArtifactMockedReferenceTests.cs @@ -0,0 +1,249 @@ +// 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 OciArtifactMockedReferenceTests + { + [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) + { + OciArtifactMockedReference.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 = OciArtifactMockedReference.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 = OciArtifactMockedReference.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(OciArtifactReferenceFacts.MockedScheme); + } + + [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 = OciArtifactMockedReference.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 = OciArtifactMockedReference.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 = OciArtifactMockedReference.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 = OciArtifactMockedReference.TryParse( + referencingFile, "../bicepModules", configFileUri, "keyvault:1.0.0", fileExplorer); + var result2 = OciArtifactMockedReference.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 = OciArtifactMockedReference.TryParse( + referencingFile, "../bicepModules", configFileUri, "keyvault:1.0.0", fileExplorer); + var result2 = OciArtifactMockedReference.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_RegistryNotSet_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("registry"); + } + + [TestMethod] + public void TryGetOciArtifactModuleAliasMock_OnlyMapToFilePathSet_ShouldSucceed() + { + var configuration = BicepTestConstants.CreateMockConfiguration( + new() + { + ["moduleAliasesMock.br.myAlias.mapToFilePath"] = "../bicepModules", + }); + + var result = configuration.ModuleAliasesMock.TryGetOciArtifactModuleAliasMock("myAlias"); + + result.IsSuccess(out var alias, out _).Should().BeTrue(); + alias!.MapToFilePath.Should().Be("../bicepModules"); + } + + [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"); + } + + [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 = OciArtifactMockedReference.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/AnalyzersConfigurationExtensions.cs b/src/Bicep.Core/Configuration/AnalyzersConfigurationExtensions.cs index ada8e48fa5e..2170f6cc41e 100644 --- a/src/Bicep.Core/Configuration/AnalyzersConfigurationExtensions.cs +++ b/src/Bicep.Core/Configuration/AnalyzersConfigurationExtensions.cs @@ -40,6 +40,7 @@ public static RootConfiguration WithAnalyzersConfiguration(this RootConfiguratio new( current.Cloud, current.ModuleAliases, + current.ModuleAliasesMock, current.Extensions, current.ImplicitExtensions, analyzersConfiguration, @@ -63,6 +64,7 @@ public static RootConfiguration WithCloudConfiguration(this RootConfiguration cu new( cloudConfiguration, current.ModuleAliases, + current.ModuleAliasesMock, current.Extensions, current.ImplicitExtensions, current.Analyzers, diff --git a/src/Bicep.Core/Configuration/ExperimentalFeaturesExtensions.cs b/src/Bicep.Core/Configuration/ExperimentalFeaturesExtensions.cs index f30c7baab06..8e34ce80e59 100644 --- a/src/Bicep.Core/Configuration/ExperimentalFeaturesExtensions.cs +++ b/src/Bicep.Core/Configuration/ExperimentalFeaturesExtensions.cs @@ -9,6 +9,7 @@ public static RootConfiguration WithExperimentalFeaturesConfiguration(this RootC new( current.Cloud, current.ModuleAliases, + current.ModuleAliasesMock, current.Extensions, current.ImplicitExtensions, current.Analyzers, diff --git a/src/Bicep.Core/Configuration/ExtensionsConfigurationExtensions.cs b/src/Bicep.Core/Configuration/ExtensionsConfigurationExtensions.cs index 3320d14f5b7..7380759c4c5 100644 --- a/src/Bicep.Core/Configuration/ExtensionsConfigurationExtensions.cs +++ b/src/Bicep.Core/Configuration/ExtensionsConfigurationExtensions.cs @@ -18,6 +18,7 @@ public static RootConfiguration WithExtensions(this RootConfiguration rootConfig return new RootConfiguration( rootConfiguration.Cloud, rootConfiguration.ModuleAliases, + rootConfiguration.ModuleAliasesMock, rootConfiguration.Extensions.WithExtensions(payload), rootConfiguration.ImplicitExtensions, rootConfiguration.Analyzers, @@ -34,6 +35,7 @@ public static RootConfiguration WithImplicitExtensions(this RootConfiguration ro return new RootConfiguration( rootConfiguration.Cloud, rootConfiguration.ModuleAliases, + rootConfiguration.ModuleAliasesMock, rootConfiguration.Extensions, rootConfiguration.ImplicitExtensions.WithImplicitExtensions(payload), rootConfiguration.Analyzers, diff --git a/src/Bicep.Core/Configuration/ModuleAliasesConfiguration.cs b/src/Bicep.Core/Configuration/ModuleAliasesConfiguration.cs index 1e3f30ba77a..65a08e0b516 100644 --- a/src/Bicep.Core/Configuration/ModuleAliasesConfiguration.cs +++ b/src/Bicep.Core/Configuration/ModuleAliasesConfiguration.cs @@ -38,8 +38,8 @@ public record OciArtifactModuleAlias public string? ModulePath { get; init; } public override string ToString() => this.ModulePath is not null - ? $"{Registry}/{ModulePath}" - : $"{Registry}"; + ? $"{Registry}/{ModulePath}" + : $"{Registry}"; } public partial class ModuleAliasesConfiguration : ConfigurationSection @@ -122,6 +122,6 @@ private static bool ValidateAliasName(string aliasName, [NotNullWhen(false)] out } [GeneratedRegex("^[a-zA-Z0-9-_]+$", RegexOptions.CultureInvariant)] - private static partial Regex ModuleAliasNameRegex(); + internal static partial Regex ModuleAliasNameRegex(); } } diff --git a/src/Bicep.Core/Configuration/ModuleAliasesMockConfiguration.cs b/src/Bicep.Core/Configuration/ModuleAliasesMockConfiguration.cs new file mode 100644 index 00000000000..289b7d0a145 --- /dev/null +++ b/src/Bicep.Core/Configuration/ModuleAliasesMockConfiguration.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; +using Bicep.Core.Diagnostics; +using Bicep.Core.Extensions; +using Bicep.IO.Abstraction; +using static Bicep.Core.Diagnostics.DiagnosticBuilder; + +namespace Bicep.Core.Configuration +{ + public record ModuleAliasesMock + { + [JsonPropertyName("br")] + public ImmutableSortedDictionary OciArtifactModuleAliasesMock { get; init; } = ImmutableSortedDictionary.Empty; + } + + public record OciArtifactModuleAliasMock + { + public string? MapToFilePath { get; init; } + + public override string ToString() => $"{MapToFilePath}"; + } + + public partial class ModuleAliasesMockConfiguration : ConfigurationSection + { + private readonly IOUri? configFileUri; + + private ModuleAliasesMockConfiguration(ModuleAliasesMock data, IOUri? configFileUri) + : base(data) + { + this.configFileUri = configFileUri; + } + + public static ModuleAliasesMockConfiguration Bind(JsonElement element, IOUri? configFileUri) => new(element.ToNonNullObject(), configFileUri); + + public ImmutableSortedDictionary GetOciArtifactModuleAliasesMock() + { + return this.Data.OciArtifactModuleAliasesMock; + } + + public ResultWithDiagnosticBuilder TryGetOciArtifactModuleAliasMock(string aliasName) + { + if (!ValidateAliasName(aliasName, out var errorBuilder)) + { + return new(errorBuilder); + } + + if (!this.Data.OciArtifactModuleAliasesMock.TryGetValue(aliasName, out var alias)) + { + return new(x => x.OciArtifactModuleAliasNameDoesNotExistInConfiguration(aliasName, configFileUri)); + } + + if (alias.MapToFilePath is null) + { + return new(x => x.InvalidOciArtifactModuleAliasRegistryNullOrUndefined(aliasName, configFileUri)); + } + + return new(alias); + } + + private static bool ValidateAliasName(string aliasName, [NotNullWhen(false)] out DiagnosticBuilderDelegate? errorBuilder) + { + // To ensure consistency with module alias, we're referring to the same regex for validating alias names + if (!ModuleAliasesConfiguration.ModuleAliasNameRegex().IsMatch(aliasName)) + { + errorBuilder = x => x.InvalidModuleAliasName(aliasName); + return false; + } + + errorBuilder = null; + return true; + } + } +} diff --git a/src/Bicep.Core/Configuration/RootConfiguration.cs b/src/Bicep.Core/Configuration/RootConfiguration.cs index f8727bf206e..496b8480da6 100644 --- a/src/Bicep.Core/Configuration/RootConfiguration.cs +++ b/src/Bicep.Core/Configuration/RootConfiguration.cs @@ -7,6 +7,7 @@ using System.Text.Json; using Bicep.Core.Diagnostics; using Bicep.Core.Extensions; +using Bicep.Core.Json; using Bicep.IO.Abstraction; namespace Bicep.Core.Configuration @@ -17,6 +18,8 @@ public class RootConfiguration public const string ModuleAliasesKey = "moduleAliases"; + public const string ModuleAliasesMockKey = "moduleAliasesMock"; + public const string ExtensionsKey = "extensions"; public const string ImplicitExtensionsKey = "implicitExtensions"; @@ -34,6 +37,7 @@ public class RootConfiguration public RootConfiguration( CloudConfiguration cloud, ModuleAliasesConfiguration moduleAliases, + ModuleAliasesMockConfiguration moduleAliasesMock, ExtensionsConfiguration extensions, ImplicitExtensionsConfiguration implicitExtensions, AnalyzersConfiguration analyzers, @@ -46,6 +50,7 @@ public RootConfiguration( { this.Cloud = cloud; this.ModuleAliases = moduleAliases; + this.ModuleAliasesMock = moduleAliasesMock; this.Extensions = extensions; this.ImplicitExtensions = implicitExtensions; this.Analyzers = analyzers; @@ -61,6 +66,9 @@ public static RootConfiguration Bind(JsonElement element, IOUri? configFileUri = { var cloud = CloudConfiguration.Bind(element.GetProperty(CloudKey)); var moduleAliases = ModuleAliasesConfiguration.Bind(element.GetProperty(ModuleAliasesKey), configFileUri); + var moduleAliasesMock = element.TryGetProperty(ModuleAliasesMockKey, out var mockElement) + ? ModuleAliasesMockConfiguration.Bind(mockElement, configFileUri) + : ModuleAliasesMockConfiguration.Bind(JsonElementFactory.CreateElement(new ModuleAliasesMock()), configFileUri); var analyzers = new AnalyzersConfiguration(element.GetProperty(AnalyzersKey)); var cacheRootDirectory = element.TryGetProperty(CacheRootDirectoryKey, out var e) ? e.GetString() : default; var experimentalFeaturesWarning = element.TryGetProperty(ExperimentalFeaturesWarningKey, out var value) && value.GetBoolean(); @@ -70,13 +78,15 @@ public static RootConfiguration Bind(JsonElement element, IOUri? configFileUri = var extensions = ExtensionsConfiguration.Bind(element.GetProperty(ExtensionsKey)); var implicitExtensions = ImplicitExtensionsConfiguration.Bind(element.GetProperty(ImplicitExtensionsKey)); - return new(cloud, moduleAliases, extensions, implicitExtensions, analyzers, cacheRootDirectory, experimentalFeaturesWarning, experimentalFeaturesEnabled, formatting, configFileUri, null); + return new(cloud, moduleAliases, moduleAliasesMock, extensions, implicitExtensions, analyzers, cacheRootDirectory, experimentalFeaturesWarning, experimentalFeaturesEnabled, formatting, configFileUri, null); } public CloudConfiguration Cloud { get; } public ModuleAliasesConfiguration ModuleAliases { get; } + public ModuleAliasesMockConfiguration ModuleAliasesMock { get; } + public ExtensionsConfiguration Extensions { get; } public ImplicitExtensionsConfiguration ImplicitExtensions { get; } @@ -100,6 +110,7 @@ public static RootConfiguration Bind(JsonElement element, IOUri? configFileUri = public RootConfiguration With( CloudConfiguration? cloud = null, ModuleAliasesConfiguration? moduleAliases = null, + ModuleAliasesMockConfiguration? moduleAliasesMock = null, ExtensionsConfiguration? extensions = null, ImplicitExtensionsConfiguration? implicitExtensions = null, AnalyzersConfiguration? analyzers = null, @@ -113,6 +124,7 @@ public RootConfiguration With( return new RootConfiguration( cloud ?? this.Cloud, moduleAliases ?? this.ModuleAliases, + moduleAliasesMock ?? this.ModuleAliasesMock, extensions ?? this.Extensions, implicitExtensions ?? this.ImplicitExtensions, analyzers ?? this.Analyzers, @@ -137,6 +149,9 @@ public string ToUtf8Json() writer.WritePropertyName(ModuleAliasesKey); this.ModuleAliases.WriteTo(writer); + writer.WritePropertyName(ModuleAliasesMockKey); + this.ModuleAliasesMock.WriteTo(writer); + writer.WritePropertyName(ExtensionsKey); this.Extensions.WriteTo(writer); diff --git a/src/Bicep.Core/Configuration/bicepconfig.json b/src/Bicep.Core/Configuration/bicepconfig.json index 5ece485979c..0612830eb3f 100644 --- a/src/Bicep.Core/Configuration/bicepconfig.json +++ b/src/Bicep.Core/Configuration/bicepconfig.json @@ -1,66 +1,69 @@ -{ - // This is the base configuration which provides the defaults for all values (end users don't see this file). - // Intellisense for bicepconfig.json is controlled by src/vscode-bicep/schemas/bicepconfig.schema.json - - "cloud": { - "currentProfile": "AzureCloud", - "profiles": { - "AzureCloud": { - "resourceManagerEndpoint": "https://management.azure.com", - "activeDirectoryAuthority": "https://login.microsoftonline.com" - }, - "AzureChinaCloud": { - "resourceManagerEndpoint": "https://management.chinacloudapi.cn", - "activeDirectoryAuthority": "https://login.chinacloudapi.cn" - }, - "AzureUSGovernment": { - "resourceManagerEndpoint": "https://management.usgovcloudapi.net", - "activeDirectoryAuthority": "https://login.microsoftonline.us" - } - }, - "credentialPrecedence": ["AzureCLI", "AzurePowerShell"] - }, - "moduleAliases": { - "ts": {}, - "br": { - "public": { - "registry": "mcr.microsoft.com", - "modulePath": "bicep" - } - } - }, - "extensions": { - "az": "builtin:", - "kubernetes": "builtin:" - }, - "implicitExtensions": ["az"], - "analyzers": { - "core": { - "verbose": false, - "enabled": true, - "rules": { - "no-hardcoded-env-urls": { - "level": "warning", - "disallowedhosts": [ - "azuredatalakeanalytics.net", - "azuredatalakestore.net", - "batch.core.windows.net", - "core.windows.net", - "database.windows.net", - "datalake.azure.net", - "gallery.azure.com", - "graph.windows.net", - "login.microsoftonline.com", - "management.azure.com", - "management.core.windows.net", - "vault.azure.net" - ], - "excludedhosts": ["schema.management.azure.com"] - } - } - } - }, +{ + // This is the base configuration which provides the defaults for all values (end users don't see this file). + // Intellisense for bicepconfig.json is controlled by src/vscode-bicep/schemas/bicepconfig.schema.json + + "cloud": { + "currentProfile": "AzureCloud", + "profiles": { + "AzureCloud": { + "resourceManagerEndpoint": "https://management.azure.com", + "activeDirectoryAuthority": "https://login.microsoftonline.com" + }, + "AzureChinaCloud": { + "resourceManagerEndpoint": "https://management.chinacloudapi.cn", + "activeDirectoryAuthority": "https://login.chinacloudapi.cn" + }, + "AzureUSGovernment": { + "resourceManagerEndpoint": "https://management.usgovcloudapi.net", + "activeDirectoryAuthority": "https://login.microsoftonline.us" + } + }, + "credentialPrecedence": ["AzureCLI", "AzurePowerShell"] + }, + "moduleAliases": { + "ts": {}, + "br": { + "public": { + "registry": "mcr.microsoft.com", + "modulePath": "bicep" + } + } + }, + "moduleAliasesMock": { + "br": {} + }, + "extensions": { + "az": "builtin:", + "kubernetes": "builtin:" + }, + "implicitExtensions": ["az"], + "analyzers": { + "core": { + "verbose": false, + "enabled": true, + "rules": { + "no-hardcoded-env-urls": { + "level": "warning", + "disallowedhosts": [ + "azuredatalakeanalytics.net", + "azuredatalakestore.net", + "batch.core.windows.net", + "core.windows.net", + "database.windows.net", + "datalake.azure.net", + "gallery.azure.com", + "graph.windows.net", + "login.microsoftonline.com", + "management.azure.com", + "management.core.windows.net", + "vault.azure.net" + ], + "excludedhosts": ["schema.management.azure.com"] + } + } + } + }, "experimentalFeaturesWarning": true, - "experimentalFeaturesEnabled": {}, - "formatting": {} -} + "experimentalFeaturesEnabled": {}, + "formatting": {} +} diff --git a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs index cff8031d4c2..e0d2da3a9b7 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.OciMocked)); 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. The \"registry\" property must be specified."); public Diagnostic InvalidTemplateSpecReferenceInvalidSubscriptionId(string? aliasName, string subscriptionId, string referenceValue) => CoreError( "BCP217", @@ -2027,6 +2027,22 @@ 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 OciArtifactModuleAliasMapToFilePathOnlySupportsModules(string aliasName) => CoreError( + "BCP448", + $"The OCI artifact module alias \"{aliasName}\" has a \"mapToFilePath\" property which is only supported for modules, not extensions."); + + public Diagnostic ModuleReferenceSchemeBrFsNotSupported() => CoreError( + "BCP449", + $"The '{Registry.Oci.OciArtifactReferenceFacts.MockedScheme}' module reference scheme is for internal use only. Use a 'br/:' reference with a configured 'mapToFilePath' alias instead."); + + public Diagnostic ConfigurationFileNotFound(string featureName) => CoreError( + "BCP450", + $"Configuration file is not found. Feature \"{featureName}\" requires a configuration file."); + + public Diagnostic InvalidOciArtifactModuleAliasMapToFilePath(string? aliasName, string path, string reason) => CoreError( + "BCP451", + $"The OCI artifact module alias{(aliasName is not null ? $" \"{aliasName}\"" : "")} has an invalid \"mapToFilePath\" 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..d52caf49697 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 OciMocked = OciArtifactReferenceFacts.MockedScheme; + public const string TemplateSpecs = "ts"; } } diff --git a/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs b/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs index 8747e7300d3..a90ef3c7a0b 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 OciArtifactMockedRegistry(), new TemplateSpecModuleRegistry(templateSpecRepositoryFactory), }) { diff --git a/src/Bicep.Core/Registry/Oci/OciArtifactMockedReference.cs b/src/Bicep.Core/Registry/Oci/OciArtifactMockedReference.cs new file mode 100644 index 00000000000..f1b13506222 --- /dev/null +++ b/src/Bicep.Core/Registry/Oci/OciArtifactMockedReference.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 mocked via a local filesystem path. + /// When a module alias is added to moduleAliasesMock configuration, + /// module references are resolved to local .bicep files instead of pulling from a container registry. + /// + public class OciArtifactMockedReference : ArtifactReference + { + private readonly IFileHandle fileHandle; + private readonly string modulePath; + private readonly string? fullyQualifiedReference; + + public OciArtifactMockedReference(BicepSourceFile referencingFile, string modulePath, IFileHandle fileHandle, string? fullyQualifiedReference = null) + : base(referencingFile.Features, referencingFile.Configuration, OciArtifactReferenceFacts.MockedScheme) + { + 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 + // mapToFilePath is the 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 mapToFilePath, + 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 mapToFilePath path ends with '/' so it's treated as a directory. + var directoryPath = mapToFilePath.EndsWith('/') || mapToFilePath.EndsWith('\\') + ? mapToFilePath + : mapToFilePath + "/"; + + if (IOUri.IsAbsoluteFilePath(mapToFilePath)) + { + try + { + baseUri = IOUri.FromFilePath(directoryPath); + } + catch (IOException ex) + { + return new(x => x.InvalidOciArtifactModuleAliasMapToFilePath(aliasName, mapToFilePath, 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 OciArtifactMockedReference(referencingFile, modulePath, fileHandle)); + } + + public override bool Equals(object? obj) + { + if (obj is not OciArtifactMockedReference 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..44dd5c7fff2 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 MockedScheme = "br-mock"; + public const string SchemeWithColon = Scheme + ":"; public const int MaxRegistryLength = 255; diff --git a/src/Bicep.Core/Registry/OciArtifactMockRegistry.cs b/src/Bicep.Core/Registry/OciArtifactMockRegistry.cs new file mode 100644 index 00000000000..9b06e6e8a90 --- /dev/null +++ b/src/Bicep.Core/Registry/OciArtifactMockRegistry.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 OciArtifactMockedRegistry : ArtifactRegistry + { + public override string Scheme => ArtifactReferenceSchemes.OciMocked; + + public override RegistryCapabilities GetCapabilities(ArtifactType artifactType, OciArtifactMockedReference reference) + => RegistryCapabilities.Default; + + public override ResultWithDiagnosticBuilder TryParseArtifactReference(BicepSourceFile referencingFile, ArtifactType artifactType, string? aliasName, string reference) + => new(x => x.ModuleReferenceSchemeBrFsNotSupported()); + + public override bool IsArtifactRestoreRequired(OciArtifactMockedReference reference) => false; + + public override Task CheckArtifactExists(ArtifactType artifactType, OciArtifactMockedReference 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(OciArtifactMockedReference reference, BinaryData compiled, BinaryData? bicepSources, string? documentationUri, string? description) + => throw new NotSupportedException("Publishing is not supported for mocked module aliases."); + + public override Task PublishExtension(OciArtifactMockedReference reference, ExtensionPackage package) + => throw new NotSupportedException("Publishing is not supported for mocked module aliases."); + + public override string? TryGetDocumentationUri(OciArtifactMockedReference reference) => null; + + public override Task TryGetModuleDescription(ModuleSymbol module, OciArtifactMockedReference reference) + => Task.FromResult(null); + } +} diff --git a/src/Bicep.Core/Registry/OciArtifactRegistry.cs b/src/Bicep.Core/Registry/OciArtifactRegistry.cs index 76eb5c07575..a838d08661b 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,14 +54,50 @@ 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 mocked alias + if (aliasName is not null) + { + if (referencingFile.Configuration.ModuleAliasesMock.TryGetOciArtifactModuleAliasMock(aliasName).IsSuccess(out var mockAlias, out var _)) + { + // Mock aliases only support modules, not extensions. + if (artifactType != ArtifactType.Module) + { + return new(x => x.OciArtifactModuleAliasMapToFilePathOnlySupportsModules(aliasName)); + } + + if (referencingFile.Configuration.ConfigFileUri is null) + { + return new(x => x.ConfigurationFileNotFound("OciModuleAliasesMock")); + } + + if (mockAlias.MapToFilePath is null) + { + return new(x => x.InvalidOciArtifactModuleAliasRegistryNullOrUndefined(aliasName, referencingFile.Configuration.ConfigFileUri)); + } + + if (!OciArtifactMockedReference.TryParse( + referencingFile, + mockAlias.MapToFilePath, + referencingFile.Configuration.ConfigFileUri, + reference, + this.fileExplorer, + aliasName).IsSuccess(out var mockedRef, out var mockedFailureBuilder)) + { + return new(mockedFailureBuilder!); + } + + return new(mockedRef!); + } + } + if (!OciArtifactReference.TryParse(referencingFile.Features, referencingFile.Configuration, artifactType, aliasName, reference).IsSuccess(out var @ref, out var failureBuilder)) { return new(failureBuilder); } + return new(@ref); } - public override bool IsArtifactRestoreRequired(OciArtifactReference reference) { /* 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/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs b/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs index 525fa4258f7..b6a3cc57b58 100644 --- a/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs +++ b/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs @@ -387,6 +387,7 @@ private static bool TryGetValidModuleAlias( registry = null; modulePath = null; + // Mock aliases supersede real aliases with the same name. if (configuration.ModuleAliases.GetOciArtifactModuleAliases().TryGetValue(aliasName, out var aliasConfig) && !string.IsNullOrWhiteSpace(aliasConfig.Registry)) { diff --git a/src/vscode-bicep/schemas/bicepconfig.schema.json b/src/vscode-bicep/schemas/bicepconfig.schema.json index 81c7759bf6b..6c812f8ab7e 100644 --- a/src/vscode-bicep/schemas/bicepconfig.schema.json +++ b/src/vscode-bicep/schemas/bicepconfig.schema.json @@ -155,6 +155,21 @@ } } }, + "bicepRegistryModuleAliasMock": { + "type": "object", + "additionalProperties": false, + "required": [ + "mapToFilePath" + ], + "properties": { + "mapToFilePath": { + "title": "Map To File Path", + "description": "The path relative to bicepconfig.json used to emulate a registry alias", + "type": "string", + "minLength": 1 + } + } + }, "extensionConfig": { "type": "string" }, @@ -288,6 +303,25 @@ } } }, + "moduleAliasesMock": { + "title": "Module Aliases Mock", + "description": "Mock module aliases that supersede moduleAliases when present", + "type": "object", + "additionalProperties": false, + "default": { + "br": {} + }, + "properties": { + "br": { + "title": "Bicep Registry Module Aliases Mock", + "description": "Mock Bicep Registry module alias definitions", + "additionalProperties": { + "$ref": "#/definitions/bicepRegistryModuleAliasMock", + "additionalProperties": false + } + } + } + }, "extensions": { "title": "Bicep Extensions", "description": "Bicep extension references",