Skip to content

Commit 171e5b1

Browse files
authored
Add function group to metadata and sync triggers (#9777)
* Add function group to metadata and sync triggers * update release_notes.md * Fix csproj changes, check for webhook, only add grouping for flex * Add IsWebHookTrigger tests * Fix test build * fix tests
1 parent 40b0d43 commit 171e5b1

File tree

8 files changed

+280
-18
lines changed

8 files changed

+280
-18
lines changed

release_notes.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
<!-- Please add your release notes in the following format:
44
- My change description (#PR)
55
-->
6-
6+
- Update Python Worker Version to [4.23.0](https://github.com/Azure/azure-functions-python-worker/releases/tag/4.23.0)
77
- Updated `Microsoft.Azure.Functions.DotNetIsolatedNativeHost` version to 1.0.5 (#9753)
8+
- Add function grouping information (https://github.com/Azure/azure-functions-host/pull/9735)

src/WebJobs.Script.WebHost/Extensions/FunctionMetadataExtensions.cs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ public static class FunctionMetadataExtensions
1818
/// Maps FunctionMetadata to FunctionMetadataResponse.
1919
/// </summary>
2020
/// <param name="functionMetadata">FunctionMetadata to be mapped.</param>
21-
/// <param name="hostOptions">The host options</param>
22-
/// <returns>Promise of a FunctionMetadataResponse</returns>
21+
/// <param name="hostOptions">The host options.</param>
22+
/// <returns>Promise of a FunctionMetadataResponse.</returns>
2323
public static async Task<FunctionMetadataResponse> ToFunctionMetadataResponse(this FunctionMetadata functionMetadata, ScriptJobHostOptions hostOptions, string routePrefix, string baseUrl)
2424
{
2525
string functionPath = GetFunctionPathOrNull(hostOptions.RootScriptPath, functionMetadata.Name);
@@ -76,25 +76,36 @@ public static async Task<FunctionMetadataResponse> ToFunctionMetadataResponse(th
7676
/// </summary>
7777
/// <param name="functionMetadata">FunctionMetadata object to convert to a JObject.</param>
7878
/// <param name="config">ScriptHostConfiguration to read RootScriptPath from.</param>
79-
/// <returns>JObject that represent the trigger for scale controller to consume</returns>
80-
public static async Task<JObject> ToFunctionTrigger(this FunctionMetadata functionMetadata, ScriptJobHostOptions config)
79+
/// <param name="isFlexConsumption">True if this is for flex consumption, false otherwise.</param>
80+
/// <returns>JObject that represent the trigger for scale controller to consume.</returns>
81+
public static async Task<JObject> ToFunctionTrigger(
82+
this FunctionMetadata functionMetadata, ScriptJobHostOptions config, bool isFlexConsumption = false)
8183
{
8284
var functionPath = Path.Combine(config.RootScriptPath, functionMetadata.Name);
8385
var functionMetadataFilePath = Path.Combine(functionPath, ScriptConstants.FunctionMetadataFileName);
8486

8587
// Read function.json as a JObject
8688
var functionConfig = await GetFunctionConfig(functionMetadata, functionMetadataFilePath);
8789

88-
if (functionConfig.TryGetValue("bindings", out JToken value) &&
89-
value is JArray)
90+
if (functionConfig.TryGetValue("bindings", out JToken value) && value is JArray jArray)
9091
{
9192
// Find the trigger and add functionName to it
92-
foreach (JObject binding in (JArray)value)
93+
foreach (JObject binding in jArray)
9394
{
9495
var type = (string)binding["type"];
9596
if (type != null && type.EndsWith("Trigger", StringComparison.OrdinalIgnoreCase))
9697
{
9798
binding.Add("functionName", functionMetadata.Name);
99+
100+
if (isFlexConsumption)
101+
{
102+
string group = functionMetadata.GetFunctionGroup();
103+
if (!string.IsNullOrEmpty(group))
104+
{
105+
binding.Add("functionGroup", group);
106+
}
107+
}
108+
98109
return binding;
99110
}
100111
}

src/WebJobs.Script.WebHost/Management/FunctionsSyncManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,7 @@ internal async Task<IEnumerable<JObject>> GetFunctionTriggers(IEnumerable<Functi
449449
{
450450
var triggers = (await functionsMetadata
451451
.Where(f => !f.IsProxy())
452-
.Select(f => f.ToFunctionTrigger(hostOptions))
452+
.Select(f => f.ToFunctionTrigger(hostOptions, _environment.IsFlexConsumptionSku()))
453453
.WhenAll())
454454
.Where(t => t != null);
455455

src/WebJobs.Script/Extensions/BindingMetadataExtensions.cs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,90 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

4+
using System;
5+
using System.Collections.Generic;
46
using Microsoft.Azure.WebJobs.Script.Description;
7+
using Newtonsoft.Json.Linq;
58

69
namespace Microsoft.Azure.WebJobs.Script.Extensions
710
{
811
public static class BindingMetadataExtensions
912
{
13+
private const string HttpTriggerKey = "httpTrigger";
14+
private const string EventGridTriggerKey = "eventGridTrigger";
15+
private const string BlobTriggerKey = "blobTrigger";
16+
17+
private static readonly HashSet<string> DurableTriggers = new(StringComparer.OrdinalIgnoreCase)
18+
{
19+
"entityTrigger",
20+
"activityTrigger",
21+
"orchestrationTrigger"
22+
};
23+
24+
/// <summary>
25+
/// Checks if a <see cref="BindingMetadata"/> represents an HTTP trigger.
26+
/// </summary>
27+
/// <param name="binding">The binding metadata to check.</param>
28+
/// <returns><c>true</c> if an HTTP trigger, <c>false</c> otherwise.</returns>
29+
public static bool IsHttpTrigger(this BindingMetadata binding)
30+
{
31+
if (binding is null)
32+
{
33+
throw new ArgumentNullException(nameof(binding));
34+
}
35+
36+
return string.Equals(HttpTriggerKey, binding.Type, StringComparison.OrdinalIgnoreCase);
37+
}
38+
39+
/// <summary>
40+
/// Checks if a <see cref="BindingMetadata"/> represents an webhook trigger.
41+
/// </summary>
42+
/// <param name="binding">The binding metadata to check.</param>
43+
/// <returns><c>true</c> if a webhook trigger, <c>false</c> otherwise.</returns>
44+
/// <remarks>
45+
/// Known webhook triggers includes Event Grid and Event Grid sourced blob triggers.
46+
/// </remarks>
47+
public static bool IsWebHookTrigger(this BindingMetadata binding)
48+
{
49+
if (binding is null)
50+
{
51+
throw new ArgumentNullException(nameof(binding));
52+
}
53+
54+
if (string.Equals(EventGridTriggerKey, binding.Type, StringComparison.OrdinalIgnoreCase))
55+
{
56+
return true;
57+
}
58+
59+
if (string.Equals(BlobTriggerKey, binding.Type, StringComparison.OrdinalIgnoreCase))
60+
{
61+
if (binding.Raw is { } obj)
62+
{
63+
if (obj.TryGetValue("source", StringComparison.OrdinalIgnoreCase, out JToken token) && token is not null)
64+
{
65+
return string.Equals(token.ToString(), "eventGrid", StringComparison.OrdinalIgnoreCase);
66+
}
67+
}
68+
}
69+
70+
return false;
71+
}
72+
73+
/// <summary>
74+
/// Checks if a <see cref="BindingMetadata"/> represents a durable trigger (entity, orchestration, or activity).
75+
/// </summary>
76+
/// <param name="binding">The binding metadata to check.</param>
77+
/// <returns><c>true</c> if a durable trigger, <c>false</c> otherwise.</returns>
78+
public static bool IsDurableTrigger(this BindingMetadata binding)
79+
{
80+
if (binding is null)
81+
{
82+
throw new ArgumentNullException(nameof(binding));
83+
}
84+
85+
return DurableTriggers.Contains(binding.Type);
86+
}
87+
1088
public static bool SupportsDeferredBinding(this BindingMetadata metadata)
1189
{
1290
Utility.TryReadAsBool(metadata.Properties, ScriptConstants.SupportsDeferredBindingKey, out bool result);

src/WebJobs.Script/Extensions/FunctionMetadataExtensions.cs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Collections.Generic;
65
using System.Linq;
76
using Microsoft.Azure.WebJobs.Script.Description;
7+
using Microsoft.Azure.WebJobs.Script.Extensions;
88
using Newtonsoft.Json.Linq;
99

1010
namespace Microsoft.Azure.WebJobs.Script
@@ -15,6 +15,7 @@ public static class FunctionMetadataExtensions
1515
private const string IsDisabledKey = "IsDisabled";
1616
private const string IsCodelessKey = "IsCodeless";
1717
private const string FunctionIdKey = "FunctionId";
18+
private const string FunctionGroupKey = "FunctionGroup";
1819
private const string HttpTriggerKey = "HttpTrigger";
1920
private const string HttpOutputKey = "Http";
2021
private const string BlobTriggerType = "blobTrigger";
@@ -41,7 +42,7 @@ public static bool IsHttpInAndOutFunction(this FunctionMetadata metadata)
4142

4243
public static bool IsHttpTriggerFunction(this FunctionMetadata metadata)
4344
{
44-
return metadata.InputBindings.Any(b => string.Equals(HttpTriggerKey, b.Type, StringComparison.OrdinalIgnoreCase));
45+
return metadata.InputBindings.Any(b => b.IsHttpTrigger());
4546
}
4647

4748
public static bool IsLegacyBlobTriggerFunction(this FunctionMetadata metadata)
@@ -60,6 +61,40 @@ public static bool IsLegacyBlobTriggerFunction(this FunctionMetadata metadata)
6061
return false;
6162
}
6263

64+
/// <summary>
65+
/// Gets the function group for the specified <see cref="FunctionMetadata"/>.
66+
/// </summary>
67+
/// <param name="metadata">The function metadata.</param>
68+
/// <returns>The function group for this metadata or <c>null</c> if no group specified.</returns>
69+
public static string GetFunctionGroup(this FunctionMetadata metadata)
70+
{
71+
if (metadata == null)
72+
{
73+
throw new ArgumentNullException(nameof(metadata));
74+
}
75+
76+
if (metadata.Properties.TryGetValue(FunctionGroupKey, out object group))
77+
{
78+
return group.ToString();
79+
}
80+
81+
// Ensure http and durable triggers are grouped together.
82+
foreach (BindingMetadata binding in metadata.InputBindings)
83+
{
84+
if (binding.IsHttpTrigger() || binding.IsWebHookTrigger())
85+
{
86+
return "http";
87+
}
88+
89+
if (binding.IsDurableTrigger())
90+
{
91+
return "durable";
92+
}
93+
}
94+
95+
return null;
96+
}
97+
6398
public static string GetFunctionId(this FunctionMetadata metadata)
6499
{
65100
if (!metadata.Properties.TryGetValue(FunctionIdKey, out object idObj)

test/WebJobs.Script.Tests/Extensions/BindingMetadataExtensionsTests.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,60 @@ public void SkipDeferredBinding_InvalidValue_ReturnsFalse()
5454
bool result = bindingMetadata.SkipDeferredBinding();
5555
Assert.False(result);
5656
}
57+
58+
[Theory]
59+
[InlineData("httpTrigger", true)]
60+
[InlineData("otherTrigger", false)]
61+
[InlineData("http2Trigger", false)]
62+
[InlineData("inputBinding", false)]
63+
public void IsHttpTrigger_ReturnsExpectedValue(string type, bool expected)
64+
{
65+
var bindingMetadata = new BindingMetadata
66+
{
67+
Type = type,
68+
};
69+
70+
Assert.Equal(expected, bindingMetadata.IsHttpTrigger());
71+
}
72+
73+
[Theory]
74+
[InlineData("eventGridTrigger", true)]
75+
[InlineData("blobTrigger", true, "eventGrid")]
76+
[InlineData("blobTrigger", false, "other")]
77+
[InlineData("httpTrigger", false)]
78+
[InlineData("inputBinding", false)]
79+
public void IsWebHookTrigger_ReturnsExpectedValue(string type, bool expected, string source = null)
80+
{
81+
var bindingMetadata = new BindingMetadata
82+
{
83+
Type = type,
84+
};
85+
86+
if (source is not null)
87+
{
88+
bindingMetadata.Raw = new JObject
89+
{
90+
["source"] = source,
91+
};
92+
}
93+
94+
Assert.Equal(expected, bindingMetadata.IsWebHookTrigger());
95+
}
96+
97+
[Theory]
98+
[InlineData("orchestrationTrigger", true)]
99+
[InlineData("activityTrigger", true)]
100+
[InlineData("entityTrigger", true)]
101+
[InlineData("httpTrigger", false)]
102+
[InlineData("inputBinding", false)]
103+
public void IsDurableTrigger_ReturnsExpectedValue(string type, bool expected)
104+
{
105+
var bindingMetadata = new BindingMetadata
106+
{
107+
Type = type,
108+
};
109+
110+
Assert.Equal(expected, bindingMetadata.IsDurableTrigger());
111+
}
57112
}
58113
}

test/WebJobs.Script.Tests/Extensions/FunctionMetadataExtensionsTests.cs

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

4-
using System;
54
using System.IO;
6-
using System.Linq;
75
using System.Threading.Tasks;
86
using Microsoft.Azure.WebJobs.Script.Description;
97
using Microsoft.Azure.WebJobs.Script.WebHost.Extensions;
@@ -112,6 +110,95 @@ public async Task ToFunctionTrigger_Codeless_ReturnsExpected()
112110
Assert.Equal("httpTrigger", functionMetadata.Bindings[0].Raw["type"].Value<string>());
113111
}
114112

113+
[Theory]
114+
[InlineData("httpTrigger", "http")]
115+
[InlineData("orchestrationTrigger", "durable")]
116+
[InlineData("activityTrigger", "durable")]
117+
[InlineData("entityTrigger", "durable")]
118+
[InlineData("timerTrigger", null)]
119+
[InlineData("blobTrigger", null)]
120+
[InlineData("queueTrigger", null)]
121+
[InlineData("serviceBusTrigger", null)]
122+
[InlineData("otherTrigger", null)]
123+
public async Task ToFunctionTrigger_Grouping_ReturnsExpected(string trigger, string group)
124+
{
125+
var functionMetadata = new FunctionMetadata
126+
{
127+
Name = "TestFunction1",
128+
Bindings =
129+
{
130+
new BindingMetadata
131+
{
132+
Name = "input",
133+
Type = trigger,
134+
Direction = BindingDirection.In,
135+
Raw = new JObject()
136+
{
137+
["name"] = "input",
138+
["type"] = trigger,
139+
["direction"] = "in",
140+
},
141+
}
142+
}
143+
};
144+
var options = new ScriptJobHostOptions
145+
{
146+
RootScriptPath = _testRootScriptPath
147+
};
148+
149+
var result = await functionMetadata.ToFunctionTrigger(options, isFlexConsumption: true);
150+
Assert.Equal("TestFunction1", result["functionName"].Value<string>());
151+
Assert.Equal(trigger, result["type"].Value<string>());
152+
153+
Assert.Equal(group != null, result.TryGetValue("functionGroup", out JToken functionGroup));
154+
if (functionGroup is not null)
155+
{
156+
Assert.Equal(group, functionGroup.Value<string>());
157+
}
158+
}
159+
160+
[Theory]
161+
[InlineData("httpTrigger", "other0")]
162+
[InlineData("orchestrationTrigger", "other1")]
163+
[InlineData("activityTrigger", "other2")]
164+
[InlineData("entityTrigger", "other3")]
165+
[InlineData("otherTrigger", "other4")]
166+
public async Task ToFunctionTrigger_ExplicitGrouping_ReturnsExpected(string trigger, string group)
167+
{
168+
var functionMetadata = new FunctionMetadata
169+
{
170+
Name = "TestFunction1",
171+
Bindings =
172+
{
173+
new BindingMetadata
174+
{
175+
Name = "input",
176+
Type = trigger,
177+
Direction = BindingDirection.In,
178+
Raw = new JObject()
179+
{
180+
["name"] = "input",
181+
["type"] = trigger,
182+
["direction"] = "in",
183+
},
184+
}
185+
},
186+
Properties =
187+
{
188+
["FunctionGroup"] = group,
189+
},
190+
};
191+
var options = new ScriptJobHostOptions
192+
{
193+
RootScriptPath = _testRootScriptPath
194+
};
195+
196+
var result = await functionMetadata.ToFunctionTrigger(options, isFlexConsumption: true);
197+
Assert.Equal("TestFunction1", result["functionName"].Value<string>());
198+
Assert.Equal(group, result["functionGroup"].Value<string>());
199+
Assert.Equal(trigger, result["type"].Value<string>());
200+
}
201+
115202
[Fact]
116203
public async Task ToFunctionMetadataResponse_WithoutFiles_ReturnsExpected()
117204
{

0 commit comments

Comments
 (0)