-
Notifications
You must be signed in to change notification settings - Fork 419
/
StartHostAction.cs
421 lines (362 loc) · 19.2 KB
/
StartHostAction.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Azure.Functions.Cli.Common;
using Azure.Functions.Cli.Extensions;
using Azure.Functions.Cli.Helpers;
using Azure.Functions.Cli.Interfaces;
using Azure.Functions.Cli.NativeMethods;
using Colors.Net;
using Fclp;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Script;
using Microsoft.Azure.WebJobs.Script.Description;
using Microsoft.Azure.WebJobs.Script.WebHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using static Azure.Functions.Cli.Common.OutputTheme;
using static Colors.Net.StringStaticMethods;
namespace Azure.Functions.Cli.Actions.HostActions
{
[Action(Name = "start", Context = Context.Host, HelpText = "Launches the functions runtime host")]
[Action(Name = "start", HelpText = "Launches the functions runtime host")]
internal class StartHostAction : BaseAction
{
private const int DefaultPort = 7071;
private const int DefaultTimeout = 20;
private readonly ISecretsManager _secretsManager;
private LoggingFilterOptions _loggingFilterOptions;
public int Port { get; set; }
public string CorsOrigins { get; set; }
public bool CorsCredentials { get; set; }
public int Timeout { get; set; }
public bool UseHttps { get; set; }
public string CertPath { get; set; }
public string CertPassword { get; set; }
public string LanguageWorkerSetting { get; set; }
public bool NoBuild { get; set; }
public bool EnableAuth { get; set; }
public bool VerboseLogging { get; set; }
public List<string> EnabledFunctions { get; private set; }
public StartHostAction(ISecretsManager secretsManager)
{
_secretsManager = secretsManager;
}
public override ICommandLineParserResult ParseArgs(string[] args)
{
var hostSettings = _secretsManager.GetHostStartSettings();
Parser
.Setup<int>('p', "port")
.WithDescription($"Local port to listen on. Default: {DefaultPort}")
.SetDefault(hostSettings.LocalHttpPort == default(int) ? DefaultPort : hostSettings.LocalHttpPort)
.Callback(p => Port = p);
Parser
.Setup<string>("cors")
.WithDescription($"A comma separated list of CORS origins with no spaces. Example: https://functions.azure.com,https://functions-staging.azure.com")
.SetDefault(hostSettings.Cors ?? string.Empty)
.Callback(c => CorsOrigins = c);
Parser
.Setup<bool>("cors-credentials")
.WithDescription($"Allow cross-origin authenticated requests (i.e. cookies and the Authentication header)")
.SetDefault(hostSettings.CorsCredentials)
.Callback(v => CorsCredentials = v);
Parser
.Setup<int>('t', "timeout")
.WithDescription($"Timeout for on the functions host to start in seconds. Default: {DefaultTimeout} seconds.")
.SetDefault(DefaultTimeout)
.Callback(t => Timeout = t);
Parser
.Setup<bool>("useHttps")
.WithDescription("Bind to https://localhost:{port} rather than http://localhost:{port}. By default it creates and trusts a certificate.")
.SetDefault(false)
.Callback(s => UseHttps = s);
Parser
.Setup<string>("cert")
.WithDescription("for use with --useHttps. The path to a pfx file that contains a private key")
.Callback(c => CertPath = c);
Parser
.Setup<string>("password")
.WithDescription("to use with --cert. Either the password, or a file that contains the password for the pfx file")
.Callback(p => CertPassword = p);
Parser
.Setup<string>("language-worker")
.WithDescription("Arguments to configure the language worker.")
.Callback(w => LanguageWorkerSetting = w);
Parser
.Setup<bool>("no-build")
.WithDescription("Do no build current project before running. For dotnet projects only. Default is set to false.")
.SetDefault(false)
.Callback(b => NoBuild = b);
Parser
.Setup<bool>("enableAuth")
.WithDescription("Enable full authentication handling pipeline.")
.SetDefault(false)
.Callback(e => EnableAuth = e);
Parser
.Setup<List<string>>("functions")
.WithDescription("A space seperated list of functions to load.")
.Callback(f => EnabledFunctions = f);
Parser
.Setup<bool>('v', "verbose")
.WithDescription("Sets Default LogLevel to Information.")
.SetDefault(false)
.Callback(v => VerboseLogging = v);
return base.ParseArgs(args);
}
private async Task<IWebHost> BuildWebHost(ScriptApplicationHostOptions hostOptions, Uri listenAddress, Uri baseAddress, X509Certificate2 certificate)
{
IDictionary<string, string> settings = await GetConfigurationSettings(hostOptions.ScriptPath, baseAddress);
settings.AddRange(LanguageWorkerHelper.GetWorkerConfiguration(LanguageWorkerSetting));
UpdateEnvironmentVariables(settings);
var defaultBuilder = Microsoft.AspNetCore.WebHost.CreateDefaultBuilder(Array.Empty<string>());
_loggingFilterOptions = new LoggingFilterOptions(VerboseLogging);
if (UseHttps)
{
defaultBuilder
.UseKestrel(options =>
{
options.Listen(IPAddress.Any, listenAddress.Port, listenOptins =>
{
listenOptins.UseHttps(certificate);
});
});
}
return defaultBuilder
.UseSetting(WebHostDefaults.ApplicationKey, typeof(Startup).Assembly.GetName().Name)
.UseUrls(listenAddress.ToString())
.ConfigureAppConfiguration(configBuilder =>
{
configBuilder.AddEnvironmentVariables();
})
.ConfigureLogging(loggingBuilder =>
{
loggingBuilder.ClearProviders();
_loggingFilterOptions.AddConsoleLoggingProvider(loggingBuilder);
})
.ConfigureServices((context, services) => services.AddSingleton((IStartup)new Startup(context, hostOptions, CorsOrigins, CorsCredentials, EnableAuth, _loggingFilterOptions)))
.Build();
}
private async Task<IDictionary<string, string>> GetConfigurationSettings(string scriptPath, Uri uri)
{
var settings = _secretsManager.GetSecrets();
settings.Add(Constants.WebsiteHostname, uri.Authority);
// Add our connection strings
var connectionStrings = _secretsManager.GetConnectionStrings();
settings.AddRange(connectionStrings.ToDictionary(c => $"ConnectionStrings:{c.Name}", c => c.Value));
settings.Add(EnvironmentSettingNames.AzureWebJobsScriptRoot, scriptPath);
var environment = Environment
.GetEnvironmentVariables()
.Cast<DictionaryEntry>()
.ToDictionary(k => k.Key.ToString(), v => v.Value.ToString());
await CheckNonOptionalSettings(settings.Union(environment), scriptPath);
// when running locally in CLI we want the host to run in debug mode
// which optimizes host responsiveness
settings.Add("AZURE_FUNCTIONS_ENVIRONMENT", "Development");
return settings;
}
private void UpdateEnvironmentVariables(IDictionary<string, string> secrets)
{
foreach (var secret in secrets)
{
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(secret.Key)))
{
ColoredConsole.WriteLine(WarningColor($"Skipping '{secret.Key}' from local settings as it's already defined in current environment variables."));
}
else if (!string.IsNullOrEmpty(secret.Value))
{
Environment.SetEnvironmentVariable(secret.Key, secret.Value, EnvironmentVariableTarget.Process);
}
else if (secret.Value == string.Empty)
{
EnvironmentNativeMethods.SetEnvironmentVariable(secret.Key, secret.Value);
}
else
{
ColoredConsole.WriteLine(WarningColor($"Skipping '{secret.Key}' because value is null"));
}
}
if (EnabledFunctions != null && EnabledFunctions.Count > 0)
{
for (var i = 0; i < EnabledFunctions.Count; i++)
{
Environment.SetEnvironmentVariable($"AzureFunctionsJobHost__functions__{i}", EnabledFunctions[i]);
}
}
}
public override async Task RunAsync()
{
await PreRunConditions();
if (VerboseLogging)
{
Utilities.PrintLogo();
}
Utilities.PrintVersion();
ValidateHostJsonConfiguration();
var settings = SelfHostWebHostSettingsFactory.Create(Environment.CurrentDirectory);
(var listenUri, var baseUri, var certificate) = await Setup();
IWebHost host = await BuildWebHost(settings, listenUri, baseUri, certificate);
var runTask = host.RunAsync();
var hostService = host.Services.GetRequiredService<WebJobsScriptHostService>();
await hostService.DelayUntilHostReady();
var scriptHost = hostService.Services.GetRequiredService<IScriptJobHost>();
var httpOptions = hostService.Services.GetRequiredService<IOptions<HttpOptions>>();
if (scriptHost != null && scriptHost.Functions.Any())
{
DisplayFunctionsInfoUtilities.DisplayFunctionsInfo(scriptHost.Functions, httpOptions.Value, baseUri);
}
if (!VerboseLogging)
{
ColoredConsole.WriteLine(Cyan("For detailed output, run func with --verbose flag."));
}
await runTask;
}
private void ValidateHostJsonConfiguration()
{
bool IsPreCompiledApp = IsPreCompiledFunctionApp();
var hostJsonPath = Path.Combine(Environment.CurrentDirectory, Constants.HostJsonFileName);
if (IsPreCompiledApp && !File.Exists(hostJsonPath))
{
throw new CliException($"Host.json file in missing. Please make sure host.json file is preset at {Environment.CurrentDirectory}");
}
if (IsPreCompiledApp && BundleConfigurationExists(hostJsonPath))
{
throw new CliException($"Extension bundle configuration should not be present for the function app with pre-compiled functions. Please remove extension bundle configuration from host.json: {Path.Combine(Environment.CurrentDirectory, "host.json")}");
}
}
private async Task PreRunConditions()
{
if (GlobalCoreToolsSettings.CurrentWorkerRuntime == WorkerRuntime.python)
{
var pythonVersion = await PythonHelpers.GetEnvironmentPythonVersion();
PythonHelpers.AssertPythonVersion(pythonVersion, errorIfNotSupported: true, errorIfNoVersion: true);
PythonHelpers.SetWorkerPath(pythonVersion?.ExecutablePath, overwrite: false);
PythonHelpers.SetWorkerRuntimeVersionPython(pythonVersion);
}
else if (GlobalCoreToolsSettings.CurrentWorkerRuntime == WorkerRuntime.dotnet && !NoBuild)
{
if (DotnetHelpers.CanDotnetBuild())
{
var outputPath = Path.Combine("bin", "output");
await DotnetHelpers.BuildDotnetProject(outputPath, string.Empty);
Environment.CurrentDirectory = Path.Combine(Environment.CurrentDirectory, outputPath);
}
else if (StaticSettings.IsDebug)
{
ColoredConsole.WriteLine("Could not find a valid .csproj file. Skipping the build.");
}
}
else if (GlobalCoreToolsSettings.CurrentWorkerRuntime == WorkerRuntime.powershell && !CommandChecker.CommandExists("dotnet"))
{
throw new CliException("Dotnet is required for PowerShell Functions. Please install dotnet (.NET Core SDK) for your system from https://www.microsoft.com/net/download");
}
if (!NetworkHelpers.IsPortAvailable(Port))
{
throw new CliException($"Port {Port} is unavailable. Close the process using that port, or specify another port using --port [-p].");
}
}
private bool BundleConfigurationExists(string hostJsonPath)
{
var hostJson = FileSystemHelpers.ReadAllTextFromFile(hostJsonPath);
return hostJson.Contains(Constants.ExtensionBundleConfigPropertyName, StringComparison.OrdinalIgnoreCase);
}
private bool IsPreCompiledFunctionApp()
{
bool isPrecompiled = false;
foreach (var directory in FileSystemHelpers.GetDirectories(Environment.CurrentDirectory))
{
var functionMetadataFile = Path.Combine(directory, Constants.FunctionJsonFileName);
if (File.Exists(functionMetadataFile))
{
var functionMetadataFileContent = FileSystemHelpers.ReadAllTextFromFile(functionMetadataFile);
var functionMetadata = JsonConvert.DeserializeObject<FunctionMetadata>(functionMetadataFileContent);
string extension = Path.GetExtension(functionMetadata?.ScriptFile)?.ToLowerInvariant().TrimStart('.');
isPrecompiled = isPrecompiled || (!string.IsNullOrEmpty(extension) && extension == "dll");
}
if (isPrecompiled)
{
break;
}
}
return isPrecompiled;
}
internal static async Task CheckNonOptionalSettings(IEnumerable<KeyValuePair<string, string>> secrets, string scriptPath)
{
try
{
// FirstOrDefault returns a KeyValuePair<string, string> which is a struct so it can't be null.
var azureWebJobsStorage = secrets.FirstOrDefault(pair => pair.Key.Equals("AzureWebJobsStorage", StringComparison.OrdinalIgnoreCase)).Value;
var functionJsonFiles = await FileSystemHelpers
.GetDirectories(scriptPath)
.Select(d => Path.Combine(d, "function.json"))
.Where(FileSystemHelpers.FileExists)
.Select(async f => (filePath: f, content: await FileSystemHelpers.ReadAllTextFromFileAsync(f)))
.WhenAll();
var functionsJsons = functionJsonFiles
.Select(t => (filePath: t.filePath, jObject: JsonConvert.DeserializeObject<JObject>(t.content)))
.Where(b => b.jObject["bindings"] != null);
var allNonStorageTriggers = functionsJsons
.Select(b => b.jObject["bindings"])
.SelectMany(i => i)
.Where(b => b?["type"] != null)
.Select(b => b["type"].ToString())
.Where(b => b.IndexOf("Trigger", StringComparison.OrdinalIgnoreCase) != -1)
.All(t => Constants.TriggersWithoutStorage.Any(tws => tws.Equals(t, StringComparison.OrdinalIgnoreCase)));
if (string.IsNullOrWhiteSpace(azureWebJobsStorage) && !allNonStorageTriggers)
{
throw new CliException($"Missing value for AzureWebJobsStorage in {SecretsManager.AppSettingsFileName}. " +
$"This is required for all triggers other than {string.Join(", ", Constants.TriggersWithoutStorage)}. "
+ $"You can run 'func azure functionapp fetch-app-settings <functionAppName>' or specify a connection string in {SecretsManager.AppSettingsFileName}.");
}
foreach ((var filePath, var functionJson) in functionsJsons)
{
foreach (JObject binding in functionJson["bindings"])
{
foreach (var token in binding)
{
if (token.Key == "connection" || token.Key == "apiKey" || token.Key == "accountSid" || token.Key == "authToken")
{
var appSettingName = token.Value.ToString();
if (string.IsNullOrWhiteSpace(appSettingName))
{
ColoredConsole.WriteLine(WarningColor($"Warning: '{token.Key}' property in '{filePath}' is empty."));
}
else if (!secrets.Any(v => v.Key.Equals(appSettingName, StringComparison.OrdinalIgnoreCase)))
{
ColoredConsole
.WriteLine(WarningColor($"Warning: Cannot find value named '{appSettingName}' in {SecretsManager.AppSettingsFileName} that matches '{token.Key}' property set on '{binding["type"]?.ToString()}' in '{filePath}'. " +
$"You can run 'func azure functionapp fetch-app-settings <functionAppName>' or specify a connection string in {SecretsManager.AppSettingsFileName}."));
}
}
}
}
}
}
catch (Exception e) when (!(e is CliException))
{
ColoredConsole.WriteLine(WarningColor($"Warning: unable to verify all settings from {SecretsManager.AppSettingsFileName} and function.json files."));
if (StaticSettings.IsDebug)
{
ColoredConsole.WriteLine(e);
}
}
}
private async Task<(Uri listenUri, Uri baseUri, X509Certificate2 cert)> Setup()
{
var protocol = UseHttps ? "https" : "http";
X509Certificate2 cert = UseHttps
? await SecurityHelpers.GetOrCreateCertificate(CertPath, CertPassword)
: null;
return (new Uri($"{protocol}://0.0.0.0:{Port}"), new Uri($"{protocol}://localhost:{Port}"), cert);
}
}
}