Skip to content

Commit fe9d31b

Browse files
soninarenbrettsam
authored andcommitted
[pack] extension bundle probing, download and extesion management (#4135)
extensionBundle probing, download and extesion management
1 parent e2994aa commit fe9d31b

22 files changed

+1418
-284
lines changed

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

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
using Microsoft.AspNetCore.Mvc;
1212
using Microsoft.Azure.WebJobs.Script.BindingExtensions;
1313
using Microsoft.Azure.WebJobs.Script.Config;
14+
using Microsoft.Azure.WebJobs.Script.ExtensionBundle;
1415
using Microsoft.Azure.WebJobs.Script.Models;
16+
using Microsoft.Azure.WebJobs.Script.Properties;
1517
using Microsoft.Azure.WebJobs.Script.WebHost.Models;
1618
using Microsoft.Azure.WebJobs.Script.WebHost.Security.Authorization.Policies;
1719
using Newtonsoft.Json;
@@ -23,11 +25,15 @@ public class ExtensionsController : Controller
2325
{
2426
private readonly IExtensionsManager _extensionsManager;
2527
private readonly ScriptSettingsManager _settingsManager;
28+
private readonly IExtensionBundleManager _extensionBundleManager;
29+
private readonly IEnvironment _environment;
2630

27-
public ExtensionsController(IExtensionsManager extensionsManager, ScriptSettingsManager settingsManager)
31+
public ExtensionsController(IExtensionsManager extensionsManager, ScriptSettingsManager settingsManager, IExtensionBundleManager extensionBundleManager, IEnvironment environment)
2832
{
2933
_extensionsManager = extensionsManager ?? throw new ArgumentNullException(nameof(extensionsManager));
3034
_settingsManager = settingsManager ?? throw new ArgumentNullException(nameof(settingsManager));
35+
_extensionBundleManager = extensionBundleManager ?? throw new ArgumentNullException(nameof(extensionBundleManager));
36+
_environment = environment;
3137
}
3238

3339
[HttpGet]
@@ -58,6 +64,16 @@ public async Task<IActionResult> Get()
5864
[Route("admin/host/extensions/{id}")]
5965
public async Task<IActionResult> Delete(string id)
6066
{
67+
if (_extensionBundleManager.IsExtensionBundleConfigured())
68+
{
69+
return BadRequest(Resources.ExtensionBundleBadRequestDelete);
70+
}
71+
72+
if (_environment.IsPersistentFileSystemAvailable())
73+
{
74+
return BadRequest(Resources.ErrorDeletingExtension);
75+
}
76+
6177
// TODO: Check if we have an active job
6278

6379
var job = await CreateJob(new ExtensionPackageReference() { Id = id, Version = string.Empty });
@@ -103,6 +119,16 @@ public async Task<IActionResult> InstallExtension(ExtensionPackageReference pack
103119
return BadRequest();
104120
}
105121

122+
if (_extensionBundleManager.IsExtensionBundleConfigured())
123+
{
124+
return BadRequest(Resources.ExtensionBundleBadRequestInstall);
125+
}
126+
127+
if (_environment.IsPersistentFileSystemAvailable())
128+
{
129+
return BadRequest(Resources.ErrorInstallingExtension);
130+
}
131+
106132
if (verifyConflict)
107133
{
108134
// If a different version of this extension is already installed, conflict:
@@ -184,6 +210,8 @@ private string GetJobBasePath()
184210
{
185211
basePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".azurefunctions", "extensions");
186212
}
213+
214+
FileUtility.EnsureDirectoryExists(basePath);
187215
return basePath;
188216
}
189217

src/WebJobs.Script.WebHost/WebScriptHostBuilderExtension.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ public static IHostBuilder AddWebScriptHost(this IHostBuilder builder, IServiceP
2626
IServiceScopeFactory rootScopeFactory, ScriptApplicationHostOptions webHostOptions, Action<IWebJobsBuilder> configureWebJobs = null)
2727
{
2828
ILoggerFactory configLoggerFactory = rootServiceProvider.GetService<ILoggerFactory>();
29-
3029
builder.UseServiceProviderFactory(new JobHostScopedServiceProviderFactory(rootServiceProvider, rootScopeFactory))
3130
.ConfigureServices(services =>
3231
{

src/WebJobs.Script/BindingExtensionBundle/ExtensionBundleOptions.cs

Lines changed: 0 additions & 15 deletions
This file was deleted.

src/WebJobs.Script/BindingExtensionBundle/ExtensionBundleOptionsSetup.cs

Lines changed: 0 additions & 57 deletions
This file was deleted.

src/WebJobs.Script/BindingExtensions/ExtensionsManager.cs

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using System.Xml;
1313
using Microsoft.Azure.WebJobs.Script.Config;
1414
using Microsoft.Azure.WebJobs.Script.Description.DotNet;
15+
using Microsoft.Azure.WebJobs.Script.ExtensionBundle;
1516
using Microsoft.Azure.WebJobs.Script.Models;
1617
using Microsoft.Build.Construction;
1718
using Microsoft.Build.Evaluation;
@@ -26,16 +27,24 @@ public class ExtensionsManager : IExtensionsManager
2627
{
2728
private readonly string _scriptRootPath;
2829
private readonly ILogger _logger;
30+
private readonly IExtensionBundleManager _extensionBundleManager;
2931
private string _nugetFallbackPath;
3032

31-
public ExtensionsManager(IOptions<ScriptJobHostOptions> hostOptions, ILogger<ExtensionsManager> logger)
33+
public ExtensionsManager(IOptions<ScriptJobHostOptions> hostOptions, ILogger<ExtensionsManager> logger, IExtensionBundleManager extensionBundleManager)
3234
{
3335
_scriptRootPath = hostOptions.Value.RootScriptPath;
3436
_nugetFallbackPath = hostOptions.Value.NugetFallBackPath;
3537
_logger = logger;
38+
_extensionBundleManager = extensionBundleManager;
3639
}
3740

38-
internal string ProjectPath => Path.Combine(_scriptRootPath, ExtensionsProjectFileName);
41+
internal string DefaultExtensionsProjectPath => Path.Combine(_scriptRootPath, ExtensionsProjectFileName);
42+
43+
private async Task<string> GetBundleProjectPath()
44+
{
45+
string bundlePath = await _extensionBundleManager.GetExtensionBundlePath();
46+
return !string.IsNullOrEmpty(bundlePath) ? Path.Combine(bundlePath, ExtensionsProjectFileName) : null;
47+
}
3948

4049
public async Task AddExtensions(params ExtensionPackageReference[] references)
4150
{
@@ -44,7 +53,7 @@ public async Task AddExtensions(params ExtensionPackageReference[] references)
4453
return;
4554
}
4655

47-
var project = await GetOrCreateProjectAsync(ProjectPath);
56+
var project = await GetOrCreateProjectAsync(DefaultExtensionsProjectPath);
4857

4958
// Ensure the metadata generator version we're using is what we expect
5059
project.AddPackageReference(MetadataGeneratorPackageId, MetadataGeneratorPackageVersion);
@@ -76,7 +85,7 @@ public async Task DeleteExtensions(params string[] extensionIds)
7685
return;
7786
}
7887

79-
var project = await GetOrCreateProjectAsync(ProjectPath);
88+
var project = await GetOrCreateProjectAsync(DefaultExtensionsProjectPath);
8089
foreach (var id in extensionIds)
8190
{
8291
project.RemovePackageReference(id);
@@ -87,7 +96,13 @@ public async Task DeleteExtensions(params string[] extensionIds)
8796

8897
public async Task<IEnumerable<ExtensionPackageReference>> GetExtensions()
8998
{
90-
var project = await GetOrCreateProjectAsync(ProjectPath);
99+
string extensionsProjectPath = _extensionBundleManager.IsExtensionBundleConfigured() ? await GetBundleProjectPath() : DefaultExtensionsProjectPath;
100+
if (string.IsNullOrEmpty(extensionsProjectPath))
101+
{
102+
return Enumerable.Empty<ExtensionPackageReference>();
103+
}
104+
105+
var project = await GetOrCreateProjectAsync(extensionsProjectPath);
91106

92107
return project.Items
93108
.Where(i => PackageReferenceElementName.Equals(i.ItemType, StringComparison.Ordinal) && !MetadataGeneratorPackageId.Equals(i.Include, StringComparison.Ordinal))
@@ -122,7 +137,7 @@ internal virtual Task ProcessExtensionsProject(string projectFolder)
122137
Arguments = $"build \"{ExtensionsProjectFileName}\" -o bin --force --no-incremental"
123138
};
124139

125-
string nugetPath = Path.Combine(Path.GetDirectoryName(ProjectPath), "nuget.config");
140+
string nugetPath = Path.Combine(Path.GetDirectoryName(DefaultExtensionsProjectPath), "nuget.config");
126141
if (File.Exists(nugetPath))
127142
{
128143
startInfo.Arguments += $" --configfile \"{nugetPath}\"";
@@ -243,7 +258,7 @@ private async Task ProcessResults(string tempFolder)
243258

244259
FileUtility.CopyDirectory(sourceBin, target);
245260

246-
File.Copy(Path.Combine(tempFolder, ExtensionsProjectFileName), ProjectPath, true);
261+
File.Copy(Path.Combine(tempFolder, ExtensionsProjectFileName), DefaultExtensionsProjectPath, true);
247262
}
248263

249264
private Task<ProjectRootElement> GetOrCreateProjectAsync(string path)
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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.IO;
6+
using Microsoft.Azure.WebJobs.Script.Properties;
7+
using Microsoft.Extensions.Configuration;
8+
using Microsoft.Extensions.Hosting;
9+
using NuGet.Packaging;
10+
using NuGet.Versioning;
11+
12+
namespace Microsoft.Azure.WebJobs.Script.Configuration
13+
{
14+
public class ExtensionBundleConfigurationHelper
15+
{
16+
private readonly IConfiguration _configuration;
17+
private readonly IEnvironment _environment;
18+
private readonly IHostingEnvironment _hostingEnvironment;
19+
20+
public ExtensionBundleConfigurationHelper(IConfiguration configuration, IEnvironment environment, IHostingEnvironment hostingEnvironment)
21+
{
22+
_configuration = configuration;
23+
_environment = environment;
24+
_hostingEnvironment = hostingEnvironment;
25+
}
26+
27+
public void Configure(ExtensionBundleOptions options)
28+
{
29+
IConfigurationSection jobHostSection = _configuration.GetSection(ConfigurationSectionNames.JobHost);
30+
var extensionBundleSection = jobHostSection.GetSection(ConfigurationSectionNames.ExtensionBundle);
31+
32+
if (extensionBundleSection.Exists())
33+
{
34+
extensionBundleSection.Bind(options);
35+
ValidateBundleId(options.Id);
36+
ConfigureBundleVersion(extensionBundleSection, options);
37+
38+
if (_environment.IsAppServiceEnvironment() || _hostingEnvironment.IsDevelopment())
39+
{
40+
options.DownloadPath = Path.Combine(_environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteHomePath),
41+
"data", "Functions", ScriptConstants.ExtensionBundleDirectory, options.Id);
42+
ConfigureProbingPaths(options);
43+
}
44+
}
45+
}
46+
47+
private void ConfigureBundleVersion(IConfigurationSection configurationSection, ExtensionBundleOptions options)
48+
{
49+
string bundleVersion = configurationSection.GetValue<string>("version");
50+
if (string.IsNullOrWhiteSpace(bundleVersion) || !VersionRange.TryParse(bundleVersion.ToString(), allowFloating: true, out VersionRange version))
51+
{
52+
string message = string.Format(Resources.ExtensionBundleConfigMissingVersion, ScriptConstants.HostMetadataFileName);
53+
throw new ArgumentException(message);
54+
}
55+
options.Version = version;
56+
}
57+
58+
private void ValidateBundleId(string id)
59+
{
60+
if (string.IsNullOrWhiteSpace(id) || !PackageIdValidator.IsValidPackageId(id))
61+
{
62+
string message = string.Format(Resources.ExtensionBundleConfigMissingId, ScriptConstants.HostMetadataFileName);
63+
throw new ArgumentException(message);
64+
}
65+
}
66+
67+
private void ConfigureProbingPaths(ExtensionBundleOptions options)
68+
{
69+
if (_environment.IsAppServiceWindowsEnvironment() || _hostingEnvironment.IsDevelopment())
70+
{
71+
string windowsDefaultPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
72+
ScriptConstants.DefaultExtensionBundleDirectory,
73+
options.Id);
74+
75+
options.ProbingPaths.Add(windowsDefaultPath);
76+
}
77+
78+
if (_environment.IsLinuxAppServiceEnvironment())
79+
{
80+
string linuxDefaultPath = Path.Combine(Path.PathSeparator.ToString(), ScriptConstants.DefaultExtensionBundleDirectory, options.Id);
81+
82+
string deploymentPackageBundlePath = Path.Combine(
83+
_environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteHomePath),
84+
"site", "wwwroot", ".azureFunctions", ScriptConstants.ExtensionBundleDirectory, options.Id);
85+
86+
options.ProbingPaths.Add(linuxDefaultPath);
87+
options.ProbingPaths.Add(deploymentPackageBundlePath);
88+
}
89+
}
90+
}
91+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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 NuGet.Versioning;
8+
9+
namespace Microsoft.Azure.WebJobs.Script.Configuration
10+
{
11+
public class ExtensionBundleOptions
12+
{
13+
/// <summary>
14+
/// Gets or Sets the Id of the extension bundle
15+
/// </summary>
16+
public string Id { get; set; }
17+
18+
/// <summary>
19+
/// Gets or Sets the version range or version of the extension bundle.
20+
/// </summary>
21+
public VersionRange Version { get; set; }
22+
23+
/// <summary>
24+
/// Gets the Probing path of the Extension Bundle.
25+
/// Probing path are configured by the host depending on the hosting enviroment the default location where the runtime would look for an extension bundle first.
26+
/// To be configured by the host or consuming service
27+
/// </summary>
28+
public ICollection<string> ProbingPaths { get; private set; } = new Collection<string>();
29+
30+
/// <summary>
31+
/// Gets or Sets the download path for the extension bundle.
32+
/// This is the path where the runtime would download the extension bundle in case it is not present at the probing path.
33+
/// To be configured by the host or consuming service
34+
/// </summary>
35+
public string DownloadPath { get; set; }
36+
37+
/// <summary>
38+
/// Gets or Sets a value indicating whether the runtime should force fetch the latest version of extension bundle available on CDN, even when there is a matching extension bundle available locally.
39+
/// To be configured by the host or consuming service
40+
/// </summary>
41+
public bool EnsureLatest { get; set; }
42+
}
43+
}

0 commit comments

Comments
 (0)