Skip to content

Commit

Permalink
Function App (Flex Cosumption) ZipDeploy publish support (#2712)
Browse files Browse the repository at this point in the history
* In ZipDeploy, add support for publishing a Flex Consumption function app

* Update release_notes.md with PR number

* Accept review comments
  • Loading branch information
anvillan authored Sep 23, 2024
1 parent 1ee6449 commit 24cd553
Show file tree
Hide file tree
Showing 10 changed files with 418 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and
DeploymentPassword="$(Password)"
SiteName="$(DeployIisAppPath)"
PublishUrl="$(PublishUrl)"
UserAgentVersion="$(ZipDeployUserAgent)"/>
UserAgentVersion="$(ZipDeployUserAgent)"
UseBlobContainerDeploy="$(UseBlobContainerDeploy)"/>
</Target>

</Project>
6 changes: 4 additions & 2 deletions sdk/Sdk/Tasks/ZipDeploy/DeployStatus.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

// IMPORTANT: Do not modify this file directly with major changes
Expand All @@ -14,6 +14,8 @@ public enum DeployStatus
Building = 1,
Deploying = 2,
Failed = 3,
Success = 4
Success = 4,
Conflict = 5,
PartialSuccess = 6
}
}
1 change: 1 addition & 0 deletions sdk/Sdk/Tasks/ZipDeploy/StringMessages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
public static class StringMessages
{
public const string DeploymentStatus = "Deployment status is {0}.";
public const string DeploymentStatusWithText = "Deployment status is {0}: {1}";
public const string DeploymentStatusPolling = "Polling for deployment status...";
public const string NeitherSiteNameNorPublishUrlGivenError = "Neither SiteName nor PublishUrl was given a value.";
public const string PublishingZipViaZipDeploy = "Publishing {0} to {1}...";
Expand Down
30 changes: 20 additions & 10 deletions sdk/Sdk/Tasks/ZipDeploy/ZipDeployTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
Expand Down Expand Up @@ -41,6 +40,7 @@ public class ZipDeployTask : Task

public string? PublishUrl { get; set; }

public bool UseBlobContainerDeploy { get; set; }

/// <summary>
/// Our fallback if PublishUrl is not given, which is the case for ZIP Deploy profiles created prior to 15.8 Preview 4.
Expand All @@ -49,16 +49,21 @@ public class ZipDeployTask : Task
public string? SiteName { get; set; }

public override bool Execute()
{
{
using (DefaultHttpClient client = new DefaultHttpClient())
{
System.Threading.Tasks.Task<bool> t = ZipDeployAsync(ZipToPublishPath!, DeploymentUsername!, DeploymentPassword!, PublishUrl, SiteName!, UserAgentVersion!, client, true);
System.Threading.Tasks.Task<bool> t = ZipDeployAsync(ZipToPublishPath!, DeploymentUsername!, DeploymentPassword!, PublishUrl, SiteName!, UserAgentVersion!, UseBlobContainerDeploy, client, true);
t.Wait();
return t.Result;
}
}

internal async System.Threading.Tasks.Task<bool> ZipDeployAsync(string zipToPublishPath, string userName, string password, string? publishUrl, string siteName, string userAgentVersion, IHttpClient client, bool logMessages)
internal System.Threading.Tasks.Task<bool> ZipDeployAsync(string zipToPublishPath, string userName, string password, string publishUrl, string siteName, string userAgentVersion, IHttpClient client, bool logMessages)
{
return ZipDeployAsync(zipToPublishPath, userName, password, publishUrl, siteName, userAgentVersion, useBlobContainerDeploy: false, client, logMessages);
}

internal async System.Threading.Tasks.Task<bool> ZipDeployAsync(string zipToPublishPath, string userName, string password, string? publishUrl, string siteName, string userAgentVersion, bool useBlobContainerDeploy, IHttpClient client, bool logMessages)
{
if (!File.Exists(zipToPublishPath) || client == null)
{
Expand All @@ -73,11 +78,11 @@ internal async System.Threading.Tasks.Task<bool> ZipDeployAsync(string zipToPubl
publishUrl += "/";
}

zipDeployPublishUrl = publishUrl + "api/zipdeploy";
zipDeployPublishUrl = publishUrl + "api";
}
else if (!string.IsNullOrEmpty(siteName))
{
zipDeployPublishUrl = $"https://{siteName}.scm.azurewebsites.net/api/zipdeploy";
zipDeployPublishUrl = $"https://{siteName}.scm.azurewebsites.net/api";
}
else
{
Expand All @@ -89,13 +94,18 @@ internal async System.Threading.Tasks.Task<bool> ZipDeployAsync(string zipToPubl
return false;
}

// publish endpoint differs when using a blob storage container
var publishUriPath = useBlobContainerDeploy ? "publish?RemoteBuild=false" : "zipdeploy?isAsync=true";

// "<publishUrl>/api/zipdeploy?isAsync=true" or "<publishUrl>/api/publish?RemoteBuild=false"
zipDeployPublishUrl = $"{zipDeployPublishUrl}/{publishUriPath}";

if (logMessages)
{
Log.LogMessage(MessageImportance.High, String.Format(StringMessages.PublishingZipViaZipDeploy, zipToPublishPath, zipDeployPublishUrl));
}

// use the async version of the api
Uri uri = new Uri($"{zipDeployPublishUrl}?isAsync=true", UriKind.Absolute);
Uri uri = new Uri($"{zipDeployPublishUrl}", UriKind.Absolute);
string userAgent = $"{UserAgentName}/{userAgentVersion}";
FileStream stream = File.OpenRead(zipToPublishPath);
IHttpResponse response = await client.PostRequestAsync(uri, userName, password, "application/zip", userAgent, Encoding.UTF8, stream);
Expand All @@ -120,12 +130,12 @@ internal async System.Threading.Tasks.Task<bool> ZipDeployAsync(string zipToPubl
{
ZipDeploymentStatus deploymentStatus = new ZipDeploymentStatus(client, userAgent, Log, logMessages);
DeployStatus status = await deploymentStatus.PollDeploymentStatusAsync(deploymentUrl, userName, password);
if (status == DeployStatus.Success)
if (status == DeployStatus.Success || status == DeployStatus.PartialSuccess)
{
Log.LogMessage(MessageImportance.High, StringMessages.ZipDeploymentSucceeded);
return true;
}
else if (status == DeployStatus.Failed || status == DeployStatus.Unknown)
else if (status == DeployStatus.Failed || status == DeployStatus.Conflict || status == DeployStatus.Unknown)
{
Log.LogError(String.Format(StringMessages.ZipDeployFailureErrorMessage, zipDeployPublishUrl, status));
return false;
Expand Down
57 changes: 47 additions & 10 deletions sdk/Sdk/Tasks/ZipDeploy/ZipDeploymentStatus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -43,21 +41,33 @@ public ZipDeploymentStatus(IHttpClient client, string userAgent, TaskLoggingHelp

public async Task<DeployStatus> PollDeploymentStatusAsync(string deploymentUrl, string userName, string password)
{
DeployStatus deployStatus = DeployStatus.Pending;
var deployStatus = DeployStatus.Pending;
var deployStatusText = string.Empty;
var tokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(MaxMinutesToWait));

if (_logMessages)
{
_log.LogMessage(StringMessages.DeploymentStatusPolling);
}
while (!tokenSource.IsCancellationRequested && deployStatus != DeployStatus.Success && deployStatus != DeployStatus.Failed && deployStatus != DeployStatus.Unknown)
while (!tokenSource.IsCancellationRequested
&& deployStatus != DeployStatus.Success
&& deployStatus != DeployStatus.PartialSuccess
&& deployStatus != DeployStatus.Failed
&& deployStatus != DeployStatus.Conflict
&& deployStatus != DeployStatus.Unknown)
{
try
{
deployStatus = await GetDeploymentStatusAsync(deploymentUrl, userName, password, RetryCount, TimeSpan.FromSeconds(RetryDelaySeconds), tokenSource);
(deployStatus, deployStatusText) = await GetDeploymentStatusAsync(deploymentUrl, userName, password, RetryCount, TimeSpan.FromSeconds(RetryDelaySeconds), tokenSource);
if (_logMessages)
{
_log.LogMessage(String.Format(StringMessages.DeploymentStatus, Enum.GetName(typeof(DeployStatus), deployStatus)));
var deployStatusName = Enum.GetName(typeof(DeployStatus), deployStatus);

var message = string.IsNullOrEmpty(deployStatusText)
? string.Format(StringMessages.DeploymentStatus, deployStatusName)
: string.Format(StringMessages.DeploymentStatusWithText, deployStatusName, deployStatusText);

_log.LogMessage(message);
}
}
catch (HttpRequestException)
Expand All @@ -71,16 +81,29 @@ public async Task<DeployStatus> PollDeploymentStatusAsync(string deploymentUrl,
return deployStatus;
}

private async Task<DeployStatus> GetDeploymentStatusAsync(string deploymentUrl, string userName, string password, int retryCount, TimeSpan retryDelay, CancellationTokenSource cts)
private async Task<(DeployStatus, string)> GetDeploymentStatusAsync(string deploymentUrl, string userName, string password, int retryCount, TimeSpan retryDelay, CancellationTokenSource cts)
{
var status = DeployStatus.Unknown;
var statusText = string.Empty;

IDictionary<string, object>? json = await InvokeGetRequestWithRetryAsync<Dictionary<string, object>>(deploymentUrl, userName, password, retryCount, retryDelay, cts);

if (json != null && TryParseDeploymentStatus(json, out DeployStatus result))
if (json is not null)
{
return result;
// status
if (TryParseDeploymentStatus(json, out DeployStatus result))
{
status = result;
}

// status text message
if (TryParseDeploymentStatusText(json, out string text))
{
statusText = text;
}
}

return DeployStatus.Unknown;
return (status, statusText);
}

private static bool TryParseDeploymentStatus(IDictionary<string, object> json, out DeployStatus status)
Expand All @@ -97,6 +120,20 @@ private static bool TryParseDeploymentStatus(IDictionary<string, object> json, o
return false;
}

private static bool TryParseDeploymentStatusText(IDictionary<string, object> json, out string statusText)
{
statusText = string.Empty;

if (json.TryGetValue("status_text", out var textObj)
&& textObj is not null)
{
statusText = textObj.ToString();
return true;
}

return false;
}

private async Task<T?> InvokeGetRequestWithRetryAsync<T>(string url, string userName, string password, int retryCount, TimeSpan retryDelay, CancellationTokenSource cts)
{
IHttpResponse? response = null;
Expand Down
3 changes: 3 additions & 0 deletions sdk/release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

- Fix incorrect function version in build message (#2606)
- Fix inner build failures when central package management is enabled (#2689)
- Add support to publish a Function App (Flex Consumption) with `ZipDeploy` (#2712)
- Add `'UseBlobContainerDeploy'` property to identify when to use `OneDeploy` publish API endpoint (`"<publish_url>/api/publish"`)
- Enhance `ZipDeploy` deployment status logging by appending the `'status_message'` (when defined) to the output messages

### Microsoft.Azure.Functions.Worker.Sdk.Generators <version>

Expand Down
Binary file not shown.
5 changes: 5 additions & 0 deletions test/FunctionMetadataGeneratorTests/SdkTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,9 @@
<ProjectReference Include="..\TestUtility\TestUtility.csproj" />
</ItemGroup>

<ItemGroup>
<None Update="Resources\TestPublishContents.zip">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
Loading

0 comments on commit 24cd553

Please sign in to comment.