Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -240,14 +240,13 @@ private async Task<IDictionary<string, string>> ValidateFunctionAppPublish(Site

if (functionApp.IsLinux && !functionApp.IsDynamic && !string.IsNullOrEmpty(functionApp.LinuxFxVersion))
{
var allImages = Constants.WorkerRuntimeImages.Values.SelectMany(image => image).ToList();
if (!allImages.Any(image => functionApp.LinuxFxVersion.IndexOf(image, StringComparison.OrdinalIgnoreCase) != -1))
// If linuxFxVersion does not match any of our images
if (PublishHelper.IsLinuxFxVersionUsingCustomImage(functionApp.LinuxFxVersion))
{
ColoredConsole.WriteLine($"Your functionapp is using a custom image {functionApp.LinuxFxVersion}.\nAssuming that the image contains the correct framework.\n");
}
// If there the functionapp is our image but does not match the worker runtime image, we either fail or force update
else if (Constants.WorkerRuntimeImages.TryGetValue(workerRuntime, out IEnumerable<string> linuxFxImages) &&
!linuxFxImages.Any(image => functionApp.LinuxFxVersion.IndexOf(image, StringComparison.OrdinalIgnoreCase) != -1))
else if (!PublishHelper.IsLinuxFxVersionRuntimeMatched(functionApp.LinuxFxVersion, workerRuntime))
{
if (Force)
{
Expand Down Expand Up @@ -439,7 +438,7 @@ await WaitForAppSettingUpdateSCM(functionApp, shouldHaveSettings: functionApp.Az
shouldNotHaveSettings: new Dictionary<string, string> { { "WEBSITE_RUN_FROM_PACKAGE", "1" } }, timeOutSeconds: 300);
}

Task<DeployStatus> pollDedicatedBuild(HttpClient client) => KuduLiteDeploymentHelpers.WaitForDedicatedBuildToComplete(client, functionApp);
Task<DeployStatus> pollDedicatedBuild(HttpClient client) => KuduLiteDeploymentHelpers.WaitForRemoteBuild(client, functionApp);
await PerformServerSideBuild(functionApp, zipStreamFactory, pollDedicatedBuild);
}

Expand All @@ -457,7 +456,7 @@ private async Task<bool> HandleLinuxConsumptionPublish(Site functionApp, Func<Ta
{
await EnsureRemoteBuildIsSupported(functionApp);
await RemoveFunctionAppAppSetting(functionApp, Constants.WebsiteRunFromPackage, Constants.WebsiteContentAzureFileConnectionString, Constants.WebsiteContentShared);
Task<DeployStatus> pollConsumptionBuild(HttpClient client) => KuduLiteDeploymentHelpers.WaitForConsumptionServerSideBuild(client, functionApp, AccessToken, ManagementURL);
Task<DeployStatus> pollConsumptionBuild(HttpClient client) => KuduLiteDeploymentHelpers.WaitForRemoteBuild(client, functionApp);
var deployStatus = await PerformServerSideBuild(functionApp, zipFileFactory, pollConsumptionBuild);
return deployStatus == DeployStatus.Success;
}
Expand Down Expand Up @@ -726,6 +725,10 @@ public async Task<DeployStatus> PerformServerSideBuild(Site functionApp, Func<Ta
{
throw new CliException("Remote build failed!");
}
else if (status == DeployStatus.Unknown)
{
ColoredConsole.WriteLine(Yellow($"Failed to retrieve remote build status, please visit https://{functionApp.ScmUri}/api/deployments"));
}
return status;
}
}
Expand Down
1 change: 1 addition & 0 deletions src/Azure.Functions.Cli/Common/DeploymentStatus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace Azure.Functions.Cli.Common
{
public enum DeployStatus
{
Unknown = -1,
Pending = 0,
Building = 1,
Deploying = 2,
Expand Down
79 changes: 36 additions & 43 deletions src/Azure.Functions.Cli/Helpers/KuduLiteDeploymentHelpers.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using Azure.Functions.Cli.Arm.Models;
using Azure.Functions.Cli.Common;
using Colors.Net;
using static Colors.Net.StringStaticMethods;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

namespace Azure.Functions.Cli.Helpers
Expand All @@ -17,7 +19,7 @@ public static async Task<Dictionary<string, string>> GetAppSettings(HttpClient c
return await InvokeRequest<Dictionary<string, string>>(client, HttpMethod.Get, "/api/settings");
}

public static async Task<DeployStatus> WaitForConsumptionServerSideBuild(HttpClient client, Site functionApp, string accessToken, string managementUrl)
public static async Task<DeployStatus> WaitForRemoteBuild(HttpClient client, Site functionApp)
{
ColoredConsole.WriteLine("Remote build in progress, please wait...");
DeployStatus statusCode = DeployStatus.Pending;
Expand All @@ -30,7 +32,7 @@ public static async Task<DeployStatus> WaitForConsumptionServerSideBuild(HttpCli
await Task.Delay(TimeSpan.FromSeconds(Constants.KuduLiteDeploymentConstants.StatusRefreshSeconds));
}

while (statusCode != DeployStatus.Success && statusCode != DeployStatus.Failed)
while (statusCode != DeployStatus.Success && statusCode != DeployStatus.Failed && statusCode != DeployStatus.Unknown)
{
statusCode = await GetDeploymentStatusById(client, functionApp, id);
logLastUpdate = await DisplayDeploymentLog(client, functionApp, id, logLastUpdate);
Expand All @@ -40,35 +42,6 @@ public static async Task<DeployStatus> WaitForConsumptionServerSideBuild(HttpCli
return statusCode;
}

public static async Task<DeployStatus> WaitForDedicatedBuildToComplete(HttpClient client, Site functionApp)
{
// There is a tracked Locking issue in kudulite causing Race conditions, so we have to use this API
// to gather deployment progress.
ColoredConsole.Write("Remote build in progress, please wait");
while (true)
{
var json = await InvokeRequest<IDictionary<string, string>>(client, HttpMethod.Get, "/api/isdeploying");

if (bool.TryParse(json["value"], out bool isDeploying))
{
if (!isDeploying)
{
string deploymentId = await GetLatestDeploymentId(client, functionApp);
DeployStatus status = await GetDeploymentStatusById(client, functionApp, id: deploymentId);
ColoredConsole.Write($"done{Environment.NewLine}");
return status;
}
}
else
{
throw new CliException($"Expected \"value\" from /api/isdeploying endpoing to be a boolean. Actual: {json["value"]}");
}

ColoredConsole.Write(".");
await Task.Delay(5000);
}
}

private static async Task<string> GetLatestDeploymentId(HttpClient client, Site functionApp)
{
var json = await InvokeRequest<List<Dictionary<string, string>>>(client, HttpMethod.Get, "/deployments");
Expand All @@ -78,7 +51,8 @@ private static async Task<string> GetLatestDeploymentId(HttpClient client, Site
if (latestDeployment.TryGetValue("status", out string statusString))
{
DeployStatus status = ConvertToDeployementStatus(statusString);
if (status != DeployStatus.Pending)
if (status == DeployStatus.Building || status == DeployStatus.Deploying
|| status == DeployStatus.Success || status == DeployStatus.Failed)
{
return latestDeployment["id"];
}
Expand All @@ -88,36 +62,44 @@ private static async Task<string> GetLatestDeploymentId(HttpClient client, Site

private static async Task<DeployStatus> GetDeploymentStatusById(HttpClient client, Site functionApp, string id)
{
var json = await InvokeRequest<Dictionary<string, string>>(client,
HttpMethod.Get, $"/deployments/{id}");
Dictionary<string, string> json;
try
{
json = await InvokeRequest<Dictionary<string, string>>(client, HttpMethod.Get, $"/deployments/{id}");
} catch (HttpRequestException)
{
return DeployStatus.Unknown;
}

if (json.TryGetValue("status", out string statusString))
if (!json.TryGetValue("status", out string statusString))
{
return ConvertToDeployementStatus(json["status"]);
return DeployStatus.Unknown;
}
return DeployStatus.Failed;

return ConvertToDeployementStatus(statusString);
}

private static async Task<DateTime> DisplayDeploymentLog(HttpClient client, Site functionApp, string id, DateTime lastUpdate, Uri innerUrl = null)
private static async Task<DateTime> DisplayDeploymentLog(HttpClient client, Site functionApp, string id, DateTime lastUpdate, Uri innerUrl = null, StringBuilder innerLogger = null)
{
string logUrl = innerUrl != null ? innerUrl.ToString() : $"/deployments/{id}/log";
var json = await InvokeRequest<List<Dictionary<string, string>>>(client,
HttpMethod.Get, logUrl);
StringBuilder sbLogger = innerLogger != null ? innerLogger : new StringBuilder();

var json = await InvokeRequest<List<Dictionary<string, string>>>(client, HttpMethod.Get, logUrl);
var logs = json.Where(dict => DateTime.Parse(dict["log_time"]) > lastUpdate || dict["details_url"] != null);
DateTime currentLogDatetime = lastUpdate;

foreach (var log in logs)
{
// Filter out details_url log
if (DateTime.Parse(log["log_time"]) > lastUpdate)
{
ColoredConsole.WriteLine(log["message"]);
sbLogger.AppendLine(log["message"]);
}

// Recursively log details_url from scm/api/deployments/xxx/log endpoint
if (log["details_url"] != null && Uri.TryCreate(log["details_url"], UriKind.Absolute, out Uri detailsUrl))
{
DateTime innerLogDatetime = await DisplayDeploymentLog(client, functionApp, id, currentLogDatetime, detailsUrl);
DateTime innerLogDatetime = await DisplayDeploymentLog(client, functionApp, id, currentLogDatetime, detailsUrl, sbLogger);
currentLogDatetime = innerLogDatetime > currentLogDatetime ? innerLogDatetime : currentLogDatetime;
}
}
Expand All @@ -127,6 +109,13 @@ private static async Task<DateTime> DisplayDeploymentLog(HttpClient client, Site
DateTime lastLogDatetime = DateTime.Parse(logs.Last()["log_time"]);
currentLogDatetime = lastLogDatetime > currentLogDatetime ? lastLogDatetime : currentLogDatetime;
}

// Report build status on the root level parser
if (innerUrl == null && sbLogger.Length > 0)
{
ColoredConsole.Write(sbLogger.ToString());
}

return currentLogDatetime;
}

Expand Down Expand Up @@ -154,7 +143,11 @@ await RetryHelper.Retry(async () =>

private static DeployStatus ConvertToDeployementStatus(string statusString)
{
return Enum.Parse<DeployStatus>(statusString);
if (Enum.TryParse(statusString, out DeployStatus result))
{
return result;
}
return DeployStatus.Unknown;
}
}
}
38 changes: 38 additions & 0 deletions src/Azure.Functions.Cli/Helpers/PublishHelper.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Handlers;
using System.Threading.Tasks;
Expand Down Expand Up @@ -96,5 +98,41 @@ public static async Task CheckResponseStatusAsync(HttpResponseMessage response,
throw new CliException(errorMessage);
}
}

public static bool IsLinuxFxVersionUsingCustomImage(string linuxFxVersion)
{
if (string.IsNullOrEmpty(linuxFxVersion))
{
return false;
}

bool isStartingWithDocker = linuxFxVersion.StartsWith("docker|", StringComparison.OrdinalIgnoreCase);
bool isLegacyImageMatched = Constants.WorkerRuntimeImages.Values
.SelectMany(image => image)
.Any(image => linuxFxVersion.Contains(image, StringComparison.OrdinalIgnoreCase));

return isStartingWithDocker && !isLegacyImageMatched;
}

public static bool IsLinuxFxVersionRuntimeMatched(string linuxFxVersion, WorkerRuntime runtime) {
if (string.IsNullOrEmpty(linuxFxVersion))
{
// Suppress the check since when LinuxFxVersion == "", runtime image will depends on FUNCTIONS_WORKER_RUNTIME setting
return true;
}

// Test if linux fx version matches any legacy runtime image (e.g. DOCKER|mcr.microsoft.com/azure-functions/dotnet)
bool isStartingWithDocker = linuxFxVersion.StartsWith("docker|", StringComparison.OrdinalIgnoreCase);
bool isLegacyImageMatched = false;
if (Constants.WorkerRuntimeImages.TryGetValue(runtime, out IEnumerable<string> legacyImages)) {
isLegacyImageMatched = legacyImages
.Any(image => linuxFxVersion.Contains(image, StringComparison.OrdinalIgnoreCase));
}

// Test if linux fx version matches any official runtime image (e.g. DOTNET, DOTNET|2)
bool isOfficialImageMatched = linuxFxVersion.StartsWith(runtime.ToString(), StringComparison.OrdinalIgnoreCase);

return isOfficialImageMatched || (isStartingWithDocker && isLegacyImageMatched);
}
}
}
9 changes: 7 additions & 2 deletions src/Azure.Functions.Cli/Helpers/PythonHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,15 +125,20 @@ public static async Task<WorkerLanguageVersionInfo> GetEnvironmentPythonVersion(
return await GetVersion(pythonDefaultExecutablePath);
}

var pythonGetVersionTask = GetVersion("python");
// Windows Python Launcher (https://www.python.org/dev/peps/pep-0486/)
var pyGetVersionTask = PlatformHelper.IsWindows ? GetVersion("py") : Task.FromResult<WorkerLanguageVersionInfo>(null);

// Linux / OSX / Venv Interpreter Entrypoints
var python3GetVersionTask = GetVersion("python3");
var pythonGetVersionTask = GetVersion("python");
var python36GetVersionTask = GetVersion("python3.6");
var python37GetVersionTask = GetVersion("python3.7");

var versions = new List<WorkerLanguageVersionInfo>
{
await pythonGetVersionTask,
await pyGetVersionTask,
await python3GetVersionTask,
await pythonGetVersionTask,
await python36GetVersionTask,
await python37GetVersionTask
};
Expand Down
34 changes: 34 additions & 0 deletions test/Azure.Functions.Cli.Tests/PublishHelperTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Azure.Functions.Cli.Helpers;
using System;
using System.Collections.Generic;
using System.Text;
using Xunit;

namespace Azure.Functions.Cli.PublishHelperTests
{
public class PublishHelperTests
{
[Theory]
[InlineData("DOCKER|mcr.microsoft.com/azure-functions/node", false)]
[InlineData("DOCKER|customimage", true)]
[InlineData("PYTHON|3.6", false)]
[InlineData("DOTNET", false)]
[InlineData("", false)]
public void IsLinuxFxVersionUsingCustomImageTest(string linuxFxVersion, bool expected)
{
Assert.Equal(expected, PublishHelper.IsLinuxFxVersionUsingCustomImage(linuxFxVersion));
}

[Theory]
[InlineData("DOCKER|mcr.microsoft.com/azure-functions/dotnet", WorkerRuntime.dotnet, true)]
[InlineData("DOCKER|mcr.microsoft.com/azure-functions/node", WorkerRuntime.dotnet, false)]
[InlineData("DOCKER|customimage", WorkerRuntime.dotnet, false)]
[InlineData("PYTHON|3.7", WorkerRuntime.python, true)]
[InlineData("PYTHON|3.7", WorkerRuntime.node, false)]
[InlineData("", WorkerRuntime.dotnet, true)]
public void IsLinuxFxVersionRuntimeMatchedTest(string linuxFxVersion, WorkerRuntime runtime, bool expected)
{
Assert.Equal(expected, PublishHelper.IsLinuxFxVersionRuntimeMatched(linuxFxVersion, runtime));
}
}
}