Skip to content

Commit 76aca0e

Browse files
authored
Flex Deployment (#3404)
* Flex Deployment. * Skipping the test as it is failing because of PPE. * type * Changed back the API verison. * Not need to skip * Removing the extra params which I added for private stamp testing. * Removing the mitigation. * logs * More changes for the deployment. * Minor changes * Changes for 502 issue. * 90 sec to 60 secs wait. * logs url for flex * Removed the comment. * Removed the second check * Removed WebsiteRunFromPackage validation.
1 parent 66f9632 commit 76aca0e

File tree

5 files changed

+235
-10
lines changed

5 files changed

+235
-10
lines changed

src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Globalization;
34
using System.IO;
45
using System.Linq;
56
using System.Net.Http;
@@ -10,6 +11,7 @@
1011
using Azure.Functions.Cli.Actions.LocalActions;
1112
using Azure.Functions.Cli.Arm.Models;
1213
using Azure.Functions.Cli.Common;
14+
using Azure.Functions.Cli.Diagnostics;
1315
using Azure.Functions.Cli.Extensions;
1416
using Azure.Functions.Cli.Helpers;
1517
using Azure.Functions.Cli.Interfaces;
@@ -18,6 +20,7 @@
1820
using Microsoft.WindowsAzure.Storage;
1921
using Microsoft.WindowsAzure.Storage.Blob;
2022
using Newtonsoft.Json;
23+
using NuGet.Common;
2124
using static Azure.Functions.Cli.Common.OutputTheme;
2225

2326
namespace Azure.Functions.Cli.Actions.AzureActions
@@ -105,6 +108,7 @@ public override ICommandLineParserResult ParseArgs(string[] args)
105108
.Setup<string>("additional-packages")
106109
.WithDescription("List of packages to install when building native dependencies. For example: \"python3-dev libevent-dev\"")
107110
.Callback(p => AdditionalPackages = p);
111+
108112
Parser
109113
.Setup<bool>("force")
110114
.WithDescription("Depending on the publish scenario, this will ignore pre-publish checks")
@@ -429,7 +433,7 @@ private async Task PublishFunctionApp(Site functionApp, GitIgnoreParser ignorePa
429433
var functionAppRoot = ScriptHostHelpers.GetFunctionAppRootDirectory(Environment.CurrentDirectory);
430434

431435
// For dedicated linux apps, we do not support run from package right now
432-
var isFunctionAppDedicatedLinux = functionApp.IsLinux && !functionApp.IsDynamic && !functionApp.IsElasticPremium;
436+
var isFunctionAppDedicatedLinux = functionApp.IsLinux && !functionApp.IsDynamic && !functionApp.IsElasticPremium && !functionApp.IsFlex;
433437

434438
if (GlobalCoreToolsSettings.CurrentWorkerRuntime == WorkerRuntime.python && !functionApp.IsLinux)
435439
{
@@ -442,6 +446,7 @@ private async Task PublishFunctionApp(Site functionApp, GitIgnoreParser ignorePa
442446
ColoredConsole.WriteLine(WarningColor("Recommend using '--build remote' to resolve project dependencies remotely on Azure"));
443447
}
444448

449+
ColoredConsole.WriteLine(GetLogMessage("Starting the function app deployment..."));
445450
Func<Task<Stream>> zipStreamFactory = () => ZipHelper.GetAppZipFile(functionAppRoot, BuildNativeDeps, PublishBuildOption,
446451
NoBuild, ignoreParser, AdditionalPackages, ignoreDotNetCheck: true);
447452

@@ -457,6 +462,11 @@ private async Task PublishFunctionApp(Site functionApp, GitIgnoreParser ignorePa
457462
// Consumption Linux
458463
shouldSyncTriggers = await HandleLinuxConsumptionPublish(functionApp, zipStreamFactory);
459464
}
465+
else if (functionApp.IsFlex)
466+
{
467+
// Flex
468+
shouldSyncTriggers = await HandleFlexConsumptionPublish(functionApp, zipStreamFactory);
469+
}
460470
else if (functionApp.IsLinux && functionApp.IsElasticPremium)
461471
{
462472
// Elastic Premium Linux
@@ -496,8 +506,8 @@ private async Task PublishFunctionApp(Site functionApp, GitIgnoreParser ignorePa
496506
{
497507
await PublishZipDeploy(functionApp, zipStreamFactory);
498508
}
499-
500-
if (shouldSyncTriggers)
509+
510+
if (shouldSyncTriggers && !functionApp.IsFlex)
501511
{
502512
await Task.Delay(TimeSpan.FromSeconds(5));
503513
await SyncTriggers(functionApp);
@@ -518,7 +528,7 @@ private async Task SyncTriggers(Site functionApp)
518528
{
519529
await RetryHelper.Retry(async () =>
520530
{
521-
ColoredConsole.WriteLine("Syncing triggers...");
531+
ColoredConsole.WriteLine(GetLogMessage("Syncing triggers..."));
522532
HttpResponseMessage response = null;
523533
// This SyncTriggers function calls the endpoint for linux syncTriggers
524534
response = await AzureHelper.SyncTriggers(functionApp, AccessToken, ManagementURL);
@@ -585,6 +595,7 @@ private async Task PerformAppServiceRemoteBuild(Func<Task<Stream>> zipStreamFact
585595
functionApp.AzureAppSettings.Remove("WEBSITE_RUN_FROM_PACKAGE");
586596
appSettingsUpdated = true;
587597
}
598+
588599
appSettingsUpdated = functionApp.AzureAppSettings.SafeLeftMerge(Constants.KuduLiteDeploymentConstants.LinuxDedicatedBuildSettings) || appSettingsUpdated;
589600
if (appSettingsUpdated)
590601
{
@@ -602,6 +613,85 @@ await WaitForAppSettingUpdateSCM(functionApp, shouldHaveSettings: functionApp.Az
602613
await PerformServerSideBuild(functionApp, zipStreamFactory, pollDedicatedBuild);
603614
}
604615

616+
/// <summary>
617+
/// Handler for Linux Consumption publish event
618+
/// </summary>
619+
/// <param name="functionApp">Function App in Azure</param>
620+
/// <param name="zipFileFactory">Factory for local project zipper</param>
621+
/// <returns>ShouldSyncTrigger value</returns>
622+
private async Task<bool> HandleFlexConsumptionPublish(Site functionApp, Func<Task<Stream>> zipFileFactory)
623+
{
624+
Task<DeployStatus> pollDeploymentStatusTask(HttpClient client) => KuduLiteDeploymentHelpers.WaitForFlexDeployment(client, functionApp);
625+
var deploymentParameters = new Dictionary<string, string>();
626+
627+
if (PublishBuildOption == BuildOption.Remote)
628+
{
629+
deploymentParameters.Add("RemoteBuild", true.ToString());
630+
}
631+
632+
var deploymentStatus = await PerformFlexDeployment(functionApp, zipFileFactory, pollDeploymentStatusTask, deploymentParameters);
633+
634+
return deploymentStatus == DeployStatus.Success;
635+
}
636+
637+
public async Task<DeployStatus> PerformFlexDeployment(Site functionApp, Func<Task<Stream>> zipFileFactory, Func<HttpClient, Task<DeployStatus>> deploymentStatusPollTask, IDictionary<string, string> deploymentParameters)
638+
{
639+
using (var handler = new ProgressMessageHandler(new HttpClientHandler()))
640+
using (var client = GetRemoteZipClient(functionApp, handler))
641+
using (var request = new HttpRequestMessage(HttpMethod.Post, new Uri(
642+
$"api/Deploy/Zip?isAsync=true&author={Environment.MachineName}&Deployer=core_tools&{string.Join("&", deploymentParameters?.Select(kvp => $"{kvp.Key}={kvp.Value}")) ?? string.Empty}", UriKind.Relative)))
643+
{
644+
ColoredConsole.WriteLine(GetLogMessage("Creating archive for current directory..."));
645+
646+
request.Headers.IfMatch.Add(EntityTagHeaderValue.Any);
647+
648+
(var content, var length) = CreateStreamContentZip(await zipFileFactory());
649+
request.Content = content;
650+
HttpResponseMessage response = await PublishHelper.InvokeLongRunningRequest(client, handler, request, length, "Uploading");
651+
await PublishHelper.CheckResponseStatusAsync(response, GetLogMessage("Uploading archive..."));
652+
653+
// Streaming deployment status for Linux Server Side Build
654+
DeployStatus status = await deploymentStatusPollTask(client);
655+
656+
if (status == DeployStatus.Success)
657+
{
658+
// the deployment was successful. Waiting for 60 seconds so that Kudu finishes the sync trigger.
659+
await Task.Delay(TimeSpan.FromSeconds(60));
660+
661+
// Checking the function app host status
662+
try
663+
{
664+
await AzureHelper.CheckFunctionHostStatusForFlex(functionApp, AccessToken, ManagementURL);
665+
}
666+
catch (Exception)
667+
{
668+
throw new CliException("Deployment was successful but the app appears to be unhealthy, please check the app logs.");
669+
}
670+
671+
ColoredConsole.WriteLine(VerboseColor(GetLogMessage("The deployment was successful!")));
672+
}
673+
else if (status == DeployStatus.Failed)
674+
{
675+
throw new CliException("The deployment failed, Please check the printed logs.");
676+
}
677+
else if (status == DeployStatus.Conflict)
678+
{
679+
throw new CliException("Deployment was cancelled, another deployment in progress.");
680+
}
681+
else if (status == DeployStatus.PartialSuccess)
682+
{
683+
ColoredConsole.WriteLine(WarningColor(GetLogMessage("\"Deployment was partially successful, Please check the printed logs.")));
684+
}
685+
else if (status == DeployStatus.Unknown)
686+
{
687+
ColoredConsole.WriteLine(WarningColor(GetLogMessage($"Failed to retrieve deployment status, please visit https://{functionApp.ScmUri}/api/deployments")));
688+
}
689+
690+
return status;
691+
}
692+
}
693+
694+
605695
/// <summary>
606696
/// Handler for Linux Consumption publish event
607697
/// </summary>
@@ -1069,6 +1159,7 @@ private static (StreamContent, long) CreateStreamContentZip(Stream zipFile)
10691159
private HttpClient GetRemoteZipClient(Site functionApp, HttpMessageHandler handler = null)
10701160
{
10711161
handler = handler ?? new HttpClientHandler();
1162+
10721163
var client = new HttpClient(handler)
10731164
{
10741165
BaseAddress = new Uri($"https://{functionApp.ScmUri}"),
@@ -1109,6 +1200,16 @@ private static string NormalizeDotnetFrameworkVersion(string version)
11091200
return $"{parsedVersion.Major}.{parsedVersion.Minor}";
11101201
}
11111202

1203+
private string GetLogMessage(string message)
1204+
{
1205+
return GetLogPrefix() + message;
1206+
}
1207+
1208+
private string GetLogPrefix()
1209+
{
1210+
return $"[{DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffZ", CultureInfo.InvariantCulture)}] ".ToString();
1211+
}
1212+
11121213
// For testing
11131214
internal class AzureHelperService
11141215
{

src/Azure.Functions.Cli/Arm/Models/Site.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ public bool IsLinux
4040
public bool IsDynamic
4141
=> Sku?.Equals("dynamic", StringComparison.OrdinalIgnoreCase) == true;
4242

43+
public bool IsFlex
44+
=> Sku?.Equals("flexconsumption", StringComparison.OrdinalIgnoreCase) == true;
45+
4346
public bool IsElasticPremium
4447
=> Sku?.Equals("elasticpremium", StringComparison.OrdinalIgnoreCase) == true;
4548

src/Azure.Functions.Cli/Common/DeploymentStatus.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ public enum DeployStatus
1111
Building = 1,
1212
Deploying = 2,
1313
Failed = 3,
14-
Success = 4
14+
Success = 4,
15+
Conflict = 5,
16+
PartialSuccess = 6
1517
}
1618
}

src/Azure.Functions.Cli/Helpers/AzureHelper.cs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ private static async Task<Site> TryGetFunctionAppFromArg(string name, IEnumerabl
6666
try
6767
{
6868
string siteId = await GetResourceIDFromArg(subscriptions, query, accessToken, managementURL);
69+
6970
var app = new Site(siteId);
7071

7172
// The implementation of the load function is different for certain function apps like function apps based on Container Apps.
@@ -122,6 +123,7 @@ internal static async Task<string> GetResourceIDFromArg(IEnumerable<string> subI
122123
?? throw new CliException("Error finding the Azure Resource information.");
123124
}
124125

126+
125127
internal static async Task<bool> IsBasicAuthAllowedForSCM(Site functionApp, string accessToken, string managementURL)
126128
{
127129
var url = new Uri($"{managementURL}{functionApp.SiteId}/basicPublishingCredentialsPolicies/scm?api-version={ArmUriTemplates.BasicAuthCheckApiVersion}");
@@ -362,6 +364,53 @@ public static Task<HttpResponseMessage> SyncTriggers(Site functionApp, string ac
362364
return ArmClient.HttpInvoke(HttpMethod.Post, url, accessToken);
363365
}
364366

367+
internal static async Task CheckFunctionHostStatusForFlex(Site functionApp, string accessToken, string managementURL,
368+
HttpMessageHandler messageHandler = null)
369+
{
370+
ColoredConsole.Write("Checking the app health...");
371+
var masterKey = await GetMasterKeyAsync(functionApp.SiteId, accessToken, managementURL);
372+
373+
if (masterKey is null)
374+
{
375+
throw new CliException($"The masterKey is null. hostname: {functionApp.HostName}.");
376+
}
377+
378+
HttpMessageHandler handler = messageHandler ?? new HttpClientHandler();
379+
if (StaticSettings.IsDebug)
380+
{
381+
handler = new LoggingHandler(handler);
382+
}
383+
384+
var functionAppReadyClient = new HttpClient(handler);
385+
const string jsonContentType = "application/json";
386+
functionAppReadyClient.DefaultRequestHeaders.Add("User-Agent", Constants.CliUserAgent);
387+
functionAppReadyClient.DefaultRequestHeaders.Add("Accept", jsonContentType);
388+
389+
await RetryHelper.Retry(async () =>
390+
{
391+
functionAppReadyClient.DefaultRequestHeaders.Add("x-ms-request-id", Guid.NewGuid().ToString());
392+
var uri = new Uri($"https://{functionApp.HostName}/admin/host/status?code={masterKey}");
393+
var request = new HttpRequestMessage()
394+
{
395+
RequestUri = uri,
396+
Method = HttpMethod.Get
397+
};
398+
399+
var response = await functionAppReadyClient.SendAsync(request);
400+
ColoredConsole.Write(".");
401+
402+
if (response.IsSuccessStatusCode)
403+
{
404+
ColoredConsole.WriteLine(" done");
405+
}
406+
else
407+
{
408+
throw new CliException($"The host didn't return success status. Returned: {response.StatusCode}");
409+
}
410+
411+
}, 15, TimeSpan.FromSeconds(3));
412+
}
413+
365414
public static async Task<Site> LoadSiteObjectAsync(Site site, string accessToken, string managementURL)
366415
{
367416
var url = new Uri($"{managementURL}{site.SiteId}?api-version={ArmUriTemplates.WebsitesApiVersion}");
@@ -558,10 +607,10 @@ public static async Task PrintFunctionsInfo(Site functionApp, string accessToken
558607
}
559608

560609
ArmArrayWrapper<FunctionInfo> functions = await GetFunctions(functionApp, accessToken, managementURL);
561-
562610
functions = await GetFunctions(functionApp, accessToken, managementURL);
563611

564612
ColoredConsole.WriteLine(TitleColor($"Functions in {functionApp.SiteName}:"));
613+
565614
if (functionApp.IsKubeApp && !functions.value.Any())
566615
{
567616
ColoredConsole.WriteLine(WarningColor(

0 commit comments

Comments
 (0)