diff --git a/release_notes.md b/release_notes.md index ca06990915..2114db33a8 100644 --- a/release_notes.md +++ b/release_notes.md @@ -5,4 +5,5 @@ --> - Update Python Worker Version to [4.40.2](https://github.com/Azure/azure-functions-python-worker/releases/tag/4.40.2) - Add JitTrace Files for v4.1044 +- Remove duplicate function names from sync triggers payload(#11371) - Avoid emitting empty tag values for health check metrics (#11393) diff --git a/src/WebJobs.Script.WebHost/Extensions/FunctionMetadataExtensions.cs b/src/WebJobs.Script.WebHost/Extensions/FunctionMetadataExtensions.cs index 3b8b740751..d24b791872 100644 --- a/src/WebJobs.Script.WebHost/Extensions/FunctionMetadataExtensions.cs +++ b/src/WebJobs.Script.WebHost/Extensions/FunctionMetadataExtensions.cs @@ -5,7 +5,6 @@ using System.IO; using System.Linq; using System.Threading.Tasks; -using Microsoft.Azure.WebJobs.Logging; using Microsoft.Azure.WebJobs.Script.Description; using Microsoft.Azure.WebJobs.Script.Management.Models; using Microsoft.Azure.WebJobs.Script.WebHost.Management; diff --git a/src/WebJobs.Script.WebHost/Management/FunctionsSyncManager.cs b/src/WebJobs.Script.WebHost/Management/FunctionsSyncManager.cs index 829739f976..43bf91371a 100644 --- a/src/WebJobs.Script.WebHost/Management/FunctionsSyncManager.cs +++ b/src/WebJobs.Script.WebHost/Management/FunctionsSyncManager.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System; @@ -309,7 +309,7 @@ private async Task GetSyncTriggersPayload() PrepareSyncTriggers(); var hostOptions = _applicationHostOptions.CurrentValue.ToHostOptions(); - var functionsMetadata = _functionMetadataManager.GetFunctionMetadata().Where(m => !m.IsProxy()); + var functionsMetadata = _functionMetadataManager.GetFunctionMetadata().Where(m => !m.IsProxy()).DistinctBy(m => m.Name, StringComparer.OrdinalIgnoreCase); // trigger information used by the ScaleController var triggers = await GetFunctionTriggers(functionsMetadata, hostOptions); diff --git a/test/WebJobs.Script.Tests.Integration/Management/FunctionsSyncManagerTests.cs b/test/WebJobs.Script.Tests.Integration/Management/FunctionsSyncManagerTests.cs index 2bdb4bfc31..89c6fd61a4 100644 --- a/test/WebJobs.Script.Tests.Integration/Management/FunctionsSyncManagerTests.cs +++ b/test/WebJobs.Script.Tests.Integration/Management/FunctionsSyncManagerTests.cs @@ -1,8 +1,9 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.IO; using System.IO.Abstractions; using System.Linq; @@ -13,7 +14,9 @@ using System.Threading.Tasks; using Microsoft.Azure.WebJobs.Host.Executors; using Microsoft.Azure.WebJobs.Host.Scale; +using Microsoft.Azure.WebJobs.Host.Storage; using Microsoft.Azure.WebJobs.Script.Config; +using Microsoft.Azure.WebJobs.Script.Description; using Microsoft.Azure.WebJobs.Script.WebHost; using Microsoft.Azure.WebJobs.Script.WebHost.Management; using Microsoft.Azure.WebJobs.Script.Workers.Http; @@ -27,6 +30,7 @@ using Newtonsoft.Json.Linq; using Xunit; using static Microsoft.Azure.WebJobs.Script.Tests.TestHelpers; +using FunctionMetadata = Microsoft.Azure.WebJobs.Script.Description.FunctionMetadata; namespace Microsoft.Azure.WebJobs.Script.Tests.Managment { @@ -1021,6 +1025,128 @@ public void Managed_Kubernetes_Environment_SyncTrigger_Url_Validation(string kub Assert.Equal(HttpMethod.Post, httpRequest.Method); } + [Fact] + public async Task GetSyncTriggersPayload_DeduplicatesFunctionMetadataByName_KeepsFirstOccurrence() + { + // Create two FunctionMetadata objects with the same name but different authLevels + var duplicateName = "DuplicateFunction"; + var firstMetadata = new FunctionMetadata + { + Name = duplicateName, + ScriptFile = "file1.csx" + }; + firstMetadata.Bindings.Add(new BindingMetadata + { + Name = "req", + Type = "httpTrigger", + Direction = BindingDirection.In, + Raw = new JObject + { + { "authLevel", "function" }, + { "type", "httpTrigger" }, + { "direction", "in" }, + { "name", "req" } + } + }); + firstMetadata.Bindings.Add(new BindingMetadata + { + Name = "$return", + Type = "http", + Direction = BindingDirection.Out, + Raw = new JObject + { + { "type", "http" }, + { "direction", "out" }, + { "name", "$return" } + } + }); + + var secondMetadata = new FunctionMetadata + { + Name = duplicateName, + ScriptFile = "file2.csx" + }; + secondMetadata.Bindings.Add(new BindingMetadata + { + Name = "req", + Type = "httpTrigger", + Direction = BindingDirection.In, + Raw = new JObject + { + { "authLevel", "anonymous" }, + { "type", "httpTrigger" }, + { "direction", "in" }, + { "name", "req" } + } + }); + secondMetadata.Bindings.Add(new BindingMetadata + { + Name = "$return", + Type = "http", + Direction = BindingDirection.Out, + Raw = new JObject + { + { "type", "http" }, + { "direction", "out" }, + { "name", "$return" } + } + }); + + var functionMetadataManagerMock = new Mock(MockBehavior.Strict); + functionMetadataManagerMock + .Setup(m => m.GetFunctionMetadata(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(ImmutableArray.Create(firstMetadata, secondMetadata)); + + var environmentMock = new Mock(); + environmentMock.Setup(e => e.GetEnvironmentVariable(EnvironmentSettingNames.WebSiteAuthEncryptionKey)) + .Returns("non-null value"); + environmentMock.Setup(e => e.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteArmCacheEnabled)) + .Returns("0"); + + var hostNameProviderMock = new Mock(environmentMock.Object) { CallBase = true }; + + var scriptAppHostOptions = new ScriptApplicationHostOptions + { + ScriptPath = "somePath", + IsSelfHost = false, + TestDataPath = "testDataPath", + LogPath = "rootLogPath" + }; + + var optionsMonitorMock = new Mock>(); + optionsMonitorMock.Setup(m => m.CurrentValue).Returns(scriptAppHostOptions); + optionsMonitorMock.Setup(m => m.Get(It.IsAny())).Returns(scriptAppHostOptions); + + var hostingConfigOptions = new FunctionsHostingConfigOptions(); + var hostingConfigOptionsMock = new Mock>(); + hostingConfigOptionsMock.Setup(m => m.Value).Returns(hostingConfigOptions); + + var syncManager = new FunctionsSyncManager( + Mock.Of(), + optionsMonitorMock.Object, + NullLogger.Instance, + Mock.Of(), + Mock.Of(), + Mock.Of(), + environmentMock.Object, + hostNameProviderMock.Object, + functionMetadataManagerMock.Object, + Mock.Of(), + hostingConfigOptionsMock.Object, + Mock.Of()); + + // Use the public GetTriggersAsync method (main change is in a private method wrapped by this) + var result = await syncManager.GetTriggersAsync(); + + var content = JObject.Parse(result.Content); + var triggers = content["triggers"] as JArray; + Assert.NotNull(triggers); + Assert.Single(triggers); + Assert.Equal(duplicateName, triggers[0]["functionName"]?.ToString()); + // check authLevel to verify first occurrence was kept + Assert.Equal("function", triggers[0]["authLevel"]?.ToString()); + } + private static LanguageWorkerOptions CreateLanguageWorkerConfigSettings() { return new LanguageWorkerOptions