Skip to content

Commit

Permalink
Flex Deployment Changes (#3625)
Browse files Browse the repository at this point in the history
* fixing bug where auth parameter was ignored by Python v2.

* Flex - Public preview changes.

* Update runtime for flex.

* Update default .NET version to 8.0 in tests.

* Flex runtime update changes.

* Added timeout to a test.

* Updated the python default to 3.10.

* Changed the python default back to 3.10.

* Added 3.11 back in the condition.
  • Loading branch information
khkh-ms authored Mar 21, 2024
1 parent 01d17db commit 591b8ae
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 18 deletions.
130 changes: 120 additions & 10 deletions src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,20 @@
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Azure.Core;
using Azure.Functions.Cli.Actions.LocalActions;
using Azure.Functions.Cli.Arm.Models;
using Azure.Functions.Cli.Common;
using Azure.Functions.Cli.Diagnostics;
using Azure.Functions.Cli.Extensions;
using Azure.Functions.Cli.Helpers;
using Azure.Functions.Cli.Interfaces;
using Azure.Functions.Cli.StacksApi;
using Colors.Net;
using Fclp;
using Microsoft.Build.Framework;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
using Newtonsoft.Json;
using NuGet.Common;
using static Azure.Functions.Cli.Common.OutputTheme;
using Site = Azure.Functions.Cli.Arm.Models.Site;

namespace Azure.Functions.Cli.Actions.AzureActions
{
Expand All @@ -34,7 +32,7 @@ internal class PublishFunctionAppAction : BaseFunctionAppAction

private readonly ISettings _settings;
private readonly ISecretsManager _secretsManager;
private static string _requiredNetFrameworkVersion = "6.0";
private static string _requiredNetFrameworkVersion = "8.0";

public bool PublishLocalSettings { get; set; }
public bool OverwriteSettings { get; set; }
Expand Down Expand Up @@ -268,7 +266,14 @@ private async Task<IDictionary<string, string>> ValidateFunctionAppPublish(Site
}
}

if (functionApp.AzureAppSettings.TryGetValue(Constants.FunctionsWorkerRuntime, out string workerRuntimeStr))
string workerRuntimeStr = null;
if (functionApp.IsFlex)
{
workerRuntimeStr = functionApp.FunctionAppConfig.runtime.name;
}

if ((functionApp.IsFlex && !string.IsNullOrEmpty(workerRuntimeStr) ||
(!functionApp.IsFlex && functionApp.AzureAppSettings.TryGetValue(Constants.FunctionsWorkerRuntime, out workerRuntimeStr))))
{
var resolution = $"You can pass --force to update your Azure app with '{workerRuntime}' as a '{Constants.FunctionsWorkerRuntime}'";
try
Expand Down Expand Up @@ -312,7 +317,18 @@ private async Task<IDictionary<string, string>> ValidateFunctionAppPublish(Site
throw new CliException($"Azure Functions Core Tools does not support this deployment path. Please configure the app to deploy from a remote package using the steps here: https://aka.ms/deployfromurl");
}

await UpdateFrameworkVersions(functionApp, workerRuntime, DotnetFrameworkVersion, Force, azureHelperService);
if (functionApp.IsFlex)
{
if (result.ContainsKey(Constants.FunctionsWorkerRuntime))
{
await UpdateRuntimeConfigForFlex(functionApp, WorkerRuntimeLanguageHelper.GetRuntimeMoniker(workerRuntime), null, azureHelperService);
result.Remove(Constants.FunctionsWorkerRuntime);
}
}
else
{
await UpdateFrameworkVersions(functionApp, workerRuntime, DotnetFrameworkVersion, Force, azureHelperService);
}

// Special checks for python dependencies
if (workerRuntime == WorkerRuntime.python)
Expand All @@ -321,10 +337,12 @@ private async Task<IDictionary<string, string>> ValidateFunctionAppPublish(Site
await PythonHelpers.WarnIfAzureFunctionsWorkerInRequirementsTxt();
// Check if remote LinuxFxVersion exists and is different from local version
var localVersion = await PythonHelpers.GetEnvironmentPythonVersion();
if (!PythonHelpers.IsLinuxFxVersionRuntimeVersionMatched(functionApp.LinuxFxVersion, localVersion.Major, localVersion.Minor))

if ((!functionApp.IsFlex && !PythonHelpers.IsLinuxFxVersionRuntimeVersionMatched(functionApp.LinuxFxVersion, localVersion.Major, localVersion.Minor)) ||
(functionApp.IsFlex && !PythonHelpers.IsFlexPythonRuntimeVersionMatched(functionApp.FunctionAppConfig?.runtime?.name, functionApp.FunctionAppConfig?.runtime?.version, localVersion.Major, localVersion.Minor)))
{
ColoredConsole.WriteLine(WarningColor($"Local python version '{localVersion.Version}' is different from the version expected for your deployed Function App." +
$" This may result in 'ModuleNotFound' errors in Azure Functions. Please create a Python Function App for version {localVersion.Major}.{localVersion.Minor} or change the virtual environment on your local machine to match '{functionApp.LinuxFxVersion}'."));
$" This may result in 'ModuleNotFound' errors in Azure Functions. Please create a Python Function App for version {localVersion.Major}.{localVersion.Minor} or change the virtual environment on your local machine to match '{(functionApp.IsFlex? functionApp.FunctionAppConfig.runtime.version: functionApp.LinuxFxVersion)}'."));
}
}

Expand All @@ -336,6 +354,59 @@ private async Task<IDictionary<string, string>> ValidateFunctionAppPublish(Site
return result;
}

public static async Task UpdateRuntimeConfigForFlex(Site site, string runtimeName, string runtimeVersion, AzureHelperService helperService)
{
if (string.IsNullOrEmpty(runtimeName))
{
return;
}

if (string.IsNullOrEmpty(runtimeVersion))
{
if (runtimeName.Equals("python", StringComparison.OrdinalIgnoreCase))
{
var localVersion = await PythonHelpers.GetEnvironmentPythonVersion();
runtimeVersion = $"{localVersion.Major}.{localVersion.Minor}";
if (runtimeVersion != "3.10" && runtimeVersion != "3.11")
{
// todo: default will be 3.11 after 3.11 support is added.
runtimeVersion = "3.10";
}
}
else if (runtimeName.Equals("dotnet-isolated", StringComparison.OrdinalIgnoreCase))
{
// Only .NET 8.0 is supported in flex.
if (runtimeVersion != "8.0")
runtimeVersion = "8.0";
}
else if (runtimeName.Equals("node", StringComparison.OrdinalIgnoreCase))
{
// Only Node 18 is supported.
if (runtimeVersion != "18")
runtimeVersion = "18";
}
else if (runtimeName.Equals("powershell", StringComparison.OrdinalIgnoreCase))
{
// Only Python 7.2 is supported.
if (runtimeVersion != "7.2")
runtimeVersion = "7.2";
}
else if (runtimeName.Equals("java", StringComparison.OrdinalIgnoreCase))
{
// Warning: Java is not supported by core tools at the moment.
ColoredConsole.WriteLine(WarningColor($"Java is not supported in core tools at the moment. Please use az cli to update the runtime information."));
}
else
{
// Warning: Runtime name is unknown.
ColoredConsole.WriteLine(WarningColor($"Runtime is not updated. Only dotnet-isolated, node, java, and powershell is supported in core tools for Flex SKU."));
return;
}
}

await helperService.UpdateFlexRuntime(site, runtimeName, runtimeVersion);
}

internal static async Task UpdateFrameworkVersions(Site functionApp, WorkerRuntime workerRuntime, string requestedDotNetVersion, bool force, AzureHelperService helperService)
{
if (workerRuntime == WorkerRuntime.dotnetIsolated)
Expand Down Expand Up @@ -686,6 +757,14 @@ await WaitForAppSettingUpdateSCM(functionApp, shouldHaveSettings: functionApp.Az
/// <returns>ShouldSyncTrigger value</returns>
private async Task<bool> HandleFlexConsumptionPublish(Site functionApp, Func<Task<Stream>> zipFileFactory)
{
// Get the WorkerRuntime
var workerRuntime = GlobalCoreToolsSettings.CurrentWorkerRuntime;

if (workerRuntime == WorkerRuntime.dotnetIsolated && _requiredNetFrameworkVersion != "8.0")
{
throw new CliException($"You are deploying .NET Isolated {_requiredNetFrameworkVersion} to Flex consumption. Flex consumpton only supports .NET 8. Please upgrade your app to .NET 8 and try the deployment again.");
}

Task<DeployStatus> pollDeploymentStatusTask(HttpClient client) => KuduLiteDeploymentHelpers.WaitForFlexDeployment(client, functionApp);
var deploymentParameters = new Dictionary<string, string>();

Expand All @@ -704,7 +783,7 @@ public async Task<DeployStatus> PerformFlexDeployment(Site functionApp, Func<Tas
using (var handler = new ProgressMessageHandler(new HttpClientHandler()))
using (var client = GetRemoteZipClient(functionApp, handler))
using (var request = new HttpRequestMessage(HttpMethod.Post, new Uri(
$"api/Deploy/Zip?isAsync=true&author={Environment.MachineName}&Deployer=core_tools&{string.Join("&", deploymentParameters?.Select(kvp => $"{kvp.Key}={kvp.Value}")) ?? string.Empty}", UriKind.Relative)))
$"api/publish?isAsync=true&author={Environment.MachineName}&Deployer=core_tools&{string.Join("&", deploymentParameters?.Select(kvp => $"{kvp.Key}={kvp.Value}")) ?? string.Empty}", UriKind.Relative)))
{
ColoredConsole.WriteLine(GetLogMessage("Creating archive for current directory..."));

Expand Down Expand Up @@ -1141,7 +1220,29 @@ private async Task<bool> PublishLocalAppSettings(Site functionApp, IDictionary<s

private async Task<bool> PublishAppSettings(Site functionApp, IDictionary<string, string> local, IDictionary<string, string> additional)
{
string flexRuntimeName = null;
string flexRuntimeVersion = null;
if (functionApp.IsFlex)
{
// if the additiona keys has runtime, it would mean that runtime is already updated.
if (!additional.ContainsKey(Constants.FunctionsWorkerRuntime))
{
if (local.ContainsKey(Constants.FunctionsWorkerRuntime))
{
flexRuntimeName = local[Constants.FunctionsWorkerRuntime];
local.Remove(Constants.FunctionsWorkerRuntime);
}

if (local.ContainsKey(Constants.FunctionsWorkerRuntimeVersion))
{
flexRuntimeVersion = local[Constants.FunctionsWorkerRuntimeVersion];
local.Remove(Constants.FunctionsWorkerRuntimeVersion);
}
}
}

functionApp.AzureAppSettings = MergeAppSettings(functionApp.AzureAppSettings, local, additional);

var result = await AzureHelper.UpdateFunctionAppAppSettings(functionApp, AccessToken, ManagementURL);
if (!result.IsSuccessful)
{
Expand All @@ -1151,6 +1252,12 @@ private async Task<bool> PublishAppSettings(Site functionApp, IDictionary<string
.WriteLine(ErrorColor(result.ErrorResult));
return false;
}

if (functionApp.IsFlex && !string.IsNullOrEmpty(flexRuntimeName))
{
await UpdateRuntimeConfigForFlex(functionApp, flexRuntimeName, flexRuntimeVersion, new AzureHelperService(AccessToken, ManagementURL));
}

return true;
}

Expand Down Expand Up @@ -1288,6 +1395,9 @@ public AzureHelperService(string accessToken, string managementUrl)

public virtual Task<HttpResult<string, string>> UpdateWebSettings(Site functionApp, Dictionary<string, string> updatedSettings) =>
AzureHelper.UpdateWebSettings(functionApp, updatedSettings, _accessToken, _managementUrl);

public virtual Task UpdateFlexRuntime(Site functionApp, string runtimeName, string runtimeVersion) =>
AzureHelper.UpdateFlexRuntime(functionApp, runtimeName, runtimeVersion, _accessToken, _managementUrl);
}

private void ShowEolMessage(FunctionsStacks stacks, WindowsRuntimeSettings currentRuntimeSettings, int? majorDotnetVersion)
Expand Down
1 change: 1 addition & 0 deletions src/Azure.Functions.Cli/Arm/ArmUriTemplates.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ internal static class ArmUriTemplates
{
public const string ArmApiVersion = "2018-09-01";
public const string WebsitesApiVersion = "2015-08-01";
public const string FlexApiVersion = "2023-12-01";
public const string SyncTriggersApiVersion = "2016-08-01";
public const string ArgApiVersion = "2019-04-01";
public const string FunctionAppOnContainerAppsApiVersion = "2022-09-01";
Expand Down
42 changes: 42 additions & 0 deletions src/Azure.Functions.Cli/Arm/Models/ArmWebsite.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,47 @@ internal class ArmWebsite
public IEnumerable<string> enabledHostNames { get; set; }

public string sku { get; set; }

public FunctionAppConfig functionAppConfig { get; set; }
}

public class Authentication
{
public string type { get; set; }
public object userAssignedIdentityResourceId { get; set; }
public string storageAccountConnectionStringName { get; set; }
}

public class Deployment
{
public Storage storage { get; set; }
}

public class FunctionAppConfig
{
public Deployment deployment { get; set; }
public Runtime runtime { get; set; }
public ScaleAndConcurrency scaleAndConcurrency { get; set; }
}

public class Runtime
{
public string name { get; set; }
public string version { get; set; }
}

public class ScaleAndConcurrency
{
public List<object> alwaysReady { get; set; }
public int maximumInstanceCount { get; set; }
public int instanceMemoryMB { get; set; }
public object triggers { get; set; }
}

public class Storage
{
public string type { get; set; }
public string value { get; set; }
public Authentication authentication { get; set; }
}
}
2 changes: 2 additions & 0 deletions src/Azure.Functions.Cli/Arm/Models/Site.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,7 @@ public Site(string siteId)
{
SiteId = siteId;
}

public FunctionAppConfig FunctionAppConfig { get; set; }
}
}
30 changes: 30 additions & 0 deletions src/Azure.Functions.Cli/Helpers/AzureHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -423,9 +423,38 @@ public static async Task<Site> LoadSiteObjectAsync(Site site, string accessToken
site.Kind = armSite.kind;
site.Sku = armSite.properties.sku;
site.SiteName = armSite.name;
site.FunctionAppConfig = armSite.properties.functionAppConfig;
return site;
}

public static async Task<bool> UpdateFlexRuntime(Site site, string runtimeName, string runtimeValue, string accessToken, string managementURL)
{
var url = new Uri($"{managementURL}{site.SiteId}?api-version={ArmUriTemplates.WebsitesApiVersion}");
var functionAppJson = await ArmHttpAsyncForFlex(HttpMethod.Get, url, accessToken);
dynamic functionApp = JsonConvert.DeserializeObject(functionAppJson);
var runtimeConfig = functionApp?.properties?.functionAppConfig?.runtime;

if (runtimeConfig == null)
{
return false;
}

runtimeConfig.name = runtimeName;
runtimeConfig.version = runtimeValue;

url = new Uri($"{managementURL}{site.SiteId}?api-version={ArmUriTemplates.FlexApiVersion}");
var result = await ArmHttpAsyncForFlex(new HttpMethod("PUT"), url, accessToken, functionApp);
return true;
}

private static async Task<string> ArmHttpAsyncForFlex(HttpMethod method, Uri uri, string accessToken, object payload = null)
{
var response = await ArmClient.HttpInvoke(method, uri, accessToken, payload, retryCount: 3);
response.EnsureSuccessStatusCode();

return await response.Content.ReadAsStringAsync();
}

private static async Task<T> ArmHttpAsync<T>(HttpMethod method, Uri uri, string accessToken, object payload = null)
{
var response = await ArmClient.HttpInvoke(method, uri, accessToken, payload, retryCount: 3);
Expand Down Expand Up @@ -464,6 +493,7 @@ public static async Task<HttpResult<string, string>> UpdateWebSettings(Site site
public static async Task<HttpResult<Dictionary<string, string>, string>> UpdateFunctionAppAppSettings(Site site, string accessToken, string managementURL)
{
var url = new Uri($"{managementURL}{site.SiteId}/config/AppSettings?api-version={ArmUriTemplates.WebsitesApiVersion}");

var response = await ArmClient.HttpInvoke(HttpMethod.Put, url, accessToken, new { properties = site.AzureAppSettings });
if (response.IsSuccessStatusCode)
{
Expand Down
17 changes: 17 additions & 0 deletions src/Azure.Functions.Cli/Helpers/PythonHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,23 @@ public static bool IsLinuxFxVersionRuntimeVersionMatched(string linuxFxVersion,
return string.Equals(linuxFxVersion, $"PYTHON|{major}.{minor}", StringComparison.OrdinalIgnoreCase);
}

public static bool IsFlexPythonRuntimeVersionMatched(string flexRuntime, string flexRuntimeVersion, int? major, int? minor)
{
if (string.IsNullOrEmpty(flexRuntime) || string.IsNullOrEmpty(flexRuntimeVersion))
{
// Match if version is 3.10
return major == 3 && minor == 10;
}
// Only validate for python.
else if (!flexRuntime.Equals("python", StringComparison.OrdinalIgnoreCase))
{
return true;
}

// Validate LinuxFxVersion that follows the pattern PYTHON|<major>.<minor>
return flexRuntimeVersion.Equals($"{major}.{minor}", StringComparison.OrdinalIgnoreCase);
}

public static bool IsNewPythonProgrammingModel(string language)
{
return string.Equals(language, Languages.Python, StringComparison.InvariantCultureIgnoreCase) && HasPySteinFile();
Expand Down
3 changes: 2 additions & 1 deletion test/Azure.Functions.Cli.Tests/E2E/CreateFunctionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ await CliTester.Run(new RunConfiguration
OutputContains = new[]
{
"Authorization level is applicable to templates that use Http trigger, Allowed values: [function, anonymous, admin]. Authorization level is not enforced when running functions from core tools"
}
},
CommandTimeout = TimeSpan.FromSeconds(120)
}, _output);
}

Expand Down
Loading

0 comments on commit 591b8ae

Please sign in to comment.