| File: Helpers\AzureAuthenticationHelpers.cs | Web Access |
| Project: src\tests\Aspire.Deployment.EndToEnd.Tests\Aspire.Deployment.EndToEnd.Tests.csproj (Aspire.Deployment.EndToEnd.Tests) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Azure.Core; using Azure.Identity; namespace Aspire.Deployment.EndToEnd.Tests.Helpers; /// <summary> /// Helper methods for Azure authentication in deployment tests. /// Supports both OIDC (Workload Identity Federation) in CI and Azure CLI locally. /// </summary> internal static class AzureAuthenticationHelpers { private const string SubscriptionEnvVar = "ASPIRE_DEPLOYMENT_TEST_SUBSCRIPTION"; private const string ResourceGroupPrefixEnvVar = "ASPIRE_DEPLOYMENT_TEST_RG_PREFIX"; private const string DefaultResourceGroupPrefix = "aspire-e2e"; /// <summary> /// Gets whether Azure authentication is available. /// Returns true if we have either OIDC credentials (CI) or Azure CLI credentials (local). /// </summary> internal static bool IsAzureAuthAvailable() { try { var credential = GetAzureCredential(); // Try to get a token to validate credentials work var context = new TokenRequestContext(["https://management.azure.com/.default"]); credential.GetToken(context, CancellationToken.None); return true; } catch { return false; } } /// <summary> /// Gets the Azure credential to use for authentication. /// In CI (GitHub Actions), uses DefaultAzureCredential which will pick up OIDC. /// Locally, falls back to Azure CLI credentials. /// </summary> internal static TokenCredential GetAzureCredential() { // DefaultAzureCredential tries multiple authentication methods in order: // 1. Environment variables (AZURE_CLIENT_ID, AZURE_TENANT_ID for OIDC in CI) // 2. Managed Identity // 3. Azure CLI // 4. Azure PowerShell // 5. etc. return new DefaultAzureCredential(); } /// <summary> /// Gets the subscription ID from environment variable. /// </summary> /// <exception cref="InvalidOperationException">Thrown when subscription ID is not configured.</exception> internal static string GetSubscriptionId() { var subscriptionId = Environment.GetEnvironmentVariable(SubscriptionEnvVar); if (string.IsNullOrEmpty(subscriptionId)) { // Also check AZURE_SUBSCRIPTION_ID as fallback subscriptionId = Environment.GetEnvironmentVariable("AZURE_SUBSCRIPTION_ID"); } if (string.IsNullOrEmpty(subscriptionId)) { throw new InvalidOperationException( $"Azure subscription ID not configured. Set the {SubscriptionEnvVar} or AZURE_SUBSCRIPTION_ID environment variable."); } return subscriptionId; } /// <summary> /// Gets the subscription ID if available, or null if not configured. /// </summary> internal static string? TryGetSubscriptionId() { try { return GetSubscriptionId(); } catch { return null; } } /// <summary> /// Gets the resource group prefix for naming test resources. /// </summary> internal static string GetResourceGroupPrefix() { var prefix = Environment.GetEnvironmentVariable(ResourceGroupPrefixEnvVar); return string.IsNullOrEmpty(prefix) ? DefaultResourceGroupPrefix : prefix; } /// <summary> /// Generates a unique resource group name for a test run. /// Format: {prefix}-{testname-hash}-{YYYYMMDD-HHMMSS}-{runId} /// The timestamp is embedded for cleanup workflow to determine age. /// </summary> internal static string GenerateResourceGroupName(string? testName = null) { var prefix = GetResourceGroupPrefix(); // Include full timestamp for cleanup age detection var timestamp = DateTime.UtcNow.ToString("yyyyMMddHHmmss"); // Use GitHub run ID if available, otherwise generate random suffix var runId = Environment.GetEnvironmentVariable("GITHUB_RUN_ID"); var suffix = !string.IsNullOrEmpty(runId) ? runId[..Math.Min(8, runId.Length)] : Guid.NewGuid().ToString("N")[..8]; if (!string.IsNullOrEmpty(testName)) { // Create a short hash of the test name for uniqueness var hash = GetTestNameHash(testName); // Format: aspire-e2e-{hash}-{timestamp}-{runId} // Max length: 10 + 1 + 8 + 1 + 14 + 1 + 8 = 43 chars (well under 63 limit) return $"{prefix}-{hash}-{timestamp}-{suffix}"; } return $"{prefix}-{timestamp}-{suffix}"; } /// <summary> /// Creates a short hash of the test name for resource naming. /// </summary> private static string GetTestNameHash(string testName) { // Use a simple hash to create a short, deterministic identifier var hash = 0; foreach (var c in testName) { hash = (hash * 31) + c; } // Return 8 hex chars (lowercase for Azure naming) return Math.Abs(hash).ToString("x8"); } /// <summary> /// Parses the timestamp from a resource group name. /// Expected format: {prefix}-{hash}-{YYYYMMDDHHMMSS}-{suffix} /// Returns null if the format is invalid. /// </summary> internal static DateTime? ParseResourceGroupTimestamp(string resourceGroupName) { // Look for 14-digit timestamp pattern (YYYYMMDDHHMMSS) var parts = resourceGroupName.Split('-'); foreach (var part in parts) { if (part.Length == 14 && long.TryParse(part, out _)) { if (DateTime.TryParseExact(part, "yyyyMMddHHmmss", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.AssumeUniversal, out var timestamp)) { return DateTime.SpecifyKind(timestamp, DateTimeKind.Utc); } } } return null; } /// <summary> /// Gets the Azure tenant ID if configured. /// </summary> internal static string? GetTenantId() { return Environment.GetEnvironmentVariable("AZURE_TENANT_ID"); } /// <summary> /// Gets the Azure client ID for OIDC authentication if configured. /// </summary> internal static string? GetClientId() { return Environment.GetEnvironmentVariable("AZURE_CLIENT_ID"); } /// <summary> /// Checks if OIDC credentials are configured (for CI environment). /// </summary> internal static bool IsOidcConfigured() { return !string.IsNullOrEmpty(GetClientId()) && !string.IsNullOrEmpty(GetTenantId()); } }