Skip to content

Commit c54adb7

Browse files
committed
Support for Bring-Your-Own-Bindings, leverage the SDK's new metadata provider
1 parent 54e8730 commit c54adb7

20 files changed

+507
-350
lines changed

src/WebJobs.Script.WebHost/Controllers/AdminController.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +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;
45
using System.Collections.Generic;
56
using System.Collections.ObjectModel;
67
using System.Diagnostics;
@@ -12,6 +13,8 @@
1213
using System.Web.Http;
1314
using System.Web.Http.Controllers;
1415
using Microsoft.Azure.WebJobs.Host;
16+
using Microsoft.Azure.WebJobs.Host.Config;
17+
using Microsoft.Azure.WebJobs.Script.Config;
1518
using Microsoft.Azure.WebJobs.Script.Description;
1619
using Microsoft.Azure.WebJobs.Script.WebHost.Filters;
1720
using Microsoft.Azure.WebJobs.Script.WebHost.Models;
@@ -168,5 +171,40 @@ public override Task<HttpResponseMessage> ExecuteAsync(HttpControllerContext con
168171

169172
return base.ExecuteAsync(controllerContext, cancellationToken);
170173
}
174+
175+
[Route("admin/extensions/{name}/{*extra}")]
176+
[HttpGet]
177+
[HttpPost]
178+
[AllowAnonymous]
179+
public async Task<HttpResponseMessage> ExtensionHook(string name, CancellationToken token)
180+
{
181+
var provider = this._scriptHostManager.BindingWebHookProvider;
182+
183+
var hook = provider.GetHandlerOrNull(name);
184+
if (hook != null)
185+
{
186+
var response = await hook.ConvertAsync(this.Request, token);
187+
return response;
188+
}
189+
190+
return new HttpResponseMessage(HttpStatusCode.NotFound);
191+
}
192+
193+
// Provides the URL for accessing the admin/hook route.
194+
internal static Uri GetRouteForExtensionHook(string name)
195+
{
196+
var settings = ScriptSettingsManager.Instance;
197+
198+
var hostName = settings.GetSetting(EnvironmentSettingNames.AzureWebsiteHostName);
199+
if (hostName == null)
200+
{
201+
return null;
202+
}
203+
204+
bool isLocalhost = hostName.StartsWith("localhost:", StringComparison.OrdinalIgnoreCase);
205+
var scheme = isLocalhost ? "http" : "https";
206+
207+
return new Uri($"{scheme}://{hostName}/admin/extensions/{name}");
208+
}
171209
}
172210
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Net.Http;
7+
using Microsoft.Azure.WebJobs.Host.Config;
8+
using Microsoft.Azure.WebJobs.Script.WebHost.Controllers;
9+
using HttpHandler = Microsoft.Azure.WebJobs.IAsyncConverter<System.Net.Http.HttpRequestMessage, System.Net.Http.HttpResponseMessage>;
10+
11+
namespace Microsoft.Azure.WebJobs.Script.WebHost
12+
{
13+
// Gives binding extensions access to a http handler.
14+
// This is registered with the JobHostConfiguration and extensions will call on it to register for a handler.
15+
internal class WebJobsSdkExtensionHookProvider : IWebHookProvider
16+
{
17+
// Map from an extension name to a http handler.
18+
private IDictionary<string, HttpHandler> _customHttpHandlers = new Dictionary<string, HttpHandler>(StringComparer.OrdinalIgnoreCase);
19+
20+
// Get a registered handler, or null
21+
public HttpHandler GetHandlerOrNull(string name)
22+
{
23+
HttpHandler handler;
24+
_customHttpHandlers.TryGetValue(name, out handler);
25+
return handler;
26+
}
27+
28+
// Exposed to extensions to get get the URL for their http handler.
29+
public Uri GetUrl(IExtensionConfigProvider extension)
30+
{
31+
var extensionType = extension.GetType();
32+
var handler = extension as HttpHandler;
33+
if (handler == null)
34+
{
35+
throw new InvalidOperationException($"Extension must implemnent IAsyncConverter<HttpRequestMessage, HttpResponseMessage> in order to receive hooks");
36+
}
37+
38+
string name = extensionType.Name;
39+
_customHttpHandlers[name] = handler;
40+
41+
return AdminController.GetRouteForExtensionHook(name);
42+
}
43+
}
44+
}

src/WebJobs.Script.WebHost/WebJobs.Script.WebHost.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,7 @@
478478
<Compile Include="Models\Swagger\SwaggerDocument.cs" />
479479
<Compile Include="Swagger\SwaggerDataType.cs" />
480480
<Compile Include="Swagger\SwaggerDocumentManager.cs" />
481+
<Compile Include="WebHooks\WebJobsSdkExtensionHookProvider.cs" />
481482
<Compile Include="WebHooks\DynamicWebHookReceiverConfig.cs" />
482483
<Compile Include="WebHooks\WebHookReceiverManager.cs" />
483484
<Compile Include="WebScriptHostExceptionHandler.cs" />

src/WebJobs.Script.WebHost/WebScriptHostManager.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616
using System.Web.Http.Routing;
1717
using Microsoft.AspNet.WebHooks;
1818
using Microsoft.Azure.WebJobs.Host;
19+
using Microsoft.Azure.WebJobs.Host.Config;
1920
using Microsoft.Azure.WebJobs.Host.Loggers;
2021
using Microsoft.Azure.WebJobs.Host.Timers;
2122
using Microsoft.Azure.WebJobs.Script.Binding;
2223
using Microsoft.Azure.WebJobs.Script.Binding.Http;
2324
using Microsoft.Azure.WebJobs.Script.Config;
2425
using Microsoft.Azure.WebJobs.Script.Description;
2526
using Microsoft.Azure.WebJobs.Script.Diagnostics;
27+
using Microsoft.Azure.WebJobs.Script.WebHost.Controllers;
2628
using Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics;
2729

2830
namespace Microsoft.Azure.WebJobs.Script.WebHost
@@ -39,6 +41,9 @@ public class WebScriptHostManager : ScriptHostManager
3941
private readonly ScriptHostConfiguration _config;
4042
private readonly ISwaggerDocumentManager _swaggerDocumentManager;
4143
private readonly object _syncLock = new object();
44+
45+
private readonly WebJobsSdkExtensionHookProvider _bindingWebHookProvider = new WebJobsSdkExtensionHookProvider();
46+
4247
private bool _warmupComplete = false;
4348
private bool _hostStarted = false;
4449
private IDictionary<IHttpRoute, FunctionDescriptor> _httpFunctions;
@@ -83,6 +88,8 @@ public WebScriptHostManager(ScriptHostConfiguration config, ISecretManagerFactor
8388
{
8489
}
8590

91+
internal WebJobsSdkExtensionHookProvider BindingWebHookProvider => _bindingWebHookProvider;
92+
8693
public ISecretManager SecretManager => _secretManager;
8794

8895
public HostPerformanceManager PerformanceManager => _performanceManager;
@@ -397,6 +404,8 @@ protected override void OnInitializeConfig(ScriptHostConfiguration config)
397404
var hostConfig = config.HostConfig;
398405
hostConfig.AddService<IMetricsLogger>(_metricsLogger);
399406

407+
config.HostConfig.AddService<IWebHookProvider>(this._bindingWebHookProvider);
408+
400409
// Add our exception handler
401410
hostConfig.AddService<IWebJobsExceptionHandler>(_exceptionHandler);
402411

src/WebJobs.Script/Binding/ExtensionBinding.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ namespace Microsoft.Azure.WebJobs.Script.Binding
1919
/// Wrapper used to adapt a <see cref="ScriptBinding"/> to the binding pipeline.
2020
/// </summary>
2121
[CLSCompliant(false)]
22-
public class ExtensionBinding : FunctionBinding
22+
public class ExtensionBinding : FunctionBinding, IResultProcessingBinding
2323
{
2424
private ScriptBinding _binding;
2525
private Collection<Attribute> _attributes;
@@ -179,6 +179,25 @@ internal static IDictionary<string, object> GetAttributeData(Attribute attribute
179179
return attributeData;
180180
}
181181

182+
public virtual bool CanProcessResult(object result)
183+
{
184+
var returnBinding = _binding as IResultProcessingBinding;
185+
if (returnBinding != null)
186+
{
187+
return returnBinding.CanProcessResult(result);
188+
}
189+
return false;
190+
}
191+
192+
public virtual void ProcessResult(IDictionary<string, object> functionArguments, object[] systemArguments, string triggerInputName, object result)
193+
{
194+
var returnBinding = _binding as IResultProcessingBinding;
195+
if (returnBinding != null)
196+
{
197+
returnBinding.ProcessResult(functionArguments, systemArguments, triggerInputName, result);
198+
}
199+
}
200+
182201
internal class AttributeBuilderInfo
183202
{
184203
public ConstructorInfo Constructor { get; set; }
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Collections.ObjectModel;
7+
using System.IO;
8+
using System.Reflection;
9+
using Microsoft.Azure.WebJobs.Host;
10+
using Microsoft.Azure.WebJobs.Script.Description;
11+
using Microsoft.Azure.WebJobs.Script.Extensibility;
12+
using Newtonsoft.Json.Linq;
13+
14+
namespace Microsoft.Azure.WebJobs.Script.Binding
15+
{
16+
// This single binder can service all SDK extensions by leveraging the SDK metadata provider.
17+
internal class GeneralScriptBindingProvider : ScriptBindingProvider
18+
{
19+
private IJobHostMetadataProvider _metadataProvider;
20+
21+
public GeneralScriptBindingProvider(
22+
JobHostConfiguration config,
23+
JObject hostMetadata,
24+
TraceWriter traceWriter)
25+
: base(config, hostMetadata, traceWriter)
26+
{
27+
}
28+
29+
// The constructor is fixed and ScriptBindingProvider are instantated for us by the Script runtime.
30+
// Extensions may get registered after this class is instantiated.
31+
// So we need a final call that lets us get the tooling snapshot of the graph after all extensions are set.
32+
public void CompleteInitialization()
33+
{
34+
this._metadataProvider = this.Config.CreateMetadataProvider();
35+
}
36+
37+
public override bool TryCreate(ScriptBindingContext context, out ScriptBinding binding)
38+
{
39+
string name = context.Type;
40+
var attrType = this._metadataProvider.GetAttributeTypeFromName(name);
41+
if (attrType == null)
42+
{
43+
binding = null;
44+
return false;
45+
}
46+
47+
var attr = this._metadataProvider.GetAttribute(attrType, context.Metadata);
48+
49+
binding = new GeneralScriptBinding(this._metadataProvider, attr, context);
50+
return true;
51+
}
52+
53+
public override bool TryResolveAssembly(string assemblyName, out Assembly assembly)
54+
{
55+
return this._metadataProvider.TryResolveAssembly(assemblyName, out assembly);
56+
}
57+
58+
// Function.json specifies a type via optional DataType and Cardinality properties.
59+
// Read the properties and convert that into a System.Type.
60+
internal static Type GetRequestedType(ScriptBindingContext context)
61+
{
62+
Type type = ParseDataType(context);
63+
64+
Cardinality cardinality;
65+
if (!Enum.TryParse<Cardinality>(context.Cardinality, true, out cardinality))
66+
{
67+
cardinality = Cardinality.One; // default
68+
}
69+
70+
if (cardinality == Cardinality.Many)
71+
{
72+
// arrays are supported for both trigger input as well
73+
// as output bindings
74+
type = type.MakeArrayType();
75+
}
76+
return type;
77+
}
78+
79+
// Parse the DataType field and return as a System.Type.
80+
// Never return null. Use typeof(object) to refer to an unnkown.
81+
private static Type ParseDataType(ScriptBindingContext context)
82+
{
83+
DataType result;
84+
if (Enum.TryParse<DataType>(context.DataType, true, out result))
85+
{
86+
switch (result)
87+
{
88+
case DataType.Binary:
89+
return typeof(byte[]);
90+
91+
case DataType.Stream:
92+
return typeof(Stream);
93+
94+
case DataType.String:
95+
return typeof(string);
96+
}
97+
}
98+
99+
return typeof(object);
100+
}
101+
102+
private class GeneralScriptBinding : ScriptBinding, IResultProcessingBinding
103+
{
104+
private readonly Attribute _attribute;
105+
private readonly IJobHostMetadataProvider _metadataProvider;
106+
107+
private Type _defaultType;
108+
109+
private MethodInfo _applyReturn; // Action<object,object>
110+
111+
public GeneralScriptBinding(IJobHostMetadataProvider metadataProvider, Attribute attribute, ScriptBindingContext context)
112+
: base(context)
113+
{
114+
_metadataProvider = metadataProvider;
115+
_attribute = attribute;
116+
117+
_applyReturn = attribute.GetType().GetMethod("ApplyReturn", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
118+
}
119+
120+
// This should only be called in script scenarios (not C#).
121+
// So explicitly make it lazy.
122+
public override Type DefaultType
123+
{
124+
get
125+
{
126+
if (_defaultType == null)
127+
{
128+
Type requestedType = GetRequestedType(this.Context);
129+
_defaultType = _metadataProvider.GetDefaultType(_attribute, this.Context.Access, requestedType);
130+
}
131+
return _defaultType;
132+
}
133+
}
134+
135+
public bool CanProcessResult(object result)
136+
{
137+
return _applyReturn != null;
138+
}
139+
140+
public void ProcessResult(
141+
IDictionary<string, object> functionArguments,
142+
object[] systemArguments,
143+
string triggerInputName,
144+
object result)
145+
{
146+
if (result == null)
147+
{
148+
return;
149+
}
150+
151+
object context;
152+
if (functionArguments.TryGetValue(triggerInputName, out context))
153+
{
154+
_applyReturn.Invoke(null, new object[] { context, result });
155+
}
156+
}
157+
158+
public override Collection<Attribute> GetAttributes() => new Collection<Attribute> { _attribute };
159+
}
160+
}
161+
}

src/WebJobs.Script/Binding/Manual/ManualTriggerAttribute.cs

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

44
using System;
5+
using Microsoft.Azure.WebJobs.Description;
56

67
namespace Microsoft.Azure.WebJobs.Script.Binding
78
{
89
[AttributeUsage(AttributeTargets.Parameter)]
10+
[Binding]
911
public sealed class ManualTriggerAttribute : Attribute
1012
{
1113
}

src/WebJobs.Script/Binding/ScriptJobHostConfigurationExtensions.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ public void Initialize(ExtensionConfigContext context)
2828
}
2929

3030
context.Config.RegisterBindingExtension(new HttpTriggerAttributeBindingProvider());
31-
context.Config.RegisterBindingExtension(new ManualTriggerAttributeBindingProvider());
31+
32+
context.AddBindingRule<ManualTriggerAttribute>()
33+
.BindToTrigger(new ManualTriggerAttributeBindingProvider());
3234
}
3335
}
3436
}

0 commit comments

Comments
 (0)