Skip to content
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

[pack] extension bundle probing and download #4135

Merged
merged 8 commits into from
Mar 27, 2019
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion src/WebJobs.Script.WebHost/Controllers/ExtensionsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs.Script.BindingExtensions;
using Microsoft.Azure.WebJobs.Script.Config;
using Microsoft.Azure.WebJobs.Script.ExtensionBundle;
using Microsoft.Azure.WebJobs.Script.Models;
using Microsoft.Azure.WebJobs.Script.Properties;
using Microsoft.Azure.WebJobs.Script.WebHost.Models;
using Microsoft.Azure.WebJobs.Script.WebHost.Security.Authorization.Policies;
using Newtonsoft.Json;
Expand All @@ -23,11 +25,15 @@ public class ExtensionsController : Controller
{
private readonly IExtensionsManager _extensionsManager;
private readonly ScriptSettingsManager _settingsManager;
private readonly IExtensionBundleManager _extensionBundleManager;
private readonly IEnvironment _environment;

public ExtensionsController(IExtensionsManager extensionsManager, ScriptSettingsManager settingsManager)
public ExtensionsController(IExtensionsManager extensionsManager, ScriptSettingsManager settingsManager, IExtensionBundleManager extensionBundleManager, IEnvironment environment)
{
_extensionsManager = extensionsManager ?? throw new ArgumentNullException(nameof(extensionsManager));
_settingsManager = settingsManager ?? throw new ArgumentNullException(nameof(settingsManager));
_extensionBundleManager = extensionBundleManager ?? throw new ArgumentNullException(nameof(extensionBundleManager));
_environment = environment;
}

[HttpGet]
Expand Down Expand Up @@ -58,6 +64,16 @@ public async Task<IActionResult> Get()
[Route("admin/host/extensions/{id}")]
public async Task<IActionResult> Delete(string id)
{
if (_extensionBundleManager.IsExtensionBundleConfigured())
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed in person, please add tests

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There aren't any tests cases for any of the controller yet. We might need to setup something in e2e tests. Have added test cases for changes to extension manager in the extension manager tests.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scenarios based tests would be a good place for this. Some of the examples we have are the keys controller tests.

return BadRequest(Resources.ExtensionBundleBadRequestDelete);
}

if (_environment.IsPersistentFileSystemAvailable())
{
return BadRequest(Resources.ErrorDeletingExtension);
}

// TODO: Check if we have an active job

var job = await CreateJob(new ExtensionPackageReference() { Id = id, Version = string.Empty });
Expand Down Expand Up @@ -103,6 +119,16 @@ public async Task<IActionResult> InstallExtension(ExtensionPackageReference pack
return BadRequest();
pragnagopa marked this conversation as resolved.
Show resolved Hide resolved
}

if (_extensionBundleManager.IsExtensionBundleConfigured())
{
return BadRequest(Resources.ExtensionBundleBadRequestInstall);
}

if (_environment.IsPersistentFileSystemAvailable())
{
return BadRequest(Resources.ErrorInstallingExtension);
}

if (verifyConflict)
{
// If a different version of this extension is already installed, conflict:
Expand Down Expand Up @@ -184,6 +210,8 @@ private string GetJobBasePath()
{
basePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".azurefunctions", "extensions");
}

FileUtility.EnsureDirectoryExists(basePath);
soninaren marked this conversation as resolved.
Show resolved Hide resolved
return basePath;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ public static IHostBuilder AddWebScriptHost(this IHostBuilder builder, IServiceP
IServiceScopeFactory rootScopeFactory, ScriptApplicationHostOptions webHostOptions, Action<IWebJobsBuilder> configureWebJobs = null)
{
ILoggerFactory configLoggerFactory = rootServiceProvider.GetService<ILoggerFactory>();

builder.UseServiceProviderFactory(new JobHostScopedServiceProviderFactory(rootServiceProvider, rootScopeFactory))
.ConfigureServices(services =>
{
Expand Down

This file was deleted.

This file was deleted.

29 changes: 22 additions & 7 deletions src/WebJobs.Script/BindingExtensions/ExtensionsManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using System.Xml;
using Microsoft.Azure.WebJobs.Script.Config;
using Microsoft.Azure.WebJobs.Script.Description.DotNet;
using Microsoft.Azure.WebJobs.Script.ExtensionBundle;
using Microsoft.Azure.WebJobs.Script.Models;
using Microsoft.Build.Construction;
using Microsoft.Build.Evaluation;
Expand All @@ -26,16 +27,24 @@ public class ExtensionsManager : IExtensionsManager
{
private readonly string _scriptRootPath;
private readonly ILogger _logger;
private readonly IExtensionBundleManager _extensionBundleManager;
private string _nugetFallbackPath;

public ExtensionsManager(IOptions<ScriptJobHostOptions> hostOptions, ILogger<ExtensionsManager> logger)
public ExtensionsManager(IOptions<ScriptJobHostOptions> hostOptions, ILogger<ExtensionsManager> logger, IExtensionBundleManager extensionBundleManager)
{
_scriptRootPath = hostOptions.Value.RootScriptPath;
_nugetFallbackPath = hostOptions.Value.NugetFallBackPath;
_logger = logger;
_extensionBundleManager = extensionBundleManager;
}

internal string ProjectPath => Path.Combine(_scriptRootPath, ExtensionsProjectFileName);
internal string DefaultExtensionsProjectPath => Path.Combine(_scriptRootPath, ExtensionsProjectFileName);

private async Task<string> GetBundleProjectPath()
{
string bundlePath = await _extensionBundleManager.GetExtensionBundlePath();
return !string.IsNullOrEmpty(bundlePath) ? Path.Combine(bundlePath, ExtensionsProjectFileName) : null;
}

public async Task AddExtensions(params ExtensionPackageReference[] references)
{
Expand All @@ -44,7 +53,7 @@ public async Task AddExtensions(params ExtensionPackageReference[] references)
return;
}

var project = await GetOrCreateProjectAsync(ProjectPath);
var project = await GetOrCreateProjectAsync(DefaultExtensionsProjectPath);

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

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

public async Task<IEnumerable<ExtensionPackageReference>> GetExtensions()
{
var project = await GetOrCreateProjectAsync(ProjectPath);
string extensionsProjectPath = _extensionBundleManager.IsExtensionBundleConfigured() ? await GetBundleProjectPath() : DefaultExtensionsProjectPath;
if (string.IsNullOrEmpty(extensionsProjectPath))
{
return Enumerable.Empty<ExtensionPackageReference>();
}

var project = await GetOrCreateProjectAsync(extensionsProjectPath);

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

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

FileUtility.CopyDirectory(sourceBin, target);

File.Copy(Path.Combine(tempFolder, ExtensionsProjectFileName), ProjectPath, true);
File.Copy(Path.Combine(tempFolder, ExtensionsProjectFileName), DefaultExtensionsProjectPath, true);
}

private Task<ProjectRootElement> GetOrCreateProjectAsync(string path)
Expand Down
43 changes: 43 additions & 0 deletions src/WebJobs.Script/Config/ExtensionBundleOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 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.Collections.ObjectModel;
using NuGet.Versioning;

namespace Microsoft.Azure.WebJobs.Script.Configuration
{
public class ExtensionBundleOptions
{
/// <summary>
/// Gets or Sets the Id of the extension bundle
/// </summary>
public string Id { get; set; }

/// <summary>
/// Gets or Sets the version range or version of the extension bundle.
/// </summary>
public VersionRange Version { get; set; }

/// <summary>
/// Gets the Probing path of the Extension Bundle.
/// 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.
/// To be configured by the host or consuming service
/// </summary>
public ICollection<string> ProbingPaths { get; private set; } = new Collection<string>();

/// <summary>
/// Gets or Sets the download path for the extension bundle.
/// This is the path where the runtime would download the extension bundle in case it is not present at the probing path.
/// To be configured by the host or consuming service
/// </summary>
public string DownloadPath { get; set; }

/// <summary>
/// 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.
/// To be configured by the host or consuming service
/// </summary>
public bool EnsureLatest { get; set; }
soninaren marked this conversation as resolved.
Show resolved Hide resolved
}
}
93 changes: 93 additions & 0 deletions src/WebJobs.Script/Config/ExtensionBundleOptionsSetup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// 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.IO;
using Microsoft.Azure.WebJobs.Script.Properties;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using NuGet.Packaging;
using NuGet.Versioning;

namespace Microsoft.Azure.WebJobs.Script.Configuration
{
public class ExtensionBundleOptionsSetup : IConfigureOptions<ExtensionBundleOptions>
{
private readonly IConfiguration _configuration;
private readonly IEnvironment _environment;
private readonly IHostingEnvironment _hostingEnvironment;

public ExtensionBundleOptionsSetup(IConfiguration configuration, IEnvironment environment, IHostingEnvironment hostingEnvironment)
{
_configuration = configuration;
_environment = environment;
_hostingEnvironment = hostingEnvironment;
}

public void Configure(ExtensionBundleOptions options)
{
IConfigurationSection jobHostSection = _configuration.GetSection(ConfigurationSectionNames.JobHost);
var extensionBundleSection = jobHostSection.GetSection(ConfigurationSectionNames.ExtensionBundle);

if (extensionBundleSection.Exists())
{
extensionBundleSection.Bind(options);
ValidateBundleId(options.Id);
ConfigureBundleVersion(extensionBundleSection, options);

if (_environment.IsAppServiceEnvironment() || _hostingEnvironment.IsDevelopment())
{
options.DownloadPath = Path.Combine(_environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteHomePath),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is not configurable when running in those environments? (configuration will be ignored?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non AppService environment would have to override this configuration. For example core tools running in windows would set this to be a different location than core tools running in linux.

"data", "Functions", ScriptConstants.ExtensionBundleDirectory, options.Id);
ConfigureProbingPaths(options);
}
}
}

private void ConfigureBundleVersion(IConfigurationSection configurationSection, ExtensionBundleOptions options)
{
string bundleVersion = configurationSection.GetValue<string>("version");
if (string.IsNullOrWhiteSpace(bundleVersion) || !VersionRange.TryParse(bundleVersion.ToString(), allowFloating: true, out VersionRange version))
{
string message = string.Format(Resources.ExtensionBundleConfigMissingVersion, ScriptConstants.HostMetadataFileName);
throw new ArgumentException(message);
}
options.Version = version;
}

private void ValidateBundleId(string id)
{
if (string.IsNullOrWhiteSpace(id) || !PackageIdValidator.IsValidPackageId(id))
{
string message = string.Format(Resources.ExtensionBundleConfigMissingId, ScriptConstants.HostMetadataFileName);
throw new ArgumentException(message);
}
}

private void ConfigureProbingPaths(ExtensionBundleOptions options)
{
if (_environment.IsAppServiceWindowsEnvironment() || _hostingEnvironment.IsDevelopment())
{
string windowsDefaultPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
ScriptConstants.DefaultExtensionBundleDirectory,
options.Id);

options.ProbingPaths.Add(windowsDefaultPath);
}

if (_environment.IsLinuxAppServiceEnvironment())
{
string linuxDefaultPath = Path.Combine(Path.PathSeparator.ToString(), ScriptConstants.DefaultExtensionBundleDirectory, options.Id);

string deploymentPackageBundlePath = Path.Combine(
_environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteHomePath),
"site", "wwwroot", ".azureFunctions", ScriptConstants.ExtensionBundleDirectory, options.Id);

options.ProbingPaths.Add(linuxDefaultPath);
options.ProbingPaths.Add(deploymentPackageBundlePath);
}
}
}
}
Loading