Skip to content

Commit

Permalink
Add API to download function app archive that can add local.settings.…
Browse files Browse the repository at this point in the history
…json and csproj file to the archive.
  • Loading branch information
ahmelsayed committed May 24, 2017
1 parent 20048aa commit 9877361
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 9 deletions.
2 changes: 2 additions & 0 deletions Kudu.Contracts/Functions/IFunctionManager.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -19,5 +20,6 @@ public interface IFunctionManager
string GetAdminToken();
Task<JObject> PutHostConfigAsync(JObject content);
void DeleteFunction(string name, bool ignoreErrors);
void CreateArchive(ZipArchive archive, bool includeAppSettings = false, bool includeCsproj = false, string projectName = null);
}
}
1 change: 1 addition & 0 deletions Kudu.Contracts/Kudu.Contracts.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.IO.Compression" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Net.Http.Extensions">
<HintPath>..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Extensions.dll</HintPath>
Expand Down
108 changes: 108 additions & 0 deletions Kudu.Core/Functions/FunctionManager.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -422,5 +425,110 @@ private string GetFunctionSecretsFilePath(string functionName)
{
return Path.Combine(_environment.DataPath, Constants.Functions, Constants.Secrets, $"{functionName}.json");
}

/// <summary>
/// Populates a <see cref="ZipArchive"/> 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.
/// </summary>
/// <param name="zip">the <see cref="ZipArchive"/> to be populated with function app content.</param>
/// <param name="includeAppSettings">Optional: indicates whether to add local.settings.json or not to the archive. Default is false.</param>
/// <param name="includeCsproj">Optional: indicates whether to add a .csproj to the archive. Default is false.</param>
/// <param name="projectName">Optional: the name for *.csproj file if <paramref name="includeCsproj"/> is true. Default is appName.</param>
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<ZipArchiveEntry> 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);
}
}

/// <summary>
/// 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.
/// </summary>
/// <param name="zip"><see cref="ZipArchive"/> to add csproj file to.</param>
/// <param name="files"><see cref="IEnumerable{ZipArchiveEntry}"/> of entries in the zip file to include in the csproj.</param>
/// <param name="projectName">the {projectName}.csproj</param>
private static ZipArchiveEntry AddCsprojFile(ZipArchive zip, IEnumerable<ZipArchiveEntry> 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(
$@"<Project Sdk=""Microsoft.NET.Sdk"">
<PropertyGroup>
<TargetFramework>net461</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include=""Microsoft.Azure.WebJobs"" Version=""{microsoftAzureWebJobsVersion}"" />
<PackageReference Include=""Microsoft.Azure.WebJobs.Extensions.Http"" Version=""{microsoftAzureWebJobsExtensionsHttpVersion}"" />
<PackageReference Include=""Microsoft.NET.Sdk.Functions"" Version=""{microsoftNETSdkFunctionsVersion}"" />
</ItemGroup>
<ItemGroup>
<Reference Include=""Microsoft.CSharp"" />
</ItemGroup>");

csprojFile.AppendLine(" <ItemGroup>");
foreach (var entry in files)
{
csprojFile.AppendLine(
$@" <None Update=""{entry.FullName}"">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>");
}
csprojFile.AppendLine(" </ItemGroup>");
csprojFile.AppendLine("</Project>");

return zip.AddFile($"{projectName}.csproj", csprojFile.ToString());
}

/// <summary>
/// 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.
/// </summary>
/// <param name="zip"><see cref="ZipArchive"/> to add local.settings.json file to.</param>
/// <returns></returns>
private static ZipArchiveEntry AddAppSettingsFile(ZipArchive zip)
{
const string appSettingsPrefix = "APPSETTING_";
const string localAppSettingsFileName = "local.settings.json";

var appSettings = System.Environment.GetEnvironmentVariables()
.Cast<DictionaryEntry>()
.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);
}
}
}
29 changes: 22 additions & 7 deletions Kudu.Core/Infrastructure/ZipArchiveExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Abstractions;
using System.IO.Compression;
Expand All @@ -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<ZipArchiveEntry> files)
{
files = new List<ZipArchiveEntry>();
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<ZipArchiveEntry> files = null)
{
bool any = false;
foreach (var info in directory.GetFileSystemInfos())
Expand All @@ -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);
}
}

Expand All @@ -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
Expand All @@ -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
Expand All @@ -76,20 +89,22 @@ public static void AddFile(this ZipArchive zipArchive, FileInfoBase file, ITrace
{
fileStream.CopyTo(zipStream);
}
return entry;
}
finally
{
fileStream.Dispose();
}
}

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)
Expand Down
1 change: 1 addition & 0 deletions Kudu.Services.Web/App_Start/NinjectServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
21 changes: 19 additions & 2 deletions Kudu.Services/Functions/FunctionController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -179,6 +181,21 @@ public async Task<HttpResponseMessage> 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)
{
Expand Down
2 changes: 2 additions & 0 deletions build.cmd
Original file line number Diff line number Diff line change
Expand Up @@ -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" (
Expand Down

0 comments on commit 9877361

Please sign in to comment.