From 9778c29007c6e053dd5e555698ab50cabcdf55d0 Mon Sep 17 00:00:00 2001 From: Pavel Krymets Date: Wed, 23 Aug 2017 14:55:27 -0700 Subject: [PATCH] Allow building an pushing site extension for azure functional testing (#87) --- .gitignore | 2 + AzureIntegration.sln | 19 +- build/dependencies.props | 2 + build/dotnet-install.cmd | 2 + build/dotnet-install.ps1 | 503 ++++++++++++++++++ build/repo.props | 17 + build/repo.targets | 39 ++ ...NetCore.AzureAppServices.TestBundle.csproj | 35 ++ .../applicationHost.xdt | 14 + .../AzureCollection.cs | 15 + .../AzureFixture.cs | 154 ++++++ .../CommandResult.cs | 32 ++ .../LoggingInterceptor.cs | 47 ++ ...re.AzureAppServices.FunctionalTests.csproj | 27 + .../TemplateFunctionalTests.cs | 81 +++ .../AppServicesWithSiteExtensions.json | 56 ++ .../Templates/BasicAppServices.json | 32 ++ .../TestCommand.cs | 249 +++++++++ .../TestLogger.cs | 41 ++ .../WebAppExtensions.cs | 86 +++ 20 files changed, 1452 insertions(+), 1 deletion(-) create mode 100644 build/dotnet-install.cmd create mode 100644 build/dotnet-install.ps1 create mode 100644 build/repo.props create mode 100644 build/repo.targets create mode 100644 src/Microsoft.AspNetCore.AzureAppServices.TestBundle/Microsoft.AspNetCore.AzureAppServices.TestBundle.csproj create mode 100644 src/Microsoft.AspNetCore.AzureAppServices.TestBundle/applicationHost.xdt create mode 100644 test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/AzureCollection.cs create mode 100644 test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/AzureFixture.cs create mode 100644 test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/CommandResult.cs create mode 100644 test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/LoggingInterceptor.cs create mode 100644 test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/Microsoft.AspNetCore.AzureAppServices.FunctionalTests.csproj create mode 100644 test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/TemplateFunctionalTests.cs create mode 100644 test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/Templates/AppServicesWithSiteExtensions.json create mode 100644 test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/Templates/BasicAppServices.json create mode 100644 test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/TestCommand.cs create mode 100644 test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/TestLogger.cs create mode 100644 test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/WebAppExtensions.cs diff --git a/.gitignore b/.gitignore index 946d87400a..ada37ce594 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,5 @@ project.lock.json .testPublish/ global.json korebuild-lock.txt +msbuild.binlog +.test-dotnet diff --git a/AzureIntegration.sln b/AzureIntegration.sln index 0e01abad26..cdfba4fe53 100644 --- a/AzureIntegration.sln +++ b/AzureIntegration.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26412.1 +VisualStudioVersion = 15.0.26814.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.AzureAppServicesIntegration", "src\Microsoft.AspNetCore.AzureAppServicesIntegration\Microsoft.AspNetCore.AzureAppServicesIntegration.csproj", "{5916BEB5-0969-469B-976C-A392E015DFAC}" EndProject @@ -35,6 +35,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.AzureA EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApplicationInsights.HostingStartup.Tests", "test\ApplicationInsights.HostingStartup.Tests\ApplicationInsights.HostingStartup.Tests.csproj", "{0899A101-E451-40A4-81B0-7AA18202C25D}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.AzureAppServices.FunctionalTests", "test\Microsoft.AspNetCore.AzureAppServices.FunctionalTests\Microsoft.AspNetCore.AzureAppServices.FunctionalTests.csproj", "{2B2C37FF-9249-4EA4-9A7F-038B55A15C2C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.AzureAppServices.TestBundle", "src\Microsoft.AspNetCore.AzureAppServices.TestBundle\Microsoft.AspNetCore.AzureAppServices.TestBundle.csproj", "{1EC31DA1-131D-4257-B001-BE8391E6077E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -81,6 +85,14 @@ Global {0899A101-E451-40A4-81B0-7AA18202C25D}.Debug|Any CPU.Build.0 = Debug|Any CPU {0899A101-E451-40A4-81B0-7AA18202C25D}.Release|Any CPU.ActiveCfg = Release|Any CPU {0899A101-E451-40A4-81B0-7AA18202C25D}.Release|Any CPU.Build.0 = Release|Any CPU + {2B2C37FF-9249-4EA4-9A7F-038B55A15C2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B2C37FF-9249-4EA4-9A7F-038B55A15C2C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B2C37FF-9249-4EA4-9A7F-038B55A15C2C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B2C37FF-9249-4EA4-9A7F-038B55A15C2C}.Release|Any CPU.Build.0 = Release|Any CPU + {1EC31DA1-131D-4257-B001-BE8391E6077E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1EC31DA1-131D-4257-B001-BE8391E6077E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1EC31DA1-131D-4257-B001-BE8391E6077E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1EC31DA1-131D-4257-B001-BE8391E6077E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -96,5 +108,10 @@ Global {9B22E525-FEC9-4C7C-9F9C-598C15BD0250} = {FF9B744E-6C59-40CC-9E41-9D2EBD292435} {1CE2D76B-39E6-46C0-8F6F-C63E370955A9} = {FF9B744E-6C59-40CC-9E41-9D2EBD292435} {0899A101-E451-40A4-81B0-7AA18202C25D} = {CD650B4B-81C2-4A44-AEF2-A251A877C1F0} + {2B2C37FF-9249-4EA4-9A7F-038B55A15C2C} = {CD650B4B-81C2-4A44-AEF2-A251A877C1F0} + {1EC31DA1-131D-4257-B001-BE8391E6077E} = {FF9B744E-6C59-40CC-9E41-9D2EBD292435} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5743DFE7-1AA5-439D-84AE-A480EA389927} EndGlobalSection EndGlobal diff --git a/build/dependencies.props b/build/dependencies.props index aff9dda687..9484451623 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -5,11 +5,13 @@ 2.1.1 2.1.1-* 4.7.49 + 1.1.3 2.0.0-* 2.0.0-* 2.0.0-* 15.3.0 1.4.0 2.3.0-beta4-build3742 + 8.3.0 diff --git a/build/dotnet-install.cmd b/build/dotnet-install.cmd new file mode 100644 index 0000000000..1dbf49f0a1 --- /dev/null +++ b/build/dotnet-install.cmd @@ -0,0 +1,2 @@ +@ECHO OFF +PowerShell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command "& '%~dp0dotnet-install.ps1' %*; exit $LASTEXITCODE" diff --git a/build/dotnet-install.ps1 b/build/dotnet-install.ps1 new file mode 100644 index 0000000000..93d964540f --- /dev/null +++ b/build/dotnet-install.ps1 @@ -0,0 +1,503 @@ +# +# Copyright (c) .NET Foundation and contributors. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. +# + +<# +.SYNOPSIS + Installs dotnet cli +.DESCRIPTION + Installs dotnet cli. If dotnet installation already exists in the given directory + it will update it only if the requested version differs from the one already installed. +.PARAMETER Channel + Default: LTS + Download from the Channel specified. Possible values: + - Current - most current release + - LTS - most current supported release + - 2-part version in a format A.B - represents a specific release + examples: 2.0; 1.0 + - Branch name + examples: release/2.0.0; Master +.PARAMETER Version + Default: latest + Represents a build version on specific channel. Possible values: + - latest - most latest build on specific channel + - coherent - most latest coherent build on specific channel + coherent applies only to SDK downloads + - 3-part version in a format A.B.C - represents specific version of build + examples: 2.0.0-preview2-006120; 1.1.0 +.PARAMETER InstallDir + Default: %LocalAppData%\Microsoft\dotnet + Path to where to install dotnet. Note that binaries will be placed directly in a given directory. +.PARAMETER Architecture + Default: - this value represents currently running OS architecture + Architecture of dotnet binaries to be installed. + Possible values are: , x64 and x86 +.PARAMETER SharedRuntime + Default: false + Installs just the shared runtime bits, not the entire SDK +.PARAMETER DryRun + If set it will not perform installation but instead display what command line to use to consistently install + currently requested version of dotnet cli. In example if you specify version 'latest' it will display a link + with specific version so that this command can be used deterministicly in a build script. + It also displays binaries location if you prefer to install or download it yourself. +.PARAMETER NoPath + By default this script will set environment variable PATH for the current process to the binaries folder inside installation folder. + If set it will display binaries location but not set any environment variable. +.PARAMETER Verbose + Displays diagnostics information. +.PARAMETER AzureFeed + Default: https://dotnetcli.azureedge.net/dotnet + This parameter typically is not changed by the user. + It allows to change URL for the Azure feed used by this installer. +.PARAMETER UncachedFeed + This parameter typically is not changed by the user. + It allows to change URL for the Uncached feed used by this installer. +.PARAMETER ProxyAddress + If set, the installer will use the proxy when making web requests +.PARAMETER ProxyUseDefaultCredentials + Default: false + Use default credentials, when using proxy address. +#> +[cmdletbinding()] +param( + [string]$Channel="LTS", + [string]$Version="Latest", + [string]$InstallDir="", + [string]$Architecture="", + [switch]$SharedRuntime, + [switch]$DryRun, + [switch]$NoPath, + [string]$AzureFeed="https://dotnetcli.azureedge.net/dotnet", + [string]$UncachedFeed="https://dotnetcli.blob.core.windows.net/dotnet", + [string]$ProxyAddress, + [switch]$ProxyUseDefaultCredentials +) + +Set-StrictMode -Version Latest +$ErrorActionPreference="Stop" +$ProgressPreference="SilentlyContinue" + +$BinFolderRelativePath="" + +# example path with regex: shared/1.0.0-beta-12345/somepath +$VersionRegEx="/\d+\.\d+[^/]+/" +$OverrideNonVersionedFiles=$true + +function Say($str) { + Write-Output "dotnet-install: $str" +} + +function Say-Verbose($str) { + Write-Verbose "dotnet-install: $str" +} + +function Say-Invocation($Invocation) { + $command = $Invocation.MyCommand; + $args = (($Invocation.BoundParameters.Keys | foreach { "-$_ `"$($Invocation.BoundParameters[$_])`"" }) -join " ") + Say-Verbose "$command $args" +} + +function Invoke-With-Retry([ScriptBlock]$ScriptBlock, [int]$MaxAttempts = 3, [int]$SecondsBetweenAttempts = 1) { + $Attempts = 0 + + while ($true) { + try { + return $ScriptBlock.Invoke() + } + catch { + $Attempts++ + if ($Attempts -lt $MaxAttempts) { + Start-Sleep $SecondsBetweenAttempts + } + else { + throw + } + } + } +} + +function Get-Machine-Architecture() { + Say-Invocation $MyInvocation + + # possible values: AMD64, IA64, x86 + return $ENV:PROCESSOR_ARCHITECTURE +} + +# TODO: Architecture and CLIArchitecture should be unified +function Get-CLIArchitecture-From-Architecture([string]$Architecture) { + Say-Invocation $MyInvocation + + switch ($Architecture.ToLower()) { + { $_ -eq "" } { return Get-CLIArchitecture-From-Architecture $(Get-Machine-Architecture) } + { ($_ -eq "amd64") -or ($_ -eq "x64") } { return "x64" } + { $_ -eq "x86" } { return "x86" } + default { throw "Architecture not supported. If you think this is a bug, please report it at https://github.com/dotnet/cli/issues" } + } +} + +function Get-Version-Info-From-Version-Text([string]$VersionText) { + Say-Invocation $MyInvocation + + $Data = @($VersionText.Split([char[]]@(), [StringSplitOptions]::RemoveEmptyEntries)); + + $VersionInfo = @{} + $VersionInfo.CommitHash = $Data[0].Trim() + $VersionInfo.Version = $Data[1].Trim() + return $VersionInfo +} + +function Load-Assembly([string] $Assembly) { + try { + Add-Type -Assembly $Assembly | Out-Null + } + catch { + # On Nano Server, Powershell Core Edition is used. Add-Type is unable to resolve base class assemblies because they are not GAC'd. + # Loading the base class assemblies is not unnecessary as the types will automatically get resolved. + } +} + +function GetHTTPResponse([Uri] $Uri) +{ + Invoke-With-Retry( + { + + $HttpClient = $null + + try { + # HttpClient is used vs Invoke-WebRequest in order to support Nano Server which doesn't support the Invoke-WebRequest cmdlet. + Load-Assembly -Assembly System.Net.Http + + if(-not $ProxyAddress) + { + # Despite no proxy being explicitly specified, we may still be behind a default proxy + $DefaultProxy = [System.Net.WebRequest]::DefaultWebProxy; + if($DefaultProxy -and (-not $DefaultProxy.IsBypassed($Uri))){ + $ProxyAddress = $DefaultProxy.GetProxy($Uri).OriginalString + $ProxyUseDefaultCredentials = $true + } + } + + if($ProxyAddress){ + $HttpClientHandler = New-Object System.Net.Http.HttpClientHandler + $HttpClientHandler.Proxy = New-Object System.Net.WebProxy -Property @{Address=$ProxyAddress;UseDefaultCredentials=$ProxyUseDefaultCredentials} + $HttpClient = New-Object System.Net.Http.HttpClient -ArgumentList $HttpClientHandler + } + else { + $HttpClient = New-Object System.Net.Http.HttpClient + } + # Default timeout for HttpClient is 100s. For a 50 MB download this assumes 500 KB/s average, any less will time out + # 10 minutes allows it to work over much slower connections. + $HttpClient.Timeout = New-TimeSpan -Minutes 10 + $Response = $HttpClient.GetAsync($Uri).Result + if (($Response -eq $null) -or (-not ($Response.IsSuccessStatusCode))) + { + $ErrorMsg = "Failed to download $Uri." + if ($Response -ne $null) + { + $ErrorMsg += " $Response" + } + + throw $ErrorMsg + } + + return $Response + } + finally { + if ($HttpClient -ne $null) { + $HttpClient.Dispose() + } + } + }) +} + + +function Get-Latest-Version-Info([string]$AzureFeed, [string]$Channel, [bool]$Coherent) { + Say-Invocation $MyInvocation + + $VersionFileUrl = $null + if ($SharedRuntime) { + $VersionFileUrl = "$UncachedFeed/Runtime/$Channel/latest.version" + } + else { + if ($Coherent) { + $VersionFileUrl = "$UncachedFeed/Sdk/$Channel/latest.coherent.version" + } + else { + $VersionFileUrl = "$UncachedFeed/Sdk/$Channel/latest.version" + } + } + + $Response = GetHTTPResponse -Uri $VersionFileUrl + $StringContent = $Response.Content.ReadAsStringAsync().Result + + switch ($Response.Content.Headers.ContentType) { + { ($_ -eq "application/octet-stream") } { $VersionText = [Text.Encoding]::UTF8.GetString($StringContent) } + { ($_ -eq "text/plain") } { $VersionText = $StringContent } + { ($_ -eq "text/plain; charset=UTF-8") } { $VersionText = $StringContent } + default { throw "``$Response.Content.Headers.ContentType`` is an unknown .version file content type." } + } + + $VersionInfo = Get-Version-Info-From-Version-Text $VersionText + + return $VersionInfo +} + + +function Get-Specific-Version-From-Version([string]$AzureFeed, [string]$Channel, [string]$Version) { + Say-Invocation $MyInvocation + + switch ($Version.ToLower()) { + { $_ -eq "latest" } { + $LatestVersionInfo = Get-Latest-Version-Info -AzureFeed $AzureFeed -Channel $Channel -Coherent $False + return $LatestVersionInfo.Version + } + { $_ -eq "coherent" } { + $LatestVersionInfo = Get-Latest-Version-Info -AzureFeed $AzureFeed -Channel $Channel -Coherent $True + return $LatestVersionInfo.Version + } + default { return $Version } + } +} + +function Get-Download-Link([string]$AzureFeed, [string]$Channel, [string]$SpecificVersion, [string]$CLIArchitecture) { + Say-Invocation $MyInvocation + + if ($SharedRuntime) { + $PayloadURL = "$AzureFeed/Runtime/$SpecificVersion/dotnet-runtime-$SpecificVersion-win-$CLIArchitecture.zip" + } + else { + $PayloadURL = "$AzureFeed/Sdk/$SpecificVersion/dotnet-sdk-$SpecificVersion-win-$CLIArchitecture.zip" + } + + Say-Verbose "Constructed primary payload URL: $PayloadURL" + + return $PayloadURL +} + +function Get-LegacyDownload-Link([string]$AzureFeed, [string]$Channel, [string]$SpecificVersion, [string]$CLIArchitecture) { + Say-Invocation $MyInvocation + + if ($SharedRuntime) { + $PayloadURL = "$AzureFeed/Runtime/$SpecificVersion/dotnet-win-$CLIArchitecture.$SpecificVersion.zip" + } + else { + $PayloadURL = "$AzureFeed/Sdk/$SpecificVersion/dotnet-dev-win-$CLIArchitecture.$SpecificVersion.zip" + } + + Say-Verbose "Constructed legacy payload URL: $PayloadURL" + + return $PayloadURL +} + +function Get-User-Share-Path() { + Say-Invocation $MyInvocation + + $InstallRoot = $env:DOTNET_INSTALL_DIR + if (!$InstallRoot) { + $InstallRoot = "$env:LocalAppData\Microsoft\dotnet" + } + return $InstallRoot +} + +function Resolve-Installation-Path([string]$InstallDir) { + Say-Invocation $MyInvocation + + if ($InstallDir -eq "") { + return Get-User-Share-Path + } + return $InstallDir +} + +function Get-Version-Info-From-Version-File([string]$InstallRoot, [string]$RelativePathToVersionFile) { + Say-Invocation $MyInvocation + + $VersionFile = Join-Path -Path $InstallRoot -ChildPath $RelativePathToVersionFile + Say-Verbose "Local version file: $VersionFile" + + if (Test-Path $VersionFile) { + $VersionText = cat $VersionFile + Say-Verbose "Local version file text: $VersionText" + return Get-Version-Info-From-Version-Text $VersionText + } + + Say-Verbose "Local version file not found." + + return $null +} + +function Is-Dotnet-Package-Installed([string]$InstallRoot, [string]$RelativePathToPackage, [string]$SpecificVersion) { + Say-Invocation $MyInvocation + + $DotnetPackagePath = Join-Path -Path $InstallRoot -ChildPath $RelativePathToPackage | Join-Path -ChildPath $SpecificVersion + Say-Verbose "Is-Dotnet-Package-Installed: Path to a package: $DotnetPackagePath" + return Test-Path $DotnetPackagePath -PathType Container +} + +function Get-Absolute-Path([string]$RelativeOrAbsolutePath) { + # Too much spam + # Say-Invocation $MyInvocation + + return $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($RelativeOrAbsolutePath) +} + +function Get-Path-Prefix-With-Version($path) { + $match = [regex]::match($path, $VersionRegEx) + if ($match.Success) { + return $entry.FullName.Substring(0, $match.Index + $match.Length) + } + + return $null +} + +function Get-List-Of-Directories-And-Versions-To-Unpack-From-Dotnet-Package([System.IO.Compression.ZipArchive]$Zip, [string]$OutPath) { + Say-Invocation $MyInvocation + + $ret = @() + foreach ($entry in $Zip.Entries) { + $dir = Get-Path-Prefix-With-Version $entry.FullName + if ($dir -ne $null) { + $path = Get-Absolute-Path $(Join-Path -Path $OutPath -ChildPath $dir) + if (-Not (Test-Path $path -PathType Container)) { + $ret += $dir + } + } + } + + $ret = $ret | Sort-Object | Get-Unique + + $values = ($ret | foreach { "$_" }) -join ";" + Say-Verbose "Directories to unpack: $values" + + return $ret +} + +# Example zip content and extraction algorithm: +# Rule: files if extracted are always being extracted to the same relative path locally +# .\ +# a.exe # file does not exist locally, extract +# b.dll # file exists locally, override only if $OverrideFiles set +# aaa\ # same rules as for files +# ... +# abc\1.0.0\ # directory contains version and exists locally +# ... # do not extract content under versioned part +# abc\asd\ # same rules as for files +# ... +# def\ghi\1.0.1\ # directory contains version and does not exist locally +# ... # extract content +function Extract-Dotnet-Package([string]$ZipPath, [string]$OutPath) { + Say-Invocation $MyInvocation + + Load-Assembly -Assembly System.IO.Compression.FileSystem + Set-Variable -Name Zip + try { + $Zip = [System.IO.Compression.ZipFile]::OpenRead($ZipPath) + + $DirectoriesToUnpack = Get-List-Of-Directories-And-Versions-To-Unpack-From-Dotnet-Package -Zip $Zip -OutPath $OutPath + + foreach ($entry in $Zip.Entries) { + $PathWithVersion = Get-Path-Prefix-With-Version $entry.FullName + if (($PathWithVersion -eq $null) -Or ($DirectoriesToUnpack -contains $PathWithVersion)) { + $DestinationPath = Get-Absolute-Path $(Join-Path -Path $OutPath -ChildPath $entry.FullName) + $DestinationDir = Split-Path -Parent $DestinationPath + $OverrideFiles=$OverrideNonVersionedFiles -Or (-Not (Test-Path $DestinationPath)) + if ((-Not $DestinationPath.EndsWith("\")) -And $OverrideFiles) { + New-Item -ItemType Directory -Force -Path $DestinationDir | Out-Null + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $DestinationPath, $OverrideNonVersionedFiles) + } + } + } + } + finally { + if ($Zip -ne $null) { + $Zip.Dispose() + } + } +} + +function DownloadFile([Uri]$Uri, [string]$OutPath) { + $Stream = $null + + try { + $Response = GetHTTPResponse -Uri $Uri + $Stream = $Response.Content.ReadAsStreamAsync().Result + $File = [System.IO.File]::Create($OutPath) + $Stream.CopyTo($File) + $File.Close() + } + finally { + if ($Stream -ne $null) { + $Stream.Dispose() + } + } +} + +function Prepend-Sdk-InstallRoot-To-Path([string]$InstallRoot, [string]$BinFolderRelativePath) { + $BinPath = Get-Absolute-Path $(Join-Path -Path $InstallRoot -ChildPath $BinFolderRelativePath) + if (-Not $NoPath) { + Say "Adding to current process PATH: `"$BinPath`". Note: This change will not be visible if PowerShell was run as a child process." + $env:path = "$BinPath;" + $env:path + } + else { + Say "Binaries of dotnet can be found in $BinPath" + } +} + +$CLIArchitecture = Get-CLIArchitecture-From-Architecture $Architecture +$SpecificVersion = Get-Specific-Version-From-Version -AzureFeed $AzureFeed -Channel $Channel -Version $Version +$DownloadLink = Get-Download-Link -AzureFeed $AzureFeed -Channel $Channel -SpecificVersion $SpecificVersion -CLIArchitecture $CLIArchitecture +$LegacyDownloadLink = Get-LegacyDownload-Link -AzureFeed $AzureFeed -Channel $Channel -SpecificVersion $SpecificVersion -CLIArchitecture $CLIArchitecture + +if ($DryRun) { + Say "Payload URLs:" + Say "Primary - $DownloadLink" + Say "Legacy - $LegacyDownloadLink" + Say "Repeatable invocation: .\$($MyInvocation.MyCommand) -Version $SpecificVersion -Channel $Channel -Architecture $CLIArchitecture -InstallDir $InstallDir" + exit 0 +} + +$InstallRoot = Resolve-Installation-Path $InstallDir +Say-Verbose "InstallRoot: $InstallRoot" + +$IsSdkInstalled = Is-Dotnet-Package-Installed -InstallRoot $InstallRoot -RelativePathToPackage "sdk" -SpecificVersion $SpecificVersion +Say-Verbose ".NET SDK installed? $IsSdkInstalled" +if ($IsSdkInstalled) { + Say ".NET SDK version $SpecificVersion is already installed." + Prepend-Sdk-InstallRoot-To-Path -InstallRoot $InstallRoot -BinFolderRelativePath $BinFolderRelativePath + exit 0 +} + +New-Item -ItemType Directory -Force -Path $InstallRoot | Out-Null + +$installDrive = $((Get-Item $InstallRoot).PSDrive.Name); +Write-Output "${installDrive}:"; +$free = Get-CimInstance -Class win32_logicaldisk | where Deviceid -eq "${installDrive}:" +if ($free.Freespace / 1MB -le 100 ) { + Say "There is not enough disk space on drive ${installDrive}:" + exit 0 +} + +$ZipPath = [System.IO.Path]::GetTempFileName() +Say-Verbose "Zip path: $ZipPath" +Say "Downloading link: $DownloadLink" +try { + DownloadFile -Uri $DownloadLink -OutPath $ZipPath +} +catch { + Say "Cannot download: $DownloadLink" + $DownloadLink = $LegacyDownloadLink + $ZipPath = [System.IO.Path]::GetTempFileName() + Say-Verbose "Legacy zip path: $ZipPath" + Say "Downloading legacy link: $DownloadLink" + DownloadFile -Uri $DownloadLink -OutPath $ZipPath +} + +Say "Extracting zip from $DownloadLink" +Extract-Dotnet-Package -ZipPath $ZipPath -OutPath $InstallRoot + +Remove-Item $ZipPath + +Prepend-Sdk-InstallRoot-To-Path -InstallRoot $InstallRoot -BinFolderRelativePath $BinFolderRelativePath + +Say "Installation finished" +exit 0 diff --git a/build/repo.props b/build/repo.props new file mode 100644 index 0000000000..3b59246633 --- /dev/null +++ b/build/repo.props @@ -0,0 +1,17 @@ + + + $(RepositoryRoot)test\Microsoft.AspNetCore.AzureAppServices.FunctionalTests\Microsoft.AspNetCore.AzureAppServices.FunctionalTests.csproj + + + + + + + + + \ No newline at end of file diff --git a/build/repo.targets b/build/repo.targets new file mode 100644 index 0000000000..9460d8b735 --- /dev/null +++ b/build/repo.targets @@ -0,0 +1,39 @@ + + + + $(RepositoryRoot)src\Microsoft.AspNetCore.AzureAppServices.TestBundle\ + https://dotnet.myget.org/F/aspnetcore-ci-dev/ + master + coherent + + + + + + $(RepositoryRoot).test-dotnet\ + build\dotnet.version + + + + + + + + + + + + + + + + + + + diff --git a/src/Microsoft.AspNetCore.AzureAppServices.TestBundle/Microsoft.AspNetCore.AzureAppServices.TestBundle.csproj b/src/Microsoft.AspNetCore.AzureAppServices.TestBundle/Microsoft.AspNetCore.AzureAppServices.TestBundle.csproj new file mode 100644 index 0000000000..2d8fc6b90a --- /dev/null +++ b/src/Microsoft.AspNetCore.AzureAppServices.TestBundle/Microsoft.AspNetCore.AzureAppServices.TestBundle.csproj @@ -0,0 +1,35 @@ + + + + + + ASP.NET Core Test Bundle Extensions + This extension enables testing functionality of ASP.NET Core on Azure WebSites. + net461 + false + aspnet;logging;aspnetcore;AzureSiteExtension + AzureSiteExtension + true + false + false + false + content + AspNetCoreTestBundle + + https://github.com/aspnet/AzureIntegration/blob/rel/2.0.0-preview1/LICENSE.txt + https://go.microsoft.com/fwlink/?LinkID=288859 + https://www.asp.net/ + + + + + + + + + + + + + + diff --git a/src/Microsoft.AspNetCore.AzureAppServices.TestBundle/applicationHost.xdt b/src/Microsoft.AspNetCore.AzureAppServices.TestBundle/applicationHost.xdt new file mode 100644 index 0000000000..5a7f96ddd8 --- /dev/null +++ b/src/Microsoft.AspNetCore.AzureAppServices.TestBundle/applicationHost.xdt @@ -0,0 +1,14 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/AzureCollection.cs b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/AzureCollection.cs new file mode 100644 index 0000000000..8e2e110b81 --- /dev/null +++ b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/AzureCollection.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Xunit; + +namespace Microsoft.AspNetCore.AzureAppServices.FunctionalTests +{ + [CollectionDefinition("Azure")] + public class AzureCollection : ICollectionFixture + { + // This class has no code, and is never created. Its purpose is simply + // to be the place to apply [CollectionDefinition] and all the + // ICollectionFixture<> interfaces. + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/AzureFixture.cs b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/AzureFixture.cs new file mode 100644 index 0000000000..5f6dd9e656 --- /dev/null +++ b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/AzureFixture.cs @@ -0,0 +1,154 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Microsoft.Azure.Management.AppService.Fluent; +using Microsoft.Azure.Management.Fluent; +using Microsoft.Azure.Management.ResourceManager.Fluent; +using Microsoft.Azure.Management.ResourceManager.Fluent.Core; +using Microsoft.Azure.Management.ResourceManager.Fluent.Models; +using Microsoft.Azure.Management.Storage.Fluent; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; +using Microsoft.Rest; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.AzureAppServices.FunctionalTests +{ + public class AzureFixture : IDisposable + { + public string Timestamp { get; set; } + + public AzureFixture() + { + TestLog = AssemblyTestLog.ForAssembly(typeof(AzureFixture).Assembly); + + // TODO: Temporary to see if it's useful and worth exposing + var globalLoggerFactory = + (ILoggerFactory) TestLog.GetType().GetField("_globalLoggerFactory", BindingFlags.NonPublic | BindingFlags.Instance) + .GetValue(TestLog); + + var logger = globalLoggerFactory.CreateLogger(); + + ServiceClientTracing.IsEnabled = true; + ServiceClientTracing.AddTracingInterceptor(new LoggingInterceptor(globalLoggerFactory.CreateLogger(nameof(ServiceClientTracing)))); + + var clientId = GetRequiredEnvironmentVariable("AZURE_AUTH_CLIENT_ID"); + var clientSecret = GetRequiredEnvironmentVariable("AZURE_AUTH_CLIENT_SECRET"); + var tenant = GetRequiredEnvironmentVariable("AZURE_AUTH_TENANT"); + + var credentials = SdkContext.AzureCredentialsFactory.FromServicePrincipal(clientId, clientSecret, tenant, AzureEnvironment.AzureGlobalCloud); + Azure = Microsoft.Azure.Management.Fluent.Azure.Configure() + .WithLogLevel(HttpLoggingDelegatingHandler.Level.Basic) + .Authenticate(credentials) + .WithDefaultSubscription(); + + Timestamp = DateTime.Now.ToString("yyyyMMddhhmmss"); + var testRunName = GetTimestampedName("FunctionalTests"); + + logger.LogInformation("Creating resource group {TestRunName}", testRunName); + ResourceGroup = Azure.ResourceGroups + .Define(testRunName) + .WithRegion(Region.USWest2) + .Create(); + + var servicePlanName = GetTimestampedName("TestPlan"); + logger.LogInformation("Creating service plan {servicePlanName}", testRunName); + + Plan = Azure.AppServices.AppServicePlans.Define(servicePlanName) + .WithRegion(Region.USWest2) + .WithExistingResourceGroup(ResourceGroup) + .WithFreePricingTier() + .Create(); + } + + private static string GetRequiredEnvironmentVariable(string name) + { + var authFile = Environment.GetEnvironmentVariable(name); + if (string.IsNullOrEmpty(authFile)) + { + throw new InvalidOperationException($"{name} environment variable has to be set to run these tests."); + } + + return authFile; + } + + public IAppServicePlan Plan { get; set; } + + public IStorageAccount DeploymentStorageAccount { get; set; } + + public AssemblyTestLog TestLog { get; set; } + + public bool DeleteResourceGroup { get; set; } = true; + + public IResourceGroup ResourceGroup { get; set; } + + public IAzure Azure { get; set; } + + public string GetTimestampedName(string name) + { + return name + Timestamp; + } + + public async Task Deploy(string template, IDictionary additionalArguments = null, [CallerMemberName] string baseName = null) + { + var siteName = GetTimestampedName(baseName); + var parameters = new Dictionary + { + {"siteName", siteName}, + {"hostingPlanName", Plan.Name}, + {"resourceGroupName", ResourceGroup.Name}, + }; + + foreach (var pair in additionalArguments ?? Enumerable.Empty>()) + { + parameters[pair.Key] = pair.Value; + } + + var readAllText = File.ReadAllText(template); + var deployment = await Azure.Deployments.Define(GetTimestampedName("Deployment")) + .WithExistingResourceGroup(ResourceGroup) + .WithTemplate(readAllText) + .WithParameters(ToParametersObject(parameters)) + .WithMode(DeploymentMode.Incremental) + .CreateAsync(); + + deployment = await deployment.RefreshAsync(); + + var outputs = (JObject)deployment.Outputs; + + var siteIdOutput = outputs["siteId"]; + if (siteIdOutput == null) + { + throw new InvalidOperationException("Deployment was expected to have 'siteId' output parameter"); + } + var siteId = siteIdOutput["value"].Value(); + return await Azure.AppServices.WebApps.GetByIdAsync(siteId); + } + + private JObject ToParametersObject(Dictionary parameters) + { + return new JObject( + parameters.Select(parameter => + new JProperty( + parameter.Key, + new JObject( + new JProperty("value", parameter.Value))))); + } + + public void Dispose() + { + TestLog.Dispose(); + if (DeleteResourceGroup && ResourceGroup != null) + { + Azure.ResourceGroups.DeleteByName(ResourceGroup.Name); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/CommandResult.cs b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/CommandResult.cs new file mode 100644 index 0000000000..bae995afe1 --- /dev/null +++ b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/CommandResult.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using Xunit; + +namespace Microsoft.AspNetCore.AzureAppServices.FunctionalTests +{ + public struct CommandResult + { + public static readonly CommandResult Empty = new CommandResult(); + + public ProcessStartInfo StartInfo { get; } + public int ExitCode { get; } + public string StdOut { get; } + public string StdErr { get; } + + public CommandResult(ProcessStartInfo startInfo, int exitCode, string stdOut, string stdErr) + { + StartInfo = startInfo; + ExitCode = exitCode; + StdOut = stdOut; + StdErr = stdErr; + } + + public void AssertSuccess() + { + Assert.True(0 == ExitCode, StdOut + Environment.NewLine + StdErr + Environment.NewLine); + } + } +} diff --git a/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/LoggingInterceptor.cs b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/LoggingInterceptor.cs new file mode 100644 index 0000000000..7d05212d50 --- /dev/null +++ b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/LoggingInterceptor.cs @@ -0,0 +1,47 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Rest; + +namespace Microsoft.AspNetCore.AzureAppServices.FunctionalTests +{ + public class LoggingInterceptor : IServiceClientTracingInterceptor + { + private readonly ILogger _logger; + + public LoggingInterceptor(ILogger logger) + { + _logger = logger; + } + + public void Information(string message) + { + _logger.LogInformation(message); + } + + public void TraceError(string invocationId, Exception exception) + { + _logger.LogInformation(exception, "Exception in {invocationId}", invocationId); + } + + public void ReceiveResponse(string invocationId, HttpResponseMessage response) + { + _logger.LogInformation(response.AsFormattedString()); + } + + public void SendRequest(string invocationId, HttpRequestMessage request) + { + _logger.LogInformation(request.AsFormattedString()); + } + + public void Configuration(string source, string name, string value) { } + + public void EnterMethod(string invocationId, object instance, string method, IDictionary parameters) { } + + public void ExitMethod(string invocationId, object returnValue) { } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/Microsoft.AspNetCore.AzureAppServices.FunctionalTests.csproj b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/Microsoft.AspNetCore.AzureAppServices.FunctionalTests.csproj new file mode 100644 index 0000000000..00f697511e --- /dev/null +++ b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/Microsoft.AspNetCore.AzureAppServices.FunctionalTests.csproj @@ -0,0 +1,27 @@ + + + + + + net461 + 7.1 + + + + + + + + + + + + + + + + + + + + diff --git a/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/TemplateFunctionalTests.cs b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/TemplateFunctionalTests.cs new file mode 100644 index 0000000000..e92e6b6637 --- /dev/null +++ b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/TemplateFunctionalTests.cs @@ -0,0 +1,81 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Net.Http; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Microsoft.Azure.Management.AppService.Fluent; +using Microsoft.Azure.Management.AppService.Fluent.Models; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.AzureAppServices.FunctionalTests +{ + [Collection("Azure")] + public class TemplateFunctionalTests + { + readonly AzureFixture _fixture; + + private readonly ITestOutputHelper _outputHelper; + + public TemplateFunctionalTests(AzureFixture fixture, ITestOutputHelper outputHelper) + { + _fixture = fixture; + _outputHelper = outputHelper; + } + + [Fact] + public async Task DotnetNewWebRunsInWebApp() + { + using (var logger = GetLogger()) + { + Assert.NotNull(_fixture.Azure); + + var site = await _fixture.Deploy("Templates\\BasicAppServices.json", null); + var testDirectory = GetTestDirectory(); + + var dotnet = DotNet(logger, testDirectory); + + var result = await dotnet.ExecuteAsync("new web"); + result.AssertSuccess(); + + await site.BuildPublishProfileAsync(testDirectory.FullName); + + result = await dotnet.ExecuteAsync("publish /p:PublishProfile=Profile"); + result.AssertSuccess(); + + using (var httpClient = site.CreateClient()) + { + var getResult = await httpClient.GetAsync("/"); + getResult.EnsureSuccessStatusCode(); + Assert.Equal("Hello World!", await getResult.Content.ReadAsStringAsync()); + } + } + } + + private TestLogger GetLogger([CallerMemberName] string callerName = null) + { + _fixture.TestLog.StartTestLog(_outputHelper, nameof(TemplateFunctionalTests), out var factory, callerName); + return new TestLogger(factory, factory.CreateLogger(callerName)); + } + + private TestCommand DotNet(TestLogger logger, DirectoryInfo workingDirectory) + { + return new TestCommand("dotnet") + { + Logger = logger, + WorkingDirectory = workingDirectory.FullName + }; + } + + private DirectoryInfo GetTestDirectory([CallerMemberName] string callerName = null) + { + if (Directory.Exists(callerName)) + { + Directory.Delete(callerName, recursive:true); + } + return Directory.CreateDirectory(callerName); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/Templates/AppServicesWithSiteExtensions.json b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/Templates/AppServicesWithSiteExtensions.json new file mode 100644 index 0000000000..9f0d6c8528 --- /dev/null +++ b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/Templates/AppServicesWithSiteExtensions.json @@ -0,0 +1,56 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "siteName": { + "type": "string" + }, + "hostingPlanName": { + "type": "string" + }, + "resourceGroupName": { + "type": "string" + }, + "extensionFeed": { + "type": "string" + }, + "extensionName": { + "type": "string" + }, + "extensionVersion": { + "type": "string" + } + }, + "resources": [ + { + "apiVersion": "2015-08-01", + "name": "[parameters('siteName')]", + "type": "Microsoft.Web/sites", + "location": "West US 2", + "properties": { + "serverFarmId": "[resourceId(parameters('resourceGroupName'), 'Microsoft.Web/serverFarms', parameters('hostingPlanName'))]" + }, + "resources": [ + { + "type": "extensions", + "name": "[parameters('extensionName')]", + "apiVersion": "2015-08-01", + "location": "West US 2", + "properties": { + "version": "[parameters('extensionVersion')]", + "feed_url": "[parameters('extensionFeed')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/Sites', parameters('siteName'))]" + ] + } + ] + } + ], + "outputs": { + "siteId": { + "type": "string", + "value": "[resourceId(parameters('resourceGroupName'), 'Microsoft.Web/sites', parameters('siteName'))]" + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/Templates/BasicAppServices.json b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/Templates/BasicAppServices.json new file mode 100644 index 0000000000..6eefe37051 --- /dev/null +++ b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/Templates/BasicAppServices.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "siteName": { + "type": "string" + }, + "hostingPlanName": { + "type": "string" + }, + "resourceGroupName": { + "type": "string" + } + }, + "resources": [ + { + "apiVersion": "2015-08-01", + "name": "[parameters('siteName')]", + "type": "Microsoft.Web/sites", + "location": "West US 2", + "properties": { + "serverFarmId": "[resourceId(parameters('resourceGroupName'), 'Microsoft.Web/serverFarms', parameters('hostingPlanName'))]" + } + } + ], + "outputs": { + "siteId": { + "type": "string", + "value": "[resourceId(parameters('resourceGroupName'), 'Microsoft.Web/sites', parameters('siteName'))]" + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/TestCommand.cs b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/TestCommand.cs new file mode 100644 index 0000000000..d45eb59366 --- /dev/null +++ b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/TestCommand.cs @@ -0,0 +1,249 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.Internal; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.AzureAppServices.FunctionalTests +{ + public class TestCommand + { + private string _dotnetPath = GetDotnetPath(); + + private static string GetDotnetPath() + { + var current = new DirectoryInfo(Directory.GetCurrentDirectory()); + while (current != null) + { + var dotnetSubdir = new DirectoryInfo(Path.Combine(current.FullName, ".test-dotnet")); + if (dotnetSubdir.Exists) + { + var dotnetName = Path.Combine(dotnetSubdir.FullName, "dotnet.exe"); + if (!File.Exists(dotnetName)) + { + throw new InvalidOperationException("dotnet directory was found but dotnet.exe is not in it"); + } + return dotnetName; + } + current = current.Parent; + } + + throw new InvalidOperationException("dotnet executable was not found"); + } + + private List _cliGeneratedEnvironmentVariables = new List { "MSBuildSDKsPath" }; + + protected string _command; + + public Process CurrentProcess { get; private set; } + + public Dictionary Environment { get; } = new Dictionary(); + + public event DataReceivedEventHandler ErrorDataReceived; + + public event DataReceivedEventHandler OutputDataReceived; + + public string WorkingDirectory { get; set; } + public ILogger Logger { get; set; } + + public TestCommand(string command) + { + _command = command; + } + + public void KillTree() + { + if (CurrentProcess == null) + { + throw new InvalidOperationException("No process is available to be killed"); + } + + CurrentProcess.KillTree(); + } + + public virtual async Task ExecuteAsync(string args = "") + { + var resolvedCommand = _command; + + ResolveCommand(ref resolvedCommand, ref args); + + Logger.LogInformation($"Executing - {resolvedCommand} {args} - {WorkingDirectoryInfo()}"); + + return await ExecuteAsyncInternal(resolvedCommand, args); + } + + private async Task ExecuteAsyncInternal(string executable, string args) + { + var stdOut = new List(); + + var stdErr = new List(); + + CurrentProcess = CreateProcess(executable, args); + + CurrentProcess.ErrorDataReceived += (s, e) => + { + stdErr.Add(e.Data); + + var handler = ErrorDataReceived; + + if (handler != null) + { + handler(s, e); + } + }; + + CurrentProcess.OutputDataReceived += (s, e) => + { + stdOut.Add(e.Data); + + var handler = OutputDataReceived; + + if (handler != null) + { + handler(s, e); + } + }; + + var completionTask = StartAndWaitForExitAsync(CurrentProcess); + + CurrentProcess.BeginOutputReadLine(); + + CurrentProcess.BeginErrorReadLine(); + + await completionTask; + + CurrentProcess.WaitForExit(); + + RemoveNullTerminator(stdOut); + + RemoveNullTerminator(stdErr); + + var stdOutString = String.Join(System.Environment.NewLine, stdOut); + var stdErrString = String.Join(System.Environment.NewLine, stdErr); + + if (!string.IsNullOrWhiteSpace(stdOutString)) + { + Logger.LogInformation("stdout: {out}", stdOutString); + } + + if (!string.IsNullOrWhiteSpace(stdErrString)) + { + Logger.LogInformation("stderr: {err}", stdErrString); + } + + return new CommandResult( + CurrentProcess.StartInfo, + CurrentProcess.ExitCode, + stdOutString, + stdErrString); + } + + private Process CreateProcess(string executable, string args) + { + var psi = new ProcessStartInfo + { + FileName = executable, + Arguments = args, + RedirectStandardError = true, + RedirectStandardOutput = true, + RedirectStandardInput = true, + UseShellExecute = false + }; + + RemoveCliGeneratedEnvironmentVariablesFrom(psi); + + AddEnvironmentVariablesTo(psi); + + AddWorkingDirectoryTo(psi); + + var process = new Process + { + StartInfo = psi + }; + + process.EnableRaisingEvents = true; + + return process; + } + + private string WorkingDirectoryInfo() + { + if (WorkingDirectory == null) + { + return ""; + } + + return $" in {WorkingDirectory}"; + } + + private void RemoveNullTerminator(List strings) + { + var count = strings.Count; + + if (count < 1) + { + return; + } + + if (strings[count - 1] == null) + { + strings.RemoveAt(count - 1); + } + } + + private void ResolveCommand(ref string executable, ref string args) + { + if (executable == "dotnet") + { + executable = _dotnetPath; + return; + } + + throw new ArgumentOutOfRangeException(nameof(executable)); + } + + private void RemoveCliGeneratedEnvironmentVariablesFrom(ProcessStartInfo psi) + { + foreach (var name in _cliGeneratedEnvironmentVariables) + { + psi.Environment.Remove(name); + } + } + + private void AddEnvironmentVariablesTo(ProcessStartInfo psi) + { + foreach (var item in Environment) + { + psi.Environment[item.Key] = item.Value; + } + } + + private void AddWorkingDirectoryTo(ProcessStartInfo psi) + { + if (!string.IsNullOrWhiteSpace(WorkingDirectory)) + { + psi.WorkingDirectory = WorkingDirectory; + } + } + public static Task StartAndWaitForExitAsync(Process subject) + { + var taskCompletionSource = new TaskCompletionSource(); + + subject.EnableRaisingEvents = true; + + subject.Exited += (s, a) => + { + taskCompletionSource.SetResult(null); + }; + + subject.Start(); + + return taskCompletionSource.Task; + } + } +} diff --git a/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/TestLogger.cs b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/TestLogger.cs new file mode 100644 index 0000000000..0c03a48e2e --- /dev/null +++ b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/TestLogger.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.AzureAppServices.FunctionalTests +{ + internal class TestLogger: ILogger, IDisposable + { + private readonly ILoggerFactory _factory; + + private readonly ILogger _logger; + + public TestLogger(ILoggerFactory factory, ILogger logger) + { + _factory = factory; + _logger = logger; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + _logger.Log(logLevel, eventId, state, exception, formatter); + } + + public bool IsEnabled(LogLevel logLevel) + { + return _logger.IsEnabled(logLevel); + } + + public IDisposable BeginScope(TState state) + { + return _logger.BeginScope(state); + } + + public void Dispose() + { + _factory.Dispose(); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/WebAppExtensions.cs b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/WebAppExtensions.cs new file mode 100644 index 0000000000..c53a3ca77d --- /dev/null +++ b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/WebAppExtensions.cs @@ -0,0 +1,86 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.Azure.Management.AppService.Fluent; +using Microsoft.Azure.Management.AppService.Fluent.Models; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.AzureAppServices.FunctionalTests +{ + internal static class WebAppExtensions + { + public static HttpClient CreateClient(this IWebApp site) + { + var domain = site.GetHostNameBindings().First().Key; + + return new HttpClient { BaseAddress = new Uri("http://" + domain) }; + } + + public static async Task UploadFilesAsync(this IWebApp site, DirectoryInfo from, string to, IPublishingProfile publishingProfile, ILogger logger) + { + foreach (var info in from.GetFileSystemInfos("*", SearchOption.AllDirectories)) + { + if (info is FileInfo file) + { + var address = new Uri( + "ftp://" + publishingProfile.FtpUrl + to + file.FullName.Substring(from.FullName.Length).Replace('\\', '/')); + logger.LogInformation($"Uploading {file.FullName} to {address}"); + + var request = (FtpWebRequest)WebRequest.Create(address); + request.Method = WebRequestMethods.Ftp.UploadFile; + request.KeepAlive = true; + request.UseBinary = true; + request.UsePassive = false; + request.Credentials = new NetworkCredential(publishingProfile.FtpUsername, publishingProfile.FtpPassword); + request.ConnectionGroupName = "group"; + using (var fileStream = File.OpenRead(file.FullName)) + { + using (var requestStream = await request.GetRequestStreamAsync()) + { + await fileStream.CopyToAsync(requestStream); + } + } + await request.GetResponseAsync(); + } + } + } + + public static async Task BuildPublishProfileAsync(this IWebApp site, string projectDirectory) + { + var result = await site.Manager.WebApps.Inner.ListPublishingProfileXmlWithSecretsAsync( + site.ResourceGroupName, + site.Name, + new CsmPublishingProfileOptionsInner()); + + var targetDirectory = Path.Combine(projectDirectory, "Properties", "PublishProfiles"); + Directory.CreateDirectory(targetDirectory); + + var publishSettings = XDocument.Load(result); + foreach (var profile in publishSettings.Root.Elements("publishProfile")) + { + if ((string) profile.Attribute("publishMethod") == "MSDeploy") + { + new XDocument( + new XElement("Project", + new XElement("PropertyGroup", + new XElement("WebPublishMethod", "MSDeploy"), + new XElement("PublishProvider", "AzureWebSite"), + new XElement("UserName", (string)profile.Attribute("userName")), + new XElement("Password", (string)profile.Attribute("userPWD")), + new XElement("MSDeployServiceURL", (string)profile.Attribute("publishUrl")), + new XElement("DeployIisAppPath", (string)profile.Attribute("msdeploySite")) + ))) + .Save(Path.Combine(targetDirectory, "Profile.pubxml")); + } + } + } + } +} \ No newline at end of file