Skip to content

Commit 8c7154c

Browse files
committed
Functions SyncTriggers improvements
1 parent 3bec05a commit 8c7154c

File tree

11 files changed

+353
-103
lines changed

11 files changed

+353
-103
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public FunctionsController(IWebFunctionsManager functionsManager, IWebJobsRouter
4848
[Authorize(Policy = PolicyNames.AdminAuthLevel)]
4949
public async Task<IActionResult> List(bool includeProxies = false)
5050
{
51-
var result = await _functionsManager.GetFunctionsMetadata(Request, includeProxies);
51+
var result = await _functionsManager.GetFunctionsMetadata(includeProxies);
5252
return Ok(result);
5353
}
5454

@@ -144,7 +144,7 @@ public async Task<IActionResult> GetFunctionStatus(string name, [FromServices] I
144144
{
145145
// if we don't have any errors registered, make sure the function exists
146146
// before returning empty errors
147-
var result = await _functionsManager.GetFunctionsMetadata(Request, includeProxies: true);
147+
var result = await _functionsManager.GetFunctionsMetadata(includeProxies: true);
148148
var function = result.FirstOrDefault(p => p.Name.ToLowerInvariant() == name.ToLowerInvariant());
149149
if (function == null)
150150
{

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Microsoft.AspNetCore.Authorization;
1111
using Microsoft.AspNetCore.Http;
1212
using Microsoft.AspNetCore.Mvc;
13+
using Microsoft.Azure.WebJobs.Script.WebHost.Management;
1314
using Microsoft.Azure.WebJobs.Script.WebHost.Models;
1415
using Microsoft.Azure.WebJobs.Script.WebHost.Properties;
1516
using Microsoft.Azure.WebJobs.Script.WebHost.Security.Authorization.Policies;
@@ -28,13 +29,15 @@ public class KeysController : Controller
2829
private readonly ILogger _logger;
2930
private readonly IOptions<ScriptApplicationHostOptions> _applicationOptions;
3031
private readonly IFileSystem _fileSystem;
32+
private readonly IFunctionsSyncManager _functionsSyncManager;
3133

32-
public KeysController(IOptions<ScriptApplicationHostOptions> applicationOptions, ISecretManagerProvider secretManagerProvider, ILoggerFactory loggerFactory, IFileSystem fileSystem)
34+
public KeysController(IOptions<ScriptApplicationHostOptions> applicationOptions, ISecretManagerProvider secretManagerProvider, ILoggerFactory loggerFactory, IFileSystem fileSystem, IFunctionsSyncManager functionsSyncManager)
3335
{
3436
_applicationOptions = applicationOptions;
3537
_secretManagerProvider = secretManagerProvider;
3638
_logger = loggerFactory.CreateLogger(ScriptConstants.LogCategoryKeysController);
3739
_fileSystem = fileSystem;
40+
_functionsSyncManager = functionsSyncManager;
3841
}
3942

4043
[HttpGet]
@@ -186,11 +189,13 @@ private async Task<IActionResult> AddOrUpdateSecretAsync(string keyName, string
186189
case OperationResult.Created:
187190
{
188191
var keyResponse = ApiModelUtility.CreateApiModel(new { name = keyName, value = operationResult.Secret }, Request);
192+
await _functionsSyncManager.TrySyncTriggersAsync();
189193
return Created(ApiModelUtility.GetBaseUri(Request), keyResponse);
190194
}
191195
case OperationResult.Updated:
192196
{
193197
var keyResponse = ApiModelUtility.CreateApiModel(new { name = keyName, value = operationResult.Secret }, Request);
198+
await _functionsSyncManager.TrySyncTriggersAsync();
194199
return Ok(keyResponse);
195200
}
196201
case OperationResult.NotFound:
@@ -245,6 +250,8 @@ private async Task<IActionResult> DeleteFunctionSecretAsync(string keyName, stri
245250
return NotFound();
246251
}
247252

253+
await _functionsSyncManager.TrySyncTriggersAsync();
254+
248255
_logger.LogDebug(string.Format(Resources.TraceKeysApiSecretChange, keyName, keyScope ?? "host", "Deleted"));
249256

250257
return StatusCode(StatusCodes.Status204NoContent);

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
using System.IO;
66
using System.Linq;
77
using System.Threading.Tasks;
8-
using Microsoft.AspNetCore.Http;
9-
using Microsoft.Azure.WebJobs.Extensions.Http;
108
using Microsoft.Azure.WebJobs.Script.Description;
119
using Microsoft.Azure.WebJobs.Script.Management.Models;
1210
using Microsoft.Azure.WebJobs.Script.WebHost.Management;

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

Lines changed: 88 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ namespace Microsoft.Azure.WebJobs.Script.WebHost.Management
2727
{
2828
public class FunctionsSyncManager : IFunctionsSyncManager, IDisposable
2929
{
30+
// Until ANT 82 is fully released, the default value is disabled and we continue
31+
// to return just the trigger data.
32+
// After ANT 82, we will change the default to enabled.
33+
// Note that this app setting is honored by both GeoMaster and Runtime.
34+
public const string AzureWebsiteArmCacheEnabledDefaultValue = "0";
35+
3036
private const string HubName = "HubName";
3137
private const string TaskHubName = "taskHubName";
3238
private const string Connection = "connection";
@@ -47,10 +53,10 @@ public class FunctionsSyncManager : IFunctionsSyncManager, IDisposable
4753

4854
private CloudBlockBlob _hashBlob;
4955

50-
public FunctionsSyncManager(IConfiguration configuration, IHostIdProvider hostIdProvider, IOptionsMonitor<ScriptApplicationHostOptions> applicationHostOptions, IOptions<LanguageWorkerOptions> languageWorkerOptions, ILoggerFactory loggerFactory, HttpClient httpClient, ISecretManagerProvider secretManagerProvider, IScriptWebHostEnvironment webHostEnvironment, IEnvironment environment, HostNameProvider hostNameProvider)
56+
public FunctionsSyncManager(IConfiguration configuration, IHostIdProvider hostIdProvider, IOptionsMonitor<ScriptApplicationHostOptions> applicationHostOptions, IOptions<LanguageWorkerOptions> languageWorkerOptions, ILogger<FunctionsSyncManager> logger, HttpClient httpClient, ISecretManagerProvider secretManagerProvider, IScriptWebHostEnvironment webHostEnvironment, IEnvironment environment, HostNameProvider hostNameProvider)
5157
{
5258
_applicationHostOptions = applicationHostOptions;
53-
_logger = loggerFactory?.CreateLogger(ScriptConstants.LogCategoryHostGeneral);
59+
_logger = logger;
5460
_workerConfigs = languageWorkerOptions.Value.WorkerConfigs;
5561
_httpClient = httpClient;
5662
_secretManagerProvider = secretManagerProvider;
@@ -61,6 +67,14 @@ public FunctionsSyncManager(IConfiguration configuration, IHostIdProvider hostId
6167
_hostNameProvider = hostNameProvider;
6268
}
6369

70+
internal bool ArmCacheEnabled
71+
{
72+
get
73+
{
74+
return _environment.GetEnvironmentVariableOrDefault(EnvironmentSettingNames.AzureWebsiteArmCacheEnabled, AzureWebsiteArmCacheEnabledDefaultValue) == "1";
75+
}
76+
}
77+
6478
public async Task<SyncTriggersResult> TrySyncTriggersAsync(bool checkHash = false)
6579
{
6680
var result = new SyncTriggersResult
@@ -237,15 +251,72 @@ internal async Task<CloudBlockBlob> GetHashBlobAsync()
237251
return _hashBlob;
238252
}
239253

240-
public async Task<JArray> GetSyncTriggersPayload()
254+
public async Task<JToken> GetSyncTriggersPayload()
241255
{
242256
var hostOptions = _applicationHostOptions.CurrentValue.ToHostOptions();
243-
var functionsMetadata = WebFunctionsManager.GetFunctionsMetadata(hostOptions, _workerConfigs, _logger, includeProxies: true);
257+
var functionsMetadata = WebFunctionsManager.GetFunctionsMetadata(hostOptions, _workerConfigs, _logger);
244258

245-
// Add trigger information used by the ScaleController
246-
JObject result = new JObject();
259+
// trigger information used by the ScaleController
247260
var triggers = await GetFunctionTriggers(functionsMetadata, hostOptions);
248-
return new JArray(triggers);
261+
var triggersArray = new JArray(triggers);
262+
263+
if (!ArmCacheEnabled)
264+
{
265+
// extended format is disabled - just return triggers
266+
return triggersArray;
267+
}
268+
269+
// Add triggers to the payload
270+
JObject result = new JObject();
271+
result.Add("triggers", triggersArray);
272+
273+
// Add functions details to the payload
274+
JObject functions = new JObject();
275+
string routePrefix = await WebFunctionsManager.GetRoutePrefix(hostOptions.RootScriptPath);
276+
var functionDetails = await WebFunctionsManager.GetFunctionMetadataResponse(functionsMetadata, hostOptions, _hostNameProvider);
277+
result.Add("functions", new JArray(functionDetails.Select(p => JObject.FromObject(p))));
278+
279+
// Add functions secrets to the payload
280+
// Only secret types we own/control can we cache directly
281+
// Encryption is handled by Antares before storage
282+
var secretsStorageType = _environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsSecretStorageType);
283+
if (string.IsNullOrEmpty(secretsStorageType) ||
284+
string.Compare(secretsStorageType, "files", StringComparison.OrdinalIgnoreCase) == 0 ||
285+
string.Compare(secretsStorageType, "blob", StringComparison.OrdinalIgnoreCase) == 0)
286+
{
287+
JObject secrets = new JObject();
288+
result.Add("secrets", secrets);
289+
290+
// add host secrets
291+
var hostSecretsInfo = await _secretManagerProvider.Current.GetHostSecretsAsync();
292+
var hostSecrets = new JObject();
293+
hostSecrets.Add("master", hostSecretsInfo.MasterKey);
294+
hostSecrets.Add("function", JObject.FromObject(hostSecretsInfo.FunctionKeys));
295+
hostSecrets.Add("system", JObject.FromObject(hostSecretsInfo.SystemKeys));
296+
secrets.Add("host", hostSecrets);
297+
298+
// add function secrets
299+
var functionSecrets = new JArray();
300+
var httpFunctions = functionsMetadata.Where(p => !p.IsProxy && p.InputBindings.Any(q => q.IsTrigger && string.Compare(q.Type, "httptrigger", StringComparison.OrdinalIgnoreCase) == 0)).Select(p => p.Name);
301+
foreach (var functionName in httpFunctions)
302+
{
303+
var currSecrets = await _secretManagerProvider.Current.GetFunctionSecretsAsync(functionName);
304+
var currElement = new JObject()
305+
{
306+
{ "name", functionName },
307+
{ "secrets", JObject.FromObject(currSecrets) }
308+
};
309+
functionSecrets.Add(currElement);
310+
}
311+
secrets.Add("function", functionSecrets);
312+
}
313+
else
314+
{
315+
// TODO: handle other external key storage types
316+
// like KeyVault when the feature comes online
317+
}
318+
319+
return result;
249320
}
250321

251322
internal async Task<IEnumerable<JObject>> GetFunctionTriggers(IEnumerable<FunctionMetadata> functionsMetadata, ScriptJobHostOptions hostOptions)
@@ -352,7 +423,15 @@ internal HttpRequestMessage BuildSetTriggersRequest()
352423
{
353424
var token = SimpleWebTokenHelper.CreateToken(DateTime.UtcNow.AddMinutes(5));
354425

355-
_logger.LogDebug($"SyncTriggers content: {content}");
426+
string sanitizedContentString = content;
427+
if (ArmCacheEnabled)
428+
{
429+
// sanitize the content before logging
430+
var sanitizedContent = JObject.Parse(content);
431+
sanitizedContent.Remove("secrets");
432+
sanitizedContentString = sanitizedContent.ToString();
433+
}
434+
_logger.LogDebug($"SyncTriggers content: {sanitizedContentString}");
356435

357436
using (var request = BuildSetTriggersRequest())
358437
{
@@ -381,4 +460,4 @@ public void Dispose()
381460
_syncSemaphore.Dispose();
382461
}
383462
}
384-
}
463+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace Microsoft.Azure.WebJobs.Script.WebHost.Management
1010
{
1111
public interface IWebFunctionsManager
1212
{
13-
Task<IEnumerable<FunctionMetadataResponse>> GetFunctionsMetadata(HttpRequest request, bool includeProxies);
13+
Task<IEnumerable<FunctionMetadataResponse>> GetFunctionsMetadata(bool includeProxies);
1414

1515
Task<(bool, FunctionMetadataResponse)> TryGetFunction(string name, HttpRequest request);
1616

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

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,42 +27,34 @@ public class WebFunctionsManager : IWebFunctionsManager
2727
private readonly IEnumerable<WorkerConfig> _workerConfigs;
2828
private readonly ISecretManagerProvider _secretManagerProvider;
2929
private readonly IFunctionsSyncManager _functionsSyncManager;
30+
private readonly HostNameProvider _hostNameProvider;
3031

31-
public WebFunctionsManager(IOptionsMonitor<ScriptApplicationHostOptions> applicationHostOptions, IOptions<LanguageWorkerOptions> languageWorkerOptions, ILoggerFactory loggerFactory, HttpClient client, ISecretManagerProvider secretManagerProvider, IFunctionsSyncManager functionsSyncManager)
32+
public WebFunctionsManager(IOptionsMonitor<ScriptApplicationHostOptions> applicationHostOptions, IOptions<LanguageWorkerOptions> languageWorkerOptions, ILoggerFactory loggerFactory, HttpClient client, ISecretManagerProvider secretManagerProvider, IFunctionsSyncManager functionsSyncManager, HostNameProvider hostNameProvider)
3233
{
3334
_applicationHostOptions = applicationHostOptions;
3435
_logger = loggerFactory?.CreateLogger(ScriptConstants.LogCategoryHostGeneral);
3536
_client = client;
3637
_workerConfigs = languageWorkerOptions.Value.WorkerConfigs;
3738
_secretManagerProvider = secretManagerProvider;
3839
_functionsSyncManager = functionsSyncManager;
40+
_hostNameProvider = hostNameProvider;
3941
}
4042

41-
/// <summary>
42-
/// Calls into ScriptHost to retrieve list of FunctionMetadata
43-
/// and maps them to FunctionMetadataResponse.
44-
/// </summary>
45-
/// <param name="request">Current HttpRequest for figuring out baseUrl</param>
46-
/// <returns>collection of FunctionMetadataResponse</returns>
47-
public async Task<IEnumerable<FunctionMetadataResponse>> GetFunctionsMetadata(HttpRequest request, bool includeProxies)
48-
{
49-
var baseUrl = $"{request.Scheme}://{request.Host}";
50-
return await GetFunctionsMetadata(baseUrl, includeProxies);
51-
}
52-
53-
public async Task<IEnumerable<FunctionMetadataResponse>> GetFunctionsMetadata(string baseUrl, bool includeProxies)
43+
public async Task<IEnumerable<FunctionMetadataResponse>> GetFunctionsMetadata(bool includeProxies)
5444
{
5545
var hostOptions = _applicationHostOptions.CurrentValue.ToHostOptions();
46+
var functionsMetadata = GetFunctionsMetadata(hostOptions, _workerConfigs, _logger, includeProxies);
5647

57-
string routePrefix = await GetRoutePrefix(hostOptions.RootScriptPath);
58-
var tasks = GetFunctionsMetadata(includeProxies).Select(p => p.ToFunctionMetadataResponse(hostOptions, routePrefix, baseUrl));
59-
return await tasks.WhenAll();
48+
return await GetFunctionMetadataResponse(functionsMetadata, hostOptions, _hostNameProvider);
6049
}
6150

62-
internal IEnumerable<FunctionMetadata> GetFunctionsMetadata(bool includeProxies = false)
51+
internal static async Task<IEnumerable<FunctionMetadataResponse>> GetFunctionMetadataResponse(IEnumerable<FunctionMetadata> functionsMetadata, ScriptJobHostOptions hostOptions, HostNameProvider hostNameProvider)
6352
{
64-
var hostOptions = _applicationHostOptions.CurrentValue.ToHostOptions();
65-
return GetFunctionsMetadata(hostOptions, _workerConfigs, _logger, includeProxies);
53+
string baseUrl = GetBaseUrl(hostNameProvider);
54+
string routePrefix = await GetRoutePrefix(hostOptions.RootScriptPath);
55+
var tasks = functionsMetadata.Select(p => p.ToFunctionMetadataResponse(hostOptions, routePrefix, baseUrl));
56+
57+
return await tasks.WhenAll();
6658
}
6759

6860
internal static IEnumerable<FunctionMetadata> GetFunctionsMetadata(ScriptJobHostOptions hostOptions, IEnumerable<WorkerConfig> workerConfigs, ILogger logger, bool includeProxies = false)
@@ -253,5 +245,16 @@ internal static async Task<string> GetRoutePrefix(string rootScriptPath)
253245

254246
return routePrefix;
255247
}
248+
249+
internal static string GetBaseUrl(HostNameProvider hostNameProvider)
250+
{
251+
string hostName = hostNameProvider.Value ?? "localhost";
252+
return $"https://{hostName}";
253+
}
254+
255+
internal string GetBaseUrl()
256+
{
257+
return GetBaseUrl(_hostNameProvider);
258+
}
256259
}
257260
}

src/WebJobs.Script/Environment/EnvironmentExtensions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ internal static class EnvironmentExtensions
1313
// For testing
1414
internal static string BaseDirectory { get; set; }
1515

16+
public static string GetEnvironmentVariableOrDefault(this IEnvironment environment, string name, string defaultValue)
17+
{
18+
return environment.GetEnvironmentVariable(name) ?? defaultValue;
19+
}
20+
1621
public static bool IsAppServiceEnvironment(this IEnvironment environment)
1722
{
1823
return !string.IsNullOrEmpty(environment.GetEnvironmentVariable(AzureWebsiteInstanceId));

src/WebJobs.Script/Environment/EnvironmentSettingNames.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public static class EnvironmentSettingNames
3434
public const string EasyAuthEnabled = "WEBSITE_AUTH_ENABLED";
3535
public const string AzureWebJobsSecretStorageKeyVaultName = "AzureWebJobsSecretStorageKeyVaultName";
3636
public const string AzureWebJobsSecretStorageKeyVaultConnectionString = "AzureWebJobsSecretStorageKeyVaultConnectionString";
37+
public const string AzureWebsiteArmCacheEnabled = "WEBSITE_FUNCTIONS_ARMCACHE_ENABLED";
3738

3839
/// <summary>
3940
/// Environment variable dynamically set by the platform when it is safe to

0 commit comments

Comments
 (0)