From dac2c91e970a138ad5fb0e316093c19c6ac9c29f Mon Sep 17 00:00:00 2001 From: Antonio Villanueva Date: Wed, 25 Sep 2024 10:21:30 -0700 Subject: [PATCH 1/3] Add new 'OneDeploy' web publish method. --- sdk.sln | 3 + ...icrosoft.NET.Sdk.Publish.CopyFiles.targets | 1 + .../PublishProfiles/DefaultOneDeploy.pubxml | 13 + .../DefaultWebJobOneDeploy.pubxml | 15 + ...icrosoft.NET.Sdk.Publish.OneDeploy.targets | 68 ++++ ...oft.NET.Sdk.Publish.TransformFiles.targets | 4 +- .../Publish/Tasks/Properties/AssemblyInfo.cs | 8 + .../Tasks/Properties/Resources.Designer.cs | 117 ++++++ .../Publish/Tasks/Properties/Resources.resx | 49 +++ .../Tasks/Properties/xlf/Resources.cs.xlf | 65 ++++ .../Tasks/Properties/xlf/Resources.de.xlf | 65 ++++ .../Tasks/Properties/xlf/Resources.es.xlf | 65 ++++ .../Tasks/Properties/xlf/Resources.fr.xlf | 65 ++++ .../Tasks/Properties/xlf/Resources.it.xlf | 65 ++++ .../Tasks/Properties/xlf/Resources.ja.xlf | 65 ++++ .../Tasks/Properties/xlf/Resources.ko.xlf | 65 ++++ .../Tasks/Properties/xlf/Resources.pl.xlf | 65 ++++ .../Tasks/Properties/xlf/Resources.pt-BR.xlf | 65 ++++ .../Tasks/Properties/xlf/Resources.ru.xlf | 65 ++++ .../Tasks/Properties/xlf/Resources.tr.xlf | 65 ++++ .../Properties/xlf/Resources.zh-Hans.xlf | 65 ++++ .../Properties/xlf/Resources.zh-Hant.xlf | 65 ++++ .../Tasks/Tasks/Http/DefaultHttpClient.cs | 42 +++ .../Tasks/Tasks/Http/HttpClientExtensions.cs | 328 ++++++++++++++++ .../Http/HttpResponseMessageForStatusCode.cs | 24 ++ .../Tasks/Http/HttpResponseMessageWrapper.cs | 46 +++ .../Publish/Tasks/Tasks/Http/IHttpClient.cs | 43 +++ .../Publish/Tasks/Tasks/Http/IHttpResponse.cs | 30 ++ .../Tasks/OneDeploy/CreatePackageFile.cs | 59 +++ .../Tasks/OneDeploy/DeploymentResponse.cs | 121 ++++++ .../Tasks/Tasks/OneDeploy/DeploymentStatus.cs | 56 +++ .../OneDeploy/IDeploymentStatusService.cs | 23 ++ .../Tasks/Tasks/OneDeploy/IFilePackager.cs | 24 ++ .../Tasks/Tasks/OneDeploy/ITaskLogger.cs | 42 +++ .../Tasks/Tasks/OneDeploy/OneDeploy.WebJob.cs | 62 +++ .../Tasks/Tasks/OneDeploy/OneDeploy.cs | 227 +++++++++++ .../Tasks/OneDeploy/OneDeployStatusService.cs | 83 +++++ .../Tasks/Tasks/OneDeploy/TaskLogger.cs | 58 +++ .../Tasks/Tasks/OneDeploy/ZipFilePackager.cs | 24 ++ .../Tasks/ZipDeploy/DefaultHttpClient.cs | 33 -- .../Tasks/ZipDeploy/HttpClientHelpers.cs | 79 ---- .../HttpResponseMessageForStatusCode.cs | 27 -- .../ZipDeploy/HttpResponseMessageWrapper.cs | 44 --- .../Tasks/Tasks/ZipDeploy/IHttpClient.cs | 15 - .../Tasks/Tasks/ZipDeploy/IHttpResponse.cs | 25 -- src/WebSdk/README.md | 18 + .../Tasks/OneDeploy/CreatePackageFileTests.cs | 99 +++++ .../OneDeploy/OneDeployStatusServiceTests.cs | 240 ++++++++++++ .../Tasks/OneDeploy/OneDeployTests.WebJob.cs | 189 ++++++++++ .../Tasks/OneDeploy/OneDeployTests.cs | 352 ++++++++++++++++++ 50 files changed, 3312 insertions(+), 224 deletions(-) create mode 100644 src/WebSdk/Publish/Targets/PublishProfiles/DefaultOneDeploy.pubxml create mode 100644 src/WebSdk/Publish/Targets/PublishProfiles/DefaultWebJobOneDeploy.pubxml create mode 100644 src/WebSdk/Publish/Targets/PublishTargets/Microsoft.NET.Sdk.Publish.OneDeploy.targets create mode 100644 src/WebSdk/Publish/Tasks/Properties/AssemblyInfo.cs create mode 100644 src/WebSdk/Publish/Tasks/Tasks/Http/DefaultHttpClient.cs create mode 100644 src/WebSdk/Publish/Tasks/Tasks/Http/HttpClientExtensions.cs create mode 100644 src/WebSdk/Publish/Tasks/Tasks/Http/HttpResponseMessageForStatusCode.cs create mode 100644 src/WebSdk/Publish/Tasks/Tasks/Http/HttpResponseMessageWrapper.cs create mode 100644 src/WebSdk/Publish/Tasks/Tasks/Http/IHttpClient.cs create mode 100644 src/WebSdk/Publish/Tasks/Tasks/Http/IHttpResponse.cs create mode 100644 src/WebSdk/Publish/Tasks/Tasks/OneDeploy/CreatePackageFile.cs create mode 100644 src/WebSdk/Publish/Tasks/Tasks/OneDeploy/DeploymentResponse.cs create mode 100644 src/WebSdk/Publish/Tasks/Tasks/OneDeploy/DeploymentStatus.cs create mode 100644 src/WebSdk/Publish/Tasks/Tasks/OneDeploy/IDeploymentStatusService.cs create mode 100644 src/WebSdk/Publish/Tasks/Tasks/OneDeploy/IFilePackager.cs create mode 100644 src/WebSdk/Publish/Tasks/Tasks/OneDeploy/ITaskLogger.cs create mode 100644 src/WebSdk/Publish/Tasks/Tasks/OneDeploy/OneDeploy.WebJob.cs create mode 100644 src/WebSdk/Publish/Tasks/Tasks/OneDeploy/OneDeploy.cs create mode 100644 src/WebSdk/Publish/Tasks/Tasks/OneDeploy/OneDeployStatusService.cs create mode 100644 src/WebSdk/Publish/Tasks/Tasks/OneDeploy/TaskLogger.cs create mode 100644 src/WebSdk/Publish/Tasks/Tasks/OneDeploy/ZipFilePackager.cs delete mode 100644 src/WebSdk/Publish/Tasks/Tasks/ZipDeploy/DefaultHttpClient.cs delete mode 100644 src/WebSdk/Publish/Tasks/Tasks/ZipDeploy/HttpClientHelpers.cs delete mode 100644 src/WebSdk/Publish/Tasks/Tasks/ZipDeploy/HttpResponseMessageForStatusCode.cs delete mode 100644 src/WebSdk/Publish/Tasks/Tasks/ZipDeploy/HttpResponseMessageWrapper.cs delete mode 100644 src/WebSdk/Publish/Tasks/Tasks/ZipDeploy/IHttpClient.cs delete mode 100644 src/WebSdk/Publish/Tasks/Tasks/ZipDeploy/IHttpResponse.cs create mode 100644 test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/CreatePackageFileTests.cs create mode 100644 test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/OneDeployStatusServiceTests.cs create mode 100644 test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/OneDeployTests.WebJob.cs create mode 100644 test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/OneDeployTests.cs diff --git a/sdk.sln b/sdk.sln index 1e3a6f52e474..9e6140637b8b 100644 --- a/sdk.sln +++ b/sdk.sln @@ -211,6 +211,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PublishProfiles", "PublishP src\WebSdk\Publish\Targets\PublishProfiles\DefaultMSDeploy.pubxml = src\WebSdk\Publish\Targets\PublishProfiles\DefaultMSDeploy.pubxml src\WebSdk\Publish\Targets\PublishProfiles\DefaultMSDeployPackage.pubxml = src\WebSdk\Publish\Targets\PublishProfiles\DefaultMSDeployPackage.pubxml src\WebSdk\Publish\Targets\PublishProfiles\DefaultZipDeploy.pubxml = src\WebSdk\Publish\Targets\PublishProfiles\DefaultZipDeploy.pubxml + src\WebSdk\Publish\Targets\PublishProfiles\DefaultOneDeploy.pubxml = src\WebSdk\Publish\Targets\PublishProfiles\DefaultOneDeploy.pubxml + src\WebSdk\Publish\Targets\PublishProfiles\DefaultWebJobOneDeploy.pubxml = src\WebSdk\Publish\Targets\PublishProfiles\DefaultWebJobOneDeploy.pubxml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PublishTargets", "PublishTargets", "{40E6E4B1-286B-4542-B814-2A3DA29510D1}" @@ -221,6 +223,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PublishTargets", "PublishTa src\WebSdk\Publish\Targets\PublishTargets\Microsoft.NET.Sdk.Publish.MSDeploy.targets = src\WebSdk\Publish\Targets\PublishTargets\Microsoft.NET.Sdk.Publish.MSDeploy.targets src\WebSdk\Publish\Targets\PublishTargets\Microsoft.NET.Sdk.Publish.MSDeployPackage.targets = src\WebSdk\Publish\Targets\PublishTargets\Microsoft.NET.Sdk.Publish.MSDeployPackage.targets src\WebSdk\Publish\Targets\PublishTargets\Microsoft.NET.Sdk.Publish.ZipDeploy.targets = src\WebSdk\Publish\Targets\PublishTargets\Microsoft.NET.Sdk.Publish.ZipDeploy.targets + src\WebSdk\Publish\Targets\PublishTargets\Microsoft.NET.Sdk.Publish.OneDeploy.targets = src\WebSdk\Publish\Targets\PublishTargets\Microsoft.NET.Sdk.Publish.OneDeploy.targets EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TransformTargets", "TransformTargets", "{DFA91CC3-D6E4-45B7-AF6F-4385288886E4}" diff --git a/src/WebSdk/Publish/Targets/CopyTargets/Microsoft.NET.Sdk.Publish.CopyFiles.targets b/src/WebSdk/Publish/Targets/CopyTargets/Microsoft.NET.Sdk.Publish.CopyFiles.targets index 95c3483903f4..e7317f6bb3ad 100644 --- a/src/WebSdk/Publish/Targets/CopyTargets/Microsoft.NET.Sdk.Publish.CopyFiles.targets +++ b/src/WebSdk/Publish/Targets/CopyTargets/Microsoft.NET.Sdk.Publish.CopyFiles.targets @@ -113,6 +113,7 @@ Copyright (C) Microsoft Corporation. All rights reserved. $(PublishIntermediateOutputPath)\app_data\Jobs\$(WebJobType)\$(WebJobName)\ + $(PublishIntermediateOutputPath)\ $(PublishConfiguration) diff --git a/src/WebSdk/Publish/Targets/PublishProfiles/DefaultOneDeploy.pubxml b/src/WebSdk/Publish/Targets/PublishProfiles/DefaultOneDeploy.pubxml new file mode 100644 index 000000000000..84faff9a442e --- /dev/null +++ b/src/WebSdk/Publish/Targets/PublishProfiles/DefaultOneDeploy.pubxml @@ -0,0 +1,13 @@ + + + + + OneDeploy + AzureWebSite + Release + Any CPU + + + diff --git a/src/WebSdk/Publish/Targets/PublishProfiles/DefaultWebJobOneDeploy.pubxml b/src/WebSdk/Publish/Targets/PublishProfiles/DefaultWebJobOneDeploy.pubxml new file mode 100644 index 000000000000..5c123b76b436 --- /dev/null +++ b/src/WebSdk/Publish/Targets/PublishProfiles/DefaultWebJobOneDeploy.pubxml @@ -0,0 +1,15 @@ + + + + + OneDeploy + AzureWebSite + Release + Any CPU + + + + + diff --git a/src/WebSdk/Publish/Targets/PublishTargets/Microsoft.NET.Sdk.Publish.OneDeploy.targets b/src/WebSdk/Publish/Targets/PublishTargets/Microsoft.NET.Sdk.Publish.OneDeploy.targets new file mode 100644 index 000000000000..8c3325500638 --- /dev/null +++ b/src/WebSdk/Publish/Targets/PublishTargets/Microsoft.NET.Sdk.Publish.OneDeploy.targets @@ -0,0 +1,68 @@ + + + + + + + + + <_DotNetPublishFiles> + OneDeploy; + + + + + + + + + + + + + + + + + + WebSdk + WebSdk_VisualStudio_$(VisualStudioVersion) + + + + + + + + + + diff --git a/src/WebSdk/Publish/Targets/TransformTargets/Microsoft.NET.Sdk.Publish.TransformFiles.targets b/src/WebSdk/Publish/Targets/TransformTargets/Microsoft.NET.Sdk.Publish.TransformFiles.targets index f626ff306623..4125838cd71b 100644 --- a/src/WebSdk/Publish/Targets/TransformTargets/Microsoft.NET.Sdk.Publish.TransformFiles.targets +++ b/src/WebSdk/Publish/Targets/TransformTargets/Microsoft.NET.Sdk.Publish.TransformFiles.targets @@ -245,13 +245,15 @@ Copyright (C) Microsoft Corporation. All rights reserved. <_UseAppHost Condition=" '$(_UseAppHost)' == '' Or '$(RuntimeIdentifier)' == '' ">false <_ExecutableExtension Condition=" '$(_ExecutableExtension)' == '' And $(RuntimeIdentifier.StartsWith('win')) ">.exe <_IsLinux Condition=" '$(IsLinux)' == 'True'">true + <_WebJobsDirectory>$(PublishIntermediateOutputPath)\app_data\Jobs\$(WebJobType)\$(WebJobName)\ + <_WebJobsDirectory Condition="'$(WebPublishMethod)' == 'OneDeploy'">$(PublishIntermediateOutputPath)\ diff --git a/src/WebSdk/Publish/Tasks/Properties/AssemblyInfo.cs b/src/WebSdk/Publish/Tasks/Properties/AssemblyInfo.cs new file mode 100644 index 000000000000..94ef778a731a --- /dev/null +++ b/src/WebSdk/Publish/Tasks/Properties/AssemblyInfo.cs @@ -0,0 +1,8 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; + +// Test assemblies +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] +[assembly: InternalsVisibleTo("Microsoft.NET.Sdk.Publish.Tasks.Tests, PublicKey = 0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] diff --git a/src/WebSdk/Publish/Tasks/Properties/Resources.Designer.cs b/src/WebSdk/Publish/Tasks/Properties/Resources.Designer.cs index 2577a999896f..6da73efa8d6a 100644 --- a/src/WebSdk/Publish/Tasks/Properties/Resources.Designer.cs +++ b/src/WebSdk/Publish/Tasks/Properties/Resources.Designer.cs @@ -444,6 +444,42 @@ public static string DeploymentError_MissingDbDacFx { } } + /// + /// Looks up a localized string similar to Deployment status is '{0}'.. + /// + public static string DeploymentStatus { + get { + return ResourceManager.GetString("DeploymentStatus", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Deployment status URL '{0}' is missing or invalid. + /// + public static string DeploymentStatus_InvalidPollingUrl { + get { + return ResourceManager.GetString("DeploymentStatus_InvalidPollingUrl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Polling for deployment status.... + /// + public static string DeploymentStatus_Polling { + get { + return ResourceManager.GetString("DeploymentStatus_Polling", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Deployment status is '{0}': {1}. + /// + public static string DeploymentStatusWithText { + get { + return ResourceManager.GetString("DeploymentStatusWithText", resourceCulture); + } + } + /// /// Looks up a localized string similar to Generating Entity Framework SQL Scripts.... /// @@ -673,6 +709,87 @@ public static string MsDeployReadMe { } } + /// + /// Looks up a localized string similar to OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'.. + /// + public static string ONEDEPLOY_FailedDeployRequest { + get { + return ResourceManager.GetString("ONEDEPLOY_FailedDeployRequest", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}.. + /// + public static string ONEDEPLOY_FailedDeployRequest_With_ResponseText { + get { + return ResourceManager.GetString("ONEDEPLOY_FailedDeployRequest_With_ResponseText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to retrieve publish credentials.. + /// + public static string ONEDEPLOY_FailedToRetrieveCredentials { + get { + return ResourceManager.GetString("ONEDEPLOY_FailedToRetrieveCredentials", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'.. + /// + public static string ONEDEPLOY_FailedWithLogs { + get { + return ResourceManager.GetString("ONEDEPLOY_FailedWithLogs", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to File to publish '{0}' was not found or is not accessible.. + /// + public static string ONEDEPLOY_FileToPublish_NotFound { + get { + return ResourceManager.GetString("ONEDEPLOY_FileToPublish_NotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PublishUrl '{0}' is missing or has an invalid value.. + /// + public static string ONEDEPLOY_InvalidPublishUrl { + get { + return ResourceManager.GetString("ONEDEPLOY_InvalidPublishUrl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Publishing '{0}' to '{1}'.... + /// + public static string ONEDEPLOY_PublishingOneDeploy { + get { + return ResourceManager.GetString("ONEDEPLOY_PublishingOneDeploy", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to OneDeploy deployment succeeded.. + /// + public static string ONEDEPLOY_Success { + get { + return ResourceManager.GetString("ONEDEPLOY_Success", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to File '{0}' uploaded to target instance.. + /// + public static string ONEDEPLOY_Uploaded { + get { + return ResourceManager.GetString("ONEDEPLOY_Uploaded", resourceCulture); + } + } + /// /// Looks up a localized string similar to Unable to parse the pubxml file {0}.. /// diff --git a/src/WebSdk/Publish/Tasks/Properties/Resources.resx b/src/WebSdk/Publish/Tasks/Properties/Resources.resx index ce720d327efa..f7472535d0a6 100644 --- a/src/WebSdk/Publish/Tasks/Properties/Resources.resx +++ b/src/WebSdk/Publish/Tasks/Properties/Resources.resx @@ -1203,4 +1203,53 @@ For more information on this deploy script visit: https://go.microsoft.com/fwlin The attempt to publish the ZIP file through '{0}' failed with HTTP status code '{1}'. {0} - URL to deploy zip, {1} - HTTP response code + + Failed to retrieve publish credentials. + + + File to publish '{0}' was not found or is not accessible. + {0} - file to publish + + + PublishUrl '{0}' is missing or has an invalid value. + {0} - publish URL + + + Publishing '{0}' to '{1}'... + {0} - file to publish, {1} - publish URL + + + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + {0} - publish URL, {1} - HTTP response status code + + + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + {0} - publish URL, {1} - HTTP response status code, {2} - response body as text + + + File '{0}' uploaded to target instance. + {0} - file to publish. + + + OneDeploy deployment succeeded. + + + OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + {0} - file to publish, {1} - publish URL, {2} - deployment response status code, {3} - deployment logs URL + + + Polling for deployment status... + + + Deployment status URL '{0}' is missing or invalid + {0} - deployment polling URL + + + Deployment status is '{0}'. + {0} - deployment status name + + + Deployment status is '{0}': {1} + {0} - deployment status name, {1} - deployment status text + diff --git a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.cs.xlf b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.cs.xlf index d678a6f4b63a..56dc331335f9 100644 --- a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.cs.xlf +++ b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.cs.xlf @@ -224,6 +224,26 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 + + Deployment status is '{0}'. + Deployment status is '{0}'. + {0} - deployment status name + + + Deployment status is '{0}': {1} + Deployment status is '{0}': {1} + {0} - deployment status name, {1} - deployment status text + + + Deployment status URL '{0}' is missing or invalid + Deployment status URL '{0}' is missing or invalid + {0} - deployment polling URL + + + Polling for deployment status... + Polling for deployment status... + + Generating Entity Framework SQL Scripts... Generují se skripty SQL Entity Framework... @@ -334,6 +354,51 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 Neplatný příkaz ({0}) pro zadaný zdroj ({1}) a cíl ({2}). + + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + {0} - publish URL, {1} - HTTP response status code + + + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + {0} - publish URL, {1} - HTTP response status code, {2} - response body as text + + + Failed to retrieve publish credentials. + Failed to retrieve publish credentials. + + + + OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + {0} - file to publish, {1} - publish URL, {2} - deployment response status code, {3} - deployment logs URL + + + File to publish '{0}' was not found or is not accessible. + File to publish '{0}' was not found or is not accessible. + {0} - file to publish + + + PublishUrl '{0}' is missing or has an invalid value. + PublishUrl '{0}' is missing or has an invalid value. + {0} - publish URL + + + Publishing '{0}' to '{1}'... + Publishing '{0}' to '{1}'... + {0} - file to publish, {1} - publish URL + + + OneDeploy deployment succeeded. + OneDeploy deployment succeeded. + + + + File '{0}' uploaded to target instance. + File '{0}' uploaded to target instance. + {0} - file to publish. + Unable to parse the pubxml file {0}. Soubor pubxml {0} nejde analyzovat. diff --git a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.de.xlf b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.de.xlf index 30ac55421055..c4413d928128 100644 --- a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.de.xlf +++ b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.de.xlf @@ -224,6 +224,26 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 + + Deployment status is '{0}'. + Deployment status is '{0}'. + {0} - deployment status name + + + Deployment status is '{0}': {1} + Deployment status is '{0}': {1} + {0} - deployment status name, {1} - deployment status text + + + Deployment status URL '{0}' is missing or invalid + Deployment status URL '{0}' is missing or invalid + {0} - deployment polling URL + + + Polling for deployment status... + Polling for deployment status... + + Generating Entity Framework SQL Scripts... Entity Framework SQL-Skripts werden generiert... @@ -334,6 +354,51 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 Ungültiges Verb ({0}) für die angegebene Quelle ({1}) und das Ziel ({2}). + + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + {0} - publish URL, {1} - HTTP response status code + + + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + {0} - publish URL, {1} - HTTP response status code, {2} - response body as text + + + Failed to retrieve publish credentials. + Failed to retrieve publish credentials. + + + + OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + {0} - file to publish, {1} - publish URL, {2} - deployment response status code, {3} - deployment logs URL + + + File to publish '{0}' was not found or is not accessible. + File to publish '{0}' was not found or is not accessible. + {0} - file to publish + + + PublishUrl '{0}' is missing or has an invalid value. + PublishUrl '{0}' is missing or has an invalid value. + {0} - publish URL + + + Publishing '{0}' to '{1}'... + Publishing '{0}' to '{1}'... + {0} - file to publish, {1} - publish URL + + + OneDeploy deployment succeeded. + OneDeploy deployment succeeded. + + + + File '{0}' uploaded to target instance. + File '{0}' uploaded to target instance. + {0} - file to publish. + Unable to parse the pubxml file {0}. Die PUBXML-Datei "{0}" kann nicht analysiert werden. diff --git a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.es.xlf b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.es.xlf index 273ccf4478d5..ae528678ccea 100644 --- a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.es.xlf +++ b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.es.xlf @@ -224,6 +224,26 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 Vínculo redireccionable: http://go.microsoft.com/fwlink/?LinkId=246068 + + Deployment status is '{0}'. + Deployment status is '{0}'. + {0} - deployment status name + + + Deployment status is '{0}': {1} + Deployment status is '{0}': {1} + {0} - deployment status name, {1} - deployment status text + + + Deployment status URL '{0}' is missing or invalid + Deployment status URL '{0}' is missing or invalid + {0} - deployment polling URL + + + Polling for deployment status... + Polling for deployment status... + + Generating Entity Framework SQL Scripts... Generando scripts SQL de Entity Framework... @@ -334,6 +354,51 @@ Vínculo redireccionable: http://go.microsoft.com/fwlink/?LinkId=246068 verb({0}) no válido para el origen ({1}) y el destino ({2}) proporcionados. + + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + {0} - publish URL, {1} - HTTP response status code + + + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + {0} - publish URL, {1} - HTTP response status code, {2} - response body as text + + + Failed to retrieve publish credentials. + Failed to retrieve publish credentials. + + + + OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + {0} - file to publish, {1} - publish URL, {2} - deployment response status code, {3} - deployment logs URL + + + File to publish '{0}' was not found or is not accessible. + File to publish '{0}' was not found or is not accessible. + {0} - file to publish + + + PublishUrl '{0}' is missing or has an invalid value. + PublishUrl '{0}' is missing or has an invalid value. + {0} - publish URL + + + Publishing '{0}' to '{1}'... + Publishing '{0}' to '{1}'... + {0} - file to publish, {1} - publish URL + + + OneDeploy deployment succeeded. + OneDeploy deployment succeeded. + + + + File '{0}' uploaded to target instance. + File '{0}' uploaded to target instance. + {0} - file to publish. + Unable to parse the pubxml file {0}. No se puede analizar el archivo pubxml {0}. diff --git a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.fr.xlf b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.fr.xlf index 474215104337..fea3cb7722af 100644 --- a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.fr.xlf +++ b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.fr.xlf @@ -224,6 +224,26 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 FWLink : http://go.microsoft.com/fwlink/?LinkId=246068 + + Deployment status is '{0}'. + Deployment status is '{0}'. + {0} - deployment status name + + + Deployment status is '{0}': {1} + Deployment status is '{0}': {1} + {0} - deployment status name, {1} - deployment status text + + + Deployment status URL '{0}' is missing or invalid + Deployment status URL '{0}' is missing or invalid + {0} - deployment polling URL + + + Polling for deployment status... + Polling for deployment status... + + Generating Entity Framework SQL Scripts... Génération de scripts SQL Entity Framework... @@ -334,6 +354,51 @@ FWLink : http://go.microsoft.com/fwlink/?LinkId=246068 Verbe incorrect ({0}) pour la source ({1}) fournie et la destination ({2}). + + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + {0} - publish URL, {1} - HTTP response status code + + + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + {0} - publish URL, {1} - HTTP response status code, {2} - response body as text + + + Failed to retrieve publish credentials. + Failed to retrieve publish credentials. + + + + OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + {0} - file to publish, {1} - publish URL, {2} - deployment response status code, {3} - deployment logs URL + + + File to publish '{0}' was not found or is not accessible. + File to publish '{0}' was not found or is not accessible. + {0} - file to publish + + + PublishUrl '{0}' is missing or has an invalid value. + PublishUrl '{0}' is missing or has an invalid value. + {0} - publish URL + + + Publishing '{0}' to '{1}'... + Publishing '{0}' to '{1}'... + {0} - file to publish, {1} - publish URL + + + OneDeploy deployment succeeded. + OneDeploy deployment succeeded. + + + + File '{0}' uploaded to target instance. + File '{0}' uploaded to target instance. + {0} - file to publish. + Unable to parse the pubxml file {0}. Impossible d’analyser le fichier pubxml {0}. diff --git a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.it.xlf b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.it.xlf index 80525ff3cd19..c31079265702 100644 --- a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.it.xlf +++ b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.it.xlf @@ -224,6 +224,26 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 + + Deployment status is '{0}'. + Deployment status is '{0}'. + {0} - deployment status name + + + Deployment status is '{0}': {1} + Deployment status is '{0}': {1} + {0} - deployment status name, {1} - deployment status text + + + Deployment status URL '{0}' is missing or invalid + Deployment status URL '{0}' is missing or invalid + {0} - deployment polling URL + + + Polling for deployment status... + Polling for deployment status... + + Generating Entity Framework SQL Scripts... Generazione dello script SQL di Entity Framework... @@ -334,6 +354,51 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 Verbo ({0}) non valido per l'origine ({1}) e la destinazione ({2}) specificate. + + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + {0} - publish URL, {1} - HTTP response status code + + + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + {0} - publish URL, {1} - HTTP response status code, {2} - response body as text + + + Failed to retrieve publish credentials. + Failed to retrieve publish credentials. + + + + OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + {0} - file to publish, {1} - publish URL, {2} - deployment response status code, {3} - deployment logs URL + + + File to publish '{0}' was not found or is not accessible. + File to publish '{0}' was not found or is not accessible. + {0} - file to publish + + + PublishUrl '{0}' is missing or has an invalid value. + PublishUrl '{0}' is missing or has an invalid value. + {0} - publish URL + + + Publishing '{0}' to '{1}'... + Publishing '{0}' to '{1}'... + {0} - file to publish, {1} - publish URL + + + OneDeploy deployment succeeded. + OneDeploy deployment succeeded. + + + + File '{0}' uploaded to target instance. + File '{0}' uploaded to target instance. + {0} - file to publish. + Unable to parse the pubxml file {0}. Non è possibile analizzare il file pubxml {0}. diff --git a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.ja.xlf b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.ja.xlf index 9e6a31245659..5dca3cb12919 100644 --- a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.ja.xlf +++ b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.ja.xlf @@ -224,6 +224,26 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 + + Deployment status is '{0}'. + Deployment status is '{0}'. + {0} - deployment status name + + + Deployment status is '{0}': {1} + Deployment status is '{0}': {1} + {0} - deployment status name, {1} - deployment status text + + + Deployment status URL '{0}' is missing or invalid + Deployment status URL '{0}' is missing or invalid + {0} - deployment polling URL + + + Polling for deployment status... + Polling for deployment status... + + Generating Entity Framework SQL Scripts... Entity Framework SQL スクリプトを生成しています... @@ -334,6 +354,51 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 指定された配置元 ({1}) および配置先 ({2}) の動詞 ({0}) が無効です。 + + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + {0} - publish URL, {1} - HTTP response status code + + + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + {0} - publish URL, {1} - HTTP response status code, {2} - response body as text + + + Failed to retrieve publish credentials. + Failed to retrieve publish credentials. + + + + OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + {0} - file to publish, {1} - publish URL, {2} - deployment response status code, {3} - deployment logs URL + + + File to publish '{0}' was not found or is not accessible. + File to publish '{0}' was not found or is not accessible. + {0} - file to publish + + + PublishUrl '{0}' is missing or has an invalid value. + PublishUrl '{0}' is missing or has an invalid value. + {0} - publish URL + + + Publishing '{0}' to '{1}'... + Publishing '{0}' to '{1}'... + {0} - file to publish, {1} - publish URL + + + OneDeploy deployment succeeded. + OneDeploy deployment succeeded. + + + + File '{0}' uploaded to target instance. + File '{0}' uploaded to target instance. + {0} - file to publish. + Unable to parse the pubxml file {0}. pubxml ファイル {0} を解析できません。 diff --git a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.ko.xlf b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.ko.xlf index 69c0f3f458fc..ce92d6066e47 100644 --- a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.ko.xlf +++ b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.ko.xlf @@ -224,6 +224,26 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 + + Deployment status is '{0}'. + Deployment status is '{0}'. + {0} - deployment status name + + + Deployment status is '{0}': {1} + Deployment status is '{0}': {1} + {0} - deployment status name, {1} - deployment status text + + + Deployment status URL '{0}' is missing or invalid + Deployment status URL '{0}' is missing or invalid + {0} - deployment polling URL + + + Polling for deployment status... + Polling for deployment status... + + Generating Entity Framework SQL Scripts... Entity Framework SQL 스크립트 생성 중... @@ -334,6 +354,51 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 제공된 소스({1})와 대상({2})에 대해 잘못된 동사({0})입니다. + + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + {0} - publish URL, {1} - HTTP response status code + + + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + {0} - publish URL, {1} - HTTP response status code, {2} - response body as text + + + Failed to retrieve publish credentials. + Failed to retrieve publish credentials. + + + + OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + {0} - file to publish, {1} - publish URL, {2} - deployment response status code, {3} - deployment logs URL + + + File to publish '{0}' was not found or is not accessible. + File to publish '{0}' was not found or is not accessible. + {0} - file to publish + + + PublishUrl '{0}' is missing or has an invalid value. + PublishUrl '{0}' is missing or has an invalid value. + {0} - publish URL + + + Publishing '{0}' to '{1}'... + Publishing '{0}' to '{1}'... + {0} - file to publish, {1} - publish URL + + + OneDeploy deployment succeeded. + OneDeploy deployment succeeded. + + + + File '{0}' uploaded to target instance. + File '{0}' uploaded to target instance. + {0} - file to publish. + Unable to parse the pubxml file {0}. pubxml 파일 {0}을(를) 구문 분석할 수 없습니다. diff --git a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.pl.xlf b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.pl.xlf index 12bb60fd137b..8218f58524a3 100644 --- a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.pl.xlf +++ b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.pl.xlf @@ -224,6 +224,26 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 + + Deployment status is '{0}'. + Deployment status is '{0}'. + {0} - deployment status name + + + Deployment status is '{0}': {1} + Deployment status is '{0}': {1} + {0} - deployment status name, {1} - deployment status text + + + Deployment status URL '{0}' is missing or invalid + Deployment status URL '{0}' is missing or invalid + {0} - deployment polling URL + + + Polling for deployment status... + Polling for deployment status... + + Generating Entity Framework SQL Scripts... Trwa generowanie skryptów SQL Entity Framework... @@ -334,6 +354,51 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 Nieprawidłowe zlecenie ({0}) dla podanego źródła ({1}) i elementu docelowego ({2}). + + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + {0} - publish URL, {1} - HTTP response status code + + + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + {0} - publish URL, {1} - HTTP response status code, {2} - response body as text + + + Failed to retrieve publish credentials. + Failed to retrieve publish credentials. + + + + OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + {0} - file to publish, {1} - publish URL, {2} - deployment response status code, {3} - deployment logs URL + + + File to publish '{0}' was not found or is not accessible. + File to publish '{0}' was not found or is not accessible. + {0} - file to publish + + + PublishUrl '{0}' is missing or has an invalid value. + PublishUrl '{0}' is missing or has an invalid value. + {0} - publish URL + + + Publishing '{0}' to '{1}'... + Publishing '{0}' to '{1}'... + {0} - file to publish, {1} - publish URL + + + OneDeploy deployment succeeded. + OneDeploy deployment succeeded. + + + + File '{0}' uploaded to target instance. + File '{0}' uploaded to target instance. + {0} - file to publish. + Unable to parse the pubxml file {0}. Nie można przeanalizować pliku pubxml {0}. diff --git a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.pt-BR.xlf b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.pt-BR.xlf index 5dfc66eebbb8..8a7ffbfd0bf3 100644 --- a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.pt-BR.xlf +++ b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.pt-BR.xlf @@ -224,6 +224,26 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 + + Deployment status is '{0}'. + Deployment status is '{0}'. + {0} - deployment status name + + + Deployment status is '{0}': {1} + Deployment status is '{0}': {1} + {0} - deployment status name, {1} - deployment status text + + + Deployment status URL '{0}' is missing or invalid + Deployment status URL '{0}' is missing or invalid + {0} - deployment polling URL + + + Polling for deployment status... + Polling for deployment status... + + Generating Entity Framework SQL Scripts... Gerando Scripts SQL do Entity Framework... @@ -334,6 +354,51 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 Verbo inválido ({0}) para origem ({1}) e destino ({2}) fornecidos. + + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + {0} - publish URL, {1} - HTTP response status code + + + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + {0} - publish URL, {1} - HTTP response status code, {2} - response body as text + + + Failed to retrieve publish credentials. + Failed to retrieve publish credentials. + + + + OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + {0} - file to publish, {1} - publish URL, {2} - deployment response status code, {3} - deployment logs URL + + + File to publish '{0}' was not found or is not accessible. + File to publish '{0}' was not found or is not accessible. + {0} - file to publish + + + PublishUrl '{0}' is missing or has an invalid value. + PublishUrl '{0}' is missing or has an invalid value. + {0} - publish URL + + + Publishing '{0}' to '{1}'... + Publishing '{0}' to '{1}'... + {0} - file to publish, {1} - publish URL + + + OneDeploy deployment succeeded. + OneDeploy deployment succeeded. + + + + File '{0}' uploaded to target instance. + File '{0}' uploaded to target instance. + {0} - file to publish. + Unable to parse the pubxml file {0}. Não é possível analisar o arquivo pubxml {0}. diff --git a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.ru.xlf b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.ru.xlf index e839b3a1965f..8c22509aa93f 100644 --- a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.ru.xlf +++ b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.ru.xlf @@ -224,6 +224,26 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 FWLink: http://go.microsoft.com/fwlink/?LinkId=246068. + + Deployment status is '{0}'. + Deployment status is '{0}'. + {0} - deployment status name + + + Deployment status is '{0}': {1} + Deployment status is '{0}': {1} + {0} - deployment status name, {1} - deployment status text + + + Deployment status URL '{0}' is missing or invalid + Deployment status URL '{0}' is missing or invalid + {0} - deployment polling URL + + + Polling for deployment status... + Polling for deployment status... + + Generating Entity Framework SQL Scripts... Создание сценариев SQL для Entity Framework… @@ -334,6 +354,51 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068. Недопустимая команда ({0}) для указанных источника ({1}) и назначения ({2}). + + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + {0} - publish URL, {1} - HTTP response status code + + + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + {0} - publish URL, {1} - HTTP response status code, {2} - response body as text + + + Failed to retrieve publish credentials. + Failed to retrieve publish credentials. + + + + OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + {0} - file to publish, {1} - publish URL, {2} - deployment response status code, {3} - deployment logs URL + + + File to publish '{0}' was not found or is not accessible. + File to publish '{0}' was not found or is not accessible. + {0} - file to publish + + + PublishUrl '{0}' is missing or has an invalid value. + PublishUrl '{0}' is missing or has an invalid value. + {0} - publish URL + + + Publishing '{0}' to '{1}'... + Publishing '{0}' to '{1}'... + {0} - file to publish, {1} - publish URL + + + OneDeploy deployment succeeded. + OneDeploy deployment succeeded. + + + + File '{0}' uploaded to target instance. + File '{0}' uploaded to target instance. + {0} - file to publish. + Unable to parse the pubxml file {0}. Не удалось проанализировать PUBXML-файл {0}. diff --git a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.tr.xlf b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.tr.xlf index a5fdc7161c84..0d6c30f5602a 100644 --- a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.tr.xlf +++ b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.tr.xlf @@ -224,6 +224,26 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 + + Deployment status is '{0}'. + Deployment status is '{0}'. + {0} - deployment status name + + + Deployment status is '{0}': {1} + Deployment status is '{0}': {1} + {0} - deployment status name, {1} - deployment status text + + + Deployment status URL '{0}' is missing or invalid + Deployment status URL '{0}' is missing or invalid + {0} - deployment polling URL + + + Polling for deployment status... + Polling for deployment status... + + Generating Entity Framework SQL Scripts... Entity Framework SQL Betikleri oluşturuluyor... @@ -334,6 +354,51 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 Sağlanan kaynak ({1}) ve hedef ({2}) için geçersiz eylem ({0}). + + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + {0} - publish URL, {1} - HTTP response status code + + + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + {0} - publish URL, {1} - HTTP response status code, {2} - response body as text + + + Failed to retrieve publish credentials. + Failed to retrieve publish credentials. + + + + OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + {0} - file to publish, {1} - publish URL, {2} - deployment response status code, {3} - deployment logs URL + + + File to publish '{0}' was not found or is not accessible. + File to publish '{0}' was not found or is not accessible. + {0} - file to publish + + + PublishUrl '{0}' is missing or has an invalid value. + PublishUrl '{0}' is missing or has an invalid value. + {0} - publish URL + + + Publishing '{0}' to '{1}'... + Publishing '{0}' to '{1}'... + {0} - file to publish, {1} - publish URL + + + OneDeploy deployment succeeded. + OneDeploy deployment succeeded. + + + + File '{0}' uploaded to target instance. + File '{0}' uploaded to target instance. + {0} - file to publish. + Unable to parse the pubxml file {0}. {0} pubxml dosyası ayrıştırılamadı. diff --git a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.zh-Hans.xlf b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.zh-Hans.xlf index 42b9ab03d9b5..6eb0a8098a06 100644 --- a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.zh-Hans.xlf +++ b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.zh-Hans.xlf @@ -224,6 +224,26 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 + + Deployment status is '{0}'. + Deployment status is '{0}'. + {0} - deployment status name + + + Deployment status is '{0}': {1} + Deployment status is '{0}': {1} + {0} - deployment status name, {1} - deployment status text + + + Deployment status URL '{0}' is missing or invalid + Deployment status URL '{0}' is missing or invalid + {0} - deployment polling URL + + + Polling for deployment status... + Polling for deployment status... + + Generating Entity Framework SQL Scripts... 正在生成实体框架 SQL 脚本... @@ -334,6 +354,51 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 所提供的源({1})和目标({2})的谓词({0})无效。 + + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + {0} - publish URL, {1} - HTTP response status code + + + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + {0} - publish URL, {1} - HTTP response status code, {2} - response body as text + + + Failed to retrieve publish credentials. + Failed to retrieve publish credentials. + + + + OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + {0} - file to publish, {1} - publish URL, {2} - deployment response status code, {3} - deployment logs URL + + + File to publish '{0}' was not found or is not accessible. + File to publish '{0}' was not found or is not accessible. + {0} - file to publish + + + PublishUrl '{0}' is missing or has an invalid value. + PublishUrl '{0}' is missing or has an invalid value. + {0} - publish URL + + + Publishing '{0}' to '{1}'... + Publishing '{0}' to '{1}'... + {0} - file to publish, {1} - publish URL + + + OneDeploy deployment succeeded. + OneDeploy deployment succeeded. + + + + File '{0}' uploaded to target instance. + File '{0}' uploaded to target instance. + {0} - file to publish. + Unable to parse the pubxml file {0}. 无法分析 pubxml 文件 {0}。 diff --git a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.zh-Hant.xlf b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.zh-Hant.xlf index db5b2441de84..96c6046aa817 100644 --- a/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.zh-Hant.xlf +++ b/src/WebSdk/Publish/Tasks/Properties/xlf/Resources.zh-Hant.xlf @@ -224,6 +224,26 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 + + Deployment status is '{0}'. + Deployment status is '{0}'. + {0} - deployment status name + + + Deployment status is '{0}': {1} + Deployment status is '{0}': {1} + {0} - deployment status name, {1} - deployment status text + + + Deployment status URL '{0}' is missing or invalid + Deployment status URL '{0}' is missing or invalid + {0} - deployment polling URL + + + Polling for deployment status... + Polling for deployment status... + + Generating Entity Framework SQL Scripts... 正在產生 Entity Framework SQL 指令碼... @@ -334,6 +354,51 @@ FWLink: http://go.microsoft.com/fwlink/?LinkId=246068 所提供來源 ({1}) 及目的地 ({2}) 的動詞 ({0}) 無效 + + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}'. + {0} - publish URL, {1} - HTTP response status code + + + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + OneDeploy attempt to publish file through '{0}' failed with HTTP status code '{1}': {2}. + {0} - publish URL, {1} - HTTP response status code, {2} - response body as text + + + Failed to retrieve publish credentials. + Failed to retrieve publish credentials. + + + + OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + OneDeploy attempt to publish file '{0}' through '{1}' failed with status code '{2}'. See the logs at '{3}'. + {0} - file to publish, {1} - publish URL, {2} - deployment response status code, {3} - deployment logs URL + + + File to publish '{0}' was not found or is not accessible. + File to publish '{0}' was not found or is not accessible. + {0} - file to publish + + + PublishUrl '{0}' is missing or has an invalid value. + PublishUrl '{0}' is missing or has an invalid value. + {0} - publish URL + + + Publishing '{0}' to '{1}'... + Publishing '{0}' to '{1}'... + {0} - file to publish, {1} - publish URL + + + OneDeploy deployment succeeded. + OneDeploy deployment succeeded. + + + + File '{0}' uploaded to target instance. + File '{0}' uploaded to target instance. + {0} - file to publish. + Unable to parse the pubxml file {0}. 無法剖析 pubxml 檔案 {0}。 diff --git a/src/WebSdk/Publish/Tasks/Tasks/Http/DefaultHttpClient.cs b/src/WebSdk/Publish/Tasks/Tasks/Http/DefaultHttpClient.cs new file mode 100644 index 000000000000..2d251c18d954 --- /dev/null +++ b/src/WebSdk/Publish/Tasks/Tasks/Http/DefaultHttpClient.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using System.Net.Http.Headers; + +namespace Microsoft.NET.Sdk.Publish.Tasks; + +internal class DefaultHttpClient : IHttpClient, IDisposable +{ + private readonly HttpClient _httpClient = new() + { + Timeout = Timeout.InfiniteTimeSpan + }; + + /// + public HttpRequestHeaders DefaultRequestHeaders => _httpClient.DefaultRequestHeaders; + + /// + public Task PostAsync(Uri uri, StreamContent content) + { + return _httpClient.PostAsync(uri, content); + } + + /// + public Task GetAsync(Uri uri, CancellationToken cancellationToken) + { + return _httpClient.GetAsync(uri, cancellationToken); + } + + /// + public Task PutAsync(Uri uri, StreamContent content, CancellationToken cancellationToken) + { + return _httpClient.PutAsync(uri, content, cancellationToken); + } + + /// + public void Dispose() + { + _httpClient.Dispose(); + } +} diff --git a/src/WebSdk/Publish/Tasks/Tasks/Http/HttpClientExtensions.cs b/src/WebSdk/Publish/Tasks/Tasks/Http/HttpClientExtensions.cs new file mode 100644 index 000000000000..818c8274daae --- /dev/null +++ b/src/WebSdk/Publish/Tasks/Tasks/Http/HttpClientExtensions.cs @@ -0,0 +1,328 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.Json; + +namespace Microsoft.NET.Sdk.Publish.Tasks; + +/// +/// Extension methods for and related types. +/// +internal static class HttpClientExtensions +{ + private static readonly string s_azureADUserName = Guid.Empty.ToString(); + private static readonly JsonSerializerOptions s_defaultSerializerOptions = new() + { + AllowTrailingCommas = true, + ReadCommentHandling = JsonCommentHandling.Skip + }; + + private const string BearerAuthenticationScheme = "Bearer"; + private const string BasicAuthenticationScheme = "Basic"; + + /// + /// Sends an HTTP POST request. + /// + /// uri to send the request to + /// user name + /// user password + /// content type header value + /// 'User-Agent' header value + /// encoding + /// message payload + /// HTTP response + public static async Task PostRequestAsync( + this IHttpClient client, + Uri uri, + string username, + string password, + string contentType, + string userAgent, + Encoding encoding, + Stream messageBody) + { + if (client is null) + { + return null; + } + + AddAuthenticationHeader(username, password, client); + client.DefaultRequestHeaders.Add("User-Agent", userAgent); + + StreamContent content = new(messageBody ?? new MemoryStream()) + { + Headers = + { + ContentType = new MediaTypeHeaderValue(contentType) + { + CharSet = encoding.WebName + }, + ContentEncoding = + { + encoding.WebName + } + } + }; + + try + { + HttpResponseMessage responseMessage = await client.PostAsync(uri, content); + return new HttpResponseMessageWrapper(responseMessage); + } + catch (TaskCanceledException) + { + return new HttpResponseMessageForStatusCode(HttpStatusCode.RequestTimeout); + } + } + + /// + /// Sends an HTTP PUT request. + /// + /// uri to send the request to + /// user name + /// user password + /// content type header value + /// 'User-Agent' header value + /// encoding + /// message payload + /// HTTP response + public static async Task PutRequestAsync( + this IHttpClient client, + Uri uri, + string username, + string password, + string contentType, + string userAgent, + string fileName, + Encoding encoding, + Stream messageBody, + CancellationToken cancellationToken) + { + if (client is null || cancellationToken.IsCancellationRequested) + { + return null; + } + + AddAuthenticationHeader(username, password, client); + client.DefaultRequestHeaders.Add("User-Agent", userAgent); + + StreamContent content = new(messageBody ?? new MemoryStream()) + { + Headers = + { + ContentType = new MediaTypeHeaderValue(contentType), + ContentEncoding = + { + encoding.WebName + }, + ContentDisposition = new ContentDispositionHeaderValue("attachment") + { + FileName = fileName + } + } + }; + + try + { + HttpResponseMessage responseMessage = await client.PutAsync(uri, content, cancellationToken); + return new HttpResponseMessageWrapper(responseMessage); + } + catch (TaskCanceledException) + { + return new HttpResponseMessageForStatusCode(HttpStatusCode.RequestTimeout); + } + } + + /// + /// Sends an HTTP GET request. + /// + /// uri to send the request to + /// user name + /// user password + /// 'User-Agent' header value + /// + /// HTTP response + public static async Task GetRequestAsync( + this IHttpClient client, + Uri uri, + string username, + string password, + string userAgent, + CancellationToken cancellationToken) + { + if (client is null || cancellationToken.IsCancellationRequested) + { + return null; + } + + AddAuthenticationHeader(username, password, client); + client.DefaultRequestHeaders.Add("User-Agent", userAgent); + + try + { + HttpResponseMessage responseMessage = await client.GetAsync(uri, cancellationToken); + return new HttpResponseMessageWrapper(responseMessage); + } + catch (TaskCanceledException) + { + return new HttpResponseMessageForStatusCode(HttpStatusCode.RequestTimeout); + } + } + + /// + /// Sends HTTP GET request a maximum attempts. It will retry while + /// request is not OK/Accepted or maximum number of retries has been reached. + /// + /// expected type of response object + /// URL to send requests to + /// user name + /// user password + /// 'User-Agent' header value + /// maximum number of attempts + /// time to wait between attempts; usually in seconds + /// cancellation token + /// response of given type; default value if response status code is not of success + public static async Task RetryGetRequestAsync( + this IHttpClient client, + string url, + string username, + string password, + string userAgent, + int retries, + TimeSpan delay, + CancellationToken cancellationToken) + { + if (client is null) + { + return default; + } + + // retry GET request + IHttpResponse response = null; + await RetryTaskAsync(async (ct) => + { + response = await client.GetRequestAsync(new Uri(url, UriKind.RelativeOrAbsolute), username, password, userAgent, ct); + }, retries, delay, cancellationToken); + + // response is not valid; return default value + if (!response.IsResponseSuccessful()) + { + return default; + } + + return await response.GetJsonResponseAsync(cancellationToken); + } + + /// + /// Whether given has a status of + /// or + /// + /// + /// + public static bool IsResponseSuccessful(this IHttpResponse response) + { + return response is not null + && (response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.Accepted); + + } + + /// + /// Reads the body as a string. + /// + /// cancellation token + /// the response body as text + public static async Task GetTextResponseAsync(this IHttpResponse response, CancellationToken cancellationToken) + { + var responseText = string.Empty; + + if (response is not null || cancellationToken.IsCancellationRequested) + { + return responseText; + } + + var responseBody = await response.GetResponseBodyAsync(); + if (responseBody is not null) + { + var streamReader = new StreamReader(responseBody); + responseText = streamReader.ReadToEnd(); + } + + return responseText; + } + + /// + /// Attempts to serialize the JSON content into an object of the given type. + /// + /// type to serialize to + /// cancellation token + /// object + public static async Task GetJsonResponseAsync(this IHttpResponse response, CancellationToken cancellation) + { + if (response is null || cancellation.IsCancellationRequested) + { + return default; + } + + using var stream = await response.GetResponseBodyAsync(); + var reader = new StreamReader(stream, Encoding.UTF8); + + return JsonSerializer.Deserialize(reader.ReadToEnd(), s_defaultSerializerOptions); + } + + private static void AddAuthenticationHeader(string username, string password, IHttpClient client) + { + client.DefaultRequestHeaders.Remove("Connection"); + + if (!string.Equals(username, s_azureADUserName, StringComparison.Ordinal)) + { + string plainAuth = string.Format("{0}:{1}", username, password); + byte[] plainAuthBytes = Encoding.ASCII.GetBytes(plainAuth); + string base64 = Convert.ToBase64String(plainAuthBytes); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(BasicAuthenticationScheme, base64); + } + else + { + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(BearerAuthenticationScheme, password); + } + } + + /// + /// Attempts to run the given function at least 1 time and at most times. + /// + /// function to run + /// maximum number of attempts + /// delay between each attempt + private static async System.Threading.Tasks.Task RetryTaskAsync( + Func func, + int retryCount, + TimeSpan retryDelay, + CancellationToken cancellationToken) + { + while (true) + { + try + { + if (!cancellationToken.IsCancellationRequested) + { + await func(cancellationToken); + } + + return; + } + catch (Exception) + { + if (retryCount <= 0) + { + throw; + } + + retryCount--; + } + + await System.Threading.Tasks.Task.Delay(retryDelay, cancellationToken); + } + } +} diff --git a/src/WebSdk/Publish/Tasks/Tasks/Http/HttpResponseMessageForStatusCode.cs b/src/WebSdk/Publish/Tasks/Tasks/Http/HttpResponseMessageForStatusCode.cs new file mode 100644 index 000000000000..e5de396a718d --- /dev/null +++ b/src/WebSdk/Publish/Tasks/Tasks/Http/HttpResponseMessageForStatusCode.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; + +namespace Microsoft.NET.Sdk.Publish.Tasks; + +internal class HttpResponseMessageForStatusCode(HttpStatusCode statusCode) : IHttpResponse +{ + /// + public HttpStatusCode StatusCode { get; private set; } = statusCode; + + /// + public Task GetResponseBodyAsync() + { + return System.Threading.Tasks.Task.FromResult(new MemoryStream()); + } + + /// + public IEnumerable GetHeader(string name) + { + return []; + } +} diff --git a/src/WebSdk/Publish/Tasks/Tasks/Http/HttpResponseMessageWrapper.cs b/src/WebSdk/Publish/Tasks/Tasks/Http/HttpResponseMessageWrapper.cs new file mode 100644 index 000000000000..547c071da6cf --- /dev/null +++ b/src/WebSdk/Publish/Tasks/Tasks/Http/HttpResponseMessageWrapper.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using System.Net.Http; + +namespace Microsoft.NET.Sdk.Publish.Tasks; + +internal class HttpResponseMessageWrapper : IHttpResponse +{ + private readonly HttpResponseMessage _message; + private readonly Lazy> _responseBodyTask; + + public HttpResponseMessageWrapper(HttpResponseMessage message) + { + _message = message; + _responseBodyTask = new Lazy>(GetResponseStream); + + StatusCode = message?.StatusCode ?? HttpStatusCode.InternalServerError; + } + + /// + public HttpStatusCode StatusCode { get; private set; } + + /// + public async Task GetResponseBodyAsync() + { + return await _responseBodyTask.Value; + } + + /// + public IEnumerable GetHeader(string name) + { + if (_message.Headers.TryGetValues(name, out IEnumerable values)) + { + return values; + } + + return []; + } + + private Task GetResponseStream() + { + return _message.Content.ReadAsStreamAsync(); + } +} diff --git a/src/WebSdk/Publish/Tasks/Tasks/Http/IHttpClient.cs b/src/WebSdk/Publish/Tasks/Tasks/Http/IHttpClient.cs new file mode 100644 index 000000000000..a7e5fe0f458e --- /dev/null +++ b/src/WebSdk/Publish/Tasks/Tasks/Http/IHttpClient.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using System.Net.Http.Headers; + +namespace Microsoft.NET.Sdk.Publish.Tasks; + +/// +/// Sends HTTP requests. +/// +public interface IHttpClient +{ + /// + /// The Headers of a request. + /// + HttpRequestHeaders DefaultRequestHeaders { get; } + + /// + /// Sends an HTTP POST request. + /// + /// URI to send the request to + /// request payload + /// response of request + Task PostAsync(Uri uri, StreamContent content); + + /// + /// Sends an HTTP GET request. + /// + /// URI to send the request to + /// cancellation token + /// response of the request + Task GetAsync(Uri uri, CancellationToken cancellationToken); + + /// + /// Sends an HTTP PUT request. + /// + /// URI to send the request to + /// request payload + /// cancellation token + /// response of request + Task PutAsync(Uri uri, StreamContent content, CancellationToken cancellationToken); +} diff --git a/src/WebSdk/Publish/Tasks/Tasks/Http/IHttpResponse.cs b/src/WebSdk/Publish/Tasks/Tasks/Http/IHttpResponse.cs new file mode 100644 index 000000000000..29ca812209df --- /dev/null +++ b/src/WebSdk/Publish/Tasks/Tasks/Http/IHttpResponse.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; + +namespace Microsoft.NET.Sdk.Publish.Tasks; + +/// +/// A response to an HTTP request +/// +internal interface IHttpResponse +{ + /// + /// Gets the status code the server returned + /// + HttpStatusCode StatusCode { get; } + + /// + /// Gets the body of the response + /// + Task GetResponseBodyAsync(); + + /// + /// Gets the value of an HTTP Response header + /// with the given name. + /// + /// header name + /// header value(s) + IEnumerable GetHeader(string name); +} diff --git a/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/CreatePackageFile.cs b/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/CreatePackageFile.cs new file mode 100644 index 000000000000..237fc484bc52 --- /dev/null +++ b/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/CreatePackageFile.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Framework; + +namespace Microsoft.NET.Sdk.Publish.Tasks.OneDeploy; + +/// +/// A task that creates a package file for the content in a given path. +/// +public class CreatePackageFile : Task +{ + private readonly IFilePackager _filePackager; + + public CreatePackageFile() + { + _filePackager = new ZipFilePackager(); + } + + // Test constructor + internal CreatePackageFile(IFilePackager filePackager) + { + _filePackager = filePackager; + } + + [Required] + public string ContentToPackage { get; set; } + + [Required] + public string ProjectName { get; set; } + + [Required] + public string IntermediateTempPath { get; set; } + + [Output] + public string CreatedPackageFilePath { get; set; } + + /// + public override bool Execute() + { + if (string.IsNullOrEmpty(ContentToPackage) + || string.IsNullOrEmpty(ProjectName) + || string.IsNullOrEmpty(IntermediateTempPath)) + { + return false; + } + + var packageFileName = $"{ProjectName}-{DateTime.Now:yyyyMMddHHmmssFFF}{_filePackager.Extension}"; + var packageFilePath = Path.Combine(IntermediateTempPath, packageFileName); + + // package content + var packageFileTask = _filePackager.CreatePackageAsync(ContentToPackage, packageFilePath, CancellationToken.None); + packageFileTask.Wait(); + + CreatedPackageFilePath = packageFileTask.Result ? packageFilePath : string.Empty; + + return !string.IsNullOrEmpty(CreatedPackageFilePath); + } +} diff --git a/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/DeploymentResponse.cs b/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/DeploymentResponse.cs new file mode 100644 index 000000000000..c750d9867685 --- /dev/null +++ b/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/DeploymentResponse.cs @@ -0,0 +1,121 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Serialization; +using Microsoft.NET.Sdk.Publish.Tasks.Properties; + +namespace Microsoft.NET.Sdk.Publish.Tasks.OneDeploy; + +/// +/// Represents the JSON response of a deployment operation. +/// +internal class DeploymentResponse +{ + internal static readonly DeploymentResponse s_unknownResponse = new() + { + Status = DeploymentStatus.Unknown, + }; + + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("end_time")] + public string EndTime { get; set; } + + [JsonPropertyName("log_url")] + public string LogUrl { get; set; } + + [JsonPropertyName("message")] + public string Message { get; set; } + + [JsonPropertyName("status")] + public DeploymentStatus? Status { get; set; } = DeploymentStatus.Unknown; + + [JsonPropertyName("site_name")] + public string SiteName { get; set; } + + [JsonPropertyName("status_text")] + public string StatusText { get; set; } + + [JsonPropertyName("start_time")] + public string StartTime { get; set; } + + [JsonPropertyName("url")] + public string Url { get; set; } + + /// + public override string ToString() + { + return string.IsNullOrEmpty(StatusText) + ? string.Format(Resources.DeploymentStatus, Status) + : string.Format(Resources.DeploymentStatusWithText, Status, StatusText); + } + +} + +/// +/// Extension methods for . +/// +internal static class DeploymentResponseExtensions +{ + internal static bool IsSuccessfulResponse(this DeploymentResponse response) + { + return response is not null + && response.Status is not null + && response.Status.Value.IsSuccessfulStatus(); + } + + internal static bool IsFailedResponse(this DeploymentResponse response) + { + return response is null + || response.Status is null + || response.Status.Value.IsFailedStatus(); + } + + public static string GetLogUrlWithId(this DeploymentResponse deploymentResponse) + { + if (deploymentResponse is null + || string.IsNullOrEmpty(deploymentResponse.LogUrl) + || string.IsNullOrEmpty(deploymentResponse.Id)) + { + return deploymentResponse?.LogUrl; + } + + try + { + Uri logUrl = new(deploymentResponse.LogUrl); + string pathAndQuery = logUrl.PathAndQuery; + + // try to replace '../latest/log' with '../{deploymentResponse.Id}/log' + if (!string.IsNullOrEmpty(pathAndQuery)) + { + string[] pathAndQueryParts = pathAndQuery.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + string[] pathWithIdParts = new string[pathAndQueryParts.Length]; + + for (int i = pathAndQueryParts.Length - 1; i >= 0; i--) + { + if (string.Equals("latest", pathAndQueryParts[i], StringComparison.Ordinal)) + { + pathWithIdParts[i] = deploymentResponse.Id; + continue; + } + + pathWithIdParts[i] = pathAndQueryParts[i].Trim(); + } + + return new UriBuilder() + { + Scheme = logUrl.Scheme, + Host = logUrl.Host, + Path = string.Join("/", pathWithIdParts) + }.ToString(); + } + } + catch + { + // do nothing + } + + return deploymentResponse.LogUrl; + } +} diff --git a/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/DeploymentStatus.cs b/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/DeploymentStatus.cs new file mode 100644 index 000000000000..c3485d2d6a60 --- /dev/null +++ b/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/DeploymentStatus.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.NET.Sdk.Publish.Tasks.OneDeploy; + +/// +/// Deployment operation status codes +/// +public enum DeploymentStatus +{ + Unknown = -10, + Cancelled = -1, + Pending = 0, + Building = 1, + Deploying = 2, + Failed = 3, + Success = 4, + Conflict = 5, + PartialSuccess = 6 +} + +/// +/// Extension methods for . +/// +internal static class DeployStatusExtensions +{ + /// + /// Whether this status represents a successful status. + /// + internal static bool IsSuccessfulStatus(this DeploymentStatus status) + { + return status == DeploymentStatus.Success + || status == DeploymentStatus.PartialSuccess; + } + + /// + /// Whether this status represents a failed status. + /// + internal static bool IsFailedStatus(this DeploymentStatus status) + { + return status == DeploymentStatus.Failed + || status == DeploymentStatus.Conflict + || status == DeploymentStatus.Cancelled + || status == DeploymentStatus.Unknown; + } + + /// + /// Whether this status represents the 'final' status for an on-going deployment operation. + /// + /// true if is is terminating; false, otherwise + internal static bool IsTerminatingStatus(this DeploymentStatus status) + { + return status.IsSuccessfulStatus() + || status.IsFailedStatus(); + } +} diff --git a/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/IDeploymentStatusService.cs b/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/IDeploymentStatusService.cs new file mode 100644 index 000000000000..94242b6574c1 --- /dev/null +++ b/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/IDeploymentStatusService.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.NET.Sdk.Publish.Tasks.OneDeploy; + +/// +/// A service that monitors a deployment operation. +/// +internal interface IDeploymentStatusService +{ + /// + /// Polls a deployment operation status. + /// + /// deployment response type + /// HTTP client + /// URL endpoint to poll the deployment + /// user name + /// user password + /// 'UserAgent' header value + /// cancellation token + /// the resulting deployment response + Task PollDeploymentAsync(IHttpClient httpClient, string url, string user, string password, string userAgent, CancellationToken cancellation); +} diff --git a/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/IFilePackager.cs b/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/IFilePackager.cs new file mode 100644 index 000000000000..e66e3bba8033 --- /dev/null +++ b/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/IFilePackager.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.NET.Sdk.Publish.Tasks.OneDeploy; + +/// +/// Takes the content off of a location and produces a package (e.g., a .zip file). +/// +internal interface IFilePackager +{ + /// + /// Extension of produced package (including the '.'). E.g.: '.zip', '.tar', etc. + /// + string Extension { get; } + + /// + /// Packages the content in the given sourcePath + /// + /// path to the content to package + /// path to the packaged content + /// cancellation token + /// whether content was packaged or not + Task CreatePackageAsync(string sourcePath, string destinationPath, CancellationToken cancellation); +} diff --git a/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/ITaskLogger.cs b/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/ITaskLogger.cs new file mode 100644 index 000000000000..051f82d001c2 --- /dev/null +++ b/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/ITaskLogger.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Framework; + +namespace Microsoft.NET.Sdk.Publish.Tasks.OneDeploy; + +/// +/// A simple logger definition for a . +/// +internal interface ITaskLogger +{ + /// + /// Whether this logger is enabled. + /// + bool Enabled { get; set; } + + /// + /// Logs a message. + /// + /// message to log + void LogMessage(string message); + + /// + /// Logs a message. + /// + /// message importance + /// message to log + void LogMessage(MessageImportance importance, string message); + + /// + /// Logs an error message. + /// + /// message to log + void LogError(string message); + + /// + /// Logs a warning message. + /// + /// message to log + void LogWarning(string message); +} diff --git a/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/OneDeploy.WebJob.cs b/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/OneDeploy.WebJob.cs new file mode 100644 index 000000000000..92eb1fc55149 --- /dev/null +++ b/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/OneDeploy.WebJob.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.NET.Sdk.Publish.Tasks.OneDeploy; + +/// +/// 'OneDeploy' publish task for WebJobs. +/// +public partial class OneDeploy +{ + private const string ContinuousWebJobType = "Continuous"; + private const string TriggeredWebJobType = "Triggered"; + private const string ContinuousWebJobApiPath = "continuouswebjobs"; + private const string TriggeredWebJobsApiPath = "triggeredwebjobs"; + + public string WebJobName { get; set; } + + public string WebJobType { get; set; } + + /// + /// Whether the name is a non-empty value and type is either 'Continuous' or 'Triggered'. + /// + private bool IsWebJobProject(string webjobName, string webjobType) => + !string.IsNullOrEmpty(webjobName) && + (string.Equals(ContinuousWebJobType, webjobType, StringComparison.OrdinalIgnoreCase) || + string.Equals(TriggeredWebJobType, webjobType, StringComparison.OrdinalIgnoreCase)); + + private async Task DeployWebJobAsync( + IHttpClient httpClient, + Uri publishUri, + string username, + string password, + string userAgent, + string fileToPublish, + FileStream fileToPublishStream, + CancellationToken cancellationToken) + { + var response = await httpClient.PutRequestAsync( + publishUri, username, password, DefaultRequestContentType, userAgent, Path.GetFileName(fileToPublish), Encoding.UTF8, fileToPublishStream, cancellationToken); + + return response; + } + + private bool GetWebJobPublishUri(string publishUrl, string webjobName, string webjobType, out Uri publishUri) + { + // action path differs by WebJob type + var path = string.Equals(ContinuousWebJobType, webjobType, StringComparison.OrdinalIgnoreCase) + ? ContinuousWebJobApiPath + : TriggeredWebJobsApiPath; + + // Either: + // {publishUrl}/api/continuouswebjobs/{WebJobName} + // {publishUrl}/api/triggeredwebjobs/{WebJobName} + publishUri = new UriBuilder(publishUrl) + { + Path = $"api/{path}/{webjobName}", + Port = -1 + }.Uri; + + return Uri.TryCreate(publishUri.ToString(), UriKind.Absolute, out _); + } +} diff --git a/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/OneDeploy.cs b/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/OneDeploy.cs new file mode 100644 index 000000000000..102ada7e76de --- /dev/null +++ b/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/OneDeploy.cs @@ -0,0 +1,227 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Framework; +using Microsoft.NET.Sdk.Publish.Tasks.MsDeploy; +using Microsoft.NET.Sdk.Publish.Tasks.Properties; + +namespace Microsoft.NET.Sdk.Publish.Tasks.OneDeploy; + +/// +/// 'OneDeploy' publish task. +/// +public partial class OneDeploy : Task +{ + private const string DefaultRequestContentType = "application/zip"; + private const string UserAgentName = "websdk"; + private const string OneDeployApiPath = "api/publish"; + private const string OneDeployQueryParam = "RemoteBuild=false"; + + private readonly ITaskLogger _taskLogger; + + public OneDeploy() + { + // logger is enabled by default + _taskLogger = new TaskLogger(Log, true); + } + + // Test constructor + internal OneDeploy(ITaskLogger taskLogger) + { + _taskLogger = taskLogger; + } + + [Required] + public string FileToPublishPath { get; set; } + + public string PublishUrl { get; set; } + + [Required] + public string Username { get; set; } + + public string Password { get; set; } + + [Required] + public string UserAgentVersion { get; set; } + + /// + public override bool Execute() + { + var deployTask = OneDeployAsync(FileToPublishPath, Username, Password, PublishUrl, UserAgentVersion, WebJobName, WebJobType); + deployTask.Wait(); + + return deployTask.Result; + } + + public async Task OneDeployAsync( + string fileToPublishPath, + string username, + string password, + string url, + string userAgentVersion, + string webjobName = null, + string webjobType = null, + CancellationToken cancellationToken = default) + { + using DefaultHttpClient httpClient = new(); + + var userAgent = $"{UserAgentName}/{userAgentVersion}"; + var deployStatusService = new OneDeployStatusService(_taskLogger); + + return await OneDeployAsync( + fileToPublishPath, username, password, url, userAgent, webjobName, webjobType, httpClient, deployStatusService, cancellationToken); + } + + internal async Task OneDeployAsync( + string fileToPublishPath, + string username, + string password, + string url, + string userAgent, + string webjobName, + string webjobType, + IHttpClient httpClient, + IDeploymentStatusService deploymentStatusService, + CancellationToken cancellationToken = default) + { + // missing credentials + if (string.IsNullOrEmpty(password) && !GetCredentialsFromTask(out username, out password)) + { + _taskLogger.LogError(Resources.ONEDEPLOY_FailedToRetrieveCredentials); + return false; + } + + // missing file to publish + if (!File.Exists(fileToPublishPath)) + { + _taskLogger.LogError(Resources.ONEDEPLOY_FileToPublish_NotFound); + return false; + } + + // 'PublishUrl' must be valid + if (!GetPublishUrl(url, webjobName, webjobType, out var oneDeployPublishUri)) + { + _taskLogger.LogError(string.Format(Resources.ONEDEPLOY_InvalidPublishUrl, url)); + return false; + } + + _taskLogger.LogMessage( + MessageImportance.High, + string.Format(Resources.ONEDEPLOY_PublishingOneDeploy, fileToPublishPath, oneDeployPublishUri.ToString())); + + var fileToPublishStream = File.OpenRead(fileToPublishPath); + + // push package to target instance + var response = await DeployAsync( + httpClient, oneDeployPublishUri, username, password, userAgent, fileToPublishPath, webjobName, webjobType, fileToPublishStream, cancellationToken); + + // if push failed, finish operation + if (!response.IsResponseSuccessful()) + { + var responseText = await response.GetTextResponseAsync(cancellationToken); + + var errorMessage = !string.IsNullOrEmpty(responseText) + ? string.Format(Resources.ONEDEPLOY_FailedDeployRequest_With_ResponseText, oneDeployPublishUri, response?.StatusCode, responseText) + : string.Format(Resources.ONEDEPLOY_FailedDeployRequest, oneDeployPublishUri, response?.StatusCode); + + _taskLogger.LogError(errorMessage); + return false; + } + + _taskLogger.LogMessage(string.Format(Resources.ONEDEPLOY_Uploaded, fileToPublishPath)); + + // monitor deployment (if available) + var deploymentUrl = response.GetHeader("Location").FirstOrDefault(); + if (!string.IsNullOrEmpty(deploymentUrl) && Uri.TryCreate(deploymentUrl, UriKind.Absolute, out var _)) + { + var deploymentResponse = await deploymentStatusService.PollDeploymentAsync(httpClient, deploymentUrl, username, password, userAgent, cancellationToken); + + if (deploymentResponse.IsSuccessfulResponse()) + { + _taskLogger.LogMessage(MessageImportance.High, Resources.ONEDEPLOY_Success); + return true; + } + + if (deploymentResponse.IsFailedResponse()) + { + _taskLogger.LogError(string.Format(Resources.ONEDEPLOY_FailedWithLogs, + fileToPublishPath, + oneDeployPublishUri, + deploymentResponse.Status ?? DeploymentStatus.Unknown, + deploymentResponse.GetLogUrlWithId())); + + return false; + } + } + + // no follow-up deployment response (as in WebJobs); assume deployment worked + _taskLogger.LogMessage(MessageImportance.High, Resources.ONEDEPLOY_Success); + return true; + } + + private bool GetCredentialsFromTask(out string user, out string password) + { + VSHostObject hostObj = new(HostObject as IEnumerable); + + return hostObj.ExtractCredentials(out user, out password); + } + + private Task DeployAsync( + IHttpClient httpClient, + Uri publishUri, + string username, + string password, + string userAgent, + string fileToPublish, + string webjobName, + string webjobType, + FileStream fileToPublishStream, + CancellationToken cancellationToken) + { + return IsWebJobProject(webjobName, webjobType) + ? DeployWebJobAsync(httpClient, publishUri, username, password, userAgent, fileToPublish, fileToPublishStream, cancellationToken) + : DefaultDeployAsync(httpClient, publishUri, username, password, userAgent, fileToPublishStream, cancellationToken); + } + + private bool GetPublishUrl(string publishUrl, string webjobName, string webjobType, out Uri publishUri) + { + publishUri = null; + + if (string.IsNullOrEmpty(publishUrl) || + !Uri.TryCreate(publishUrl, UriKind.Absolute, out var _)) + { + return false; + } + + return IsWebJobProject(webjobName, webjobType) + ? GetWebJobPublishUri(publishUrl, webjobName, webjobType, out publishUri) + : GetDefaultPublishUri(publishUrl, out publishUri); + } + + private async Task DefaultDeployAsync( + IHttpClient httpClient, + Uri publishUri, + string username, + string password, + string userAgent, + FileStream fileToPublishStream, + CancellationToken cancellationToken) + { + var response = await httpClient.PostRequestAsync( + publishUri, username, password, DefaultRequestContentType, userAgent, Encoding.UTF8, fileToPublishStream); + + return response; + } + + private bool GetDefaultPublishUri(string publishUrl, out Uri publishUri) + { + publishUri = new UriBuilder(publishUrl) + { + Path = OneDeployApiPath, + Query = OneDeployQueryParam, + Port = -1 + }.Uri; + + return Uri.TryCreate(publishUri.ToString(), UriKind.Absolute, out var _); + } +} diff --git a/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/OneDeployStatusService.cs b/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/OneDeployStatusService.cs new file mode 100644 index 000000000000..2929437335b5 --- /dev/null +++ b/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/OneDeployStatusService.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using Microsoft.NET.Sdk.Publish.Tasks.Properties; + +namespace Microsoft.NET.Sdk.Publish.Tasks.OneDeploy; + +internal class OneDeployStatusService(ITaskLogger taskLogger = null) : IDeploymentStatusService +{ + private static readonly TimeSpan s_maximumWaitForResult = TimeSpan.FromMinutes(3); + private static readonly TimeSpan s_refreshDelay = TimeSpan.FromSeconds(3); + private static readonly TimeSpan s_retryDelay = TimeSpan.FromSeconds(1); + private static readonly int s_retryCount = 3; + + private readonly ITaskLogger _taskLogger = taskLogger; + + /// + public async Task PollDeploymentAsync( + IHttpClient httpClient, + string url, + string user, + string password, + string userAgent, + CancellationToken cancellationToken) + { + _taskLogger?.LogMessage(Resources.DeploymentStatus_Polling); + + if (httpClient is null || cancellationToken.IsCancellationRequested) + { + return DeploymentResponse.s_unknownResponse; + } + + if (!Uri.TryCreate(url, UriKind.Absolute, out var _)) + { + _taskLogger?.LogError(string.Format(Resources.DeploymentStatus_InvalidPollingUrl, url)); + return DeploymentResponse.s_unknownResponse; + } + + var maxWaitForResultTokenSource = new CancellationTokenSource(s_maximumWaitForResult); + var retryTokenSource = CancellationTokenSource.CreateLinkedTokenSource(maxWaitForResultTokenSource.Token, cancellationToken); + var retryToken = retryTokenSource.Token; + + DeploymentResponse deploymentResponse = null; + DeploymentStatus deployStatus = DeploymentStatus.Pending; + + try + { + retryTokenSource.CancelAfter(s_maximumWaitForResult); + + while (!retryToken.IsCancellationRequested && !deployStatus.IsTerminatingStatus()) + { + deploymentResponse = await httpClient.RetryGetRequestAsync( + url, user, password, userAgent, s_retryCount, s_retryDelay, retryToken); + + // set to 'Unknown' if no response is returned + deploymentResponse ??= DeploymentResponse.s_unknownResponse; + + deployStatus = deploymentResponse is not null && deploymentResponse.Status is not null + ? deploymentResponse.Status.Value + : DeploymentStatus.Unknown; + + _taskLogger?.LogMessage(deploymentResponse.ToString()); + + await System.Threading.Tasks.Task.Delay(s_refreshDelay, retryToken); + } + } + catch (Exception ex) + { + if (ex is TaskCanceledException) + { + // 'maxWaitTimeForResult' has passed; no 'terminating' status was obtained + } + else if (ex is HttpRequestException) + { + // HTTP GET request threw; return last known response + return deploymentResponse; + } + } + + return deploymentResponse ?? DeploymentResponse.s_unknownResponse; + } +} diff --git a/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/TaskLogger.cs b/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/TaskLogger.cs new file mode 100644 index 000000000000..72d4dbe12650 --- /dev/null +++ b/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/TaskLogger.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.NET.Sdk.Publish.Tasks.OneDeploy; + +/// +/// A logger that wraps a instance. +/// +internal class TaskLogger(TaskLoggingHelper logger, bool isEnabled = true) : ITaskLogger +{ + private readonly TaskLoggingHelper _taskLoggingHelper = logger; + + /// + public bool Enabled { get; set; } = isEnabled; + + private bool CanLog => _taskLoggingHelper is not null && Enabled; + + /// + public void LogMessage(string message) + { + if (CanLog) + { + _taskLoggingHelper.LogMessage(message); + } + } + + /// + public void LogMessage(MessageImportance importance, string message) + { + if (CanLog) + { + _taskLoggingHelper.LogMessage(importance, message); + + } + } + + /// + public void LogError(string message) + { + if (CanLog) + { + _taskLoggingHelper.LogError(message); + + } + } + + /// + public void LogWarning(string message) + { + if (CanLog) + { + _taskLoggingHelper.LogWarning(message); + } + } +} diff --git a/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/ZipFilePackager.cs b/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/ZipFilePackager.cs new file mode 100644 index 000000000000..7457d91b3f3b --- /dev/null +++ b/src/WebSdk/Publish/Tasks/Tasks/OneDeploy/ZipFilePackager.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO.Compression; + +namespace Microsoft.NET.Sdk.Publish.Tasks.OneDeploy; + +/// +/// Packages file(s) to a .zip file. +/// +internal class ZipFilePackager : IFilePackager +{ + /// + public string Extension => ".zip"; + + /// + public Task CreatePackageAsync(string sourcePath, string destinationPath, CancellationToken cancellation) + { + ZipFile.CreateFromDirectory(sourcePath, destinationPath); + + // no way to know if ZipFile succeeded; assume it always does + return System.Threading.Tasks.Task.FromResult(true); + } +} diff --git a/src/WebSdk/Publish/Tasks/Tasks/ZipDeploy/DefaultHttpClient.cs b/src/WebSdk/Publish/Tasks/Tasks/ZipDeploy/DefaultHttpClient.cs deleted file mode 100644 index 5ee89130410c..000000000000 --- a/src/WebSdk/Publish/Tasks/Tasks/ZipDeploy/DefaultHttpClient.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Net.Http; -using System.Net.Http.Headers; - -namespace Microsoft.NET.Sdk.Publish.Tasks.ZipDeploy -{ - internal class DefaultHttpClient : IHttpClient, IDisposable - { - private readonly HttpClient _httpClient = new() - { - Timeout = Timeout.InfiniteTimeSpan - }; - - public HttpRequestHeaders DefaultRequestHeaders => _httpClient.DefaultRequestHeaders; - - public void Dispose() - { - _httpClient.Dispose(); - } - - public Task PostAsync(Uri uri, StreamContent content) - { - return _httpClient.PostAsync(uri, content); - } - - public Task GetAsync(Uri uri, CancellationToken cancellationToken) - { - return _httpClient.GetAsync(uri, cancellationToken); - } - } -} diff --git a/src/WebSdk/Publish/Tasks/Tasks/ZipDeploy/HttpClientHelpers.cs b/src/WebSdk/Publish/Tasks/Tasks/ZipDeploy/HttpClientHelpers.cs deleted file mode 100644 index 9a877b2e86c3..000000000000 --- a/src/WebSdk/Publish/Tasks/Tasks/ZipDeploy/HttpClientHelpers.cs +++ /dev/null @@ -1,79 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; - -namespace Microsoft.NET.Sdk.Publish.Tasks.ZipDeploy -{ - internal static class HttpClientHelpers - { - internal static readonly string AzureADUserName = Guid.Empty.ToString(); - internal static readonly string BearerAuthenticationScheme = "Bearer"; - internal static readonly string BasicAuthenticationScheme = "Basic"; - public static async Task PostRequestAsync(this IHttpClient client, Uri uri, string username, string password, string contentType, string userAgent, Encoding encoding, Stream messageBody) - { - AddAuthenticationHeader(username, password, client); - client.DefaultRequestHeaders.Add("User-Agent", userAgent); - - StreamContent content = new(messageBody ?? new MemoryStream()) - { - Headers = - { - ContentType = new MediaTypeHeaderValue(contentType) - { - CharSet = encoding.WebName - }, - ContentEncoding = - { - encoding.WebName - } - } - }; - - try - { - HttpResponseMessage responseMessage = await client.PostAsync(uri, content); - return new HttpResponseMessageWrapper(responseMessage); - } - catch (TaskCanceledException) - { - return new HttpResponseMessageForStatusCode(HttpStatusCode.RequestTimeout); - } - } - - public static async Task GetRequestAsync(this IHttpClient client, Uri uri, string username, string password, string userAgent, CancellationToken cancellationToken) - { - AddAuthenticationHeader(username, password, client); - client.DefaultRequestHeaders.Add("User-Agent", userAgent); - - try - { - HttpResponseMessage responseMessage = await client.GetAsync(uri, cancellationToken); - return new HttpResponseMessageWrapper(responseMessage); - } - catch (TaskCanceledException) - { - return new HttpResponseMessageForStatusCode(HttpStatusCode.RequestTimeout); - } - } - - private static void AddAuthenticationHeader(string username, string password, IHttpClient client) - { - client.DefaultRequestHeaders.Remove("Connection"); - - if (!string.Equals(username, AzureADUserName, StringComparison.Ordinal)) - { - string plainAuth = string.Format("{0}:{1}", username, password); - byte[] plainAuthBytes = Encoding.ASCII.GetBytes(plainAuth); - string base64 = Convert.ToBase64String(plainAuthBytes); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(BasicAuthenticationScheme, base64); - } - else - { - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(BearerAuthenticationScheme, password); - } - } - } -} diff --git a/src/WebSdk/Publish/Tasks/Tasks/ZipDeploy/HttpResponseMessageForStatusCode.cs b/src/WebSdk/Publish/Tasks/Tasks/ZipDeploy/HttpResponseMessageForStatusCode.cs deleted file mode 100644 index 30604c28fa38..000000000000 --- a/src/WebSdk/Publish/Tasks/Tasks/ZipDeploy/HttpResponseMessageForStatusCode.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Net; - -namespace Microsoft.NET.Sdk.Publish.Tasks.ZipDeploy -{ - internal class HttpResponseMessageForStatusCode : IHttpResponse - { - public HttpResponseMessageForStatusCode(HttpStatusCode statusCode) - { - StatusCode = statusCode; - } - - public HttpStatusCode StatusCode { get; private set; } - - public System.Threading.Tasks.Task GetResponseBodyAsync() - { - return System.Threading.Tasks.Task.FromResult(new MemoryStream()); - } - - public IEnumerable GetHeader(string name) - { - return new string[0]; - } - } -} diff --git a/src/WebSdk/Publish/Tasks/Tasks/ZipDeploy/HttpResponseMessageWrapper.cs b/src/WebSdk/Publish/Tasks/Tasks/ZipDeploy/HttpResponseMessageWrapper.cs deleted file mode 100644 index 2ad5d693d2e7..000000000000 --- a/src/WebSdk/Publish/Tasks/Tasks/ZipDeploy/HttpResponseMessageWrapper.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Net; -using System.Net.Http; - -namespace Microsoft.NET.Sdk.Publish.Tasks.ZipDeploy -{ - public class HttpResponseMessageWrapper : IHttpResponse - { - private readonly HttpResponseMessage _message; - private readonly Lazy> _responseBodyTask; - - public HttpResponseMessageWrapper(HttpResponseMessage message) - { - _message = message; - StatusCode = message.StatusCode; - _responseBodyTask = new Lazy>(GetResponseStream); - } - - public HttpStatusCode StatusCode { get; private set; } - - public async Task GetResponseBodyAsync() - { - return await _responseBodyTask.Value; - } - - private Task GetResponseStream() - { - return _message.Content.ReadAsStreamAsync(); - } - - public IEnumerable GetHeader(string name) - { - IEnumerable values; - if (_message.Headers.TryGetValues(name, out values)) - { - return values; - } - - return new string[0]; - } - } -} diff --git a/src/WebSdk/Publish/Tasks/Tasks/ZipDeploy/IHttpClient.cs b/src/WebSdk/Publish/Tasks/Tasks/ZipDeploy/IHttpClient.cs deleted file mode 100644 index 58af372a48a0..000000000000 --- a/src/WebSdk/Publish/Tasks/Tasks/ZipDeploy/IHttpClient.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Net.Http; -using System.Net.Http.Headers; - -namespace Microsoft.NET.Sdk.Publish.Tasks.ZipDeploy -{ - public interface IHttpClient - { - HttpRequestHeaders DefaultRequestHeaders { get; } - Task PostAsync(Uri uri, StreamContent content); - Task GetAsync(Uri uri, CancellationToken cancellationToken); - } -} diff --git a/src/WebSdk/Publish/Tasks/Tasks/ZipDeploy/IHttpResponse.cs b/src/WebSdk/Publish/Tasks/Tasks/ZipDeploy/IHttpResponse.cs deleted file mode 100644 index 914832aad511..000000000000 --- a/src/WebSdk/Publish/Tasks/Tasks/ZipDeploy/IHttpResponse.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Net; - -namespace Microsoft.NET.Sdk.Publish.Tasks.ZipDeploy -{ - /// - /// A response to an HTTP request - /// - public interface IHttpResponse - { - /// - /// Gets the status code the server returned - /// - HttpStatusCode StatusCode { get; } - - /// - /// Gets the body of the response - /// - Task GetResponseBodyAsync(); - - IEnumerable GetHeader(string name); - } -} diff --git a/src/WebSdk/README.md b/src/WebSdk/README.md index 488919c43791..5b32697f7a50 100644 --- a/src/WebSdk/README.md +++ b/src/WebSdk/README.md @@ -144,6 +144,24 @@ Using dotnet with a profile: dotnet publish WebApplication.csproj /p:PublishProfile= /p:Password= ``` +One Deploy: +--------------------- + +Using dotnet with the default profile: + +``` + +dotnet publish WebJobApplication.csproj /p:WebPublishMethod=OneDeploy /p:PublishUrl= /p:UserName= /p:Password= /p:PublishProfile=DefaultWebJobOneDeploy +``` + +Profile can be added to the following location in the project /Properties/PublishProfiles/. + +Using dotnet with a profile: + +``` +dotnet publish WebJobApplication.csproj /p:PublishProfile= /p:Password= +``` + Sample folder profile: --------------------- diff --git a/test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/CreatePackageFileTests.cs b/test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/CreatePackageFileTests.cs new file mode 100644 index 000000000000..c0b0c8bac40d --- /dev/null +++ b/test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/CreatePackageFileTests.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Moq; + +namespace Microsoft.NET.Sdk.Publish.Tasks.OneDeploy.Tests; + +/// +/// Unit Tests for . +/// +public class CreatePackageFileTests +{ + private const string TestPackageExtension = ".test"; + private const string ProjectName = "TestProject"; + private const string ContentToPackage = $@"z:\Users\testUser\source\Solution\{ProjectName}"; + private const string IntermediateTempPath = $@"{ContentToPackage}\bin\net8.0\{ProjectName}"; + + [Theory] + [InlineData(true, TestPackageExtension, IntermediateTempPath)] + [InlineData(false, null, null)] + public void CreatePackageFile_Execute(bool expectedResult, string expectedFileExtension, string expectedFileDirectory) + { + // Arrange + var testPackageFilePath = Path.Combine(IntermediateTempPath, "uniqueFileName"); + + var filePackagerMock = new Mock(); + filePackagerMock.SetupGet(fp => fp.Extension).Returns(TestPackageExtension); + filePackagerMock + .Setup(fp => fp.CreatePackageAsync(ContentToPackage, It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResult); + + var createPackageFileTask = new CreatePackageFile(filePackagerMock.Object) + { + ContentToPackage = ContentToPackage, + ProjectName = ProjectName, + IntermediateTempPath = IntermediateTempPath, + }; + + // Act + var result = createPackageFileTask.Execute(); + + // Assert: 'CreatePackageFile' task result expected results + Assert.Equal(expectedResult, result); + + if (expectedResult) + { + Assert.Equal(expectedFileDirectory, Path.GetDirectoryName(createPackageFileTask.CreatedPackageFilePath)); + Assert.Equal(expectedFileExtension, Path.GetExtension(createPackageFileTask.CreatedPackageFilePath)); + } + else + { + Assert.True(string.IsNullOrEmpty(createPackageFileTask.CreatedPackageFilePath)); + } + + filePackagerMock.VerifyAll(); + } + + [Theory] + [InlineData(null, ProjectName, IntermediateTempPath)] + [InlineData("", ProjectName, IntermediateTempPath)] + [InlineData(ContentToPackage, null, IntermediateTempPath)] + [InlineData(ContentToPackage, "", IntermediateTempPath)] + [InlineData(ContentToPackage, ProjectName, null)] + [InlineData(ContentToPackage, ProjectName, "")] + [InlineData("", "", "")] + [InlineData(null, null, null)] + public void CreatePackageFile_Execute_MissingValues(string contentToPackage, string projectName, string intermediateTempPath) + { + // Arrange + var filePackagerMock = new Mock(); + + var createPackageFileTask = new CreatePackageFile(filePackagerMock.Object) + { + ContentToPackage = contentToPackage, + ProjectName = projectName, + IntermediateTempPath = intermediateTempPath, + }; + + // Act + var result = createPackageFileTask.Execute(); + + // Assert: 'CreatePackageFile' task results in 'False' due to missing values + Assert.False(result); + Assert.True(string.IsNullOrEmpty(createPackageFileTask.CreatedPackageFilePath)); + filePackagerMock.VerifyAll(); + } + + [Fact] + public void CreatePackagerFile_ZipPackager_Default() + { + // Act + var createPackageFile = new CreatePackageFile(); + + // Assert: + // - Default ctor (as used by MSBuild) can correctly instantiate an instance. + // - A 'ZipFilePackager' is set as the default 'IFilePackager' (though we can't verify that, here) + Assert.True(createPackageFile is not null); + } +} diff --git a/test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/OneDeployStatusServiceTests.cs b/test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/OneDeployStatusServiceTests.cs new file mode 100644 index 000000000000..52c5c3de3129 --- /dev/null +++ b/test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/OneDeployStatusServiceTests.cs @@ -0,0 +1,240 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using System.Net.Http; +using System.Text.Json; +using Microsoft.NET.Sdk.Publish.Tasks.Properties; +using Moq; + +namespace Microsoft.NET.Sdk.Publish.Tasks.OneDeploy.Tests; + +/// +/// Unit Tests for +/// +public class OneDeployStatusServiceTests +{ + private const string Username = "someUser"; + private const string Password = "123secret"; + private const string UserAgent = "websdk/8.0"; // as OneDeploy.UserAgentName + private const string PublishUrl = "https://mysite.scm.azurewebsites.net"; + private const string DeploymentId = "056f49ce-fcd7-497c-929b-d74bc6f8905e"; + + private static readonly Uri DeploymentUri = new UriBuilder($@"{PublishUrl}/api/deployments/{DeploymentId}").Uri; + + [Theory] + [InlineData(HttpStatusCode.OK, DeploymentStatus.Success)] + [InlineData(HttpStatusCode.Accepted, DeploymentStatus.Success)] + [InlineData(HttpStatusCode.OK, DeploymentStatus.PartialSuccess)] + [InlineData(HttpStatusCode.Accepted, DeploymentStatus.PartialSuccess)] + [InlineData(HttpStatusCode.OK, DeploymentStatus.Failed)] + [InlineData(HttpStatusCode.Accepted, DeploymentStatus.Failed)] + [InlineData(HttpStatusCode.OK, DeploymentStatus.Conflict)] + [InlineData(HttpStatusCode.Accepted, DeploymentStatus.Conflict)] + [InlineData(HttpStatusCode.OK, DeploymentStatus.Cancelled)] + [InlineData(HttpStatusCode.Accepted, DeploymentStatus.Cancelled)] + [InlineData(HttpStatusCode.OK, DeploymentStatus.Unknown)] + [InlineData(HttpStatusCode.Accepted, DeploymentStatus.Unknown)] + public async Task PollDeployment_Completes(HttpStatusCode statusCode, DeploymentStatus expectedDeploymentStatus) + { + // Arrange + var cancellationTokenSource = new CancellationTokenSource(); + var cancellationToken = cancellationTokenSource.Token; + + var taskLoggerMock = new Mock(); + taskLoggerMock.Setup(l => l.LogMessage(Resources.DeploymentStatus_Polling)); + taskLoggerMock.Setup(l => l.LogMessage(string.Format(Resources.DeploymentStatus, expectedDeploymentStatus))); + + var deploymentResponse = new DeploymentResponse(); + if (expectedDeploymentStatus != DeploymentStatus.Unknown) + { + deploymentResponse.Id = DeploymentId; + deploymentResponse.Status = expectedDeploymentStatus; + } + + var httpClientMock = GetHttpClientMock(statusCode, deploymentResponse); + + var oneDeployStatusService = new OneDeployStatusService(taskLoggerMock.Object); + + // Act + var result = await oneDeployStatusService.PollDeploymentAsync(httpClientMock.Object, DeploymentUri.AbsoluteUri, Username, Password, UserAgent, cancellationToken); + + // Assert: poll deployment status runs to completion, resulting in the given expectedDeploymentStatus + Assert.Equal(expectedDeploymentStatus, deploymentResponse.Status); + + taskLoggerMock.VerifyAll(); + httpClientMock.VerifyAll(); + } + + [Theory] + [InlineData(HttpStatusCode.Forbidden)] + [InlineData(HttpStatusCode.NotFound)] + [InlineData(HttpStatusCode.InternalServerError)] + public async Task PollDeployment_HttpResponse_Fail(HttpStatusCode statusCode) + { + // Arrange + var cancellationTokenSource = new CancellationTokenSource(); + var cancellationToken = cancellationTokenSource.Token; + + var taskLoggerMock = new Mock(); + taskLoggerMock.Setup(l => l.LogMessage(Resources.DeploymentStatus_Polling)); + taskLoggerMock.Setup(l => l.LogMessage(string.Format(Resources.DeploymentStatus, DeploymentStatus.Unknown))); + + var httpClientMock = GetHttpClientMock(statusCode, null); + + var oneDeployStatusService = new OneDeployStatusService(taskLoggerMock.Object); + + // Act + var result = await oneDeployStatusService.PollDeploymentAsync(httpClientMock.Object, DeploymentUri.AbsoluteUri, Username, Password, UserAgent, cancellationToken); + + // Assert: poll deployment status runs to completion, resulting in 'Unknown' status because failed HTTP Response Status code + Assert.Equal(DeploymentStatus.Unknown, result.Status); + + taskLoggerMock.VerifyAll(); + httpClientMock.VerifyAll(); + } + + [Fact] + public async Task PollDeployment_Completes_No_Logger() + { + // Arrange + var cancellationTokenSource = new CancellationTokenSource(); + var cancellationToken = cancellationTokenSource.Token; + + var deploymentResponse = new DeploymentResponse() + { + Id = DeploymentId, + Status = DeploymentStatus.Success + }; + + var httpClientMock = GetHttpClientMock(HttpStatusCode.OK, deploymentResponse); + + var oneDeployStatusService = new OneDeployStatusService(); + + // Act + var result = await oneDeployStatusService.PollDeploymentAsync(httpClientMock.Object, DeploymentUri.AbsoluteUri, Username, Password, UserAgent, cancellationToken); + + // Assert: poll deployment status runs to completion with NULL ITaskLogger + Assert.Equal(DeploymentStatus.Success, deploymentResponse.Status); + + httpClientMock.VerifyAll(); + } + + [Fact] + public async Task PollDeployment_Halted() + { + // Arrange + var cancellationTokenSource = new CancellationTokenSource(); + var cancellationToken = cancellationTokenSource.Token; + + var taskLoggerMock = new Mock(); + taskLoggerMock.Setup(l => l.LogMessage(Resources.DeploymentStatus_Polling)); + + var httpClientMock = new Mock(); + + var oneDeployStatusService = new OneDeployStatusService(taskLoggerMock.Object); + + // Act + cancellationTokenSource.Cancel(); + var result = await oneDeployStatusService.PollDeploymentAsync(httpClientMock.Object, DeploymentUri.AbsoluteUri, Username, Password, UserAgent, cancellationToken); + + // Assert: deployment status won't poll for deployment as 'CancellationToken' is already cancelled + Assert.Equal(DeploymentStatus.Unknown, result.Status); + + taskLoggerMock.VerifyAll(); + httpClientMock.VerifyAll(); + } + + [Theory] + [InlineData("not-valid-url")] + [InlineData("")] + [InlineData(null)] + public async Task PollDeployment_InvalidURL(string invalidUrl) + { + // Arrange + var cancellationTokenSource = new CancellationTokenSource(); + var cancellationToken = cancellationTokenSource.Token; + + var taskLoggerMock = new Mock(); + taskLoggerMock.Setup(l => l.LogMessage(Resources.DeploymentStatus_Polling)); + taskLoggerMock.Setup(l => l.LogError(string.Format(Resources.DeploymentStatus_InvalidPollingUrl, invalidUrl))); + + var httpClientMock = new Mock(); + + var oneDeployStatusService = new OneDeployStatusService(taskLoggerMock.Object); + + // Act + var result = await oneDeployStatusService.PollDeploymentAsync(httpClientMock.Object, invalidUrl, Username, Password, UserAgent, cancellationToken); + + // Assert: deployment status won't poll for deployment because given polling URL is invalid + Assert.Equal(DeploymentStatus.Unknown, result.Status); + + taskLoggerMock.VerifyAll(); + httpClientMock.VerifyAll(); + } + + [Fact] + public async Task PollDeployment_Missing_HttpClient() + { + // Arrange + var cancellationTokenSource = new CancellationTokenSource(); + var cancellationToken = cancellationTokenSource.Token; + + var taskLoggerMock = new Mock(); + taskLoggerMock.Setup(l => l.LogMessage(Resources.DeploymentStatus_Polling)); + + var oneDeployStatusService = new OneDeployStatusService(taskLoggerMock.Object); + + // Act + var result = await oneDeployStatusService.PollDeploymentAsync(null, DeploymentUri.AbsoluteUri, Username, Password, UserAgent, cancellationToken); + + // Assert: deployment status won't poll for deployment because IHttpClient is NULL + Assert.Equal(DeploymentStatus.Unknown, result.Status); + + taskLoggerMock.VerifyAll(); + } + + [Fact] + public void Constructor_OK() + { + var oneDeployStatusService = new OneDeployStatusService(); + + // no-arg ctor instantiate instance + Assert.NotNull(oneDeployStatusService); + } + + private Mock GetHttpClientMock( + HttpStatusCode statusCode, + DeploymentResponse deploymentResponse) + { + var httpClientMock = new Mock(); + + // Request + HttpRequestMessage requestMessage = new(); + httpClientMock + .Setup(hc => hc.DefaultRequestHeaders) + .Returns(requestMessage.Headers); + + // Response + HttpContent responseContent = null; + string deploymentResponseJson = null; + if (deploymentResponse is not null) + { + deploymentResponseJson = JsonSerializer.Serialize(deploymentResponse); + responseContent = new StringContent(deploymentResponseJson, Encoding.UTF8, "application/json"); + } + + HttpResponseMessage responseMessage = new(statusCode); + if (responseContent is not null) + { + responseMessage.Content = responseContent; + } + + // GetAsync() + httpClientMock + .Setup(hc => hc.GetAsync(DeploymentUri, It.IsAny())) + .ReturnsAsync(responseMessage); + + return httpClientMock; + } +} diff --git a/test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/OneDeployTests.WebJob.cs b/test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/OneDeployTests.WebJob.cs new file mode 100644 index 000000000000..059805fd69c2 --- /dev/null +++ b/test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/OneDeployTests.WebJob.cs @@ -0,0 +1,189 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using System.Net.Http; +using System.Security.Policy; +using Microsoft.NET.Sdk.Publish.Tasks.Properties; +using Moq; + +namespace Microsoft.NET.Sdk.Publish.Tasks.OneDeploy.Tests; + +public partial class OneDeployTests +{ + private const string WebJobName = "TestWebJob"; + private const string ContinuousWebJob = "Continuous"; // as OneDeploy.WebJob.ContinuousWebJobType + private const string TriggeredWebJob = "Triggered"; // as OneDeploy.WebJob.TriggeredWebJobType + private const string ContinuousApiPath = "api/continuouswebjobs"; // as OneDeploy.WebJob.ContinuousWebJobApiPath + private const string TriggeredApiPath = "api/triggeredwebjobs"; // as OneDeploy.WebJob.TriggeredWebJobsApiPath + private const string PutErrorResponseMessage = "Missing run.sh file"; + + private static readonly Uri OneDeploy_WebJob_Continuous_Uri = new UriBuilder(PublishUrl) + { + Path = $"{ContinuousApiPath}/{WebJobName}", + }.Uri; + + private static readonly Uri OneDeploy_WebJob_Triggered_Uri = new UriBuilder(PublishUrl) + { + Path = $"{TriggeredApiPath}/{WebJobName}", + }.Uri; + + [Theory] + [InlineData(DeploymentStatus.Success, HttpStatusCode.OK, ContinuousWebJob, true)] + [InlineData(DeploymentStatus.Success, HttpStatusCode.OK, TriggeredWebJob, true)] + [InlineData(DeploymentStatus.Failed, HttpStatusCode.BadRequest, ContinuousWebJob, false)] + [InlineData(DeploymentStatus.Failed, HttpStatusCode.NotFound, TriggeredWebJob, false)] + public async Task OneDeploy_WebJob_Execute_Completes( + DeploymentStatus deployStatus, HttpStatusCode statusCode, string webJobType, bool expectedResult) + { + // Arrange + var publishUri = GetWebJobUri(webJobType); + var httpClientMock = GetWebJobHttpClientMock(publishUri, statusCode, deployStatus); + + var deploymentStatusServiceMock = new Mock>(); + + // set messages to log according to result + var taskLoggerMock = new Mock(); + taskLoggerMock.Setup(l => l.LogMessage(Build.Framework.MessageImportance.High, string.Format(Resources.ONEDEPLOY_PublishingOneDeploy, FileToPublish, publishUri.AbsoluteUri.ToString()))); + + if (expectedResult) + { + taskLoggerMock.Setup(l => l.LogMessage(string.Format(Resources.ONEDEPLOY_Uploaded, FileToPublish))); + taskLoggerMock.Setup(l => l.LogMessage(Build.Framework.MessageImportance.High, Resources.ONEDEPLOY_Success)); + } + else + { + var failedDeployMsg = string.Format(Resources.ONEDEPLOY_FailedDeployRequest, publishUri.AbsoluteUri, statusCode, PutErrorResponseMessage); + taskLoggerMock.Setup(l => l.LogError(failedDeployMsg)); + } + + var oneDeployTask = new OneDeploy(taskLoggerMock.Object); + + // Act + var result = await oneDeployTask.OneDeployAsync( + FileToPublish, Username, Password, PublishUrl, $"{UserAgentName}/8.0", WebJobName, webJobType, httpClientMock.Object, deploymentStatusServiceMock.Object, CancellationToken.None); + + // Assert: WebJob deployment operation runs to completion with expected result + Assert.Equal(expectedResult, result); + + httpClientMock.VerifyAll(); + deploymentStatusServiceMock.VerifyAll(); + taskLoggerMock.VerifyAll(); + } + + [Theory] + [InlineData("not-valid-url", ContinuousWebJob)] + [InlineData("not-valid-url", TriggeredWebJob)] + [InlineData("", ContinuousWebJob)] + [InlineData("", TriggeredWebJob)] + [InlineData(null, ContinuousWebJob)] + [InlineData(null, TriggeredWebJob)] + public async Task OneDeploy_WebJob_PublishUrl_Invalid(string invalidUrl, string webjobType) + { + // Arrange + var httpClientMock = new Mock(); + + var deploymentStatusServiceMock = new Mock>(); + + // set messages to log + var taskLoggerMock = new Mock(); + taskLoggerMock.Setup(l => l.LogError(string.Format(Resources.ONEDEPLOY_InvalidPublishUrl, invalidUrl))); + + var oneDeployTask = new OneDeploy(taskLoggerMock.Object); + + // Act + var result = await oneDeployTask.OneDeployAsync( + FileToPublish, Username, Password, invalidUrl, $"{UserAgentName}/8.0", WebJobName, webjobType, httpClientMock.Object, deploymentStatusServiceMock.Object, CancellationToken.None); + + // Assert: deployment operation fails because 'PublishUrl' is not valid + Assert.False(result); + + httpClientMock.VerifyAll(); + deploymentStatusServiceMock.VerifyAll(); + taskLoggerMock.VerifyAll(); + } + + [Theory] + [InlineData("", ContinuousWebJob)] + [InlineData("", TriggeredWebJob)] + [InlineData(null, ContinuousWebJob)] + [InlineData(null, TriggeredWebJob)] + [InlineData(WebJobName, "NotValidType")] + [InlineData(WebJobName, "")] + [InlineData(WebJobName, null)] + [InlineData("", "")] + [InlineData(null, null)] + public async Task OneDeploy_WebJob_Missing_NameOrType(string webjobName, string webjobType) + { + // Arrange + var httpClientMock = new Mock(); + + // Request + HttpRequestMessage requestMessage = new(); + httpClientMock.Setup(hc => hc.DefaultRequestHeaders).Returns(requestMessage.Headers); + + // PostAsync() + httpClientMock + .Setup(hc => hc.PostAsync(OneDeployUri, It.IsAny())) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.BadRequest)); + + var deploymentStatusServiceMock = new Mock>(); + + // set messages to log + var taskLoggerMock = new Mock(); + taskLoggerMock.Setup(l => l.LogMessage(Build.Framework.MessageImportance.High, string.Format(Resources.ONEDEPLOY_PublishingOneDeploy, FileToPublish, OneDeployUri.AbsoluteUri.ToString()))); + taskLoggerMock.Setup(l => l.LogError(string.Format(Resources.ONEDEPLOY_FailedDeployRequest, OneDeployUri.AbsoluteUri.ToString(), HttpStatusCode.BadRequest))); + + var oneDeployTask = new OneDeploy(taskLoggerMock.Object); + + // Act + var result = await oneDeployTask.OneDeployAsync( + FileToPublish, Username, Password, PublishUrl, $"{UserAgentName}/8.0", webjobName, webjobType, httpClientMock.Object, deploymentStatusServiceMock.Object, CancellationToken.None); + + // Assert: deployment operation fails because since 'WebJobName' and/or 'WebJobType' is invalid, so we calculate the + // default OneDeploy URI ('/api/publish'), which target instance does not recognized as valid + Assert.False(result); + + httpClientMock.VerifyAll(); + deploymentStatusServiceMock.VerifyAll(); + taskLoggerMock.VerifyAll(); + } + + private Mock GetWebJobHttpClientMock( + Uri publishUri, + HttpStatusCode statusCode, + DeploymentStatus deploymentStatus) + { + var httpClientMock = new Mock(); + + // Request + HttpRequestMessage requestMessage = new(); + httpClientMock + .Setup(hc => hc.DefaultRequestHeaders) + .Returns(requestMessage.Headers); + + // Response + HttpResponseMessage responseMessage = new(statusCode); + if (deploymentStatus.IsFailedStatus()) + { + responseMessage.Content = new StringContent(PutErrorResponseMessage); + } + + // PutAsync() + httpClientMock + .Setup(hc => hc.PutAsync(publishUri, It.IsAny(), It.IsAny())) + .ReturnsAsync(responseMessage); + + return httpClientMock; + } + + private Uri GetWebJobUri(string webJobType) + { + return webJobType switch + { + TriggeredWebJob => OneDeploy_WebJob_Triggered_Uri, + ContinuousWebJob + or _ => OneDeploy_WebJob_Continuous_Uri + }; + } +} diff --git a/test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/OneDeployTests.cs b/test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/OneDeployTests.cs new file mode 100644 index 000000000000..936e02882984 --- /dev/null +++ b/test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/OneDeployTests.cs @@ -0,0 +1,352 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + + +using System.Net; +using System.Net.Http; +using Microsoft.NET.Sdk.Publish.Tasks.MsDeploy; +using Microsoft.NET.Sdk.Publish.Tasks.Properties; +using Moq; + +namespace Microsoft.NET.Sdk.Publish.Tasks.OneDeploy.Tests; + +/// +/// Unit Tests for +/// +public partial class OneDeployTests +{ + private const string Username = "someUser"; + private const string Password = "123secret"; + private const string PublishUrl = "https://mysite.scm.azurewebsites.net"; + private const string UserAgentName = "websdk"; // as OneDeploy.UserAgentName + private const string DeploymentUrl = $@"{PublishUrl}/api/deployments/056f49ce-fcd7-497c-929b-d74bc6f8905e"; + private const string DeploymentLogUrl = $@"{DeploymentUrl}/log"; + private const string DefaultApiPath = "api/publish"; // as OneDeploy.OneDeployApiPath + private const string DefaultQueryParam = "RemoteBuild=false"; // as OneDeploy.OneDeployQueryParam + + private static readonly Uri OneDeployUri = new UriBuilder(PublishUrl) + { + Path = DefaultApiPath, + Query = DefaultQueryParam + }.Uri; + + private static string _fileToPublish; + public static string FileToPublish + { + get + { + if (_fileToPublish == null) + { + string codebase = typeof(OneDeployTests).Assembly.Location; + string assemblyPath = new Uri(codebase, UriKind.Absolute).LocalPath; + string baseDirectory = Path.GetDirectoryName(assemblyPath); + _fileToPublish = Path.Combine(baseDirectory, Path.Combine("Resources", "TestPublishContents.zip")); + } + + return _fileToPublish; + } + } + + [Theory] + [InlineData(DeploymentStatus.Success, HttpStatusCode.OK, true)] + [InlineData(DeploymentStatus.Success, HttpStatusCode.Accepted, true)] + [InlineData(DeploymentStatus.PartialSuccess, HttpStatusCode.OK, true)] + [InlineData(DeploymentStatus.PartialSuccess, HttpStatusCode.Accepted, true)] + [InlineData(DeploymentStatus.Failed, HttpStatusCode.OK, false)] + [InlineData(DeploymentStatus.Failed, HttpStatusCode.Accepted, false)] + [InlineData(DeploymentStatus.Conflict, HttpStatusCode.OK, false)] + [InlineData(DeploymentStatus.Conflict, HttpStatusCode.Accepted, false)] + [InlineData(DeploymentStatus.Cancelled, HttpStatusCode.OK, false)] + [InlineData(DeploymentStatus.Cancelled, HttpStatusCode.Accepted, false)] + [InlineData(DeploymentStatus.Unknown, HttpStatusCode.OK, false)] + [InlineData(DeploymentStatus.Unknown, HttpStatusCode.Accepted, false)] + + public async Task OneDeploy_Execute_Completes(DeploymentStatus deployStatus, HttpStatusCode statusCode, bool expectedResult) + { + // Arrange + var httpClientMock = GetHttpClientMock(statusCode); + + var deploymentStatusServiceMock = GetDeploymentStatusServiceMock(httpClientMock.Object, deployStatus); + + // set messages to log according to result + var taskLoggerMock = new Mock(); + taskLoggerMock.Setup(l => l.LogMessage(Build.Framework.MessageImportance.High, string.Format(Resources.ONEDEPLOY_PublishingOneDeploy, FileToPublish, OneDeployUri.AbsoluteUri.ToString()))); + taskLoggerMock.Setup(l => l.LogMessage(string.Format(Resources.ONEDEPLOY_Uploaded, FileToPublish))); + + if (expectedResult) + { + taskLoggerMock.Setup(l => l.LogMessage(Build.Framework.MessageImportance.High, Resources.ONEDEPLOY_Success)); + } + else + { + var failedDeployMsg = string.Format(Resources.ONEDEPLOY_FailedWithLogs, FileToPublish, OneDeployUri.AbsoluteUri, deployStatus, DeploymentLogUrl); + taskLoggerMock.Setup(l => l.LogError(failedDeployMsg)); + } + + var oneDeployTask = new OneDeploy(taskLoggerMock.Object); + + // Act + var result = await oneDeployTask.OneDeployAsync( + FileToPublish, Username, Password, PublishUrl, $"{UserAgentName}/8.0", webjobName: null, webjobType: null, httpClientMock.Object, deploymentStatusServiceMock.Object, CancellationToken.None); + + // Assert: deployment operation runs to completion with expected result + Assert.Equal(expectedResult, result); + + httpClientMock.VerifyAll(); + deploymentStatusServiceMock.VerifyAll(); + taskLoggerMock.VerifyAll(); + } + + [Theory] + [InlineData("not-a-location-url")] + [InlineData(null)] + public async Task OneDeploy_Execute_Deploy_Location_Missing(string invalidLocationHeaderValue) + { + // Arrange + var httpClientMock = new Mock(); + + // Request + HttpRequestMessage requestMessage = new(); + httpClientMock.Setup(hc => hc.DefaultRequestHeaders).Returns(requestMessage.Headers); + + // Response + HttpResponseMessage responseMessage = new(HttpStatusCode.OK); + if (invalidLocationHeaderValue is not null) + { + responseMessage.Headers.Add("Location", invalidLocationHeaderValue); + } + + // PostAsync() + httpClientMock.Setup(hc => hc.PostAsync(OneDeployUri, It.IsAny())).ReturnsAsync(responseMessage); + + var deploymentStatusServiceMock = new Mock>(); + + // set messages to log according to result + var taskLoggerMock = new Mock(); + taskLoggerMock.Setup(l => l.LogMessage(Build.Framework.MessageImportance.High, string.Format(Resources.ONEDEPLOY_PublishingOneDeploy, FileToPublish, OneDeployUri.AbsoluteUri.ToString()))); + taskLoggerMock.Setup(l => l.LogMessage(string.Format(Resources.ONEDEPLOY_Uploaded, FileToPublish))); + + var oneDeployTask = new OneDeploy(taskLoggerMock.Object); + + // Act + var result = await oneDeployTask.OneDeployAsync( + FileToPublish, Username, Password, PublishUrl, $"{UserAgentName}/8.0", webjobName: null, webjobType: null, httpClientMock.Object, deploymentStatusServiceMock.Object, CancellationToken.None); + + // Assert: deployment operation runs to completion, without polling the deployment because 'Location' header was not found in response + Assert.True(result); + + httpClientMock.VerifyAll(); + deploymentStatusServiceMock.VerifyAll(); + taskLoggerMock.VerifyAll(); + } + + [Theory] + [InlineData(HttpStatusCode.Forbidden)] + [InlineData(HttpStatusCode.NotFound)] + [InlineData(HttpStatusCode.RequestTimeout)] + [InlineData(HttpStatusCode.InternalServerError)] + public async Task OneDeploy_Execute_HttpResponse_Fail(HttpStatusCode statusCode) + { + // Arrange + var httpClientMock = GetHttpClientMock(statusCode, deployLocationHeader: null); + + var deploymentStatusServiceMock = new Mock>(); + + // set messages to log + var taskLoggerMock = new Mock(); + taskLoggerMock.Setup(l => l.LogMessage(Build.Framework.MessageImportance.High, string.Format(Resources.ONEDEPLOY_PublishingOneDeploy, FileToPublish, OneDeployUri.AbsoluteUri.ToString()))); + taskLoggerMock.Setup(l => l.LogError(string.Format(Resources.ONEDEPLOY_FailedDeployRequest, OneDeployUri.AbsoluteUri.ToString(), statusCode))); + + var oneDeployTask = new OneDeploy(taskLoggerMock.Object); + + // Act + var result = await oneDeployTask.OneDeployAsync( + FileToPublish, Username, Password, PublishUrl, $"{UserAgentName}/8.0", webjobName: null, webjobType: null, httpClientMock.Object, deploymentStatusServiceMock.Object, CancellationToken.None); + + // Assert: deployment operation fails because HTTP POST request to upload the package returns a failed HTTP Response + Assert.False(result); + + httpClientMock.VerifyAll(); + deploymentStatusServiceMock.VerifyAll(); + taskLoggerMock.VerifyAll(); + } + + [Theory] + [InlineData("not-valid-url")] + [InlineData("")] + [InlineData(null)] + public async Task OneDeploy_Execute_PublishUrl_Invalid(string invalidUrl) + { + // Arrange + var httpClientMock = new Mock(); + + var deploymentStatusServiceMock = new Mock>(); + + // set messages to log + var taskLoggerMock = new Mock(); + taskLoggerMock.Setup(l => l.LogError(string.Format(Resources.ONEDEPLOY_InvalidPublishUrl, invalidUrl))); + + var oneDeployTask = new OneDeploy(taskLoggerMock.Object); + + // Act + var result = await oneDeployTask.OneDeployAsync( + FileToPublish, Username, Password, invalidUrl, $"{UserAgentName}/8.0", webjobName: null, webjobType: null, httpClientMock.Object, deploymentStatusServiceMock.Object, CancellationToken.None); + + // Assert: deployment operation fails because 'PublishUrl' is not valid + Assert.False(result); + + httpClientMock.VerifyAll(); + deploymentStatusServiceMock.VerifyAll(); + taskLoggerMock.VerifyAll(); + } + + [Theory] + [InlineData("z:\\Missing\\Directory\\File")] + [InlineData("")] + [InlineData(null)] + public async Task OneDeploy_Execute_FileToPublish_Missing(string invalidFileToPublish) + { + // Arrange + var httpClientMock = new Mock(); + + var deploymentStatusServiceMock = new Mock>(); + + // set messages to log + var taskLoggerMock = new Mock(); + taskLoggerMock.Setup(l => l.LogError(Resources.ONEDEPLOY_FileToPublish_NotFound)); + + var oneDeployTask = new OneDeploy(taskLoggerMock.Object); + + // Act + var result = await oneDeployTask.OneDeployAsync( + invalidFileToPublish, Username, Password, PublishUrl, $"{UserAgentName}/8.0", webjobName: null, webjobType: null, httpClientMock.Object, deploymentStatusServiceMock.Object, CancellationToken.None); + + // Assert: deployment operation fails because 'FileToPublishPath' is not valid + Assert.False(result); + + httpClientMock.VerifyAll(); + deploymentStatusServiceMock.VerifyAll(); + taskLoggerMock.VerifyAll(); + } + + [Theory] + [InlineData(Username, "")] + [InlineData(Username, null)] + [InlineData("", "")] + [InlineData(null, null)] + public async Task OneDeploy_Execute_Credentials_Missing_Args(string invalidUsername, string invalidPassword) + { + // Arrange + var httpClientMock = new Mock(); + + var deploymentStatusServiceMock = new Mock>(); + + // set messages to log + var taskLoggerMock = new Mock(); + taskLoggerMock.Setup(l => l.LogError(Resources.ONEDEPLOY_FailedToRetrieveCredentials)); + + var oneDeployTask = new OneDeploy(taskLoggerMock.Object); + + // Act + var result = await oneDeployTask.OneDeployAsync( + FileToPublish, invalidUsername, invalidPassword, PublishUrl, $"{UserAgentName}/8.0", webjobName: null, webjobType: null, httpClientMock.Object, deploymentStatusServiceMock.Object, CancellationToken.None); + + // Assert: deployment operation fails because 'Username' and/or 'Password' is + // not valid nor the could be retrieved from Task HostObject + Assert.False(result); + + httpClientMock.VerifyAll(); + deploymentStatusServiceMock.VerifyAll(); + taskLoggerMock.VerifyAll(); + } + + [Fact] + public async Task OneDeploy_Execute_Credentials_From_TaskHostObject() + { + // Arrange + var httpClientMock = GetHttpClientMock(HttpStatusCode.OK); + + var deploymentStatusServiceMock = GetDeploymentStatusServiceMock(httpClientMock.Object, DeploymentStatus.PartialSuccess); + + // set messages to log according to result + var taskLoggerMock = new Mock(); + taskLoggerMock.Setup(l => l.LogMessage(Build.Framework.MessageImportance.High, string.Format(Resources.ONEDEPLOY_PublishingOneDeploy, FileToPublish, OneDeployUri.AbsoluteUri.ToString()))); + taskLoggerMock.Setup(l => l.LogMessage(string.Format(Resources.ONEDEPLOY_Uploaded, FileToPublish))); + taskLoggerMock.Setup(l => l.LogMessage(Build.Framework.MessageImportance.High, Resources.ONEDEPLOY_Success)); + + var oneDeployTask = new OneDeploy(taskLoggerMock.Object); + + var msbuildHostObject = new VSMsDeployTaskHostObject(); + msbuildHostObject.AddCredentialTaskItemIfExists(Username, Password); + oneDeployTask.HostObject = msbuildHostObject; + + // Act + var result = await oneDeployTask.OneDeployAsync( + FileToPublish, Username, Password, PublishUrl, $"{UserAgentName}/8.0", webjobName: null, webjobType: null, httpClientMock.Object, deploymentStatusServiceMock.Object, CancellationToken.None); + + // Assert: deployment operation runs to completion + // obtaining the credentials from the Task HostObject + Assert.True(result); + + httpClientMock.VerifyAll(); + deploymentStatusServiceMock.VerifyAll(); + taskLoggerMock.VerifyAll(); + } + + private Mock GetHttpClientMock( + HttpStatusCode statusCode, + string deployLocationHeader = DeploymentUrl) + { + var httpClientMock = new Mock(); + + // Request + HttpRequestMessage requestMessage = new(); + httpClientMock + .Setup(hc => hc.DefaultRequestHeaders) + .Returns(requestMessage.Headers); + + // Response + HttpResponseMessage responseMessage = new(statusCode); + if (!string.IsNullOrEmpty(deployLocationHeader)) + { + responseMessage.Headers.Add("Location", deployLocationHeader); + } + + // PostAsync() + httpClientMock + .Setup(hc => hc.PostAsync(OneDeployUri, It.IsAny())) + .ReturnsAsync(responseMessage); + + return httpClientMock; + } + + [Fact] + public void Constructor_OK() + { + var oneDeploy = new OneDeploy(); + + // no-arg ctor (as invoked by MSBuild) instantiate instance + Assert.NotNull(oneDeploy); + } + + private Mock> GetDeploymentStatusServiceMock( + IHttpClient httpClient, + DeploymentStatus status = DeploymentStatus.Success, + string statusText = null) + { + var deploymentResponse = new DeploymentResponse() + { + Status = status, + StatusText = statusText, + LogUrl = DeploymentLogUrl + }; + + var statusServiceMock = new Mock>(); + + statusServiceMock + .Setup(s => s.PollDeploymentAsync(httpClient, DeploymentUrl, Username, Password, $"{UserAgentName}/8.0", It.IsAny())) + .ReturnsAsync(deploymentResponse); + + return statusServiceMock; + } +} From 02e2ea95bc64a078f539aef74b1ed5058a27faf4 Mon Sep 17 00:00:00 2001 From: Antonio Villanueva Date: Thu, 26 Sep 2024 18:17:25 -0700 Subject: [PATCH 2/3] Fix 'IHttpResponse.GetTextResponseAsyc' extension method, 'HttpResponseMessageWrapper' against NRE and update UTs --- .../Publish/Tasks/Tasks/Http/HttpClientExtensions.cs | 2 +- .../Tasks/Tasks/Http/HttpResponseMessageWrapper.cs | 12 +++++++++--- .../Tasks/OneDeploy/OneDeployTests.WebJob.cs | 10 +++++++--- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/WebSdk/Publish/Tasks/Tasks/Http/HttpClientExtensions.cs b/src/WebSdk/Publish/Tasks/Tasks/Http/HttpClientExtensions.cs index 818c8274daae..5565638afd51 100644 --- a/src/WebSdk/Publish/Tasks/Tasks/Http/HttpClientExtensions.cs +++ b/src/WebSdk/Publish/Tasks/Tasks/Http/HttpClientExtensions.cs @@ -238,7 +238,7 @@ public static async Task GetTextResponseAsync(this IHttpResponse respons { var responseText = string.Empty; - if (response is not null || cancellationToken.IsCancellationRequested) + if (response is null || cancellationToken.IsCancellationRequested) { return responseText; } diff --git a/src/WebSdk/Publish/Tasks/Tasks/Http/HttpResponseMessageWrapper.cs b/src/WebSdk/Publish/Tasks/Tasks/Http/HttpResponseMessageWrapper.cs index 547c071da6cf..61cc82f02fad 100644 --- a/src/WebSdk/Publish/Tasks/Tasks/Http/HttpResponseMessageWrapper.cs +++ b/src/WebSdk/Publish/Tasks/Tasks/Http/HttpResponseMessageWrapper.cs @@ -25,13 +25,17 @@ public HttpResponseMessageWrapper(HttpResponseMessage message) /// public async Task GetResponseBodyAsync() { - return await _responseBodyTask.Value; + return _responseBodyTask.Value is not null + ? await _responseBodyTask.Value + : null; } /// public IEnumerable GetHeader(string name) { - if (_message.Headers.TryGetValues(name, out IEnumerable values)) + if (_message is not null + && _message.Headers is not null + && _message.Headers.TryGetValues(name, out IEnumerable values)) { return values; } @@ -41,6 +45,8 @@ public IEnumerable GetHeader(string name) private Task GetResponseStream() { - return _message.Content.ReadAsStreamAsync(); + return _message is not null && _message.Content is not null + ? _message.Content.ReadAsStreamAsync() + : null; } } diff --git a/test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/OneDeployTests.WebJob.cs b/test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/OneDeployTests.WebJob.cs index 059805fd69c2..dd0ea370efb3 100644 --- a/test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/OneDeployTests.WebJob.cs +++ b/test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/OneDeployTests.WebJob.cs @@ -53,7 +53,7 @@ public async Task OneDeploy_WebJob_Execute_Completes( } else { - var failedDeployMsg = string.Format(Resources.ONEDEPLOY_FailedDeployRequest, publishUri.AbsoluteUri, statusCode, PutErrorResponseMessage); + var failedDeployMsg = string.Format(Resources.ONEDEPLOY_FailedDeployRequest_With_ResponseText, publishUri.AbsoluteUri, statusCode, PutErrorResponseMessage); taskLoggerMock.Setup(l => l.LogError(failedDeployMsg)); } @@ -125,14 +125,18 @@ public async Task OneDeploy_WebJob_Missing_NameOrType(string webjobName, string // PostAsync() httpClientMock .Setup(hc => hc.PostAsync(OneDeployUri, It.IsAny())) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.BadRequest)); + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.BadGateway) + { + Content = new StringContent(PutErrorResponseMessage) + } + ); var deploymentStatusServiceMock = new Mock>(); // set messages to log var taskLoggerMock = new Mock(); taskLoggerMock.Setup(l => l.LogMessage(Build.Framework.MessageImportance.High, string.Format(Resources.ONEDEPLOY_PublishingOneDeploy, FileToPublish, OneDeployUri.AbsoluteUri.ToString()))); - taskLoggerMock.Setup(l => l.LogError(string.Format(Resources.ONEDEPLOY_FailedDeployRequest, OneDeployUri.AbsoluteUri.ToString(), HttpStatusCode.BadRequest))); + taskLoggerMock.Setup(l => l.LogError(string.Format(Resources.ONEDEPLOY_FailedDeployRequest_With_ResponseText, OneDeployUri.AbsoluteUri, HttpStatusCode.BadGateway, PutErrorResponseMessage))); var oneDeployTask = new OneDeploy(taskLoggerMock.Object); From c6b02fe41679a5b3bc2c54947ed7e57fd8c54f32 Mon Sep 17 00:00:00 2001 From: Antonio Villanueva Date: Wed, 9 Oct 2024 13:16:19 -0700 Subject: [PATCH 3/3] Apply changes of PR-43943 [main] --- .../OneDeploy/OneDeployStatusServiceTests.cs | 14 +++++++------- .../Tasks/OneDeploy/OneDeployTests.WebJob.cs | 12 ++++++------ .../Tasks/OneDeploy/OneDeployTests.cs | 18 +++++++++--------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/OneDeployStatusServiceTests.cs b/test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/OneDeployStatusServiceTests.cs index 52c5c3de3129..f5338561961e 100644 --- a/test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/OneDeployStatusServiceTests.cs +++ b/test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/OneDeployStatusServiceTests.cs @@ -15,7 +15,7 @@ namespace Microsoft.NET.Sdk.Publish.Tasks.OneDeploy.Tests; public class OneDeployStatusServiceTests { private const string Username = "someUser"; - private const string Password = "123secret"; + private const string NotShareableValue = "PLACEHOLDER"; private const string UserAgent = "websdk/8.0"; // as OneDeploy.UserAgentName private const string PublishUrl = "https://mysite.scm.azurewebsites.net"; private const string DeploymentId = "056f49ce-fcd7-497c-929b-d74bc6f8905e"; @@ -57,7 +57,7 @@ public async Task PollDeployment_Completes(HttpStatusCode statusCode, Deployment var oneDeployStatusService = new OneDeployStatusService(taskLoggerMock.Object); // Act - var result = await oneDeployStatusService.PollDeploymentAsync(httpClientMock.Object, DeploymentUri.AbsoluteUri, Username, Password, UserAgent, cancellationToken); + var result = await oneDeployStatusService.PollDeploymentAsync(httpClientMock.Object, DeploymentUri.AbsoluteUri, Username, NotShareableValue, UserAgent, cancellationToken); // Assert: poll deployment status runs to completion, resulting in the given expectedDeploymentStatus Assert.Equal(expectedDeploymentStatus, deploymentResponse.Status); @@ -85,7 +85,7 @@ public async Task PollDeployment_HttpResponse_Fail(HttpStatusCode statusCode) var oneDeployStatusService = new OneDeployStatusService(taskLoggerMock.Object); // Act - var result = await oneDeployStatusService.PollDeploymentAsync(httpClientMock.Object, DeploymentUri.AbsoluteUri, Username, Password, UserAgent, cancellationToken); + var result = await oneDeployStatusService.PollDeploymentAsync(httpClientMock.Object, DeploymentUri.AbsoluteUri, Username, NotShareableValue, UserAgent, cancellationToken); // Assert: poll deployment status runs to completion, resulting in 'Unknown' status because failed HTTP Response Status code Assert.Equal(DeploymentStatus.Unknown, result.Status); @@ -112,7 +112,7 @@ public async Task PollDeployment_Completes_No_Logger() var oneDeployStatusService = new OneDeployStatusService(); // Act - var result = await oneDeployStatusService.PollDeploymentAsync(httpClientMock.Object, DeploymentUri.AbsoluteUri, Username, Password, UserAgent, cancellationToken); + var result = await oneDeployStatusService.PollDeploymentAsync(httpClientMock.Object, DeploymentUri.AbsoluteUri, Username, NotShareableValue, UserAgent, cancellationToken); // Assert: poll deployment status runs to completion with NULL ITaskLogger Assert.Equal(DeploymentStatus.Success, deploymentResponse.Status); @@ -136,7 +136,7 @@ public async Task PollDeployment_Halted() // Act cancellationTokenSource.Cancel(); - var result = await oneDeployStatusService.PollDeploymentAsync(httpClientMock.Object, DeploymentUri.AbsoluteUri, Username, Password, UserAgent, cancellationToken); + var result = await oneDeployStatusService.PollDeploymentAsync(httpClientMock.Object, DeploymentUri.AbsoluteUri, Username, NotShareableValue, UserAgent, cancellationToken); // Assert: deployment status won't poll for deployment as 'CancellationToken' is already cancelled Assert.Equal(DeploymentStatus.Unknown, result.Status); @@ -164,7 +164,7 @@ public async Task PollDeployment_InvalidURL(string invalidUrl) var oneDeployStatusService = new OneDeployStatusService(taskLoggerMock.Object); // Act - var result = await oneDeployStatusService.PollDeploymentAsync(httpClientMock.Object, invalidUrl, Username, Password, UserAgent, cancellationToken); + var result = await oneDeployStatusService.PollDeploymentAsync(httpClientMock.Object, invalidUrl, Username, NotShareableValue, UserAgent, cancellationToken); // Assert: deployment status won't poll for deployment because given polling URL is invalid Assert.Equal(DeploymentStatus.Unknown, result.Status); @@ -186,7 +186,7 @@ public async Task PollDeployment_Missing_HttpClient() var oneDeployStatusService = new OneDeployStatusService(taskLoggerMock.Object); // Act - var result = await oneDeployStatusService.PollDeploymentAsync(null, DeploymentUri.AbsoluteUri, Username, Password, UserAgent, cancellationToken); + var result = await oneDeployStatusService.PollDeploymentAsync(null, DeploymentUri.AbsoluteUri, Username, NotShareableValue, UserAgent, cancellationToken); // Assert: deployment status won't poll for deployment because IHttpClient is NULL Assert.Equal(DeploymentStatus.Unknown, result.Status); diff --git a/test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/OneDeployTests.WebJob.cs b/test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/OneDeployTests.WebJob.cs index dd0ea370efb3..c266f6f9e90f 100644 --- a/test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/OneDeployTests.WebJob.cs +++ b/test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/OneDeployTests.WebJob.cs @@ -61,7 +61,7 @@ public async Task OneDeploy_WebJob_Execute_Completes( // Act var result = await oneDeployTask.OneDeployAsync( - FileToPublish, Username, Password, PublishUrl, $"{UserAgentName}/8.0", WebJobName, webJobType, httpClientMock.Object, deploymentStatusServiceMock.Object, CancellationToken.None); + FileToPublish, Username, NotShareableValue, PublishUrl, $"{UserAgentName}/8.0", WebJobName, webJobType, httpClientMock.Object, deploymentStatusServiceMock.Object, CancellationToken.None); // Assert: WebJob deployment operation runs to completion with expected result Assert.Equal(expectedResult, result); @@ -93,7 +93,7 @@ public async Task OneDeploy_WebJob_PublishUrl_Invalid(string invalidUrl, string // Act var result = await oneDeployTask.OneDeployAsync( - FileToPublish, Username, Password, invalidUrl, $"{UserAgentName}/8.0", WebJobName, webjobType, httpClientMock.Object, deploymentStatusServiceMock.Object, CancellationToken.None); + FileToPublish, Username, NotShareableValue, invalidUrl, $"{UserAgentName}/8.0", WebJobName, webjobType, httpClientMock.Object, deploymentStatusServiceMock.Object, CancellationToken.None); // Assert: deployment operation fails because 'PublishUrl' is not valid Assert.False(result); @@ -126,9 +126,9 @@ public async Task OneDeploy_WebJob_Missing_NameOrType(string webjobName, string httpClientMock .Setup(hc => hc.PostAsync(OneDeployUri, It.IsAny())) .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.BadGateway) - { - Content = new StringContent(PutErrorResponseMessage) - } + { + Content = new StringContent(PutErrorResponseMessage) + } ); var deploymentStatusServiceMock = new Mock>(); @@ -142,7 +142,7 @@ public async Task OneDeploy_WebJob_Missing_NameOrType(string webjobName, string // Act var result = await oneDeployTask.OneDeployAsync( - FileToPublish, Username, Password, PublishUrl, $"{UserAgentName}/8.0", webjobName, webjobType, httpClientMock.Object, deploymentStatusServiceMock.Object, CancellationToken.None); + FileToPublish, Username, NotShareableValue, PublishUrl, $"{UserAgentName}/8.0", webjobName, webjobType, httpClientMock.Object, deploymentStatusServiceMock.Object, CancellationToken.None); // Assert: deployment operation fails because since 'WebJobName' and/or 'WebJobType' is invalid, so we calculate the // default OneDeploy URI ('/api/publish'), which target instance does not recognized as valid diff --git a/test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/OneDeployTests.cs b/test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/OneDeployTests.cs index 936e02882984..050230cb4be1 100644 --- a/test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/OneDeployTests.cs +++ b/test/Microsoft.NET.Sdk.Publish.Tasks.Tests/Tasks/OneDeploy/OneDeployTests.cs @@ -16,7 +16,7 @@ namespace Microsoft.NET.Sdk.Publish.Tasks.OneDeploy.Tests; public partial class OneDeployTests { private const string Username = "someUser"; - private const string Password = "123secret"; + private const string NotShareableValue = "PLACEHOLDER"; private const string PublishUrl = "https://mysite.scm.azurewebsites.net"; private const string UserAgentName = "websdk"; // as OneDeploy.UserAgentName private const string DeploymentUrl = $@"{PublishUrl}/api/deployments/056f49ce-fcd7-497c-929b-d74bc6f8905e"; @@ -87,7 +87,7 @@ public async Task OneDeploy_Execute_Completes(DeploymentStatus deployStatus, Htt // Act var result = await oneDeployTask.OneDeployAsync( - FileToPublish, Username, Password, PublishUrl, $"{UserAgentName}/8.0", webjobName: null, webjobType: null, httpClientMock.Object, deploymentStatusServiceMock.Object, CancellationToken.None); + FileToPublish, Username, NotShareableValue, PublishUrl, $"{UserAgentName}/8.0", webjobName: null, webjobType: null, httpClientMock.Object, deploymentStatusServiceMock.Object, CancellationToken.None); // Assert: deployment operation runs to completion with expected result Assert.Equal(expectedResult, result); @@ -130,7 +130,7 @@ public async Task OneDeploy_Execute_Deploy_Location_Missing(string invalidLocati // Act var result = await oneDeployTask.OneDeployAsync( - FileToPublish, Username, Password, PublishUrl, $"{UserAgentName}/8.0", webjobName: null, webjobType: null, httpClientMock.Object, deploymentStatusServiceMock.Object, CancellationToken.None); + FileToPublish, Username, NotShareableValue, PublishUrl, $"{UserAgentName}/8.0", webjobName: null, webjobType: null, httpClientMock.Object, deploymentStatusServiceMock.Object, CancellationToken.None); // Assert: deployment operation runs to completion, without polling the deployment because 'Location' header was not found in response Assert.True(result); @@ -161,7 +161,7 @@ public async Task OneDeploy_Execute_HttpResponse_Fail(HttpStatusCode statusCode) // Act var result = await oneDeployTask.OneDeployAsync( - FileToPublish, Username, Password, PublishUrl, $"{UserAgentName}/8.0", webjobName: null, webjobType: null, httpClientMock.Object, deploymentStatusServiceMock.Object, CancellationToken.None); + FileToPublish, Username, NotShareableValue, PublishUrl, $"{UserAgentName}/8.0", webjobName: null, webjobType: null, httpClientMock.Object, deploymentStatusServiceMock.Object, CancellationToken.None); // Assert: deployment operation fails because HTTP POST request to upload the package returns a failed HTTP Response Assert.False(result); @@ -190,7 +190,7 @@ public async Task OneDeploy_Execute_PublishUrl_Invalid(string invalidUrl) // Act var result = await oneDeployTask.OneDeployAsync( - FileToPublish, Username, Password, invalidUrl, $"{UserAgentName}/8.0", webjobName: null, webjobType: null, httpClientMock.Object, deploymentStatusServiceMock.Object, CancellationToken.None); + FileToPublish, Username, NotShareableValue, invalidUrl, $"{UserAgentName}/8.0", webjobName: null, webjobType: null, httpClientMock.Object, deploymentStatusServiceMock.Object, CancellationToken.None); // Assert: deployment operation fails because 'PublishUrl' is not valid Assert.False(result); @@ -219,7 +219,7 @@ public async Task OneDeploy_Execute_FileToPublish_Missing(string invalidFileToPu // Act var result = await oneDeployTask.OneDeployAsync( - invalidFileToPublish, Username, Password, PublishUrl, $"{UserAgentName}/8.0", webjobName: null, webjobType: null, httpClientMock.Object, deploymentStatusServiceMock.Object, CancellationToken.None); + invalidFileToPublish, Username, NotShareableValue, PublishUrl, $"{UserAgentName}/8.0", webjobName: null, webjobType: null, httpClientMock.Object, deploymentStatusServiceMock.Object, CancellationToken.None); // Assert: deployment operation fails because 'FileToPublishPath' is not valid Assert.False(result); @@ -277,12 +277,12 @@ public async Task OneDeploy_Execute_Credentials_From_TaskHostObject() var oneDeployTask = new OneDeploy(taskLoggerMock.Object); var msbuildHostObject = new VSMsDeployTaskHostObject(); - msbuildHostObject.AddCredentialTaskItemIfExists(Username, Password); + msbuildHostObject.AddCredentialTaskItemIfExists(Username, NotShareableValue); oneDeployTask.HostObject = msbuildHostObject; // Act var result = await oneDeployTask.OneDeployAsync( - FileToPublish, Username, Password, PublishUrl, $"{UserAgentName}/8.0", webjobName: null, webjobType: null, httpClientMock.Object, deploymentStatusServiceMock.Object, CancellationToken.None); + FileToPublish, Username, NotShareableValue, PublishUrl, $"{UserAgentName}/8.0", webjobName: null, webjobType: null, httpClientMock.Object, deploymentStatusServiceMock.Object, CancellationToken.None); // Assert: deployment operation runs to completion // obtaining the credentials from the Task HostObject @@ -344,7 +344,7 @@ private Mock> GetDeploymentStatusSe var statusServiceMock = new Mock>(); statusServiceMock - .Setup(s => s.PollDeploymentAsync(httpClient, DeploymentUrl, Username, Password, $"{UserAgentName}/8.0", It.IsAny())) + .Setup(s => s.PollDeploymentAsync(httpClient, DeploymentUrl, Username, NotShareableValue, $"{UserAgentName}/8.0", It.IsAny())) .ReturnsAsync(deploymentResponse); return statusServiceMock;