-
Notifications
You must be signed in to change notification settings - Fork 469
Add function management APIs to the runtime #2005
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
166 changes: 166 additions & 0 deletions
166
src/WebJobs.Script.WebHost/Controllers/FunctionsController.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the MIT License. See License.txt in the project root for license information. | ||
|
||
using System.Collections.Generic; | ||
using System.Collections.ObjectModel; | ||
using System.Linq; | ||
using System.Text.RegularExpressions; | ||
using System.Threading.Tasks; | ||
using Microsoft.AspNetCore.Authorization; | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.AspNetCore.Http.Extensions; | ||
using Microsoft.AspNetCore.Mvc; | ||
using Microsoft.Azure.WebJobs.Script.Description; | ||
using Microsoft.Azure.WebJobs.Script.Management.Models; | ||
using Microsoft.Azure.WebJobs.Script.WebHost.Filters; | ||
using Microsoft.Azure.WebJobs.Script.WebHost.Management; | ||
using Microsoft.Azure.WebJobs.Script.WebHost.Models; | ||
using Microsoft.Azure.WebJobs.Script.WebHost.Security.Authorization.Policies; | ||
using Microsoft.Extensions.Logging; | ||
|
||
namespace Microsoft.Azure.WebJobs.Script.WebHost.Controllers | ||
{ | ||
/// <summary> | ||
/// Controller responsible for administrative and management operations on functions | ||
/// example retriving a list of functions, invoking a function, creating a function, etc | ||
/// </summary> | ||
public class FunctionsController : Controller | ||
{ | ||
private readonly IWebFunctionsManager _functionsManager; | ||
private readonly ScriptHostManager _scriptHostManager; | ||
private readonly ILogger _logger; | ||
private static readonly Regex FunctionNameValidationRegex = new Regex(@"^[a-z][a-z0-9_\-]{0,127}$(?<!^host$)", RegexOptions.Compiled | RegexOptions.IgnoreCase); | ||
|
||
public FunctionsController(IWebFunctionsManager functionsManager, WebScriptHostManager scriptHostManager, ILoggerFactory loggerFactory) | ||
{ | ||
_functionsManager = functionsManager; | ||
_scriptHostManager = scriptHostManager; | ||
_logger = loggerFactory?.CreateLogger(ScriptConstants.LogCategoryFunctionsController); | ||
} | ||
|
||
[HttpGet] | ||
[Route("admin/functions")] | ||
[Authorize(Policy = PolicyNames.AdminAuthLevel)] | ||
public async Task<IActionResult> List() | ||
{ | ||
return Ok(await _functionsManager.GetFunctionsMetadata(Request)); | ||
} | ||
|
||
[HttpGet] | ||
[Route("admin/functions/{name}")] | ||
[Authorize(Policy = PolicyNames.AdminAuthLevel)] | ||
public async Task<IActionResult> Get(string name) | ||
{ | ||
(var success, var function) = await _functionsManager.TryGetFunction(name, Request); | ||
|
||
return success | ||
? Ok(function) | ||
: NotFound() as IActionResult; | ||
} | ||
|
||
[HttpPut] | ||
[Route("admin/functions/{name}")] | ||
[Authorize(Policy = PolicyNames.AdminAuthLevel)] | ||
public async Task<IActionResult> CreateOrUpdate(string name, [FromBody] FunctionMetadataResponse functionMetadata) | ||
{ | ||
if (!FunctionNameValidationRegex.IsMatch(name)) | ||
{ | ||
return BadRequest($"{name} is not a valid function name"); | ||
} | ||
|
||
(var success, var configChanged, var functionMetadataResponse) = await _functionsManager.CreateOrUpdate(name, functionMetadata, Request); | ||
|
||
if (success) | ||
{ | ||
if (configChanged) | ||
{ | ||
// TODO: sync triggers | ||
} | ||
|
||
return Created(Request.GetDisplayUrl(), functionMetadataResponse); | ||
} | ||
else | ||
{ | ||
return StatusCode(500); | ||
} | ||
} | ||
|
||
[HttpPost] | ||
[Route("admin/functions/{name}")] | ||
[Authorize(Policy = PolicyNames.AdminAuthLevel)] | ||
[RequiresRunningHost] | ||
public IActionResult Invoke(string name, [FromBody] FunctionInvocation invocation) | ||
{ | ||
if (invocation == null) | ||
{ | ||
return BadRequest(); | ||
} | ||
|
||
FunctionDescriptor function = _scriptHostManager.Instance.GetFunctionOrNull(name); | ||
if (function == null) | ||
{ | ||
return NotFound(); | ||
} | ||
|
||
ParameterDescriptor inputParameter = function.Parameters.First(p => p.IsTrigger); | ||
Dictionary<string, object> arguments = new Dictionary<string, object>() | ||
{ | ||
{ inputParameter.Name, invocation.Input } | ||
}; | ||
Task.Run(() => _scriptHostManager.Instance.CallAsync(function.Name, arguments)); | ||
|
||
return Accepted(); | ||
} | ||
|
||
[HttpGet] | ||
[Route("admin/functions/{name}/status")] | ||
[Authorize(Policy = PolicyNames.AdminAuthLevel)] | ||
[RequiresRunningHost] | ||
public IActionResult GetFunctionStatus(string name) | ||
{ | ||
FunctionStatus status = new FunctionStatus(); | ||
Collection<string> functionErrors = null; | ||
|
||
// first see if the function has any errors | ||
if (_scriptHostManager.Instance.FunctionErrors.TryGetValue(name, out functionErrors)) | ||
{ | ||
status.Errors = functionErrors; | ||
} | ||
else | ||
{ | ||
// if we don't have any errors registered, make sure the function exists | ||
// before returning empty errors | ||
FunctionDescriptor function = _scriptHostManager.Instance.Functions.FirstOrDefault(p => p.Name.ToLowerInvariant() == name.ToLowerInvariant()); | ||
if (function == null) | ||
{ | ||
return NotFound(); | ||
} | ||
} | ||
|
||
return Ok(status); | ||
} | ||
|
||
[HttpDelete] | ||
[Route("admin/functions/{name}")] | ||
[Authorize(Policy = PolicyNames.AdminAuthLevel)] | ||
public async Task<IActionResult> Delete(string name) | ||
{ | ||
(var found, var function) = await _functionsManager.TryGetFunction(name, Request); | ||
if (!found) | ||
{ | ||
return NotFound(); | ||
} | ||
|
||
(var deleted, var error) = _functionsManager.TryDeleteFunction(function); | ||
|
||
if (deleted) | ||
{ | ||
return NoContent(); | ||
} | ||
else | ||
{ | ||
return StatusCode(StatusCodes.Status500InternalServerError, error); | ||
} | ||
} | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
20 changes: 20 additions & 0 deletions
20
src/WebJobs.Script.WebHost/Extensions/EntityTagHeaderValueExtensions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
// 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.Linq; | ||
using System.Threading.Tasks; | ||
|
||
namespace Microsoft.Azure.WebJobs.Script.WebHost.Extensions | ||
{ | ||
public static class EntityTagHeaderValueExtensions | ||
{ | ||
public static System.Net.Http.Headers.EntityTagHeaderValue ToSystemETag(this Microsoft.Net.Http.Headers.EntityTagHeaderValue value) | ||
{ | ||
return value.Tag.Value.StartsWith("\"") && value.Tag.Value.EndsWith("\"") | ||
? new System.Net.Http.Headers.EntityTagHeaderValue(value.Tag.Value, value.IsWeak) | ||
: new System.Net.Http.Headers.EntityTagHeaderValue($"\"{value.Tag}\"", value.IsWeak); | ||
} | ||
} | ||
} |
98 changes: 98 additions & 0 deletions
98
src/WebJobs.Script.WebHost/Extensions/FunctionMetadataExtensions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
// 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.IO; | ||
using System.Runtime.InteropServices; | ||
using System.Threading.Tasks; | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.Azure.WebJobs.Script.Config; | ||
using Microsoft.Azure.WebJobs.Script.Description; | ||
using Microsoft.Azure.WebJobs.Script.Management.Models; | ||
using Microsoft.Azure.WebJobs.Script.WebHost.Helpers; | ||
using Microsoft.Azure.WebJobs.Script.WebHost.Management; | ||
using Newtonsoft.Json.Linq; | ||
|
||
namespace Microsoft.Azure.WebJobs.Script.WebHost.Extensions | ||
{ | ||
public static class FunctionMetadataExtensions | ||
{ | ||
/// <summary> | ||
/// Maps FunctionMetadata to FunctionMetadataResponse. | ||
/// </summary> | ||
/// <param name="functionMetadata">FunctionMetadata to be mapped.</param> | ||
/// <param name="request">Current HttpRequest</param> | ||
/// <param name="config">ScriptHostConfig</param> | ||
/// <returns>Promise of a FunctionMetadataResponse</returns> | ||
public static async Task<FunctionMetadataResponse> ToFunctionMetadataResponse(this FunctionMetadata functionMetadata, HttpRequest request, ScriptHostConfiguration config) | ||
{ | ||
var functionPath = Path.Combine(config.RootScriptPath, functionMetadata.Name); | ||
var functionMetadataFilePath = Path.Combine(functionPath, ScriptConstants.FunctionMetadataFileName); | ||
var baseUrl = request != null | ||
? $"{request.Scheme}://{request.Host}" | ||
: "https://localhost/"; | ||
|
||
var response = new FunctionMetadataResponse | ||
{ | ||
Name = functionMetadata.Name, | ||
|
||
// Q: can functionMetadata.ScriptFile be null or empty? | ||
ScriptHref = VirtualFileSystem.FilePathToVfsUri(Path.Combine(config.RootScriptPath, functionMetadata.ScriptFile), baseUrl, config), | ||
ConfigHref = VirtualFileSystem.FilePathToVfsUri(functionMetadataFilePath, baseUrl, config), | ||
ScriptRootPathHref = VirtualFileSystem.FilePathToVfsUri(functionPath, baseUrl, config, isDirectory: true), | ||
TestDataHref = VirtualFileSystem.FilePathToVfsUri(functionMetadata.GetTestDataFilePath(config), baseUrl, config), | ||
Href = GetFunctionHref(functionMetadata.Name, baseUrl), | ||
TestData = await GetTestData(functionMetadata.GetTestDataFilePath(config), config), | ||
Config = await GetFunctionConfig(functionMetadataFilePath, config), | ||
|
||
// Properties below this comment are not present in the kudu version. | ||
IsDirect = functionMetadata.IsDirect, | ||
IsDisabled = functionMetadata.IsDisabled, | ||
IsProxy = functionMetadata.IsProxy | ||
}; | ||
return response; | ||
} | ||
|
||
public static string GetTestDataFilePath(this FunctionMetadata functionMetadata, ScriptHostConfiguration config) => | ||
GetTestDataFilePath(functionMetadata.Name, config); | ||
|
||
public static string GetTestDataFilePath(string functionName, ScriptHostConfiguration config) => | ||
Path.Combine(config.TestDataPath, $"{functionName}.dat"); | ||
|
||
private static async Task<JObject> GetFunctionConfig(string path, ScriptHostConfiguration config) | ||
{ | ||
try | ||
{ | ||
if (FileUtility.FileExists(path)) | ||
{ | ||
using (var reader = File.OpenText(path)) | ||
{ | ||
return JObject.Parse(await reader.ReadToEndAsync()); | ||
} | ||
} | ||
} | ||
catch | ||
{ | ||
// no-op | ||
} | ||
|
||
// If there are any errors parsing function.json return an empty object. | ||
// This is current kudu behavior. | ||
// TODO: add an error field that can be used to communicate the JSON parse error. | ||
return new JObject(); | ||
} | ||
|
||
private static async Task<string> GetTestData(string testDataPath, ScriptHostConfiguration config) | ||
{ | ||
if (!File.Exists(testDataPath)) | ||
{ | ||
await FileUtility.WriteAsync(testDataPath, string.Empty); | ||
} | ||
|
||
return await FileUtility.ReadAsync(testDataPath); | ||
} | ||
|
||
private static Uri GetFunctionHref(string functionName, string baseUrl) => | ||
new Uri($"{baseUrl}/admin/functions/{functionName}"); | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
src/WebJobs.Script.WebHost/Extensions/FunctionMetadataResponseExtensions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
// 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.Linq; | ||
using System.Threading.Tasks; | ||
using Microsoft.Azure.WebJobs.Script.Management.Models; | ||
using Microsoft.Azure.WebJobs.Script.WebHost.Management; | ||
|
||
namespace Microsoft.Azure.WebJobs.Script.WebHost.Extensions | ||
{ | ||
public static class FunctionMetadataResponseExtensions | ||
{ | ||
public static string GetFunctionPath(this FunctionMetadataResponse function, ScriptHostConfiguration config) | ||
=> VirtualFileSystem.VfsUriToFilePath(function.ScriptRootPathHref, config); | ||
|
||
public static string GetFunctionTestDataFilePath(this FunctionMetadataResponse function, ScriptHostConfiguration config) | ||
=> VirtualFileSystem.VfsUriToFilePath(function.TestDataHref, config); | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
src/WebJobs.Script.WebHost/Extensions/HttpHeadersExtensions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
// 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.Linq; | ||
using System.Net.Http.Headers; | ||
using Microsoft.Extensions.Primitives; | ||
|
||
namespace Microsoft.Azure.WebJobs.Script.WebHost.Extensions | ||
{ | ||
public static class HttpHeadersExtensions | ||
{ | ||
public static Dictionary<string, StringValues> ToCoreHeaders(this HttpHeaders headers, params string[] exclude) | ||
{ | ||
return headers | ||
.Where(h => !exclude.Any(e => e.Equals(h.Key, StringComparison.OrdinalIgnoreCase))) | ||
.ToDictionary(k => k.Key, v => new StringValues(v.Value.ToArray())); | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@fabiocav do you know the answer to this?