From 98773610480521451cbe10846e33457268e02cc2 Mon Sep 17 00:00:00 2001 From: Ahmed ElSayed Date: Tue, 23 May 2017 11:45:06 -0700 Subject: [PATCH] Add API to download function app archive that can add local.settings.json and csproj file to the archive. --- Kudu.Contracts/Functions/IFunctionManager.cs | 2 + Kudu.Contracts/Kudu.Contracts.csproj | 1 + Kudu.Core/Functions/FunctionManager.cs | 108 ++++++++++++++++++ .../Infrastructure/ZipArchiveExtensions.cs | 29 +++-- .../App_Start/NinjectServices.cs | 1 + Kudu.Services/Functions/FunctionController.cs | 21 +++- build.cmd | 2 + 7 files changed, 155 insertions(+), 9 deletions(-) diff --git a/Kudu.Contracts/Functions/IFunctionManager.cs b/Kudu.Contracts/Functions/IFunctionManager.cs index 23e2e5004..106aaef2e 100644 --- a/Kudu.Contracts/Functions/IFunctionManager.cs +++ b/Kudu.Contracts/Functions/IFunctionManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO.Compression; using System.Threading.Tasks; using Kudu.Contracts.Functions; using Kudu.Contracts.Tracing; @@ -19,5 +20,6 @@ public interface IFunctionManager string GetAdminToken(); Task PutHostConfigAsync(JObject content); void DeleteFunction(string name, bool ignoreErrors); + void CreateArchive(ZipArchive archive, bool includeAppSettings = false, bool includeCsproj = false, string projectName = null); } } diff --git a/Kudu.Contracts/Kudu.Contracts.csproj b/Kudu.Contracts/Kudu.Contracts.csproj index b8fa2e387..32ca34fa4 100644 --- a/Kudu.Contracts/Kudu.Contracts.csproj +++ b/Kudu.Contracts/Kudu.Contracts.csproj @@ -78,6 +78,7 @@ + ..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Extensions.dll diff --git a/Kudu.Core/Functions/FunctionManager.cs b/Kudu.Core/Functions/FunctionManager.cs index 87e13810f..e41881540 100644 --- a/Kudu.Core/Functions/FunctionManager.cs +++ b/Kudu.Core/Functions/FunctionManager.cs @@ -1,8 +1,11 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Configuration; using System.IO; +using System.IO.Compression; using System.Linq; +using System.Text; using System.Threading.Tasks; using Kudu.Contracts.Functions; using Kudu.Contracts.Tracing; @@ -422,5 +425,110 @@ private string GetFunctionSecretsFilePath(string functionName) { return Path.Combine(_environment.DataPath, Constants.Functions, Constants.Secrets, $"{functionName}.json"); } + + /// + /// Populates a with the content of the function app. + /// It can also include local.settings.json and .csproj files for a full Visual Studio project. + /// sln file is not included since it changes between VS versions and VS can auto-generate it from the csproj. + /// All existing functions are added as content with "Always" copy to output. + /// + /// the to be populated with function app content. + /// Optional: indicates whether to add local.settings.json or not to the archive. Default is false. + /// Optional: indicates whether to add a .csproj to the archive. Default is false. + /// Optional: the name for *.csproj file if is true. Default is appName. + public void CreateArchive(ZipArchive zip, bool includeAppSettings = false, bool includeCsproj = false, string projectName = null) + { + var tracer = _traceFactory.GetTracer(); + var directoryInfo = FileSystemHelpers.DirectoryInfoFromDirectoryName(_environment.FunctionsPath); + + // First add the entire wwwroot folder at the root of the zip. + zip.AddDirectory(directoryInfo, tracer, string.Empty, out IList files); + + if (includeAppSettings) + { + // include local.settings.json if needed. + files.Add(AddAppSettingsFile(zip)); + } + + if (includeCsproj) + { + // include .csproj for Visual Studio if needed. + projectName = projectName ?? ServerConfiguration.GetApplicationName(); + AddCsprojFile(zip, files, projectName); + } + } + + /// + /// Creates a csproj file and adds it to the passed in ZipArchive + /// The csproj contain references to the core SDK, Http trigger, and build task + /// it also contain 'Always' copy entries for all the files that are in wwwroot. + /// + /// to add csproj file to. + /// of entries in the zip file to include in the csproj. + /// the {projectName}.csproj + private static ZipArchiveEntry AddCsprojFile(ZipArchive zip, IEnumerable files, string projectName) + { + const string microsoftAzureWebJobsVersion = "2.1.0-beta1"; + const string microsoftAzureWebJobsExtensionsHttpVersion = "1.0.0-beta1"; + const string microsoftNETSdkFunctionsVersion = "1.0.0-alpha4"; + + var csprojFile = new StringBuilder(); + csprojFile.AppendLine( + $@" + + net461 + + + + + + + + + "); + + csprojFile.AppendLine(" "); + foreach (var entry in files) + { + csprojFile.AppendLine( + $@" + Always + "); + } + csprojFile.AppendLine(" "); + csprojFile.AppendLine(""); + + return zip.AddFile($"{projectName}.csproj", csprojFile.ToString()); + } + + /// + /// creates a local.settings.json file and populates it with the values in AppSettings + /// The AppSettings are read from EnvVars with prefix APPSETTING_. + /// local.settings.json looks like: + /// { + /// "IsEncrypted": true|false, + /// "Values": { + /// "Name": "Value" + /// } + /// } + /// This method doesn't include Connection Strings. Unlike AppSettings, connection strings + /// have 10 different prefixes depending on the type. + /// + /// to add local.settings.json file to. + /// + private static ZipArchiveEntry AddAppSettingsFile(ZipArchive zip) + { + const string appSettingsPrefix = "APPSETTING_"; + const string localAppSettingsFileName = "local.settings.json"; + + var appSettings = System.Environment.GetEnvironmentVariables() + .Cast() + .Where(p => p.Key.ToString().StartsWith(appSettingsPrefix, StringComparison.OrdinalIgnoreCase)) + .ToDictionary(k => k.Key, v => v.Value); + + var localSettings = JsonConvert.SerializeObject(new { IsEncrypted = false, Value = appSettings }, Formatting.Indented); + + return zip.AddFile(localAppSettingsFileName, localSettings); + } } } \ No newline at end of file diff --git a/Kudu.Core/Infrastructure/ZipArchiveExtensions.cs b/Kudu.Core/Infrastructure/ZipArchiveExtensions.cs index 2c1b86e8e..3b44c28a4 100644 --- a/Kudu.Core/Infrastructure/ZipArchiveExtensions.cs +++ b/Kudu.Core/Infrastructure/ZipArchiveExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.IO.Abstractions; using System.IO.Compression; @@ -15,7 +16,18 @@ public static void AddDirectory(this ZipArchive zipArchive, string directoryPath zipArchive.AddDirectory(directoryInfo, tracer, directoryNameInArchive); } + public static void AddDirectory(this ZipArchive zipArchive, DirectoryInfoBase directory, ITracer tracer, string directoryNameInArchive, out IList files) + { + files = new List(); + InternalAddDirectory(zipArchive, directory, tracer, directoryNameInArchive, files); + } + public static void AddDirectory(this ZipArchive zipArchive, DirectoryInfoBase directory, ITracer tracer, string directoryNameInArchive) + { + InternalAddDirectory(zipArchive, directory, tracer, directoryNameInArchive); + } + + private static void InternalAddDirectory(ZipArchive zipArchive, DirectoryInfoBase directory, ITracer tracer, string directoryNameInArchive, IList files = null) { bool any = false; foreach (var info in directory.GetFileSystemInfos()) @@ -25,11 +37,12 @@ public static void AddDirectory(this ZipArchive zipArchive, DirectoryInfoBase di if (subDirectoryInfo != null) { string childName = ForwardSlashCombine(directoryNameInArchive, subDirectoryInfo.Name); - zipArchive.AddDirectory(subDirectoryInfo, tracer, childName); + InternalAddDirectory(zipArchive, subDirectoryInfo, tracer, childName, files); } else { - zipArchive.AddFile((FileInfoBase)info, tracer, directoryNameInArchive); + var entry = zipArchive.AddFile((FileInfoBase)info, tracer, directoryNameInArchive); + files?.Add(entry); } } @@ -45,13 +58,13 @@ private static string ForwardSlashCombine(string part1, string part2) return Path.Combine(part1, part2).Replace('\\', '/'); } - public static void AddFile(this ZipArchive zipArchive, string filePath, ITracer tracer, string directoryNameInArchive = "") + public static ZipArchiveEntry AddFile(this ZipArchive zipArchive, string filePath, ITracer tracer, string directoryNameInArchive = "") { var fileInfo = new FileInfoWrapper(new FileInfo(filePath)); - zipArchive.AddFile(fileInfo, tracer, directoryNameInArchive); + return zipArchive.AddFile(fileInfo, tracer, directoryNameInArchive); } - public static void AddFile(this ZipArchive zipArchive, FileInfoBase file, ITracer tracer, string directoryNameInArchive) + public static ZipArchiveEntry AddFile(this ZipArchive zipArchive, FileInfoBase file, ITracer tracer, string directoryNameInArchive) { Stream fileStream = null; try @@ -63,7 +76,7 @@ public static void AddFile(this ZipArchive zipArchive, FileInfoBase file, ITrace // tolerate if file in use. // for simplicity, any exception. tracer.TraceError(String.Format("{0}, {1}", file.FullName, ex)); - return; + return null; } try @@ -76,6 +89,7 @@ public static void AddFile(this ZipArchive zipArchive, FileInfoBase file, ITrace { fileStream.CopyTo(zipStream); } + return entry; } finally { @@ -83,13 +97,14 @@ public static void AddFile(this ZipArchive zipArchive, FileInfoBase file, ITrace } } - public static void AddFile(this ZipArchive zip, string fileName, string fileContent) + public static ZipArchiveEntry AddFile(this ZipArchive zip, string fileName, string fileContent) { ZipArchiveEntry entry = zip.CreateEntry(fileName, CompressionLevel.Fastest); using (var writer = new StreamWriter(entry.Open())) { writer.Write(fileContent); } + return entry; } public static void Extract(this ZipArchive archive, string directoryName) diff --git a/Kudu.Services.Web/App_Start/NinjectServices.cs b/Kudu.Services.Web/App_Start/NinjectServices.cs index 36bf90a1c..ab9bcdd73 100644 --- a/Kudu.Services.Web/App_Start/NinjectServices.cs +++ b/Kudu.Services.Web/App_Start/NinjectServices.cs @@ -531,6 +531,7 @@ public static void RegisterRoutes(IKernel kernel, RouteCollection routes) routes.MapHttpRoute("get-masterkey", "api/functions/admin/masterkey", new { controller = "Function", action = "GetMasterKey" }, new { verb = new HttpMethodConstraint("GET") }); routes.MapHttpRoute("get-admintoken", "api/functions/admin/token", new { controller = "Function", action = "GetAdminToken" }, new { verb = new HttpMethodConstraint("GET") }); routes.MapHttpRoute("delete-function", "api/functions/{name}", new { controller = "Function", action = "Delete" }, new { verb = new HttpMethodConstraint("DELETE") }); + routes.MapHttpRoute("download-functions", "api/functions/admin/download", new { controller = "Function", action = "DownloadFunctions" }, new { verb = new HttpMethodConstraint("GET") }); // Docker Hook Endpoint if (!OSDetector.IsOnWindows()) diff --git a/Kudu.Services/Functions/FunctionController.cs b/Kudu.Services/Functions/FunctionController.cs index 9383e98aa..3c34ccce1 100644 --- a/Kudu.Services/Functions/FunctionController.cs +++ b/Kudu.Services/Functions/FunctionController.cs @@ -7,17 +7,19 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web.Http; +using Kudu.Contracts.Functions; using Kudu.Contracts.Tracing; using Kudu.Core; using Kudu.Core.Functions; +using Kudu.Core.Helpers; +using Kudu.Core.Infrastructure; using Kudu.Core.Tracing; using Kudu.Services.Arm; using Kudu.Services.Filters; -using Kudu.Core.Helpers; +using Kudu.Services.Infrastructure; using Newtonsoft.Json.Linq; using Environment = System.Environment; -using Kudu.Contracts.Functions; namespace Kudu.Services.Functions { @@ -179,6 +181,21 @@ public async Task SyncTriggers() } } + [HttpGet] + public HttpResponseMessage DownloadFunctions(bool includeCsproj = true, bool includeAppSettings = false) + { + var tracer = _traceFactory.GetTracer(); + using (tracer.Step($"{nameof(FunctionController)}.{nameof(DownloadFunctions)}({includeCsproj}, {includeAppSettings})")) + { + var appName = ServerConfiguration.GetApplicationName(); + var fileName = $"{appName}.zip"; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = ZipStreamContent.Create(fileName, tracer, zip => _manager.CreateArchive(zip, includeAppSettings, includeCsproj, appName)) + }; + } + } + // Compute the site ID, for both the top level function API case and the regular nested case private FunctionEnvelope AddFunctionAppIdToEnvelope(FunctionEnvelope function) { diff --git a/build.cmd b/build.cmd index 2acb447d4..282db60e8 100644 --- a/build.cmd +++ b/build.cmd @@ -12,6 +12,8 @@ if exist "%PROGRAMFILES%\Microsoft Visual Studio\2017\Professional\MSBuild\15.0\ set MsBuildExe="%PROGRAMFILES%\Microsoft Visual Studio\2017\Professional\MSBuild\15.0\Bin\MSBuild.exe" ) else if exist "%PROGRAMFILES(X86)%\Microsoft Visual Studio\2017\Professional\MSBuild\15.0\Bin\MSBuild.exe" ( set MsBuildExe="%PROGRAMFILES(X86)%\Microsoft Visual Studio\2017\Professional\MSBuild\15.0\Bin\MSBuild.exe" +) else if exist "%PROGRAMFILES(X86)%\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\MSBuild.exe" ( + set MsBuildExe="%PROGRAMFILES(X86)%\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\MSBuild.exe" ) else if exist "%PROGRAMFILES%\MSBuild\14.0\Bin\MsBuild.exe" ( set MsBuildExe="%PROGRAMFILES%\MSBuild\14.0\Bin\MsBuild.exe" ) else if exist "%PROGRAMFILES(X86)%\MSBuild\14.0\Bin\MsBuild.exe" (