From 453fa520e5e5b780f8199053503a6309686ac354 Mon Sep 17 00:00:00 2001 From: Bala G Date: Fri, 3 Jul 2020 18:44:42 -0700 Subject: [PATCH] [Linux Consumption] Support persistent storage using Azure file share (#121) Adds support for creating & mounting Azure file shares for Linux consumption scm sites. Function apps can opt-in using the app setting ENABLE_KUDU_PERSISTENT_STORAGE=1. Currently the persistent storage is used for deplpyment logs only. This change also moves artifacts location from /deployments path to its own path /artifacts. --- Common/Constants.cs | 6 + Kudu.Console/Program.cs | 4 +- Kudu.Contracts/IEnvironment.cs | 1 + Kudu.Core/Environment.cs | 21 ++- .../LinuxConsumptionDeploymentHelper.cs | 2 +- Kudu.Core/Infrastructure/OperationManager.cs | 16 ++ .../FileSystemPathProvider.cs | 39 ++++ .../IFileSystemPathProvider.cs | 7 + .../IMeshPersistentFileSystem.cs | 13 ++ .../LinuxConsumption/IMeshServiceClient.cs | 9 + Kudu.Core/LinuxConsumption/IStorageClient.cs | 9 + .../LinuxConsumption/ISystemEnvironment.cs | 9 + .../LinuxConsumptionEnvironment.cs | 6 +- .../MeshPersistentFileSystem.cs | 160 +++++++++++++++++ .../LinuxConsumption/MeshServiceClient.cs | 56 ++++++ .../NullMeshPersistentFileSystem.cs | 23 +++ .../LinuxConsumption/NullMeshServiceClient.cs | 12 ++ Kudu.Core/LinuxConsumption/StorageClient.cs | 42 +++++ .../LinuxConsumption/SystemEnvironment.cs | 30 ++++ Kudu.Core/Tracing/ConsoleEventGenerator.cs | 158 ----------------- .../Tracing/DefaultKuduEventGenerator.cs | 7 + Kudu.Core/Tracing/IKuduEventGenerator.cs | 2 + Kudu.Core/Tracing/KuduEvent.cs | 2 +- Kudu.Core/Tracing/KuduEventGenerator.cs | 9 +- .../Tracing/LinuxContainerEventGenerator.cs | 15 +- Kudu.Core/Tracing/Log4NetEventGenerator.cs | 6 + Kudu.Services.Web/KuduWebUtil.cs | 4 +- Kudu.Services.Web/Startup.cs | 45 ++++- .../EnvironmentReadyCheckMiddleware.cs | 29 +++ .../HealthStatus.cs | 15 ++ .../ILinuxConsumptionInstanceManager.cs | 8 - .../InstanceHealthItem.cs | 23 +++ ...LinuxConsumptionInstanceAdminController.cs | 15 +- .../LinuxConsumptionInstanceManager.cs | 80 +++++---- Kudu.Tests/Kudu.Tests.csproj | 3 +- .../FileSystemPathProviderTests.cs | 67 +++++++ .../MeshPersistentFileSystemTests.cs | 166 ++++++++++++++++++ .../MeshServiceClientTests.cs | 77 ++++++++ Kudu.Tests/LinuxConsumption/MockFileSystem.cs | 22 +++ .../TestFileSystemPathProvider.cs | 27 +++ .../LinuxConsumption/TestSystemEnvironment.cs | 25 +++ Kudu.Tests/Services/EnvironmentTests.cs | 64 ++++++- .../Services/KuduEventGeneratorTests.cs | 20 +++ Kudu.Tests/TestMockedEnvironment.cs | 6 +- Kudu.Tests/TestMockedIEnvironment.cs | 2 + 45 files changed, 1132 insertions(+), 230 deletions(-) create mode 100644 Kudu.Core/LinuxConsumption/FileSystemPathProvider.cs create mode 100644 Kudu.Core/LinuxConsumption/IFileSystemPathProvider.cs create mode 100644 Kudu.Core/LinuxConsumption/IMeshPersistentFileSystem.cs create mode 100644 Kudu.Core/LinuxConsumption/IMeshServiceClient.cs create mode 100644 Kudu.Core/LinuxConsumption/IStorageClient.cs create mode 100644 Kudu.Core/LinuxConsumption/ISystemEnvironment.cs rename Kudu.Core/{ => LinuxConsumption}/LinuxConsumptionEnvironment.cs (94%) create mode 100644 Kudu.Core/LinuxConsumption/MeshPersistentFileSystem.cs create mode 100644 Kudu.Core/LinuxConsumption/MeshServiceClient.cs create mode 100644 Kudu.Core/LinuxConsumption/NullMeshPersistentFileSystem.cs create mode 100644 Kudu.Core/LinuxConsumption/NullMeshServiceClient.cs create mode 100644 Kudu.Core/LinuxConsumption/StorageClient.cs create mode 100644 Kudu.Core/LinuxConsumption/SystemEnvironment.cs delete mode 100644 Kudu.Core/Tracing/ConsoleEventGenerator.cs create mode 100644 Kudu.Services/LinuxConsumptionInstanceAdmin/EnvironmentReadyCheckMiddleware.cs create mode 100644 Kudu.Services/LinuxConsumptionInstanceAdmin/HealthStatus.cs create mode 100644 Kudu.Services/LinuxConsumptionInstanceAdmin/InstanceHealthItem.cs create mode 100644 Kudu.Tests/LinuxConsumption/FileSystemPathProviderTests.cs create mode 100644 Kudu.Tests/LinuxConsumption/MeshPersistentFileSystemTests.cs create mode 100644 Kudu.Tests/LinuxConsumption/MeshServiceClientTests.cs create mode 100644 Kudu.Tests/LinuxConsumption/MockFileSystem.cs create mode 100644 Kudu.Tests/LinuxConsumption/TestFileSystemPathProvider.cs create mode 100644 Kudu.Tests/LinuxConsumption/TestSystemEnvironment.cs create mode 100644 Kudu.Tests/Services/KuduEventGeneratorTests.cs diff --git a/Common/Constants.cs b/Common/Constants.cs index 960f2f86..c701f399 100644 --- a/Common/Constants.cs +++ b/Common/Constants.cs @@ -34,6 +34,7 @@ public static class Constants public const string NpmDebugLogFile = "npm-debug.log"; public const string DeploymentCachePath = "deployments"; + public const string ArtifactsPath = "artifacts"; public const string SiteExtensionsCachePath = "siteextensions"; public const string DeploymentToolsPath = "tools"; public const string SiteFolder = @"site"; @@ -156,5 +157,10 @@ public static TimeSpan MaxAllowedExecutionTime public const string LinuxLogEventStreamName = "MS_KUDU_LOGS"; public const string WebSiteHomeStampName = "WEBSITE_HOME_STAMPNAME"; public const string WebSiteStampDeploymentId = "WEBSITE_STAMP_DEPLOYMENT_ID"; + public const string MeshInitURI = "MESH_INIT_URI"; + public const string AzureWebJobsStorage = "AzureWebJobsStorage"; + public const string KuduFileShareMountPath = "/kudu-mnt"; + public const string KuduFileSharePrefix = "kudu-mnt"; + public const string EnablePersistentStorage = "ENABLE_KUDU_PERSISTENT_STORAGE"; } } diff --git a/Kudu.Console/Program.cs b/Kudu.Console/Program.cs index e92ced5b..ec9a00de 100644 --- a/Kudu.Console/Program.cs +++ b/Kudu.Console/Program.cs @@ -21,6 +21,7 @@ using Kudu.Core.SourceControl.Git; using Kudu.Core.Tracing; using System.Reflection; +using Kudu.Core.LinuxConsumption; using XmlSettings; using log4net; using log4net.Config; @@ -283,7 +284,8 @@ private static IEnvironment GetEnvironment(string siteRoot, string requestId) repositoryPath, requestId, Path.Combine(AppContext.BaseDirectory, "KuduConsole", "kudu.dll"), - null); + null, + new FileSystemPathProvider(new NullMeshPersistentFileSystem())); } } } \ No newline at end of file diff --git a/Kudu.Contracts/IEnvironment.cs b/Kudu.Contracts/IEnvironment.cs index a48e40d2..a10c22f8 100644 --- a/Kudu.Contracts/IEnvironment.cs +++ b/Kudu.Contracts/IEnvironment.cs @@ -7,6 +7,7 @@ public interface IEnvironment string RepositoryPath { get; set; } // e.g. /site/repository string WebRootPath { get; } // e.g. /site/wwwroot string DeploymentsPath { get; } // e.g. /site/deployments + string ArtifactsPath { get; } // e.g /site/artifacts string DeploymentToolsPath { get; } // e.g. /site/deployments/tools string SiteExtensionSettingsPath { get; } // e.g. /site/siteextensions string DiagnosticsPath { get; } // e.g. /site/diagnostics diff --git a/Kudu.Core/Environment.cs b/Kudu.Core/Environment.cs index 3ec45edb..b18cfe5b 100644 --- a/Kudu.Core/Environment.cs +++ b/Kudu.Core/Environment.cs @@ -11,15 +11,18 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Kudu.Core.Settings; +using Kudu.Core.LinuxConsumption; namespace Kudu.Core { public class Environment : IEnvironment { private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IFileSystemPathProvider _fileSystemPathsProvider; private readonly string _webRootPath; private readonly string _deploymentsPath; + private readonly string _artifactsPath; private readonly string _deploymentToolsPath; private readonly string _siteExtensionSettingsPath; private readonly string _diagnosticsPath; @@ -106,7 +109,8 @@ public Environment( string repositoryPath, string requestId, string kuduConsoleFullPath, - IHttpContextAccessor httpContextAccessor) + IHttpContextAccessor httpContextAccessor, + IFileSystemPathProvider fileSystemPathsProvider) { RootPath = rootPath; @@ -117,6 +121,7 @@ public Environment( _zipTempPath = Path.Combine(_tempPath, Constants.ZipTempPath); _webRootPath = Path.Combine(SiteRootPath, Constants.WebRoot); _deploymentsPath = Path.Combine(SiteRootPath, Constants.DeploymentCachePath); + _artifactsPath = Path.Combine(SiteRootPath, Constants.ArtifactsPath); _deploymentToolsPath = Path.Combine(_deploymentsPath, Constants.DeploymentToolsPath); _siteExtensionSettingsPath = Path.Combine(SiteRootPath, Constants.SiteExtensionsCachePath); _diagnosticsPath = Path.Combine(SiteRootPath, Constants.DiagnosticsPath); @@ -162,6 +167,7 @@ public Environment( RequestId = !string.IsNullOrEmpty(requestId) ? requestId : Guid.Empty.ToString(); _httpContextAccessor = httpContextAccessor; + _fileSystemPathsProvider = fileSystemPathsProvider ?? throw new ArgumentNullException(nameof(fileSystemPathsProvider)); KuduConsoleFullPath = kuduConsoleFullPath; } @@ -191,10 +197,23 @@ public string DeploymentsPath { get { + if (_fileSystemPathsProvider.TryGetDeploymentsPath(out var path)) + { + return FileSystemHelpers.EnsureDirectory(path); ; + } + return FileSystemHelpers.EnsureDirectory(_deploymentsPath); } } + public string ArtifactsPath + { + get + { + return FileSystemHelpers.EnsureDirectory(_artifactsPath); + } + } + public string DeploymentToolsPath { get diff --git a/Kudu.Core/Helpers/LinuxConsumptionDeploymentHelper.cs b/Kudu.Core/Helpers/LinuxConsumptionDeploymentHelper.cs index 2ea5f02a..dd95a16c 100644 --- a/Kudu.Core/Helpers/LinuxConsumptionDeploymentHelper.cs +++ b/Kudu.Core/Helpers/LinuxConsumptionDeploymentHelper.cs @@ -31,7 +31,7 @@ public static async Task SetupLinuxConsumptionFunctionAppDeployment( string sas = settings.GetValue(Constants.ScmRunFromPackage) ?? System.Environment.GetEnvironmentVariable(Constants.ScmRunFromPackage); string builtFolder = context.OutputPath; - string packageFolder = env.DeploymentsPath; + string packageFolder = env.ArtifactsPath; string packageFileName = OryxBuildConstants.FunctionAppBuildSettings.LinuxConsumptionArtifactName; // Package built content from oryx build artifact diff --git a/Kudu.Core/Infrastructure/OperationManager.cs b/Kudu.Core/Infrastructure/OperationManager.cs index 69a347db..0a266876 100644 --- a/Kudu.Core/Infrastructure/OperationManager.cs +++ b/Kudu.Core/Infrastructure/OperationManager.cs @@ -104,5 +104,21 @@ public static T SafeExecute(Func action) return default(T); } } + + public static async Task ExecuteWithTimeout(Task task, TimeSpan timeout) + { + var timeoutCancellationTokenSource = new CancellationTokenSource(); + + var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token)); + if (completedTask == task) + { + timeoutCancellationTokenSource.Cancel(); + return await task; + } + else + { + throw new TimeoutException("The operation has timed out."); + } + } } } diff --git a/Kudu.Core/LinuxConsumption/FileSystemPathProvider.cs b/Kudu.Core/LinuxConsumption/FileSystemPathProvider.cs new file mode 100644 index 00000000..25c1898d --- /dev/null +++ b/Kudu.Core/LinuxConsumption/FileSystemPathProvider.cs @@ -0,0 +1,39 @@ +using System; +using System.Diagnostics.Tracing; +using Kudu.Core.Infrastructure; +using Kudu.Core.Tracing; + +namespace Kudu.Core.LinuxConsumption +{ + public class FileSystemPathProvider : IFileSystemPathProvider + { + private readonly IMeshPersistentFileSystem _persistentFileSystem; + + public FileSystemPathProvider(IMeshPersistentFileSystem persistentFileSystem) + { + _persistentFileSystem = + persistentFileSystem ?? throw new ArgumentNullException(nameof(persistentFileSystem)); + } + + public bool TryGetDeploymentsPath(out string path) + { + path = _persistentFileSystem.GetDeploymentsPath(); + return !string.IsNullOrEmpty(path) && EnsureMountedDeploymentsPath(path); + } + + private bool EnsureMountedDeploymentsPath(string path) + { + try + { + FileSystemHelpers.EnsureDirectory(path); + return true; + } + catch (Exception e) + { + KuduEventGenerator.Log().LogMessage(EventLevel.Informational, ServerConfiguration.GetApplicationName(), + $"{nameof(EnsureMountedDeploymentsPath)} Failed. Path = {path}", e.ToString()); + return false; + } + } + } +} \ No newline at end of file diff --git a/Kudu.Core/LinuxConsumption/IFileSystemPathProvider.cs b/Kudu.Core/LinuxConsumption/IFileSystemPathProvider.cs new file mode 100644 index 00000000..361973fe --- /dev/null +++ b/Kudu.Core/LinuxConsumption/IFileSystemPathProvider.cs @@ -0,0 +1,7 @@ +namespace Kudu.Core.LinuxConsumption +{ + public interface IFileSystemPathProvider + { + bool TryGetDeploymentsPath(out string path); + } +} diff --git a/Kudu.Core/LinuxConsumption/IMeshPersistentFileSystem.cs b/Kudu.Core/LinuxConsumption/IMeshPersistentFileSystem.cs new file mode 100644 index 00000000..d394865e --- /dev/null +++ b/Kudu.Core/LinuxConsumption/IMeshPersistentFileSystem.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; + +namespace Kudu.Core.LinuxConsumption +{ + public interface IMeshPersistentFileSystem + { + Task MountFileShare(); + + bool GetStatus(out string message); + + string GetDeploymentsPath(); + } +} \ No newline at end of file diff --git a/Kudu.Core/LinuxConsumption/IMeshServiceClient.cs b/Kudu.Core/LinuxConsumption/IMeshServiceClient.cs new file mode 100644 index 00000000..93243373 --- /dev/null +++ b/Kudu.Core/LinuxConsumption/IMeshServiceClient.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Kudu.Core.LinuxConsumption +{ + public interface IMeshServiceClient + { + Task MountCifs(string connectionString, string contentShare, string targetPath); + } +} diff --git a/Kudu.Core/LinuxConsumption/IStorageClient.cs b/Kudu.Core/LinuxConsumption/IStorageClient.cs new file mode 100644 index 00000000..506b2786 --- /dev/null +++ b/Kudu.Core/LinuxConsumption/IStorageClient.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Kudu.Core.LinuxConsumption +{ + public interface IStorageClient + { + Task CreateFileShare(string siteName, string connectionString, string fileShareName); + } +} diff --git a/Kudu.Core/LinuxConsumption/ISystemEnvironment.cs b/Kudu.Core/LinuxConsumption/ISystemEnvironment.cs new file mode 100644 index 00000000..726d82a5 --- /dev/null +++ b/Kudu.Core/LinuxConsumption/ISystemEnvironment.cs @@ -0,0 +1,9 @@ +namespace Kudu.Core.LinuxConsumption +{ + public interface ISystemEnvironment + { + string GetEnvironmentVariable(string name); + + void SetEnvironmentVariable(string name, string value); + } +} diff --git a/Kudu.Core/LinuxConsumptionEnvironment.cs b/Kudu.Core/LinuxConsumption/LinuxConsumptionEnvironment.cs similarity index 94% rename from Kudu.Core/LinuxConsumptionEnvironment.cs rename to Kudu.Core/LinuxConsumption/LinuxConsumptionEnvironment.cs index 51bdd7bd..f539c89b 100644 --- a/Kudu.Core/LinuxConsumptionEnvironment.cs +++ b/Kudu.Core/LinuxConsumption/LinuxConsumptionEnvironment.cs @@ -1,12 +1,10 @@ using System; -using System.Collections.Generic; -using System.Text; using System.Threading; using System.Threading.Tasks; using Kudu.Contracts; using Kudu.Contracts.Settings; -namespace Kudu.Core +namespace Kudu.Core.LinuxConsumption { public class LinuxConsumptionEnvironment : ILinuxConsumptionEnvironment { @@ -59,8 +57,6 @@ public bool InStandbyMode } } - Task ILinuxConsumptionEnvironment.DelayCompletionTask => throw new NotImplementedException(); - public void DelayRequests() { _delayLock.EnterUpgradeableReadLock(); diff --git a/Kudu.Core/LinuxConsumption/MeshPersistentFileSystem.cs b/Kudu.Core/LinuxConsumption/MeshPersistentFileSystem.cs new file mode 100644 index 00000000..579edae5 --- /dev/null +++ b/Kudu.Core/LinuxConsumption/MeshPersistentFileSystem.cs @@ -0,0 +1,160 @@ +using System; +using System.Diagnostics.Tracing; +using System.IO; +using System.Threading.Tasks; +using Kudu.Core.Infrastructure; +using Kudu.Core.Tracing; + +namespace Kudu.Core.LinuxConsumption +{ + /// + /// Provides persistent storage using Fileshares + /// + public class MeshPersistentFileSystem : IMeshPersistentFileSystem + { + private const string FileShareFormat = "{0}-{1}"; + + private readonly ISystemEnvironment _environment; + private readonly IMeshServiceClient _meshServiceClient; + private readonly IStorageClient _storageClient; + + private bool _fileShareMounted; + private string _fileShareMountMessage; + + public MeshPersistentFileSystem(ISystemEnvironment environment, IMeshServiceClient meshServiceClient, IStorageClient storageClient) + { + _fileShareMounted = false; + _fileShareMountMessage = string.Empty; + _environment = environment; + _meshServiceClient = meshServiceClient; + _storageClient = storageClient; + } + + private bool IsPersistentStorageEnabled() + { + var persistentStorageEnabled = _environment.GetEnvironmentVariable(Constants.EnablePersistentStorage); + if (!string.IsNullOrWhiteSpace(persistentStorageEnabled)) + { + return string.Equals("1", persistentStorageEnabled, StringComparison.OrdinalIgnoreCase) || + string.Equals("true", persistentStorageEnabled, StringComparison.OrdinalIgnoreCase); + } + + return false; + } + + private bool TryGetStorageConnectionString(out string connectionString) + { + connectionString = _environment.GetEnvironmentVariable(Constants.AzureWebJobsStorage); + return !string.IsNullOrWhiteSpace(connectionString); + } + + private bool IsLinuxConsumption() + { + return !string.IsNullOrEmpty(_environment.GetEnvironmentVariable(Constants.ContainerName)); + } + + private bool IsKuduShareMounted() + { + return _fileShareMounted; + } + + private void UpdateStatus(bool status, string message) + { + _fileShareMounted = status; + _fileShareMountMessage = message; + } + + /// + /// Mounts file share + /// + /// + public async Task MountFileShare() + { + var siteName = ServerConfiguration.GetApplicationName(); + + if (IsKuduShareMounted()) + { + const string message = "Kudu file share mounted already"; + UpdateStatus(true, message); + KuduEventGenerator.Log(_environment).LogMessage(EventLevel.Warning, siteName, nameof(MountFileShare), message); + return true; + } + + if (!IsLinuxConsumption()) + { + const string message = + "Mounting kudu file share is only supported on Linux consumption environment"; + UpdateStatus(false, message); + KuduEventGenerator.Log(_environment).LogMessage(EventLevel.Warning, siteName, nameof(MountFileShare), message); + return false; + } + + if (!IsPersistentStorageEnabled()) + { + const string message = "Kudu file share was not mounted since persistent storage is disabled"; + UpdateStatus(false, message); + KuduEventGenerator.Log(_environment).LogMessage(EventLevel.Warning, siteName, nameof(MountFileShare), message); + return false; + } + + if (!TryGetStorageConnectionString(out var connectionString)) + { + var message = $"Kudu file share was not mounted since {Constants.AzureWebJobsStorage} is empty"; + UpdateStatus(false, message); + KuduEventGenerator.Log(_environment).LogMessage(EventLevel.Warning, siteName, nameof(MountFileShare), message); + return false; + } + + var errorMessage = await MountKuduFileShare(siteName, connectionString); + var mountResult = string.IsNullOrEmpty(errorMessage); + + UpdateStatus(mountResult, errorMessage); + KuduEventGenerator.Log(_environment).LogMessage(EventLevel.Informational, siteName, + $"Mounting Kudu file share result: {mountResult}", string.Empty); + + return mountResult; + } + + public bool GetStatus(out string message) + { + message = _fileShareMountMessage; + return _fileShareMounted; + } + + public string GetDeploymentsPath() + { + if (_fileShareMounted) + { + return Path.Combine(Constants.KuduFileShareMountPath, "deployments"); + } + + return null; + } + + private async Task MountKuduFileShare(string siteName, string connectionString) + { + try + { + var fileShareName = string.Format(FileShareFormat, Constants.KuduFileSharePrefix, + ServerConfiguration.GetApplicationName().ToLowerInvariant()); + + await _storageClient.CreateFileShare(siteName, connectionString, fileShareName); + + KuduEventGenerator.Log(_environment).LogMessage(EventLevel.Informational, siteName, + $"Mounting Kudu mount file share {fileShareName} at {Constants.KuduFileShareMountPath}", + string.Empty); + + await _meshServiceClient.MountCifs(connectionString, fileShareName, Constants.KuduFileShareMountPath); + + return string.Empty; + } + catch (Exception e) + { + var message = e.ToString(); + KuduEventGenerator.Log(_environment) + .LogMessage(EventLevel.Warning, siteName, nameof(MountKuduFileShare), message); + return message; + } + } + } +} diff --git a/Kudu.Core/LinuxConsumption/MeshServiceClient.cs b/Kudu.Core/LinuxConsumption/MeshServiceClient.cs new file mode 100644 index 00000000..d09922ba --- /dev/null +++ b/Kudu.Core/LinuxConsumption/MeshServiceClient.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Kudu.Core.Infrastructure; +using Kudu.Core.Tracing; +using Microsoft.WindowsAzure.Storage; + +namespace Kudu.Core.LinuxConsumption +{ + public class MeshServiceClient : IMeshServiceClient + { + private readonly ISystemEnvironment _environment; + private readonly HttpClient _client; + private const string Operation = "operation"; + + public MeshServiceClient(ISystemEnvironment environment, HttpClient client) + { + _environment = environment ?? throw new ArgumentNullException(nameof(environment)); + _client = client ?? throw new ArgumentNullException(nameof(client)); + } + + public async Task MountCifs(string connectionString, string contentShare, string targetPath) + { + var sa = CloudStorageAccount.Parse(connectionString); + var key = Convert.ToBase64String(sa.Credentials.ExportKey()); + var response = await SendAsync(new[] + { + new KeyValuePair(Operation, "cifs"), + new KeyValuePair("host", sa.FileEndpoint.Host), + new KeyValuePair("accountName", sa.Credentials.AccountName), + new KeyValuePair("accountKey", key), + new KeyValuePair("contentShare", contentShare), + new KeyValuePair("targetPath", targetPath), + }); + + response.EnsureSuccessStatusCode(); + } + + private async Task SendAsync(IEnumerable> formData) + { + var operationName = formData.FirstOrDefault(f => string.Equals(f.Key, Operation)).Value; + var meshUri = _environment.GetEnvironmentVariable(Constants.MeshInitURI); + + KuduEventGenerator.Log(_environment).GenericEvent(ServerConfiguration.GetApplicationName(), + $"Sending mesh request {operationName} to {meshUri}", string.Empty, string.Empty, string.Empty, string.Empty); + + var res = await _client.PostAsync(meshUri, new FormUrlEncodedContent(formData)); + + KuduEventGenerator.Log(_environment).GenericEvent(ServerConfiguration.GetApplicationName(), + $"Mesh response {res.StatusCode}", string.Empty, string.Empty, string.Empty, string.Empty); + return res; + } + } +} \ No newline at end of file diff --git a/Kudu.Core/LinuxConsumption/NullMeshPersistentFileSystem.cs b/Kudu.Core/LinuxConsumption/NullMeshPersistentFileSystem.cs new file mode 100644 index 00000000..00f7dab2 --- /dev/null +++ b/Kudu.Core/LinuxConsumption/NullMeshPersistentFileSystem.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; + +namespace Kudu.Core.LinuxConsumption +{ + public class NullMeshPersistentFileSystem : IMeshPersistentFileSystem + { + public Task MountFileShare() + { + return Task.FromResult(false); + } + + public bool GetStatus(out string message) + { + message = string.Empty; + return false; + } + + public string GetDeploymentsPath() + { + return string.Empty; + } + } +} \ No newline at end of file diff --git a/Kudu.Core/LinuxConsumption/NullMeshServiceClient.cs b/Kudu.Core/LinuxConsumption/NullMeshServiceClient.cs new file mode 100644 index 00000000..4f76f3eb --- /dev/null +++ b/Kudu.Core/LinuxConsumption/NullMeshServiceClient.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; + +namespace Kudu.Core.LinuxConsumption +{ + public class NullMeshServiceClient : IMeshServiceClient + { + public Task MountCifs(string connectionString, string contentShare, string targetPath) + { + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/Kudu.Core/LinuxConsumption/StorageClient.cs b/Kudu.Core/LinuxConsumption/StorageClient.cs new file mode 100644 index 00000000..c52cdc33 --- /dev/null +++ b/Kudu.Core/LinuxConsumption/StorageClient.cs @@ -0,0 +1,42 @@ +using System; +using System.Diagnostics.Tracing; +using System.Threading.Tasks; +using Kudu.Core.Tracing; +using Microsoft.WindowsAzure.Storage; +using Microsoft.WindowsAzure.Storage.File; + +namespace Kudu.Core.LinuxConsumption +{ + public class StorageClient : IStorageClient + { + private readonly ISystemEnvironment _environment; + + public StorageClient(ISystemEnvironment environment) + { + _environment = environment; + } + + public async Task CreateFileShare(string siteName, string connectionString, string fileShareName) + { + try + { + var storageAccount = CloudStorageAccount.Parse(connectionString); + var fileClient = storageAccount.CreateCloudFileClient(); + + // Get a reference to the file share we created previously. + CloudFileShare share = fileClient.GetShareReference(fileShareName); + + KuduEventGenerator.Log(_environment).LogMessage(EventLevel.Informational, siteName, + $"Creating Kudu mount file share {fileShareName}", string.Empty); + + await share.CreateIfNotExistsAsync(new FileRequestOptions(), new OperationContext()); + } + catch (Exception e) + { + KuduEventGenerator.Log(_environment) + .LogMessage(EventLevel.Warning, siteName, nameof(CreateFileShare), e.ToString()); + throw; + } + } + } +} \ No newline at end of file diff --git a/Kudu.Core/LinuxConsumption/SystemEnvironment.cs b/Kudu.Core/LinuxConsumption/SystemEnvironment.cs new file mode 100644 index 00000000..582bed87 --- /dev/null +++ b/Kudu.Core/LinuxConsumption/SystemEnvironment.cs @@ -0,0 +1,30 @@ +using System; + +namespace Kudu.Core.LinuxConsumption +{ + public class SystemEnvironment : ISystemEnvironment + { + private static readonly Lazy _instance = new Lazy(CreateInstance); + + private SystemEnvironment() + { + } + + public static SystemEnvironment Instance => _instance.Value; + + private static SystemEnvironment CreateInstance() + { + return new SystemEnvironment(); + } + + public string GetEnvironmentVariable(string name) + { + return System.Environment.GetEnvironmentVariable(name); + } + + public void SetEnvironmentVariable(string name, string value) + { + System.Environment.SetEnvironmentVariable(name, value); + } + } +} \ No newline at end of file diff --git a/Kudu.Core/Tracing/ConsoleEventGenerator.cs b/Kudu.Core/Tracing/ConsoleEventGenerator.cs deleted file mode 100644 index 52856b75..00000000 --- a/Kudu.Core/Tracing/ConsoleEventGenerator.cs +++ /dev/null @@ -1,158 +0,0 @@ -using System; -using System.Diagnostics.Tracing; -using System.IO; -using Kudu.Core.Settings; - -namespace Kudu.Core.Tracing -{ - class ConsoleEventGenerator : IKuduEventGenerator - { - private readonly Action _writeEvent; - private readonly bool _consoleEnabled = true; - - public ConsoleEventGenerator() - { - _writeEvent = ConsoleWriter; - } - - public void ProjectDeployed(string siteName, string projectType, string result, string error, long deploymentDurationInMilliseconds, string siteMode, string scmType, string vsProjectId) - { - KuduEvent kuduEvent = new KuduEvent - { - siteName = siteName, - projectType = projectType, - result = result, - error = error, - deploymentDurationInMilliseconds = deploymentDurationInMilliseconds, - siteMode = siteMode, - scmType = scmType, - vsProjectId = vsProjectId - }; - - LogKuduTraceEvent(kuduEvent); - } - - public void WebJobStarted(string siteName, string jobName, string scriptExtension, string jobType, string siteMode, string error, string trigger) - { - KuduEvent kuduEvent = new KuduEvent - { - siteName = siteName, - jobName = jobName, - scriptExtension = scriptExtension, - jobType = jobType, - siteMode = siteMode, - error = error, - trigger = trigger - }; - - LogKuduTraceEvent(kuduEvent); - } - - public void KuduException(string siteName, string method, string path, string result, string Message, string exception) - { - KuduEvent kuduEvent = new KuduEvent - { - level = (int)EventLevel.Warning, - siteName = siteName, - method = method, - path = path, - result = result, - Message = Message, - exception = exception - }; - - LogKuduTraceEvent(kuduEvent); - } - - public void DeprecatedApiUsed(string siteName, string route, string userAgent, string method, string path) - { - KuduEvent kuduEvent = new KuduEvent - { - level = (int)EventLevel.Warning, - siteName = siteName, - route = route, - userAgent = userAgent, - method = method, - path = path - }; - - LogKuduTraceEvent(kuduEvent); - } - - public void KuduSiteExtensionEvent(string siteName, string method, string path, string result, string deploymentDurationInMilliseconds, string Message) - { - long duration = 0; - long.TryParse(deploymentDurationInMilliseconds, out duration); - KuduEvent kuduEvent = new KuduEvent - { - siteName = siteName, - method = method, - path = path, - result = result, - deploymentDurationInMilliseconds = duration, - Message = Message - }; - - LogKuduTraceEvent(kuduEvent); - } - - public void WebJobEvent(string siteName, string jobName, string Message, string jobType, string error) - { - KuduEvent kuduEvent = new KuduEvent - { - siteName = siteName, - jobName = jobName, - Message = Message, - jobType = jobType, - error = error - }; - - LogKuduTraceEvent(kuduEvent); - } - - public void GenericEvent(string siteName, string Message, string requestId, string scmType, string siteMode, string buildVersion) - { - KuduEvent kuduEvent = new KuduEvent - { - siteName = siteName, - Message = Message, - requestId = requestId, - scmType = scmType, - siteMode = siteMode, - buildVersion = buildVersion - }; - - LogKuduTraceEvent(kuduEvent); - } - - public void ApiEvent(string siteName, string Message, string address, string verb, string requestId, int statusCode, long latencyInMilliseconds, string userAgent) - { - KuduEvent kuduEvent = new KuduEvent - { - siteName = siteName, - Message = Message, - address = address, - verb = verb, - requestId = requestId, - statusCode = statusCode, - latencyInMilliseconds = latencyInMilliseconds, - userAgent = userAgent - }; - - LogKuduTraceEvent(kuduEvent); - } - - public void LogKuduTraceEvent(KuduEvent kuduEvent) - { - _writeEvent($"{Constants.LinuxLogEventStreamName} {kuduEvent.ToString()},{Environment.StampName},{Environment.TenantId},{Environment.ContainerName}"); - } - - private void ConsoleWriter(string evt) - { - if (_consoleEnabled) - { - Console.WriteLine(evt); - } - } - } -} diff --git a/Kudu.Core/Tracing/DefaultKuduEventGenerator.cs b/Kudu.Core/Tracing/DefaultKuduEventGenerator.cs index 5136cb63..5bada329 100644 --- a/Kudu.Core/Tracing/DefaultKuduEventGenerator.cs +++ b/Kudu.Core/Tracing/DefaultKuduEventGenerator.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text; +using System.Diagnostics.Tracing; namespace Kudu.Core.Tracing { @@ -45,5 +46,11 @@ public void ApiEvent(string siteName, string Message, string address, string ver { KuduEventSource.Log.ApiEvent(siteName, Message, address, verb, requestId, statusCode, latencyInMilliseconds, userAgent); } + + public void LogMessage(EventLevel logLevel, string siteName, string message, string exception) + { + // Only used in Linux consumption currently + return; + } } } diff --git a/Kudu.Core/Tracing/IKuduEventGenerator.cs b/Kudu.Core/Tracing/IKuduEventGenerator.cs index 371b12d0..56fe0275 100644 --- a/Kudu.Core/Tracing/IKuduEventGenerator.cs +++ b/Kudu.Core/Tracing/IKuduEventGenerator.cs @@ -20,5 +20,7 @@ public interface IKuduEventGenerator void GenericEvent(string siteName, string Message, string requestId, string scmType, string siteMode, string buildVersion); void ApiEvent(string siteName, string Message, string address, string verb, string requestId, int statusCode, long latencyInMilliseconds, string userAgent); + + void LogMessage(EventLevel logLevel, string siteName, string message, string exception); } } diff --git a/Kudu.Core/Tracing/KuduEvent.cs b/Kudu.Core/Tracing/KuduEvent.cs index 69863425..db894ba7 100644 --- a/Kudu.Core/Tracing/KuduEvent.cs +++ b/Kudu.Core/Tracing/KuduEvent.cs @@ -5,7 +5,7 @@ namespace Kudu.Core.Tracing { - class KuduEvent + public class KuduEvent { public int level = (int)EventLevel.Informational; public string siteName = string.Empty; diff --git a/Kudu.Core/Tracing/KuduEventGenerator.cs b/Kudu.Core/Tracing/KuduEventGenerator.cs index f141e563..9118a9e8 100644 --- a/Kudu.Core/Tracing/KuduEventGenerator.cs +++ b/Kudu.Core/Tracing/KuduEventGenerator.cs @@ -1,4 +1,5 @@ using Kudu.Core.Helpers; +using Kudu.Core.LinuxConsumption; namespace Kudu.Core.Tracing { @@ -6,10 +7,14 @@ public class KuduEventGenerator { private static IKuduEventGenerator _eventGenerator = null; - public static IKuduEventGenerator Log() + public static IKuduEventGenerator Log(ISystemEnvironment systemEnvironment = null) { + string containerName = systemEnvironment != null + ? systemEnvironment.GetEnvironmentVariable(Constants.ContainerName) + : Environment.ContainerName; + // Linux Consumptions only - bool isLinuxContainer = !string.IsNullOrEmpty(Environment.ContainerName); + bool isLinuxContainer = !string.IsNullOrEmpty(containerName); if (isLinuxContainer) { if (_eventGenerator == null) diff --git a/Kudu.Core/Tracing/LinuxContainerEventGenerator.cs b/Kudu.Core/Tracing/LinuxContainerEventGenerator.cs index 235c8eef..8cc0bec2 100644 --- a/Kudu.Core/Tracing/LinuxContainerEventGenerator.cs +++ b/Kudu.Core/Tracing/LinuxContainerEventGenerator.cs @@ -5,7 +5,7 @@ namespace Kudu.Core.Tracing { - class LinuxContainerEventGenerator : IKuduEventGenerator + public class LinuxContainerEventGenerator : IKuduEventGenerator { private readonly Action _writeEvent; private readonly bool _consoleEnabled = true; @@ -142,6 +142,19 @@ public void ApiEvent(string siteName, string Message, string address, string ver LogKuduTraceEvent(kuduEvent); } + public void LogMessage(EventLevel logLevel, string siteName, string message, string exception) + { + var kuduEvent = new KuduEvent + { + level = (int)logLevel, + siteName = siteName, + Message = message, + exception = exception + }; + + LogKuduTraceEvent(kuduEvent); + } + public void LogKuduTraceEvent(KuduEvent kuduEvent) { _writeEvent($"{Constants.LinuxLogEventStreamName} {kuduEvent.ToString()},{Environment.StampName},{Environment.TenantId},{Environment.ContainerName}"); diff --git a/Kudu.Core/Tracing/Log4NetEventGenerator.cs b/Kudu.Core/Tracing/Log4NetEventGenerator.cs index 530a0c6e..d7810324 100644 --- a/Kudu.Core/Tracing/Log4NetEventGenerator.cs +++ b/Kudu.Core/Tracing/Log4NetEventGenerator.cs @@ -143,6 +143,12 @@ public void ApiEvent(string siteName, string Message, string address, string ver LogKuduTraceEvent(kuduEvent); } + public void LogMessage(EventLevel logLevel, string siteName, string message, string exception) + { + // Only used in Linux consumption + return; + } + public void LogKuduTraceEvent(KuduEvent kuduEvent) { diff --git a/Kudu.Services.Web/KuduWebUtil.cs b/Kudu.Services.Web/KuduWebUtil.cs index c0eeff5b..5d63697f 100644 --- a/Kudu.Services.Web/KuduWebUtil.cs +++ b/Kudu.Services.Web/KuduWebUtil.cs @@ -11,6 +11,7 @@ using Kudu.Core.Deployment; using Kudu.Core.Helpers; using Kudu.Core.Infrastructure; +using Kudu.Core.LinuxConsumption; using Kudu.Core.Settings; using Kudu.Core.Tracing; using Kudu.Services.Infrastructure; @@ -299,6 +300,7 @@ internal static ILogger GetDeploymentLogger(IServiceProvider serviceProvider) /// default configuration during the runtime. /// internal static IEnvironment GetEnvironment(IHostingEnvironment hostingEnvironment, + IFileSystemPathProvider fileSystemPathsProvider, IDeploymentSettingsManager settings = null, IHttpContextAccessor httpContextAccessor = null) { @@ -311,7 +313,7 @@ internal static IEnvironment GetEnvironment(IHostingEnvironment hostingEnvironme var kuduConsoleFullPath = Path.Combine(AppContext.BaseDirectory, KuduConsoleRelativePath, KuduConsoleFilename); return new Environment(root, EnvironmentHelper.NormalizeBinPath(binPath), repositoryPath, requestId, - kuduConsoleFullPath, httpContextAccessor); + kuduConsoleFullPath, httpContextAccessor, fileSystemPathsProvider); } /// diff --git a/Kudu.Services.Web/Startup.cs b/Kudu.Services.Web/Startup.cs index 981e2cae..25c95239 100644 --- a/Kudu.Services.Web/Startup.cs +++ b/Kudu.Services.Web/Startup.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Configuration; using System.IO; -using System.Net; +using System.Net.Http; using System.Net.Http.Formatting; using System.Reflection; using AspNetCore.RouteAnalyzer; @@ -17,6 +17,7 @@ using Kudu.Core.Deployment; using Kudu.Core.Helpers; using Kudu.Core.Infrastructure; +using Kudu.Core.LinuxConsumption; using Kudu.Core.Scan; using Kudu.Core.Settings; using Kudu.Core.SourceControl; @@ -25,7 +26,6 @@ using Kudu.Services.Diagnostics; using Kudu.Services.GitServer; using Kudu.Services.Performance; -using Kudu.Services.Scan; using Kudu.Services.TunnelServer; using Kudu.Services.Web.Infrastructure; using Kudu.Services.Web.Tracing; @@ -37,10 +37,10 @@ using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Serialization; using Swashbuckle.AspNetCore.Swagger; -using Environment = Kudu.Core.Environment; using ILogger = Kudu.Core.Deployment.ILogger; namespace Kudu.Services.Web @@ -120,17 +120,51 @@ public void ConfigureServices(IServiceCollection services) }); services.AddSingleton(new HttpContextAccessor()); + services.TryAddSingleton(SystemEnvironment.Instance); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); KuduWebUtil.EnsureHomeEnvironmentVariable(); KuduWebUtil.EnsureSiteBitnessEnvironmentVariable(); - IEnvironment environment = KuduWebUtil.GetEnvironment(_hostingEnvironment); + var fileSystemPathProvider = new FileSystemPathProvider(new MeshPersistentFileSystem(SystemEnvironment.Instance, + new MeshServiceClient(SystemEnvironment.Instance, new HttpClient()), new StorageClient(SystemEnvironment.Instance))); + IEnvironment environment = KuduWebUtil.GetEnvironment(_hostingEnvironment, fileSystemPathProvider); _webAppRuntimeEnvironment = environment; + services.AddSingleton(_ => new HttpClient()); + services.AddSingleton(s => + { + if (_webAppRuntimeEnvironment.IsOnLinuxConsumption) + { + var httpClient = s.GetService(); + var systemEnvironment = s.GetService(); + return new MeshServiceClient(systemEnvironment, httpClient); + } + else + { + return new NullMeshServiceClient(); + } + }); + services.AddSingleton(s => + { + if (_webAppRuntimeEnvironment.IsOnLinuxConsumption) + { + var meshServiceClient = s.GetService(); + var storageClient = s.GetService(); + var systemEnvironment = s.GetService(); + return new MeshPersistentFileSystem(systemEnvironment, meshServiceClient, storageClient); + } + else + { + return new NullMeshPersistentFileSystem(); + } + }); + KuduWebUtil.EnsureDotNetCoreEnvironmentVariable(environment); // CORE TODO Check this @@ -157,7 +191,7 @@ public void ConfigureServices(IServiceCollection services) // Per request environment services.AddScoped(sp => - KuduWebUtil.GetEnvironment(_hostingEnvironment, sp.GetRequiredService())); + KuduWebUtil.GetEnvironment(_hostingEnvironment, sp.GetRequiredService (), sp.GetRequiredService())); services.AddDeploymentServices(environment); @@ -321,6 +355,7 @@ public void Configure(IApplicationBuilder app, if (_webAppRuntimeEnvironment.IsOnLinuxConsumption) { app.UseLinuxConsumptionRouteMiddleware(); + app.UseMiddleware(); } var webSocketOptions = new WebSocketOptions() diff --git a/Kudu.Services/LinuxConsumptionInstanceAdmin/EnvironmentReadyCheckMiddleware.cs b/Kudu.Services/LinuxConsumptionInstanceAdmin/EnvironmentReadyCheckMiddleware.cs new file mode 100644 index 00000000..fda196db --- /dev/null +++ b/Kudu.Services/LinuxConsumptionInstanceAdmin/EnvironmentReadyCheckMiddleware.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using Kudu.Contracts; +using Microsoft.AspNetCore.Http; + +namespace Kudu.Services.LinuxConsumptionInstanceAdmin +{ + /// + /// Middleware used in Linux consumption to delay requests until specialization is complete. + /// + public class EnvironmentReadyCheckMiddleware + { + private readonly RequestDelegate _next; + + public EnvironmentReadyCheckMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext httpContext, ILinuxConsumptionEnvironment environment) + { + if (environment.DelayRequestsEnabled) + { + await environment.DelayCompletionTask; + } + + await _next.Invoke(httpContext); + } + } +} diff --git a/Kudu.Services/LinuxConsumptionInstanceAdmin/HealthStatus.cs b/Kudu.Services/LinuxConsumptionInstanceAdmin/HealthStatus.cs new file mode 100644 index 00000000..a21b0461 --- /dev/null +++ b/Kudu.Services/LinuxConsumptionInstanceAdmin/HealthStatus.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace Kudu.Services.LinuxConsumptionInstanceAdmin +{ + /// + /// Health status response + /// + public class InstanceHealth + { + /// + /// Collection of health status per component + /// + public List Items; + } +} diff --git a/Kudu.Services/LinuxConsumptionInstanceAdmin/ILinuxConsumptionInstanceManager.cs b/Kudu.Services/LinuxConsumptionInstanceAdmin/ILinuxConsumptionInstanceManager.cs index ad232765..90981335 100644 --- a/Kudu.Services/LinuxConsumptionInstanceAdmin/ILinuxConsumptionInstanceManager.cs +++ b/Kudu.Services/LinuxConsumptionInstanceAdmin/ILinuxConsumptionInstanceManager.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Threading.Tasks; using Kudu.Services.Models; namespace Kudu.Services.LinuxConsumptionInstanceAdmin @@ -15,13 +14,6 @@ public interface ILinuxConsumptionInstanceManager /// IDictionary GetInstanceInfo(); - /// - /// Validate if the assignment context matches the requirement for current instance's specialization - /// - /// Contains site information such as environment variables - /// Message on error, otherwise, null. - Task ValidateContext(HostAssignmentContext assignmentContext); - /// /// Apply assignment context and start specialization for current instance /// diff --git a/Kudu.Services/LinuxConsumptionInstanceAdmin/InstanceHealthItem.cs b/Kudu.Services/LinuxConsumptionInstanceAdmin/InstanceHealthItem.cs new file mode 100644 index 00000000..3e99f0cc --- /dev/null +++ b/Kudu.Services/LinuxConsumptionInstanceAdmin/InstanceHealthItem.cs @@ -0,0 +1,23 @@ +namespace Kudu.Services.LinuxConsumptionInstanceAdmin +{ + /// + /// Individual health status item + /// + public class InstanceHealthItem + { + /// + /// Name of the component + /// + public string Name { get; set; } + + /// + /// Success/failure + /// + public bool Success { get; set; } + + /// + /// Additional details + /// + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/Kudu.Services/LinuxConsumptionInstanceAdmin/LinuxConsumptionInstanceAdminController.cs b/Kudu.Services/LinuxConsumptionInstanceAdmin/LinuxConsumptionInstanceAdminController.cs index 03c79875..75f81631 100644 --- a/Kudu.Services/LinuxConsumptionInstanceAdmin/LinuxConsumptionInstanceAdminController.cs +++ b/Kudu.Services/LinuxConsumptionInstanceAdmin/LinuxConsumptionInstanceAdminController.cs @@ -6,6 +6,7 @@ using Kudu.Contracts.Settings; using Kudu.Core.Helpers; using System.Collections.Generic; +using Kudu.Core.LinuxConsumption; using Kudu.Services.Infrastructure.Authorization; namespace Kudu.Services.LinuxConsumptionInstanceAdmin @@ -19,6 +20,7 @@ public class LinuxConsumptionInstanceAdminController : Controller private const string _appsettingPrefix = "APPSETTING_"; private readonly ILinuxConsumptionInstanceManager _instanceManager; private readonly IDeploymentSettingsManager _settingsManager; + private readonly IMeshPersistentFileSystem _meshPersistentFileSystem; /// /// This class handles appsetting assignment and provide information when running in a Service Fabric Mesh container, @@ -26,10 +28,12 @@ public class LinuxConsumptionInstanceAdminController : Controller /// /// Allow KuduLite to interact with Service Fabric Mesh instance in Linux Consumption /// Allow instance assignment to change application setting - public LinuxConsumptionInstanceAdminController(ILinuxConsumptionInstanceManager instanceManager, IDeploymentSettingsManager settingsManager) + /// Provides persistent storage for Linux Consumption + public LinuxConsumptionInstanceAdminController(ILinuxConsumptionInstanceManager instanceManager, IDeploymentSettingsManager settingsManager, IMeshPersistentFileSystem meshPersistentFileSystem) { _instanceManager = instanceManager; _settingsManager = settingsManager; + _meshPersistentFileSystem = meshPersistentFileSystem; } /// @@ -40,7 +44,14 @@ public LinuxConsumptionInstanceAdminController(ILinuxConsumptionInstanceManager [Authorize(Policy = AuthPolicyNames.AdminAuthLevel)] public IActionResult Info() { - return Ok(_instanceManager.GetInstanceInfo()); + var instanceHealth = new InstanceHealth {Items = new List()}; + var fileShareMount = new InstanceHealthItem(); + fileShareMount.Name = "Persistent storage"; + fileShareMount.Success = _meshPersistentFileSystem.GetStatus(out var persistenceMessage); + fileShareMount.Message = persistenceMessage; + instanceHealth.Items.Add(fileShareMount); + + return Ok(instanceHealth); } /// diff --git a/Kudu.Services/LinuxConsumptionInstanceAdmin/LinuxConsumptionInstanceManager.cs b/Kudu.Services/LinuxConsumptionInstanceAdmin/LinuxConsumptionInstanceManager.cs index 813f4858..cddc4214 100644 --- a/Kudu.Services/LinuxConsumptionInstanceAdmin/LinuxConsumptionInstanceManager.cs +++ b/Kudu.Services/LinuxConsumptionInstanceAdmin/LinuxConsumptionInstanceManager.cs @@ -3,9 +3,11 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Net.Http; +using System.Diagnostics.Tracing; using System.Threading.Tasks; using Kudu.Core.Infrastructure; +using Kudu.Core.LinuxConsumption; +using Kudu.Core.Tracing; namespace Kudu.Services.LinuxConsumptionInstanceAdmin { @@ -18,16 +20,17 @@ public class LinuxConsumptionInstanceManager : ILinuxConsumptionInstanceManager private static HostAssignmentContext _assignmentContext; private readonly ILinuxConsumptionEnvironment _linuxConsumptionEnv; - private readonly HttpClient _client; + private readonly IMeshPersistentFileSystem _meshPersistentFileSystem; /// /// Create a manager to specialize KuduLite when it is running in Service Fabric Mesh /// /// Environment variables - public LinuxConsumptionInstanceManager(ILinuxConsumptionEnvironment linuxConsumptionEnv) + /// Provides persistent file storage + public LinuxConsumptionInstanceManager(ILinuxConsumptionEnvironment linuxConsumptionEnv, IMeshPersistentFileSystem meshPersistentFileSystem) { _linuxConsumptionEnv = linuxConsumptionEnv; - _client = new HttpClient(); + _meshPersistentFileSystem = meshPersistentFileSystem; } public IDictionary GetInstanceInfo() @@ -78,39 +81,6 @@ public bool StartAssignment(HostAssignmentContext context) } } - public async Task ValidateContext(HostAssignmentContext assignmentContext) - { - string error = null; - HttpResponseMessage response = null; - try - { - var zipUrl = assignmentContext.ZipUrl; - if (!string.IsNullOrEmpty(zipUrl)) - { - // make sure the zip uri is valid and accessible - await OperationManager.AttemptAsync(async () => - { - try - { - var request = new HttpRequestMessage(HttpMethod.Head, zipUrl); - response = await _client.SendAsync(request); - response.EnsureSuccessStatusCode(); - } - catch (Exception) - { - throw; - } - }, retries: 2, delayBeforeRetry: 300 /*ms*/); - } - } - catch (Exception) - { - error = $"Invalid zip url specified (StatusCode: {response?.StatusCode})"; - } - - return error; - } - private async Task Assign(HostAssignmentContext assignmentContext) { try @@ -118,6 +88,15 @@ private async Task Assign(HostAssignmentContext assignmentContext) // first make all environment and file system changes required for // the host to be specialized assignmentContext.ApplyAppSettings(); + + KuduEventGenerator.Log(null).LogMessage(EventLevel.Informational, assignmentContext.SiteName, + $"Mounting file share at {DateTime.UtcNow}", string.Empty); + + // Limit the amount of time time we allow for mounting to complete + var mounted = await MountFileShareWithin(TimeSpan.FromSeconds(30)); + + KuduEventGenerator.Log(null).LogMessage(EventLevel.Informational, assignmentContext.SiteName, + $"Mount file share result: {mounted} at {DateTime.UtcNow}", string.Empty); } catch (Exception) { @@ -133,5 +112,32 @@ private async Task Assign(HostAssignmentContext assignmentContext) _linuxConsumptionEnv.ResumeRequests(); } } + + private async Task MountFileShareWithin(TimeSpan timeLimit) + { + var startTime = DateTime.UtcNow; + + try + { + return await OperationManager.ExecuteWithTimeout(MountFileShare(), timeLimit); + } + catch (Exception e) + { + KuduEventGenerator.Log(null).LogMessage(EventLevel.Warning, ServerConfiguration.GetApplicationName(), + nameof(MountFileShareWithin), e.ToString()); + return false; + } + finally + { + KuduEventGenerator.Log(null).LogMessage(EventLevel.Informational, + ServerConfiguration.GetApplicationName(), + $"Time taken to mount = {(DateTime.UtcNow - startTime).TotalMilliseconds}", string.Empty); + } + } + + private async Task MountFileShare() + { + return await _meshPersistentFileSystem.MountFileShare(); + } } } diff --git a/Kudu.Tests/Kudu.Tests.csproj b/Kudu.Tests/Kudu.Tests.csproj index 35fea095..7d35357e 100644 --- a/Kudu.Tests/Kudu.Tests.csproj +++ b/Kudu.Tests/Kudu.Tests.csproj @@ -1,4 +1,4 @@ - + netcoreapp2.2 @@ -9,6 +9,7 @@ + diff --git a/Kudu.Tests/LinuxConsumption/FileSystemPathProviderTests.cs b/Kudu.Tests/LinuxConsumption/FileSystemPathProviderTests.cs new file mode 100644 index 00000000..82e346c0 --- /dev/null +++ b/Kudu.Tests/LinuxConsumption/FileSystemPathProviderTests.cs @@ -0,0 +1,67 @@ +using System.IO; +using System.IO.Abstractions; +using Kudu.Core.LinuxConsumption; +using Moq; +using Xunit; + +namespace Kudu.Tests.LinuxConsumption +{ + [Collection("LinuxConsumption")] + public class FileSystemPathProviderTests + { + [Fact] + public void ReturnsDeploymentsPath() + { + const string deploymentsPath = "/path-0"; + + var mockFileSystem = new Mock(MockBehavior.Strict); + var directory = new Mock(MockBehavior.Strict); + directory.Setup(d => d.Exists(deploymentsPath)).Returns(true); + mockFileSystem.SetupGet(f => f.Directory).Returns(directory.Object); + + using (new MockFileSystem(mockFileSystem.Object)) + { + var fileSystem = new Mock(MockBehavior.Strict); + const string expectedDeploymentsPath = deploymentsPath; + fileSystem.Setup(f => f.GetDeploymentsPath()).Returns(expectedDeploymentsPath); + + var fileSystemPathProvider = new FileSystemPathProvider(fileSystem.Object); + Assert.True(fileSystemPathProvider.TryGetDeploymentsPath(out string actualDeploymentsPath)); + Assert.Equal(expectedDeploymentsPath, actualDeploymentsPath); + } + } + + [Fact] + public void ReturnsEmptyPathWhenDeploymentDirectoryCreationFails() + { + const string deploymentsPath = "/path-0"; + + var mockFileSystem = new Mock(MockBehavior.Strict); + var directory = new Mock(MockBehavior.Strict); + directory.Setup(d => d.Exists(deploymentsPath)).Returns(false); + directory.Setup(d => d.CreateDirectory(deploymentsPath)).Throws(new IOException()); + mockFileSystem.SetupGet(f => f.Directory).Returns(directory.Object); + + using (new MockFileSystem(mockFileSystem.Object)) + { + var fileSystem = new Mock(MockBehavior.Strict); + fileSystem.Setup(f => f.GetDeploymentsPath()).Returns(deploymentsPath); + + var fileSystemPathProvider = new FileSystemPathProvider(fileSystem.Object); + Assert.False(fileSystemPathProvider.TryGetDeploymentsPath(out string actualDeploymentsPath)); + Assert.Equal(deploymentsPath, actualDeploymentsPath); + } + } + + [Fact] + public void ReturnsEmptyWhenNoDeploymentsPathConfigured() + { + var fileSystem = new Mock(MockBehavior.Strict); + fileSystem.Setup(f => f.GetDeploymentsPath()).Returns(string.Empty); + + var fileSystemPathProvider = new FileSystemPathProvider(fileSystem.Object); + Assert.False(fileSystemPathProvider.TryGetDeploymentsPath(out string actualDeploymentsPath)); + Assert.Equal(string.Empty, actualDeploymentsPath); + } + } +} diff --git a/Kudu.Tests/LinuxConsumption/MeshPersistentFileSystemTests.cs b/Kudu.Tests/LinuxConsumption/MeshPersistentFileSystemTests.cs new file mode 100644 index 00000000..29df3116 --- /dev/null +++ b/Kudu.Tests/LinuxConsumption/MeshPersistentFileSystemTests.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Kudu.Core.LinuxConsumption; +using Moq; +using Xunit; + +namespace Kudu.Tests.LinuxConsumption +{ + [Collection("LinuxConsumption")] + public class MeshPersistentFileSystemTests + { + private const string ConnectionString = "connection-string"; + + private readonly TestSystemEnvironment _systemEnvironment; + private readonly Mock _client; + private readonly Mock _storageClient; + + public MeshPersistentFileSystemTests() + { + var environmentVariables = new Dictionary(); + environmentVariables[Constants.ContainerName] = "container-name"; + environmentVariables[Constants.EnablePersistentStorage] = "1"; + environmentVariables[Constants.AzureWebJobsStorage] = ConnectionString; + + _systemEnvironment = new TestSystemEnvironment(environmentVariables); + + _client = new Mock(MockBehavior.Strict); + _storageClient = new Mock(MockBehavior.Strict); + + _storageClient.Setup(s => s.CreateFileShare(It.IsAny(), ConnectionString, It.IsAny())) + .Returns(Task.CompletedTask); + _client.Setup(c => c.MountCifs(ConnectionString, It.IsAny(), Constants.KuduFileShareMountPath)) + .Returns(Task.CompletedTask); + } + + [Fact] + public async Task MountShareMounted() + { + var meshPersistentFileSystem = + new MeshPersistentFileSystem(_systemEnvironment, _client.Object, _storageClient.Object); + + Assert.False(meshPersistentFileSystem.GetStatus(out var statusMessage)); + Assert.True(string.IsNullOrEmpty(statusMessage)); + + var mountResult = await meshPersistentFileSystem.MountFileShare(); + Assert.True(mountResult); + + Assert.True(meshPersistentFileSystem.GetStatus(out statusMessage)); + Assert.True(string.IsNullOrEmpty(statusMessage)); + Assert.True(!string.IsNullOrEmpty(meshPersistentFileSystem.GetDeploymentsPath())); + + _storageClient.Verify(s => s.CreateFileShare(It.IsAny(), ConnectionString, It.IsAny()), + Times.Once); + _client.Verify(c => c.MountCifs(ConnectionString, It.IsAny(), Constants.KuduFileShareMountPath), + Times.Once); + } + + [Fact] + public async Task MountsOnlyOnce() + { + var meshPersistentFileSystem = + new MeshPersistentFileSystem(_systemEnvironment, _client.Object, _storageClient.Object); + + Assert.False(meshPersistentFileSystem.GetStatus(out var statusMessage)); + Assert.True(string.IsNullOrEmpty(statusMessage)); + + // Mount once + var mountResult = await meshPersistentFileSystem.MountFileShare(); + Assert.True(mountResult); + + Assert.True(meshPersistentFileSystem.GetStatus(out statusMessage)); + Assert.True(string.IsNullOrEmpty(statusMessage)); + Assert.True(!string.IsNullOrEmpty(meshPersistentFileSystem.GetDeploymentsPath())); + + //Mount again + mountResult = await meshPersistentFileSystem.MountFileShare(); + Assert.True(mountResult); + Assert.True(meshPersistentFileSystem.GetStatus(out statusMessage)); + Assert.True(statusMessage.Contains("mounted already", StringComparison.Ordinal)); + Assert.True(!string.IsNullOrEmpty(meshPersistentFileSystem.GetDeploymentsPath())); + + // Assert share was mounted was called only once even if mount was called twice + _storageClient.Verify(s => s.CreateFileShare(It.IsAny(), ConnectionString, It.IsAny()), + Times.Once); + _client.Verify(c => c.MountCifs(ConnectionString, It.IsAny(), Constants.KuduFileShareMountPath), + Times.Once); + } + + [Fact] + public async Task MountsOnLinuxConsumptionOnly() + { + // Container name will be null on non-Linux consumption environments + _systemEnvironment.SetEnvironmentVariable(Constants.ContainerName, null); + + var meshPersistentFileSystem = + new MeshPersistentFileSystem(_systemEnvironment, _client.Object, _storageClient.Object); + + Assert.False(meshPersistentFileSystem.GetStatus(out string _)); + + var mountResult = await meshPersistentFileSystem.MountFileShare(); + Assert.False(mountResult); + + Assert.False(meshPersistentFileSystem.GetStatus(out var statusMessage)); + Assert.True(statusMessage.Contains("only supported on Linux consumption environment", StringComparison.Ordinal)); + + Assert.True(string.IsNullOrEmpty(meshPersistentFileSystem.GetDeploymentsPath())); + + _storageClient.Verify(s => s.CreateFileShare(It.IsAny(), ConnectionString, It.IsAny()), + Times.Never); + _client.Verify(c => c.MountCifs(ConnectionString, It.IsAny(), Constants.KuduFileShareMountPath), + Times.Never); + } + + + [Fact] + public async Task MountsOnlyIfPersistentStorageEnabled() + { + // Disable + _systemEnvironment.SetEnvironmentVariable(Constants.EnablePersistentStorage, null); + + var meshPersistentFileSystem = + new MeshPersistentFileSystem(_systemEnvironment, _client.Object, _storageClient.Object); + + Assert.False(meshPersistentFileSystem.GetStatus(out string _)); + + var mountResult = await meshPersistentFileSystem.MountFileShare(); + Assert.False(mountResult); + + Assert.False(meshPersistentFileSystem.GetStatus(out var statusMessage)); + Assert.True(statusMessage.Contains("persistent storage is disabled", StringComparison.Ordinal)); + + Assert.True(string.IsNullOrEmpty(meshPersistentFileSystem.GetDeploymentsPath())); + + _storageClient.Verify(s => s.CreateFileShare(It.IsAny(), ConnectionString, It.IsAny()), + Times.Never); + _client.Verify(c => c.MountCifs(ConnectionString, It.IsAny(), Constants.KuduFileShareMountPath), + Times.Never); + } + + [Fact] + public async Task MountsOnlyIfStorageAccountConfigured() + { + // Remove storage account + _systemEnvironment.SetEnvironmentVariable(Constants.AzureWebJobsStorage, null); + + var meshPersistentFileSystem = + new MeshPersistentFileSystem(_systemEnvironment, _client.Object, _storageClient.Object); + + Assert.False(meshPersistentFileSystem.GetStatus(out string _)); + + var mountResult = await meshPersistentFileSystem.MountFileShare(); + Assert.False(mountResult); + + Assert.False(meshPersistentFileSystem.GetStatus(out var statusMessage)); + Assert.True(statusMessage.Contains($"{Constants.AzureWebJobsStorage} is empty", StringComparison.Ordinal)); + + Assert.True(string.IsNullOrEmpty(meshPersistentFileSystem.GetDeploymentsPath())); + + _storageClient.Verify(s => s.CreateFileShare(It.IsAny(), ConnectionString, It.IsAny()), + Times.Never); + _client.Verify(c => c.MountCifs(ConnectionString, It.IsAny(), Constants.KuduFileShareMountPath), + Times.Never); + } + } +} diff --git a/Kudu.Tests/LinuxConsumption/MeshServiceClientTests.cs b/Kudu.Tests/LinuxConsumption/MeshServiceClientTests.cs new file mode 100644 index 00000000..447458f0 --- /dev/null +++ b/Kudu.Tests/LinuxConsumption/MeshServiceClientTests.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Kudu.Core.LinuxConsumption; +using Moq; +using Moq.Protected; +using Xunit; + +namespace Kudu.Tests.LinuxConsumption +{ + [Collection("MockedEnvironmentVariablesCollection")] + public class MeshServiceClientTests + { + private const string MeshInitUri = "http://local:6756/"; + private const string ConnectionString = "DefaultEndpointsProtocol=https;AccountName=storage-account;AccountKey=AAAABBBBBAAAABBBBBAAAABBBBBAAAABBBBBAAAABBBBBAAAABBBBBAAAABBBBBAAAABBBBBAAAABBBBBCCCCC=="; + + private readonly Mock _handlerMock; + private readonly MeshServiceClient _meshServiceClient; + + public MeshServiceClientTests() + { + var environmentVariables = new Dictionary + { + [Constants.ContainerName] = "container-name", + [Constants.MeshInitURI] = MeshInitUri + }; + + var systemEnvironment = new TestSystemEnvironment(environmentVariables); + + _handlerMock = new Mock(MockBehavior.Strict); + _meshServiceClient = new MeshServiceClient(systemEnvironment, new HttpClient(_handlerMock.Object)); + } + + [Fact] + public async Task DoesNotThrowWhenSuccessful() + { + _handlerMock.Protected().Setup>("SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()).ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK + }); + + await _meshServiceClient.MountCifs(ConnectionString, "share-name", "/target-path"); + + _handlerMock.Protected().Verify>("SendAsync", Times.Once(), + ItExpr.Is(r => string.Equals(MeshInitUri, r.RequestUri.AbsoluteUri)), + ItExpr.IsAny()); + } + + [Fact] + public async Task ThrowsExceptionOnFailure() + { + _handlerMock.Protected().Setup>("SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()).ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.InternalServerError + }); + + try + { + await _meshServiceClient.MountCifs(ConnectionString, "share-name", "/target-path"); + } + catch (HttpRequestException) + { + // Exception is expected + return; + } + + // Shouldn't reach here + Assert.False(true); + } + } +} diff --git a/Kudu.Tests/LinuxConsumption/MockFileSystem.cs b/Kudu.Tests/LinuxConsumption/MockFileSystem.cs new file mode 100644 index 00000000..63d38921 --- /dev/null +++ b/Kudu.Tests/LinuxConsumption/MockFileSystem.cs @@ -0,0 +1,22 @@ +using System; +using System.IO.Abstractions; +using Kudu.Core.Infrastructure; + +namespace Kudu.Tests.LinuxConsumption +{ + public class MockFileSystem : IDisposable + { + private readonly IFileSystem _previous; + + public MockFileSystem(IFileSystem fileSystem) + { + _previous = FileSystemHelpers.Instance; + FileSystemHelpers.Instance = fileSystem; + } + + public void Dispose() + { + FileSystemHelpers.Instance = _previous; + } + } +} diff --git a/Kudu.Tests/LinuxConsumption/TestFileSystemPathProvider.cs b/Kudu.Tests/LinuxConsumption/TestFileSystemPathProvider.cs new file mode 100644 index 00000000..9f86f2ea --- /dev/null +++ b/Kudu.Tests/LinuxConsumption/TestFileSystemPathProvider.cs @@ -0,0 +1,27 @@ +using System; +using Kudu.Core.LinuxConsumption; + +namespace Kudu.Tests.LinuxConsumption +{ + public class TestFileSystemPathProvider : IFileSystemPathProvider + { + private static readonly Lazy _instance = new Lazy(CreateInstance); + + private TestFileSystemPathProvider() + { + } + + public static TestFileSystemPathProvider Instance => _instance.Value; + + private static TestFileSystemPathProvider CreateInstance() + { + return new TestFileSystemPathProvider(); + } + + public bool TryGetDeploymentsPath(out string path) + { + path = null; + return false; + } + } +} diff --git a/Kudu.Tests/LinuxConsumption/TestSystemEnvironment.cs b/Kudu.Tests/LinuxConsumption/TestSystemEnvironment.cs new file mode 100644 index 00000000..cff0a82b --- /dev/null +++ b/Kudu.Tests/LinuxConsumption/TestSystemEnvironment.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Kudu.Core.LinuxConsumption; + +namespace Kudu.Tests.LinuxConsumption +{ + public class TestSystemEnvironment : ISystemEnvironment + { + private readonly Dictionary _environment; + + public TestSystemEnvironment(Dictionary environment = null) + { + _environment = environment ?? new Dictionary(); + } + + public string GetEnvironmentVariable(string name) + { + return _environment[name]; + } + + public void SetEnvironmentVariable(string name, string value) + { + _environment[name] = value; + } + } +} diff --git a/Kudu.Tests/Services/EnvironmentTests.cs b/Kudu.Tests/Services/EnvironmentTests.cs index 153e4fc0..9785b27e 100644 --- a/Kudu.Tests/Services/EnvironmentTests.cs +++ b/Kudu.Tests/Services/EnvironmentTests.cs @@ -1,7 +1,8 @@ -using Kudu.Core; -using Kudu.Services; -using Kudu.Tests; -using Microsoft.AspNetCore.Http; +using System.IO.Abstractions; +using Kudu.Core; +using Kudu.Core.LinuxConsumption; +using Kudu.Tests.LinuxConsumption; +using Moq; using Xunit; namespace Kudu.Tests.Services @@ -46,5 +47,60 @@ public void UnsetLinuxPlan() IEnvironment env = TestMockedEnvironment.GetMockedEnvironment(); Assert.False(env.IsOnLinuxConsumption); } + + delegate void GetDeploymentPathCallback(out string path); // needed for Callback + delegate bool GetDeploymentPathReturn(out string path); // needed for Returns + + [Fact] + public void ReturnsDeploymentsPath() + { + const string somePath = "/some-path"; + + var fileSystem = new Mock(MockBehavior.Strict); + var directory = new Mock(MockBehavior.Strict); + directory.Setup(d => d.Exists(It.IsAny())).Returns(true); + fileSystem.SetupGet(f => f.Directory).Returns(directory.Object); + + using (new MockFileSystem(fileSystem.Object)) + { + var fileSystemPathProvider = new Mock(MockBehavior.Strict); + + // Configure an override for deployments path + fileSystemPathProvider.Setup(f => f.TryGetDeploymentsPath(out It.Ref.IsAny)). + Callback(new GetDeploymentPathCallback((out string path) => + { + path = somePath; + })).Returns(new GetDeploymentPathReturn((out string path) => + { + path = somePath; + return true; + })); + + IEnvironment env = TestMockedEnvironment.GetMockedEnvironment(string.Empty, string.Empty, string.Empty, + string.Empty, string.Empty, fileSystemPathProvider.Object); + Assert.Equal(somePath, env.DeploymentsPath); + fileSystemPathProvider.Reset(); + + // Return false from filesystempath provider so the default deployments path is returned + fileSystemPathProvider.Setup(f => f.TryGetDeploymentsPath(out It.Ref.IsAny)). + Callback(new GetDeploymentPathCallback((out string path) => + { + path = somePath; + })).Returns(new GetDeploymentPathReturn((out string path) => + { + path = somePath; + return false; + })); + + env = TestMockedEnvironment.GetMockedEnvironment(string.Empty, string.Empty, string.Empty, + string.Empty, string.Empty, fileSystemPathProvider.Object); + Assert.NotEqual(somePath, env.DeploymentsPath); + fileSystemPathProvider.Reset(); + + // Uses default path when no override is set + env = TestMockedEnvironment.GetMockedEnvironment(); + Assert.NotEqual(somePath, env.DeploymentsPath); + } + } } } diff --git a/Kudu.Tests/Services/KuduEventGeneratorTests.cs b/Kudu.Tests/Services/KuduEventGeneratorTests.cs new file mode 100644 index 00000000..fc9087ea --- /dev/null +++ b/Kudu.Tests/Services/KuduEventGeneratorTests.cs @@ -0,0 +1,20 @@ +using Kudu.Core.Tracing; +using Kudu.Tests.LinuxConsumption; +using Xunit; + +namespace Kudu.Tests.Services +{ + [Collection("MockedEnvironmentVariablesCollection")] + public class KuduEventGeneratorTests + { + [Fact] + public void ReturnsLinuxEventGenerator() + { + var environment = new TestSystemEnvironment(); + environment.SetEnvironmentVariable(Constants.ContainerName, "container-name"); + + var eventGenerator = KuduEventGenerator.Log(environment); + Assert.True(eventGenerator is LinuxContainerEventGenerator); + } + } +} diff --git a/Kudu.Tests/TestMockedEnvironment.cs b/Kudu.Tests/TestMockedEnvironment.cs index 653dade2..6fa0166f 100644 --- a/Kudu.Tests/TestMockedEnvironment.cs +++ b/Kudu.Tests/TestMockedEnvironment.cs @@ -1,13 +1,15 @@ using Kudu.Core; +using Kudu.Core.LinuxConsumption; +using Kudu.Tests.LinuxConsumption; using Microsoft.AspNetCore.Http; namespace Kudu.Tests { public class TestMockedEnvironment { - public static IEnvironment GetMockedEnvironment(string rootPath = "rootPath", string binPath = "binPath", string repositoryPath = "repositoryPath", string requestId = "requestId", string kuduConsoleFullPath = "kuduConsoleFullPath") + public static IEnvironment GetMockedEnvironment(string rootPath = "rootPath", string binPath = "binPath", string repositoryPath = "repositoryPath", string requestId = "requestId", string kuduConsoleFullPath = "kuduConsoleFullPath", IFileSystemPathProvider fileSystemPathProvider = null) { - return new Environment(rootPath, binPath, repositoryPath, requestId, kuduConsoleFullPath, new HttpContextAccessor()); + return new Environment(rootPath, binPath, repositoryPath, requestId, kuduConsoleFullPath, new HttpContextAccessor(), fileSystemPathProvider ?? TestFileSystemPathProvider.Instance); } } } diff --git a/Kudu.Tests/TestMockedIEnvironment.cs b/Kudu.Tests/TestMockedIEnvironment.cs index 33608270..7175a34f 100644 --- a/Kudu.Tests/TestMockedIEnvironment.cs +++ b/Kudu.Tests/TestMockedIEnvironment.cs @@ -12,6 +12,7 @@ public class TestMockedIEnvironment : IEnvironment public string _RepositoryPath = "/site/repository"; public string _WebRootPath = "/site/wwwroot"; public string _DeploymentsPath = "/site/deployments"; + public string _artifactsPath = "/site/artifacts"; public string _DeploymentToolsPath = "/site/deployments/tools"; public string _SiteExtensionSettingsPath = "/site/siteextensions"; public string _DiagnosticsPath = "/site/diagnostics"; @@ -42,6 +43,7 @@ public class TestMockedIEnvironment : IEnvironment public string RepositoryPath { get => _RepositoryPath; set => _RepositoryPath = value; } public string WebRootPath => _WebRootPath; public string DeploymentsPath => _DeploymentsPath; + public string ArtifactsPath => _artifactsPath; public string DeploymentToolsPath => _DeploymentToolsPath; public string SiteExtensionSettingsPath => _SiteExtensionSettingsPath; public string DiagnosticsPath => _DiagnosticsPath;