From 0750fc917a6995a75409cfb2408c52575b348588 Mon Sep 17 00:00:00 2001 From: Alex Perovich Date: Wed, 10 Nov 2021 14:32:13 -0800 Subject: [PATCH] Add support for dotnetbuilds-published dotnet cli packages (#8162) --- eng/common/tools.ps1 | 55 +++++--- eng/common/tools.sh | 60 +++++---- .../Sdk/FindDotNetCliPackage.cs | 120 ++++++++++++------ 3 files changed, 145 insertions(+), 90 deletions(-) diff --git a/eng/common/tools.ps1 b/eng/common/tools.ps1 index 44484289943..6de418e9379 100644 --- a/eng/common/tools.ps1 +++ b/eng/common/tools.ps1 @@ -301,31 +301,44 @@ function InstallDotNet([string] $dotnetRoot, if ($skipNonVersionedFiles) { $installParameters.SkipNonVersionedFiles = $skipNonVersionedFiles } if ($noPath) { $installParameters.NoPath = $True } - try { - & $installScript @installParameters - } - catch { - if ($runtimeSourceFeed -or $runtimeSourceFeedKey) { - Write-Host "Failed to install dotnet from public location. Trying from '$runtimeSourceFeed'" - if ($runtimeSourceFeed) { $installParameters.AzureFeed = $runtimeSourceFeed } + $variations = @() + $variations += @($installParameters) - if ($runtimeSourceFeedKey) { - $decodedBytes = [System.Convert]::FromBase64String($runtimeSourceFeedKey) - $decodedString = [System.Text.Encoding]::UTF8.GetString($decodedBytes) - $installParameters.FeedCredential = $decodedString - } + $dotnetBuilds = $installParameters.Clone() + $dotnetbuilds.AzureFeed = "https://dotnetbuilds.azureedge.net/public" + $variations += @($dotnetBuilds) - try { - & $installScript @installParameters - } - catch { - Write-PipelineTelemetryError -Category 'InitializeToolset' -Message "Failed to install dotnet from custom location '$runtimeSourceFeed'." - ExitWithExitCode 1 - } + if ($runtimeSourceFeed) { + $runtimeSource = $installParameters.Clone() + $runtimeSource.AzureFeed = $runtimeSourceFeed + if ($runtimeSourceFeedKey) { + $decodedBytes = [System.Convert]::FromBase64String($runtimeSourceFeedKey) + $decodedString = [System.Text.Encoding]::UTF8.GetString($decodedBytes) + $runtimeSource.FeedCredential = $decodedString + } + $variations += @($runtimeSource) + } + + $installSuccess = $false + foreach ($variation in $variations) { + if ($variation | Get-Member AzureFeed) { + $location = $variation.AzureFeed } else { - Write-PipelineTelemetryError -Category 'InitializeToolset' -Message "Failed to install dotnet from public location." - ExitWithExitCode 1 + $location = "public location"; + } + Write-Host "Attempting to install dotnet from $location." + try { + & $installScript @variation + $installSuccess = $true + break } + catch { + Write-Host "Failed to install dotnet from $location." + } + } + if (-not $installSuccess) { + Write-PipelineTelemetryError -Category 'InitializeToolset' -Message "Failed to install dotnet from any of the specified locations." + ExitWithExitCode 1 } } diff --git a/eng/common/tools.sh b/eng/common/tools.sh index 6a4871ef72b..532ce42c1f5 100755 --- a/eng/common/tools.sh +++ b/eng/common/tools.sh @@ -188,28 +188,29 @@ function InstallDotNet { GetDotNetInstallScript "$root" local install_script=$_GetDotNetInstallScript - local archArg='' + local installParameters=(--version $version --install-dir "$root") + if [[ -n "${3:-}" ]] && [ "$3" != 'unset' ]; then - archArg="--architecture $3" + installParameters+=(--architecture $3) fi - local runtimeArg='' if [[ -n "${4:-}" ]] && [ "$4" != 'sdk' ]; then - runtimeArg="--runtime $4" + installParameters+=(--runtime $4) fi - local skipNonVersionedFilesArg="" if [[ "$#" -ge "5" ]] && [[ "$5" != 'false' ]]; then - skipNonVersionedFilesArg="--skip-non-versioned-files" + installParameters+=(--skip-non-versioned-files) fi - bash "$install_script" --version $version --install-dir "$root" $archArg $runtimeArg $skipNonVersionedFilesArg || { - local exit_code=$? - echo "Failed to install dotnet SDK from public location (exit code '$exit_code')." - local runtimeSourceFeed='' - if [[ -n "${6:-}" ]]; then - runtimeSourceFeed="--azure-feed $6" - fi + local variations=() # list of variable names with parameter arrays in them + + local public_location=("${installParameters[@]}") + variations+=(public_location) + + local dotnetbuilds=("${installParameters[@]}" --azure-feed "https://dotnetbuilds.azureedge.net/public") + variations+=(dotnetbuilds) - local runtimeSourceFeedKey='' + if [[ -n "${6:-}" ]]; then + variations+=(private_feed) + local private_feed=("${installParameters[@]}" --azure-feed $6) if [[ -n "${7:-}" ]]; then # The 'base64' binary on alpine uses '-d' and doesn't support '--decode' # '-d'. To work around this, do a simple detection and switch the parameter @@ -219,22 +220,27 @@ function InstallDotNet { decodeArg="-d" fi decodedFeedKey=`echo $7 | base64 $decodeArg` - runtimeSourceFeedKey="--feed-credential $decodedFeedKey" + private_feed+=(--feed-credential $decodedFeedKey) fi + fi - if [[ -n "$runtimeSourceFeed" || -n "$runtimeSourceFeedKey" ]]; then - bash "$install_script" --version $version --install-dir "$root" $archArg $runtimeArg $skipNonVersionedFilesArg $runtimeSourceFeed $runtimeSourceFeedKey || { - local exit_code=$? - Write-PipelineTelemetryError -category 'InitializeToolset' "Failed to install dotnet SDK from custom location '$runtimeSourceFeed' (exit code '$exit_code')." - ExitWithExitCode $exit_code - } - else - if [[ $exit_code != 0 ]]; then - Write-PipelineTelemetryError -category 'InitializeToolset' "Failed to install dotnet SDK from public location (exit code '$exit_code')." - fi - ExitWithExitCode $exit_code + local installSuccess=0 + for variationName in "${variations[@]}"; do + local name="$variationName[@]" + local variation=("${!name}") + echo "Attempting to install dotnet from $variationName." + bash "$install_script" "${variation[@]}" && installSuccess=1 + if [[ "$installSuccess" -eq 1 ]]; then + break fi - } + + echo "Failed to install dotnet from $variationName." + done + + if [[ "$installSuccess" -eq 0 ]]; then + Write-PipelineTelemetryError -category 'InitializeToolset' "Failed to install dotnet SDK from any of the specified locations." + ExitWithExitCode 1 + fi } function with_retries { diff --git a/src/Microsoft.DotNet.Helix/Sdk/FindDotNetCliPackage.cs b/src/Microsoft.DotNet.Helix/Sdk/FindDotNetCliPackage.cs index a7f8947e11e..1d4f62a78c1 100644 --- a/src/Microsoft.DotNet.Helix/Sdk/FindDotNetCliPackage.cs +++ b/src/Microsoft.DotNet.Helix/Sdk/FindDotNetCliPackage.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; @@ -17,7 +19,6 @@ public class FindDotNetCliPackage : BaseTask DelayBase = 3.0 }; private static readonly HttpClient _client = new HttpClient(new HttpClientHandler { CheckCertificateRevocationList = true }); - private const string DotNetCliAzureFeed = "https://dotnetcli.blob.core.windows.net/dotnet"; /// /// 'LTS' or 'Current' @@ -55,71 +56,105 @@ public override bool Execute() private async Task ExecuteAsync() { NormalizeParameters(); - await ResolveVersionAsync(); - - string downloadUrl = await GetDownloadUrlAsync(); - - Log.LogMessage($"Retrieved dotnet cli {PackageType} version {Version} package uri {downloadUrl}, testing..."); - - try + var feeds = new List { - using HttpResponseMessage res = await HeadRequestWithRetry(downloadUrl); + "https://dotnetcli.azureedge.net/dotnet", + "https://dotnetbuilds.azureedge.net/public", + }; - if (res.StatusCode == HttpStatusCode.NotFound) + string finalDownloadUrl = null; + foreach (var feed in feeds) + { + string downloadUrl = await GetDownloadUrlAsync(feed); + if (downloadUrl == null) { - // 404 means that we successfully hit the server, and it returned 404. This cannot be a network hiccup - Log.LogError(FailureCategory.Build, $"Unable to find dotnet cli {PackageType} version {Version}, tried {downloadUrl}"); + Log.LogMessage($"Could not retrieve dotnet cli {PackageType} version {Version} package uri from feed {feed}"); + continue; } - else + + Log.LogMessage($"Retrieved dotnet cli {PackageType} version {Version} package uri {downloadUrl} from feed {feed}, testing..."); + + try { + using HttpResponseMessage res = await HeadRequestWithRetry(downloadUrl); + + if (res.StatusCode == HttpStatusCode.NotFound) + { + // 404 means that we successfully hit the server, and it returned 404. This cannot be a network hiccup + Log.LogMessage($"Unable to find dotnet cli {PackageType} version {Version} from feed {feed}"); + continue; + } + res.EnsureSuccessStatusCode(); + finalDownloadUrl = downloadUrl; + } + catch (Exception ex) + { + Log.LogMessage($"Unable to access dotnet cli {PackageType} version {Version} from feed {feed}, {ex.Message}"); } } - catch (Exception ex) + + if (finalDownloadUrl == null) { - Log.LogError(FailureCategory.Build, $"Unable to access dotnet cli {PackageType} version {Version} at {downloadUrl}, {ex.Message}"); + Log.LogError(FailureCategory.Build, $"Unable to find dotnet cli {PackageType} version {Version} from any of the specified feeds."); } + if (!Log.HasLoggedErrors) { - Log.LogMessage($"Url {downloadUrl} is valid."); - PackageUri = downloadUrl; + Log.LogMessage($"Url {finalDownloadUrl} is valid."); + PackageUri = finalDownloadUrl; } } - private async Task GetDownloadUrlAsync() + private async Task GetDownloadUrlAsync(string feed) { - string extension = Runtime.StartsWith("win") ? "zip" : "tar.gz"; - string effectiveVersion = await GetEffectiveVersion(); + var oldVersion = Version; // ResolveVersionAsync will adjust the Version property, but we need it set back for other feeds to see the same initial Version + try + { + var version = await ResolveVersionAsync(feed); + string extension = Runtime.StartsWith("win") ? "zip" : "tar.gz"; + string effectiveVersion = await GetEffectiveVersion(feed, version); - return PackageType switch + return PackageType switch + { + "sdk" => $"{feed}/Sdk/{version}/dotnet-sdk-{effectiveVersion}-{Runtime}.{extension}", + "aspnetcore-runtime" => + $"{feed}/aspnetcore/Runtime/{version}/aspnetcore-runtime-{effectiveVersion}-{Runtime}.{extension}", + _ => $"{feed}/Runtime/{version}/dotnet-runtime-{effectiveVersion}-{Runtime}.{extension}" + }; + } + catch (Exception ex) { - "sdk" => $"{DotNetCliAzureFeed}/Sdk/{Version}/dotnet-sdk-{effectiveVersion}-{Runtime}.{extension}", - "aspnetcore-runtime" => $"{DotNetCliAzureFeed}/aspnetcore/Runtime/{Version}/aspnetcore-runtime-{effectiveVersion}-{Runtime}.{extension}", - _ => $"{DotNetCliAzureFeed}/Runtime/{Version}/dotnet-runtime-{effectiveVersion}-{Runtime}.{extension}" - }; + Log.LogWarning($"Unable to resolve download link from feed {feed}; {ex.Message}"); + return null; + } + finally + { + Version = oldVersion; + } } - private async Task GetEffectiveVersion() + private async Task GetEffectiveVersion(string feed, string version) { - if (NuGetVersion.TryParse(Version, out NuGetVersion semanticVersion)) + if (NuGetVersion.TryParse(version, out NuGetVersion semanticVersion)) { // Pared down version of the logic from https://github.com/dotnet/install-scripts/blob/main/src/dotnet-install.ps1 // If this functionality stops working, review changes made there. // Current strategy is to start with a runtime-specific name then fall back to 'productVersion.txt' - string effectiveVersion = Version; + string effectiveVersion = version; // Do nothing for older runtimes; the file won't exist if (semanticVersion >= new NuGetVersion("5.0.0")) { var productVersionText = PackageType switch { - "sdk" => await GetMatchingProductVersionTxtContents($"{DotNetCliAzureFeed}/Sdk/{Version}", "sdk-productVersion.txt"), - "aspnetcore-runtime" => await GetMatchingProductVersionTxtContents($"{DotNetCliAzureFeed}/aspnetcore/Runtime/{Version}", "aspnetcore-productVersion.txt"), - _ => await GetMatchingProductVersionTxtContents($"{DotNetCliAzureFeed}/Runtime/{Version}", "runtime-productVersion.txt") + "sdk" => await GetMatchingProductVersionTxtContents($"{feed}/Sdk/{version}", "sdk-productVersion.txt"), + "aspnetcore-runtime" => await GetMatchingProductVersionTxtContents($"{feed}/aspnetcore/Runtime/{version}", "aspnetcore-productVersion.txt"), + _ => await GetMatchingProductVersionTxtContents($"{feed}/Runtime/{version}", "runtime-productVersion.txt") }; - if (!productVersionText.Equals(Version)) + if (!productVersionText.Equals(version)) { effectiveVersion = productVersionText; Log.LogMessage($"Switched to effective .NET Core version '{productVersionText}' from matching productVersion.txt"); @@ -127,10 +162,8 @@ private async Task GetEffectiveVersion() } return effectiveVersion; } - else - { - throw new ArgumentException($"'{Version}' is not a valid semantic version."); - } + + throw new ArgumentException($"'{version}' is not a valid semantic version."); } private async Task GetMatchingProductVersionTxtContents(string baseUri, string customVersionTextFileName) { @@ -243,16 +276,17 @@ private void NormalizeParameters() } } - private async Task ResolveVersionAsync() + private async Task ResolveVersionAsync(string feed) { + string version = Version; if (Version == "latest") { Log.LogMessage(MessageImportance.Low, "Resolving latest dotnet cli version."); string latestVersionUrl = PackageType switch { - "sdk" => $"{DotNetCliAzureFeed}/Sdk/{Channel}/latest.version", - "aspnetcore-runtime" => $"{DotNetCliAzureFeed}/aspnetcore/Runtime/{Channel}/latest.version", - _ => $"{DotNetCliAzureFeed}/Runtime/{Channel}/latest.version" + "sdk" => $"{feed}/Sdk/{Channel}/latest.version", + "aspnetcore-runtime" => $"{feed}/aspnetcore/Runtime/{Channel}/latest.version", + _ => $"{feed}/Runtime/{Channel}/latest.version" }; Log.LogMessage(MessageImportance.Low, $"Resolving latest version from url {latestVersionUrl}"); @@ -261,9 +295,11 @@ private async Task ResolveVersionAsync() versionResponse.EnsureSuccessStatusCode(); string latestVersionContent = await versionResponse.Content.ReadAsStringAsync(); string[] versionData = latestVersionContent.Split(Array.Empty(), StringSplitOptions.RemoveEmptyEntries); - Version = versionData[1]; - Log.LogMessage(MessageImportance.Low, $"Got latest dotnet cli version {Version}"); + version = versionData[1]; + Log.LogMessage(MessageImportance.Low, $"Got latest dotnet cli version {version}"); } + + return version; } } }