Skip to content

Commit 12d79dc

Browse files
committed
Add InstanceController for instance management calls
1 parent 21b2126 commit 12d79dc

17 files changed

+704
-27
lines changed

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

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
using System.Collections.Generic;
55
using System.Collections.ObjectModel;
6+
using System.IO.Abstractions;
7+
using System.IO.Compression;
68
using System.Linq;
79
using System.Text.RegularExpressions;
810
using System.Threading.Tasks;
@@ -12,17 +14,19 @@
1214
using Microsoft.AspNetCore.Mvc;
1315
using Microsoft.Azure.WebJobs.Script.Description;
1416
using Microsoft.Azure.WebJobs.Script.Management.Models;
17+
using Microsoft.Azure.WebJobs.Script.WebHost.Extensions;
1518
using Microsoft.Azure.WebJobs.Script.WebHost.Filters;
1619
using Microsoft.Azure.WebJobs.Script.WebHost.Management;
1720
using Microsoft.Azure.WebJobs.Script.WebHost.Models;
1821
using Microsoft.Azure.WebJobs.Script.WebHost.Security.Authorization.Policies;
1922
using Microsoft.Extensions.Logging;
23+
using Microsoft.Net.Http.Headers;
2024

2125
namespace Microsoft.Azure.WebJobs.Script.WebHost.Controllers
2226
{
2327
/// <summary>
2428
/// Controller responsible for administrative and management operations on functions
25-
/// example retriving a list of functions, invoking a function, creating a function, etc
29+
/// example retrieving a list of functions, invoking a function, creating a function, etc
2630
/// </summary>
2731
public class FunctionsController : Controller
2832
{
@@ -162,5 +166,35 @@ public async Task<IActionResult> Delete(string name)
162166
return StatusCode(StatusCodes.Status500InternalServerError, error);
163167
}
164168
}
169+
170+
[HttpGet]
171+
[Route("admin/functions/download")]
172+
[Authorize(Policy = PolicyNames.AdminAuthLevel)]
173+
public IActionResult Download()
174+
{
175+
var path = _scriptHostManager.Instance.ScriptConfig.RootScriptPath;
176+
var dirInfo = FileUtility.DirectoryInfoFromDirectoryName(path);
177+
return new FileCallbackResult(new MediaTypeHeaderValue("application/octet-stream"), async (outputStream, _) =>
178+
{
179+
using (var zipArchive = new ZipArchive(outputStream, ZipArchiveMode.Create))
180+
{
181+
foreach (FileSystemInfoBase fileSysInfo in dirInfo.GetFileSystemInfos())
182+
{
183+
if (fileSysInfo is DirectoryInfoBase directoryInfo)
184+
{
185+
await zipArchive.AddDirectory(directoryInfo, fileSysInfo.Name);
186+
}
187+
else
188+
{
189+
// Add it at the root of the zip
190+
await zipArchive.AddFile(fileSysInfo.FullName, string.Empty);
191+
}
192+
}
193+
}
194+
})
195+
{
196+
FileDownloadName = (System.Environment.GetEnvironmentVariable("WEBSITE_SITE_NAME") ?? "functions") + ".zip"
197+
};
198+
}
165199
}
166200
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,15 @@ public async Task<IActionResult> SyncTriggers()
141141
: StatusCode(StatusCodes.Status500InternalServerError, new { status = error });
142142
}
143143

144+
[HttpPost]
145+
[Route("admin/host/restart")]
146+
[Authorize(Policy = PolicyNames.AdminAuthLevel)]
147+
public IActionResult Restart()
148+
{
149+
_scriptHostManager.RestartHost();
150+
return Ok(_webHostSettings);
151+
}
152+
144153
[HttpGet]
145154
[HttpPost]
146155
[Authorize(AuthenticationSchemes = AuthLevelAuthenticationDefaults.AuthenticationScheme)]
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Authorization;
6+
using Microsoft.AspNetCore.Http;
7+
using Microsoft.AspNetCore.Mvc;
8+
using Microsoft.Azure.WebJobs.Script.Config;
9+
using Microsoft.Azure.WebJobs.Script.WebHost.Management;
10+
using Microsoft.Azure.WebJobs.Script.WebHost.Models;
11+
using Microsoft.Azure.WebJobs.Script.WebHost.Security.Authorization.Policies;
12+
13+
namespace Microsoft.Azure.WebJobs.Script.WebHost.Controllers
14+
{
15+
/// <summary>
16+
/// Controller responsible for instance operations that are orthogonal to the script host.
17+
/// An instance is an unassigned generic container running with the runtime in standby mode.
18+
/// These APIs are used by the AppService Controller to validate standby instance status and info.
19+
/// </summary>
20+
public class InstanceController : Controller
21+
{
22+
private readonly WebScriptHostManager _scriptHostManager;
23+
private readonly ScriptSettingsManager _settingsManager;
24+
private readonly IInstanceManager _instanceManager;
25+
26+
public InstanceController(WebScriptHostManager scriptHostManager, ScriptSettingsManager settingsManager, IInstanceManager instanceManager)
27+
{
28+
_scriptHostManager = scriptHostManager;
29+
_settingsManager = settingsManager;
30+
_instanceManager = instanceManager;
31+
}
32+
33+
[HttpPost]
34+
[Route("admin/instance/assign")]
35+
[Authorize(Policy = PolicyNames.AdminAuthLevel)]
36+
public IActionResult Assign([FromBody] EncryptedHostAssignmentContext encryptedAssignmentContext)
37+
{
38+
var containerKey = _settingsManager.GetSetting(EnvironmentSettingNames.ContainerEncryptionKey);
39+
var assignmentContext = encryptedAssignmentContext.Decrypt(containerKey);
40+
return _instanceManager.StartAssignment(assignmentContext)
41+
? Accepted()
42+
: StatusCode(StatusCodes.Status409Conflict, "Instance already assigned");
43+
}
44+
45+
[HttpGet]
46+
[Route("admin/instance/status")]
47+
[Authorize(Policy = PolicyNames.AdminAuthLevel)]
48+
public async Task<IActionResult> GetInstanceStatus([FromQuery] int timeout = int.MaxValue)
49+
{
50+
return await _scriptHostManager.DelayUntilHostReady(timeoutSeconds: timeout)
51+
? Ok()
52+
: StatusCode(StatusCodes.Status503ServiceUnavailable);
53+
}
54+
55+
[HttpGet]
56+
[Route("admin/instance/info")]
57+
[Authorize(Policy = PolicyNames.AdminAuthLevel)]
58+
public IActionResult GetInstanceInfo()
59+
{
60+
return Ok(_instanceManager.GetInstanceInfo());
61+
}
62+
}
63+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.IO.Abstractions;
8+
using System.IO.Compression;
9+
using System.Threading.Tasks;
10+
11+
namespace Microsoft.Azure.WebJobs.Script.WebHost.Extensions
12+
{
13+
public static class ZipArchiveExtensions
14+
{
15+
public static async Task AddDirectory(this ZipArchive zipArchive, DirectoryInfoBase directory, string directoryNameInArchive)
16+
{
17+
await InternalAddDirectory(zipArchive, directory, directoryNameInArchive);
18+
}
19+
20+
private static async Task InternalAddDirectory(ZipArchive zipArchive, DirectoryInfoBase directory, string directoryNameInArchive, IList<ZipArchiveEntry> files = null)
21+
{
22+
bool any = false;
23+
foreach (var info in directory.GetFileSystemInfos())
24+
{
25+
any = true;
26+
if (info is DirectoryInfoBase subDirectoryInfo)
27+
{
28+
string childName = ForwardSlashCombine(directoryNameInArchive, subDirectoryInfo.Name);
29+
await InternalAddDirectory(zipArchive, subDirectoryInfo, childName, files);
30+
}
31+
else
32+
{
33+
var entry = await zipArchive.AddFile((FileInfoBase)info, directoryNameInArchive);
34+
files?.Add(entry);
35+
}
36+
}
37+
38+
if (!any)
39+
{
40+
// If the directory did not have any files or folders, add a entry for it
41+
zipArchive.CreateEntry(EnsureTrailingSlash(directoryNameInArchive));
42+
}
43+
}
44+
45+
private static string ForwardSlashCombine(string part1, string part2)
46+
{
47+
return Path.Combine(part1, part2).Replace('\\', '/');
48+
}
49+
50+
public static Task<ZipArchiveEntry> AddFile(this ZipArchive zipArchive, string filePath, string directoryNameInArchive = "")
51+
{
52+
var fileInfo = FileUtility.FileInfoFromFileName(filePath);
53+
return zipArchive.AddFile(fileInfo, directoryNameInArchive);
54+
}
55+
56+
public static async Task<ZipArchiveEntry> AddFile(this ZipArchive zipArchive, FileInfoBase file, string directoryNameInArchive)
57+
{
58+
using (var fileStream = file.OpenRead())
59+
{
60+
string fileName = ForwardSlashCombine(directoryNameInArchive, file.Name);
61+
ZipArchiveEntry entry = zipArchive.CreateEntry(fileName, CompressionLevel.Fastest);
62+
entry.LastWriteTime = file.LastWriteTime;
63+
64+
using (var zipStream = entry.Open())
65+
{
66+
await fileStream.CopyToAsync(zipStream);
67+
}
68+
return entry;
69+
}
70+
}
71+
72+
private static string EnsureTrailingSlash(string input)
73+
{
74+
return input.EndsWith("/", StringComparison.Ordinal) ? input : input + "/";
75+
}
76+
}
77+
}

src/WebJobs.Script.WebHost/Helpers/SimpleWebTokenHelper.cs

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Collections.Generic;
65
using System.IO;
76
using System.Linq;
87
using System.Security.Cryptography;
@@ -13,15 +12,15 @@ namespace Microsoft.Azure.WebJobs.Script.WebHost.Helpers
1312
public static class SimpleWebTokenHelper
1413
{
1514
/// <summary>
16-
/// A SWT or a Simple Web Token is a token that's made of key=value pairs seperated
15+
/// A SWT or a Simple Web Token is a token that's made of key=value pairs separated
1716
/// by &. We only specify expiration in ticks from now (exp={ticks})
1817
/// The SWT is then returned as an encrypted string
1918
/// </summary>
2019
/// <param name="validUntil">Datetime for when the token should expire</param>
2120
/// <returns>a SWT signed by this app</returns>
2221
public static string CreateToken(DateTime validUntil) => Encrypt($"exp={validUntil.Ticks}");
2322

24-
private static string Encrypt(string value)
23+
internal static string Encrypt(string value)
2524
{
2625
using (var aes = new AesManaged { Key = GetWebSiteAuthEncryptionKey() })
2726
{
@@ -46,6 +45,38 @@ private static string Encrypt(string value)
4645
}
4746
}
4847

48+
internal static string Decrypt(byte[] encryptionKey, string value)
49+
{
50+
var parts = value.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries);
51+
if (parts.Length != 2 && parts.Length != 3)
52+
{
53+
throw new InvalidOperationException("Malformed token.");
54+
}
55+
56+
var iv = Convert.FromBase64String(parts[0]);
57+
var data = Convert.FromBase64String(parts[1]);
58+
var base64KeyHash = parts.Length == 3 ? parts[2] : null;
59+
60+
if (!string.IsNullOrEmpty(base64KeyHash) && !string.Equals(GetSHA256Base64String(encryptionKey), base64KeyHash))
61+
{
62+
throw new InvalidOperationException(string.Format("Key with hash {0} does not exist.", base64KeyHash));
63+
}
64+
65+
using (var aes = new AesManaged { Key = encryptionKey })
66+
{
67+
using (var ms = new MemoryStream())
68+
{
69+
using (var cs = new CryptoStream(ms, aes.CreateDecryptor(aes.Key, iv), CryptoStreamMode.Write))
70+
using (var binaryWriter = new BinaryWriter(cs))
71+
{
72+
binaryWriter.Write(data, 0, data.Length);
73+
}
74+
75+
return Encoding.UTF8.GetString(ms.ToArray());
76+
}
77+
}
78+
}
79+
4980
private static string GetSHA256Base64String(byte[] key)
5081
{
5182
using (var sha256 = new SHA256Managed())
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System.Collections.Generic;
5+
using Microsoft.Azure.WebJobs.Script.WebHost.Models;
6+
7+
namespace Microsoft.Azure.WebJobs.Script.WebHost.Management
8+
{
9+
public interface IInstanceManager
10+
{
11+
IDictionary<string, string> GetInstanceInfo();
12+
13+
bool StartAssignment(HostAssignmentContext assignmentContext);
14+
}
15+
}

0 commit comments

Comments
 (0)