diff --git a/source/Calamari.Azure/AppServices/AzureTargetSite.cs b/source/Calamari.Azure/AppServices/AzureTargetSite.cs index 5888e1c88..4900b75fd 100644 --- a/source/Calamari.Azure/AppServices/AzureTargetSite.cs +++ b/source/Calamari.Azure/AppServices/AzureTargetSite.cs @@ -5,7 +5,6 @@ using Calamari.CloudAccounts; using Calamari.Common.Plumbing.Logging; using Calamari.Common.Plumbing.Variables; -using Microsoft.Azure.Management.Fluent; namespace Calamari.Azure.AppServices { diff --git a/source/Calamari.Azure/AzureClient.cs b/source/Calamari.Azure/AzureClient.cs index 1be95c9a8..9e211bcc9 100644 --- a/source/Calamari.Azure/AzureClient.cs +++ b/source/Calamari.Azure/AzureClient.cs @@ -21,9 +21,8 @@ public static class AzureClient /// public static ArmClient CreateArmClient(this IAzureAccount azureAccount, Action retryOptionsSetter = null) { - if (azureAccount.AccountType == AccountType.AzureOidc) + if (azureAccount is AzureOidcAccount oidcAccount) { - var oidcAccount = (AzureOidcAccount)azureAccount; var (clientOptions, _) = GetArmClientOptions(azureAccount, retryOptionsSetter); var clientAssertionCreds = new ClientAssertionCredential(oidcAccount.TenantId, oidcAccount.ClientId, () => oidcAccount.GetCredentials); return new ArmClient(clientAssertionCreds, defaultSubscriptionId: azureAccount.SubscriptionNumber, clientOptions); @@ -76,22 +75,5 @@ public static (ArmClientOptions, TokenCredentialOptions) GetArmClientOptions(thi return (armClientOptions, tokenCredentialOptions); } - - public static async Task GetAccessTokenAsync(this IAzureAccount azureAccount) - { - return azureAccount.AccountType == AccountType.AzureOidc - ? await AzureOidcAccountExtensions.GetAuthorizationToken(azureAccount.TenantId, - azureAccount.ClientId, - azureAccount.GetCredentials, - azureAccount.ResourceManagementEndpointBaseUri, - azureAccount.ActiveDirectoryEndpointBaseUri, - azureAccount.AzureEnvironment, - CancellationToken.None) - : await AzureServicePrincipalAccountExtensions.GetAuthorizationToken(azureAccount.TenantId, - azureAccount.ClientId, - azureAccount.GetCredentials, - azureAccount.ResourceManagementEndpointBaseUri, - azureAccount.ActiveDirectoryEndpointBaseUri); - } } } \ No newline at end of file diff --git a/source/Calamari.Azure/AzureKnownEnvironment.cs b/source/Calamari.Azure/AzureKnownEnvironment.cs index 21ef40666..2424eacf5 100644 --- a/source/Calamari.Azure/AzureKnownEnvironment.cs +++ b/source/Calamari.Azure/AzureKnownEnvironment.cs @@ -1,59 +1,46 @@ using System; using Azure.Identity; using Azure.ResourceManager; -using Microsoft.Azure.Management.ResourceManager.Fluent; namespace Calamari.Azure { public sealed class AzureKnownEnvironment - { + { /// The environment name exactly matching the names defined in Azure SDK (see here https://github.com/Azure/azure-libraries-for-net/blob/master/src/ResourceManagement/ResourceManager/AzureEnvironment.cs) /// Other names are allowed in case this list is ever expanded/changed, but will likely result in an error at deployment time. /// public AzureKnownEnvironment(string environment) { Value = environment; - - if (string.IsNullOrEmpty(environment) || environment == "AzureCloud") // This environment name is defined in Sashimi.Azure.Accounts.AzureEnvironmentsListAction - Value = Global.Value; // We interpret it as the normal Azure environment for historical reasons) - azureSdkEnvironment = AzureEnvironment.FromName(Value) ?? - throw new InvalidOperationException($"Unknown environment name {Value}"); + if (string.IsNullOrEmpty(environment) || environment == "AzureCloud") // This environment name is defined in Sashimi.Azure.Accounts.AzureEnvironmentsListAction + { + Value = "AzureGlobalCloud"; // We interpret it as the normal Azure environment for historical reasons) + } } - private readonly AzureEnvironment azureSdkEnvironment; public string Value { get; } - public static readonly AzureKnownEnvironment Global = new AzureKnownEnvironment("AzureGlobalCloud"); - public static readonly AzureKnownEnvironment AzureChinaCloud = new AzureKnownEnvironment("AzureChinaCloud"); - public static readonly AzureKnownEnvironment AzureUSGovernment = new AzureKnownEnvironment("AzureUSGovernment"); - public static readonly AzureKnownEnvironment AzureGermanCloud = new AzureKnownEnvironment("AzureGermanCloud"); - - public AzureEnvironment AsAzureSDKEnvironment() - { - return azureSdkEnvironment; - } - public ArmEnvironment AsAzureArmEnvironment() => ToArmEnvironment(Value); - private static ArmEnvironment ToArmEnvironment(string name) => name switch - { - "AzureGlobalCloud" => ArmEnvironment.AzurePublicCloud, - "AzureChinaCloud" => ArmEnvironment.AzureChina, - "AzureGermanCloud" => ArmEnvironment.AzureGermany, - "AzureUSGovernment" => ArmEnvironment.AzureGovernment, - _ => throw new InvalidOperationException($"ARM Environment {name} is not a known Azure Environment name.") - }; - + static ArmEnvironment ToArmEnvironment(string name) => name switch + { + "AzureGlobalCloud" => ArmEnvironment.AzurePublicCloud, + "AzureChinaCloud" => ArmEnvironment.AzureChina, + "AzureGermanCloud" => ArmEnvironment.AzureGermany, + "AzureUSGovernment" => ArmEnvironment.AzureGovernment, + _ => throw new InvalidOperationException($"ARM Environment {name} is not a known Azure Environment name.") + }; + public Uri GetAzureAuthorityHost() => ToAzureAuthorityHost(Value); - private static Uri ToAzureAuthorityHost(string name) => name switch - { - "AzureGlobalCloud" => AzureAuthorityHosts.AzurePublicCloud, - "AzureChinaCloud" => AzureAuthorityHosts.AzureChina, - "AzureGermanCloud" => AzureAuthorityHosts.AzureGermany, - "AzureUSGovernment" => AzureAuthorityHosts.AzureGovernment, - _ => throw new InvalidOperationException($"ARM Environment {name} is not a known Azure Environment name.") - }; + static Uri ToAzureAuthorityHost(string name) => name switch + { + "AzureGlobalCloud" => AzureAuthorityHosts.AzurePublicCloud, + "AzureChinaCloud" => AzureAuthorityHosts.AzureChina, + "AzureGermanCloud" => AzureAuthorityHosts.AzureGermany, + "AzureUSGovernment" => AzureAuthorityHosts.AzureGovernment, + _ => throw new InvalidOperationException($"ARM Environment {name} is not a known Azure Environment name.") + }; } -} +} \ No newline at end of file diff --git a/source/Calamari.Azure/Calamari.Azure.csproj b/source/Calamari.Azure/Calamari.Azure.csproj index e2b136310..b27774923 100644 --- a/source/Calamari.Azure/Calamari.Azure.csproj +++ b/source/Calamari.Azure/Calamari.Azure.csproj @@ -10,9 +10,10 @@ net462;net6.0 - - + + + diff --git a/source/Calamari.Azure/Kubernetes/Discovery/AzureKubernetesDiscoverer.cs b/source/Calamari.Azure/Kubernetes/Discovery/AzureKubernetesDiscoverer.cs index aba6f63ae..8f68657b7 100644 --- a/source/Calamari.Azure/Kubernetes/Discovery/AzureKubernetesDiscoverer.cs +++ b/source/Calamari.Azure/Kubernetes/Discovery/AzureKubernetesDiscoverer.cs @@ -2,10 +2,12 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using Azure; +using Azure.ResourceManager.ContainerService; +using Azure.ResourceManager.Resources; using Calamari.CloudAccounts; using Calamari.Common.Features.Discovery; using Calamari.Common.Plumbing.Logging; -using Microsoft.Rest.Azure; using Newtonsoft.Json; namespace Calamari.Azure.Kubernetes.Discovery @@ -39,38 +41,41 @@ public override IEnumerable DiscoverClusters(string contextJs Log.Verbose($" Subscription ID: {account.SubscriptionNumber}"); Log.Verbose($" Tenant ID: {account.TenantId}"); Log.Verbose($" Client ID: {account.ClientId}"); - var azureClient = account.CreateAzureClient(); - var discoveredClusters = new List(); + var armClient = account.CreateArmClient(); - // There appears to be an issue where the azure client returns stale data - // We need to upgrade this to use the newer SDK, but we need to upgrade to .NET 4.6.2 to support that. - var resourceGroups = azureClient.ResourceGroups.List(); - //we don't care about resource groups that are being deleted - foreach (var resourceGroup in resourceGroups.Where(rg => rg.ProvisioningState != "Deleting")) + var discoveredClusters = new List(); + + var subscriptionResource = armClient.GetSubscriptionResource(SubscriptionResource.CreateResourceIdentifier(account.SubscriptionNumber)); + + var resourceGroups = subscriptionResource.GetResourceGroups().GetAll();//.GetAll("provisioningState ne 'Deleting'"); + + //we don't care about resource groups that are being deleted + foreach (var resourceGroupResource in resourceGroups.Where(rg => !string.Equals(rg.Data.ResourceGroupProvisioningState,"Deleting", StringComparison.OrdinalIgnoreCase))) { try { // There appears to be an issue where the azure client returns stale data // to mitigate this, specifically for scenario's where the resource group doesn't exist anymore // we specifically list the clusters in each resource group - var clusters = azureClient.KubernetesClusters.ListByResourceGroup(resourceGroup.Name); + var clusters = resourceGroupResource.GetContainerServiceManagedClusters().GetAll(); + discoveredClusters.AddRange( clusters .Select(c => KubernetesCluster.CreateForAks( - $"aks/{account.SubscriptionNumber}/{c.ResourceGroupName}/{c.Name}", - c.Name, - c.ResourceGroupName, + $"aks/{account.SubscriptionNumber}/{resourceGroupResource.Data.Name}/{c.Data.Name}", + c.Data.Name, + resourceGroupResource.Data.Name, accountId, - c.Tags.ToTargetTags()))); + c.Data.Tags.ToTargetTags()))); } - catch (CloudException ex) + catch (RequestFailedException ex) { - Log.Verbose($"Failed to list kubernetes clusters for resource group {resourceGroup.Name}. Response message: {ex.Message}, Status code: {ex.Response.StatusCode}"); + Log.Verbose($"Failed to list kubernetes clusters for resource group {resourceGroupResource.Data.Name}. Response message: {ex.Message}, Status code: {ex.Status}"); // if the resource group was not found, we don't care and move on - if (ex.Response.StatusCode == HttpStatusCode.NotFound && ex.Message.StartsWith("Resource group")) + if (ex.Status == (int)HttpStatusCode.NotFound && ex.GetRawResponse()?.Content.ToString().StartsWith("Resource group") == true) continue; //throw in all other scenario's diff --git a/source/Calamari.AzureAppService.Tests/Calamari.AzureAppService.Tests.csproj b/source/Calamari.AzureAppService.Tests/Calamari.AzureAppService.Tests.csproj index f861374e2..e6e9763ec 100644 --- a/source/Calamari.AzureAppService.Tests/Calamari.AzureAppService.Tests.csproj +++ b/source/Calamari.AzureAppService.Tests/Calamari.AzureAppService.Tests.csproj @@ -10,15 +10,13 @@ - - + - diff --git a/source/Calamari.AzureAppService/Behaviors/AzureAppServiceZipDeployBehaviour.cs b/source/Calamari.AzureAppService/Behaviors/AzureAppServiceZipDeployBehaviour.cs index f9c6b8c39..af81055fd 100644 --- a/source/Calamari.AzureAppService/Behaviors/AzureAppServiceZipDeployBehaviour.cs +++ b/source/Calamari.AzureAppService/Behaviors/AzureAppServiceZipDeployBehaviour.cs @@ -132,6 +132,7 @@ public async Task Execute(RunningDeployment context) { uploadFile = await packageProvider.ConvertToAzureSupportedFile(packageFileInfo); } + uploadPath = uploadFile.FullName; uploadFileNeedsCleaning = packageFileInfo.Extension != uploadFile.Extension; @@ -156,14 +157,26 @@ public async Task Execute(RunningDeployment context) //Need to check if site turn off var scmPublishEnabled = await armClient.IsScmPublishEnabled(targetSite); - + if (packageProvider.SupportsAsynchronousDeployment && FeatureToggle.AsynchronousAzureZipDeployFeatureToggle.IsEnabled(context.Variables)) { - await UploadZipAndPollAsync(account, publishingProfile, scmPublishEnabled, uploadPath, targetSite.ScmSiteAndSlot, packageProvider, pollingTimeout, asyncZipDeployTimeoutPolicy); + await UploadZipAndPollAsync(account, + publishingProfile, + scmPublishEnabled, + uploadPath, + targetSite.ScmSiteAndSlot, + packageProvider, + pollingTimeout, + asyncZipDeployTimeoutPolicy); } else { - await UploadZipAsync(account, publishingProfile, scmPublishEnabled, uploadPath, targetSite.ScmSiteAndSlot, packageProvider); + await UploadZipAsync(account, + publishingProfile, + scmPublishEnabled, + uploadPath, + targetSite.ScmSiteAndSlot, + packageProvider); } } finally @@ -237,7 +250,7 @@ private async Task UploadZipAsync(IAzureAccount azureAccount, #endif using var streamContent = new StreamContent(fileStream); streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); - + //we have to create a new request message each time var request = new HttpRequestMessage(HttpMethod.Post, zipUploadUrl) { @@ -259,7 +272,7 @@ private async Task UploadZipAsync(IAzureAccount azureAccount, Log.Verbose("Finished deploying"); } - static async Task GetAuthenticationHeaderValue(IAzureAccount azureAccount, PublishingProfile publishingProfile, bool scmPublishEnabled) + async Task GetAuthenticationHeaderValue(IAzureAccount azureAccount, PublishingProfile publishingProfile, bool scmPublishEnabled) { AuthenticationHeaderValue authenticationHeader; if (scmPublishEnabled) @@ -268,7 +281,7 @@ static async Task GetAuthenticationHeaderValue(IAzure } else { - var accessToken = await azureAccount.GetAccessTokenAsync(); + var accessToken = await azureAccount.GetAuthorizationToken(Log, CancellationToken.None); authenticationHeader = new AuthenticationHeaderValue("Bearer", accessToken); } diff --git a/source/Calamari.AzureAppService/Calamari.AzureAppService.csproj b/source/Calamari.AzureAppService/Calamari.AzureAppService.csproj index 9840d4e4c..605223238 100644 --- a/source/Calamari.AzureAppService/Calamari.AzureAppService.csproj +++ b/source/Calamari.AzureAppService/Calamari.AzureAppService.csproj @@ -23,7 +23,6 @@ - diff --git a/source/Calamari.AzureResourceGroup/Calamari.AzureResourceGroup.csproj b/source/Calamari.AzureResourceGroup/Calamari.AzureResourceGroup.csproj index 72bbee5c2..bd942d08a 100644 --- a/source/Calamari.AzureResourceGroup/Calamari.AzureResourceGroup.csproj +++ b/source/Calamari.AzureResourceGroup/Calamari.AzureResourceGroup.csproj @@ -12,11 +12,9 @@ - - - - - + + + all runtime; build; native; contentfiles; analyzers diff --git a/source/Calamari.AzureResourceGroup/LegacyDeployAzureResourceGroupBehaviour.cs b/source/Calamari.AzureResourceGroup/LegacyDeployAzureResourceGroupBehaviour.cs index aab959b9f..7ea9e7829 100644 --- a/source/Calamari.AzureResourceGroup/LegacyDeployAzureResourceGroupBehaviour.cs +++ b/source/Calamari.AzureResourceGroup/LegacyDeployAzureResourceGroupBehaviour.cs @@ -12,9 +12,9 @@ using Calamari.Common.Plumbing.Variables; using Microsoft.Azure.Management.ResourceManager; using Microsoft.Azure.Management.ResourceManager.Models; +using Microsoft.Rest; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Octopus.CoreUtilities.Extensions; using AzureResourceManagerDeployment = Microsoft.Azure.Management.ResourceManager.Models.Deployment; namespace Calamari.AzureResourceGroup @@ -31,9 +31,9 @@ public LegacyDeployAzureResourceGroupBehaviour(TemplateService templateService, this.parameterNormalizer = parameterNormalizer; this.log = log; } - + public bool IsEnabled(RunningDeployment context) => !FeatureToggle.ModernAzureSdkFeatureToggle.IsEnabled(context.Variables); - + public async Task Execute(RunningDeployment deployment) { var variables = deployment.Variables; @@ -49,6 +49,7 @@ public async Task Execute(RunningDeployment deployment) templateFile = variables.Get(SpecialVariables.Action.Azure.ResourceGroupTemplate); templateParametersFile = variables.Get(SpecialVariables.Action.Azure.ResourceGroupTemplateParameters); } + var resourceManagementEndpoint = variables.Get(AzureAccountVariables.ResourceManagementEndPoint, DefaultVariables.ResourceManagementEndpoint); if (resourceManagementEndpoint != DefaultVariables.ResourceManagementEndpoint) @@ -62,7 +63,7 @@ public async Task Execute(RunningDeployment deployment) ? variables[SpecialVariables.Action.Azure.ResourceGroupDeploymentName] : GenerateDeploymentNameFromStepName(variables[ActionVariables.Name]); var deploymentMode = (DeploymentMode)Enum.Parse(typeof(DeploymentMode), - variables[SpecialVariables.Action.Azure.ResourceGroupDeploymentMode]); + variables[SpecialVariables.Action.Azure.ResourceGroupDeploymentMode]); var template = templateService.GetSubstitutedTemplateContent(templateFile, filesInPackageOrRepository, variables); var parameters = !string.IsNullOrWhiteSpace(templateParametersFile) ? parameterNormalizer.Normalize(templateService.GetSubstitutedTemplateContent(templateParametersFile, filesInPackageOrRepository, variables)) @@ -72,21 +73,25 @@ public async Task Execute(RunningDeployment deployment) // We re-create the client each time it is required in order to get a new authorization-token. Else, the token can expire during long-running deployments. Func> createArmClient = async () => - { - var token = !jwt.IsNullOrEmpty() - ? await new AzureOidcAccount(variables).Credentials(CancellationToken.None) - : await new AzureServicePrincipalAccount(variables).Credentials(); - var resourcesClient = new ResourceManagementClient(token, AuthHttpClientFactory.ProxyClientHandler()) - { - SubscriptionId = subscriptionId, - BaseUri = new Uri(resourceManagementEndpoint), - }; - resourcesClient.HttpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); - resourcesClient.HttpClient.BaseAddress = new Uri(resourceManagementEndpoint); - return resourcesClient; - }; - - await CreateDeployment(createArmClient, resourceGroupName, deploymentName, deploymentMode, template, parameters); + { + var account = AzureAccountFactory.Create(variables); + var token = new TokenCredentials(await account.GetAuthorizationToken(log, CancellationToken.None)); + var resourcesClient = new ResourceManagementClient(token, AuthHttpClientFactory.ProxyClientHandler()) + { + SubscriptionId = subscriptionId, + BaseUri = new Uri(resourceManagementEndpoint), + }; + resourcesClient.HttpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); + resourcesClient.HttpClient.BaseAddress = new Uri(resourceManagementEndpoint); + return resourcesClient; + }; + + await CreateDeployment(createArmClient, + resourceGroupName, + deploymentName, + deploymentMode, + template, + parameters); await PollForCompletion(createArmClient, resourceGroupName, deploymentName, variables); } @@ -104,8 +109,12 @@ internal static string GenerateDeploymentNameFromStepName(string stepName) return deploymentName; } - async Task CreateDeployment(Func> createArmClient, string resourceGroupName, string deploymentName, - DeploymentMode deploymentMode, string template, string parameters) + async Task CreateDeployment(Func> createArmClient, + string resourceGroupName, + string deploymentName, + DeploymentMode deploymentMode, + string template, + string parameters) { log.Verbose($"Template:\n{template}\n"); if (parameters != null) @@ -141,8 +150,10 @@ async Task CreateDeployment(Func> createArmClien } } - async Task PollForCompletion(Func> createArmClient, string resourceGroupName, - string deploymentName, IVariables variables) + async Task PollForCompletion(Func> createArmClient, + string resourceGroupName, + string deploymentName, + IVariables variables) { // While the deployment is running, we poll to check its state. // We increase the poll interval according to the Fibonacci sequence, up to a maximum of 30 seconds. @@ -250,14 +261,17 @@ void LogCloudError(Microsoft.Rest.Azure.CloudError error, int count) { log.Error($"{indent}Message: {error.Message}"); } + if (!string.IsNullOrEmpty(error.Code)) { log.Error($"{indent}Code: {error.Code}"); } + if (!string.IsNullOrEmpty(error.Target)) { log.Error($"{indent}Target: {error.Target}"); } + foreach (var errorDetail in error.Details) { LogCloudError(errorDetail, ++count); diff --git a/source/Calamari.AzureScripting/Calamari.AzureScripting.csproj b/source/Calamari.AzureScripting/Calamari.AzureScripting.csproj index deae6c7a7..28ac37fca 100644 --- a/source/Calamari.AzureScripting/Calamari.AzureScripting.csproj +++ b/source/Calamari.AzureScripting/Calamari.AzureScripting.csproj @@ -25,8 +25,4 @@ - - - - diff --git a/source/Calamari.AzureServiceFabric/Calamari.AzureServiceFabric.csproj b/source/Calamari.AzureServiceFabric/Calamari.AzureServiceFabric.csproj index 2ac000069..084149b1d 100644 --- a/source/Calamari.AzureServiceFabric/Calamari.AzureServiceFabric.csproj +++ b/source/Calamari.AzureServiceFabric/Calamari.AzureServiceFabric.csproj @@ -10,9 +10,7 @@ 8.0 - - - + + diff --git a/source/Calamari.AzureWebApp.Tests/DeployAzureWebCommandFixture.cs b/source/Calamari.AzureWebApp.Tests/DeployAzureWebCommandFixture.cs index 08c7a7c32..53c990aad 100644 --- a/source/Calamari.AzureWebApp.Tests/DeployAzureWebCommandFixture.cs +++ b/source/Calamari.AzureWebApp.Tests/DeployAzureWebCommandFixture.cs @@ -58,6 +58,8 @@ public async Task Setup() tenantId = await ExternalVariables.Get(ExternalVariable.AzureSubscriptionTenantId, cancellationToken); subscriptionId = await ExternalVariables.Get(ExternalVariable.AzureSubscriptionId, cancellationToken); var resourceGroupLocation = Environment.GetEnvironmentVariable("AZURE_NEW_RESOURCE_REGION") ?? RandomAzureRegion.GetRandomRegionWithExclusions(); + + TestContext.WriteLine($"Resource Group Location: {resourceGroupLocation}"); var servicePrincipalAccount = new AzureServicePrincipalAccount(subscriptionId, clientId, @@ -110,11 +112,20 @@ public async Task Setup() TestContext.WriteLine($"Creating app service plan {resourceGroupResource.Data.Name}"); - var servicePlanResponse = await resourceGroupResource.GetAppServicePlans() + ArmOperation servicePlanResponse; + try + { + servicePlanResponse = await resourceGroupResource.GetAppServicePlans() .CreateOrUpdateAsync(WaitUntil.Completed, resourceGroupResource.Data.Name, appServicePlanData, cancellationToken); + } + catch (RequestFailedException e) + { + //try and catch errors so we can filter out bad regions + throw new Exception($"Failed to create app service plan in resource group {resourceGroupResource.Data.Name} in region {resourceGroupLocation}.", e); + } servicePlanResource = servicePlanResponse.Value; } diff --git a/source/Calamari.AzureWebApp/Calamari.AzureWebApp.csproj b/source/Calamari.AzureWebApp/Calamari.AzureWebApp.csproj index 3bcc9e49c..19df55f98 100644 --- a/source/Calamari.AzureWebApp/Calamari.AzureWebApp.csproj +++ b/source/Calamari.AzureWebApp/Calamari.AzureWebApp.csproj @@ -11,11 +11,8 @@ - - - diff --git a/source/Calamari.CloudAccounts/AzureOidcAccount.cs b/source/Calamari.CloudAccounts/AzureOidcAccount.cs index 3d039604b..da77abc6f 100644 --- a/source/Calamari.CloudAccounts/AzureOidcAccount.cs +++ b/source/Calamari.CloudAccounts/AzureOidcAccount.cs @@ -1,15 +1,10 @@ using System; -using System.Net; -using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Calamari.Common.Plumbing.Logging; using Calamari.Common.Plumbing.Variables; -using Microsoft.Azure.Management.Fluent; -using Microsoft.Azure.Management.ResourceManager.Fluent.Authentication; -using Microsoft.Rest; +using Microsoft.Identity.Client; using Newtonsoft.Json; -using NetWebRequest = System.Net.WebRequest; -using AzureEnvironmentEnum = Microsoft.Azure.Management.ResourceManager.Fluent.AzureEnvironment; namespace Calamari.CloudAccounts { @@ -41,13 +36,14 @@ public AzureOidcAccount(IVariables variables) TenantId = variables.Get(AccountVariables.TenantId); Jwt = variables.Get(AccountVariables.Jwt); AzureEnvironment = variables.Get(AccountVariables.Environment); - ResourceManagementEndpointBaseUri = variables.Get(AccountVariables.ResourceManagementEndPoint, DefaultVariables.ResourceManagementEndpoint); - ActiveDirectoryEndpointBaseUri = variables.Get(AccountVariables.ActiveDirectoryEndPoint, DefaultVariables.OidcAuthContextUri); + ResourceManagementEndpointBaseUri = variables.Get(AccountVariables.ResourceManagementEndPoint, DefaultAccountEndpoints.ResourceManagementEndpoint); + ActiveDirectoryEndpointBaseUri = variables.Get(AccountVariables.ActiveDirectoryEndPoint, DefaultAccountEndpoints.OidcAuthContextUri); } public AccountType AccountType => AccountType.AzureOidc; public string GetCredentials => Jwt; - public string SubscriptionNumber { get; } + + public string SubscriptionNumber { get; } public string ClientId { get; } public string TenantId { get; } public string Jwt { get; } @@ -55,7 +51,47 @@ public AzureOidcAccount(IVariables variables) public string ResourceManagementEndpointBaseUri { get; } public string ActiveDirectoryEndpointBaseUri { get; } - internal static string GetDefaultScope(string environmentName) + public async Task GetAuthorizationToken(ILog log, CancellationToken cancellationToken) + { + var authClientFactory = new AuthHttpClientFactory(); + + var authContext = GetOidcContextUri(string.IsNullOrEmpty(ActiveDirectoryEndpointBaseUri) ? "https://login.microsoftonline.com/" : ActiveDirectoryEndpointBaseUri, TenantId); + log.Verbose($"Authentication Context: {authContext}"); + + var app = ConfidentialClientApplicationBuilder.Create(ClientId) + .WithClientAssertion(GetCredentials) + .WithAuthority(authContext) + .WithHttpClientFactory(authClientFactory) + .Build(); + + var result = await app.AcquireTokenForClient( + // Default values set on a per cloud basis on AzureOidcAccount, if managementEndPoint is set on the account /.default is required. + new[] + { + ResourceManagementEndpointBaseUri == DefaultAccountEndpoints.ResourceManagementEndpoint || string.IsNullOrEmpty(ResourceManagementEndpointBaseUri) + ? AzureOidcAccount.GetDefaultScope(AzureEnvironment) + : ResourceManagementEndpointBaseUri.EndsWith(".default") + ? ResourceManagementEndpointBaseUri + : $"{ResourceManagementEndpointBaseUri}/.default" + }) + .WithTenantId(TenantId) + .ExecuteAsync(cancellationToken) + .ConfigureAwait(false); + + return result.AccessToken; + } + + static string GetOidcContextUri(string activeDirectoryEndPoint, string tenantId) + { + if (!activeDirectoryEndPoint.EndsWith("/")) + { + return $"{activeDirectoryEndPoint}/{tenantId}/v2.0"; + } + + return $"{activeDirectoryEndPoint}{tenantId}/v2.0"; + } + + static string GetDefaultScope(string environmentName) { switch (environmentName) { @@ -72,28 +108,5 @@ internal static string GetDefaultScope(string environmentName) return "https://management.azure.com//.default"; } } - - public IAzure CreateAzureClient() - { - var environment = string.IsNullOrEmpty(AzureEnvironment) || AzureEnvironment == "AzureCloud" - ? AzureEnvironmentEnum.AzureGlobalCloud - : AzureEnvironmentEnum.FromName(AzureEnvironment) ?? - throw new InvalidOperationException($"Unknown environment name {AzureEnvironment}"); - - var accessToken = this.GetAuthorizationToken(CancellationToken.None).GetAwaiter().GetResult(); - var credentials = new AzureCredentials( - new TokenCredentials(accessToken), - new TokenCredentials(accessToken), - TenantId, - environment); - - // to ensure the Azure API uses the appropriate web proxy - var client = new HttpClient(new HttpClientHandler {Proxy = NetWebRequest.DefaultWebProxy}); - - return Microsoft.Azure.Management.Fluent.Azure.Configure() - .WithHttpClient(client) - .Authenticate(credentials) - .WithSubscription(SubscriptionNumber); - } } } \ No newline at end of file diff --git a/source/Calamari.CloudAccounts/AzureOidcAccountExtensions.cs b/source/Calamari.CloudAccounts/AzureOidcAccountExtensions.cs deleted file mode 100644 index ae230f52e..000000000 --- a/source/Calamari.CloudAccounts/AzureOidcAccountExtensions.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Calamari.Common.Plumbing.Logging; -using Microsoft.Identity.Client; -using Microsoft.Rest; - -namespace Calamari.CloudAccounts -{ - public static class AzureOidcAccountExtensions - { - public static async Task Credentials(this AzureOidcAccount account, CancellationToken cancellationToken) - { - return new TokenCredentials(await GetAuthorizationToken(account, cancellationToken)); - } - - public static Task GetAuthorizationToken(this AzureOidcAccount account, CancellationToken cancellationToken) - { - return GetAuthorizationToken(account.TenantId, account.ClientId, account.GetCredentials, - account.ResourceManagementEndpointBaseUri, account.ActiveDirectoryEndpointBaseUri, account.AzureEnvironment, cancellationToken); - } - - public static async Task GetAuthorizationToken(string tenantId, string applicationId, string token, string managementEndPoint, string activeDirectoryEndPoint, string aureEnvironment, CancellationToken cancellationToken) - { - var authClientFactory = new AuthHttpClientFactory(); - - var authContext = GetOidcContextUri(string.IsNullOrEmpty(activeDirectoryEndPoint) ? "https://login.microsoftonline.com/" : activeDirectoryEndPoint, tenantId); - Log.Verbose($"Authentication Context: {authContext}"); - - var app = ConfidentialClientApplicationBuilder.Create(applicationId) - .WithClientAssertion(token) - .WithAuthority(authContext) - .WithHttpClientFactory(authClientFactory) - .Build(); - - var result = await app.AcquireTokenForClient( - // Default values set on a per cloud basis on AzureOidcAccount, if managementEndPoint is set on the account /.default is required. - new[] { managementEndPoint == DefaultVariables.ResourceManagementEndpoint || string.IsNullOrEmpty(managementEndPoint) ? AzureOidcAccount.GetDefaultScope(aureEnvironment) : managementEndPoint.EndsWith(".default") ? managementEndPoint : $"{managementEndPoint}/.default" }) - .WithTenantId(tenantId) - .ExecuteAsync(cancellationToken) - .ConfigureAwait(false); - return result.AccessToken; - } - - static string GetOidcContextUri(string activeDirectoryEndPoint, string tenantId) - { - if (!activeDirectoryEndPoint.EndsWith("/")) - { - return $"{activeDirectoryEndPoint}/{tenantId}/v2.0"; - } - return $"{activeDirectoryEndPoint}{tenantId}/v2.0"; - } - } -} \ No newline at end of file diff --git a/source/Calamari.CloudAccounts/AzureServicePrincipalAccount.cs b/source/Calamari.CloudAccounts/AzureServicePrincipalAccount.cs index 3a77f93ab..34315181c 100644 --- a/source/Calamari.CloudAccounts/AzureServicePrincipalAccount.cs +++ b/source/Calamari.CloudAccounts/AzureServicePrincipalAccount.cs @@ -1,11 +1,10 @@ using System; -using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Calamari.Common.Plumbing.Logging; using Calamari.Common.Plumbing.Variables; -using Microsoft.Azure.Management.Fluent; -using Microsoft.Azure.Management.ResourceManager.Fluent; +using Microsoft.Identity.Client; using Newtonsoft.Json; -using AzureEnvironmentEnum = Microsoft.Azure.Management.ResourceManager.Fluent.AzureEnvironment; -using NetWebRequest = System.Net.WebRequest; namespace Calamari.CloudAccounts { @@ -37,39 +36,54 @@ public AzureServicePrincipalAccount(IVariables variables) TenantId = variables.Get(AccountVariables.TenantId); Password = variables.Get(AccountVariables.Password); AzureEnvironment = variables.Get(AccountVariables.Environment); - ResourceManagementEndpointBaseUri = variables.Get(AccountVariables.ResourceManagementEndPoint, DefaultVariables.ResourceManagementEndpoint); - ActiveDirectoryEndpointBaseUri = variables.Get(AccountVariables.ActiveDirectoryEndPoint, DefaultVariables.ActiveDirectoryEndpoint); + ResourceManagementEndpointBaseUri = variables.Get(AccountVariables.ResourceManagementEndPoint, DefaultAccountEndpoints.ResourceManagementEndpoint); + ActiveDirectoryEndpointBaseUri = variables.Get(AccountVariables.ActiveDirectoryEndPoint, DefaultAccountEndpoints.ActiveDirectoryEndpoint); } public AccountType AccountType => AccountType.AzureServicePrincipal; public string GetCredentials => Password; - public string SubscriptionNumber { get; } + + public string SubscriptionNumber { get; } public string ClientId { get; } + public string TenantId { get; } + // Public for JsonDeserialization public string Password { get; } public string AzureEnvironment { get; } public string ResourceManagementEndpointBaseUri { get; } public string ActiveDirectoryEndpointBaseUri { get; } - public IAzure CreateAzureClient() + public async Task GetAuthorizationToken(ILog log, CancellationToken cancellationToken) { - var environment = string.IsNullOrEmpty(AzureEnvironment) || AzureEnvironment == "AzureCloud" - ? AzureEnvironmentEnum.AzureGlobalCloud - : AzureEnvironmentEnum.FromName(AzureEnvironment) ?? - throw new InvalidOperationException($"Unknown environment name {AzureEnvironment}"); + var authClientFactory = new AuthHttpClientFactory(); + + var authContext = GetContextUri(ActiveDirectoryEndpointBaseUri, TenantId); + log.Verbose($"Authentication Context: {authContext}"); - var credentials = SdkContext.AzureCredentialsFactory.FromServicePrincipal(ClientId, - GetCredentials, TenantId, environment - ); + var app = ConfidentialClientApplicationBuilder.Create(ClientId) + .WithClientSecret(GetCredentials) + .WithAuthority(authContext) + .WithHttpClientFactory(authClientFactory) + .Build(); - // to ensure the Azure API uses the appropriate web proxy - var client = new HttpClient(new HttpClientHandler {Proxy = NetWebRequest.DefaultWebProxy}); + var result = await app.AcquireTokenForClient( + new[] { $"{ResourceManagementEndpointBaseUri}/.default" }) + .WithTenantId(TenantId) + .ExecuteAsync(cancellationToken) + .ConfigureAwait(false); + + return result.AccessToken; + } + + static string GetContextUri(string activeDirectoryEndPoint, string tenantId) + { + if (!activeDirectoryEndPoint.EndsWith("/")) + { + return $"{activeDirectoryEndPoint}/{tenantId}"; + } - return Microsoft.Azure.Management.Fluent.Azure.Configure() - .WithHttpClient(client) - .Authenticate(credentials) - .WithSubscription(SubscriptionNumber); + return $"{activeDirectoryEndPoint}{tenantId}"; } } -} +} \ No newline at end of file diff --git a/source/Calamari.CloudAccounts/AzureServicePrincipalAccountExtensions.cs b/source/Calamari.CloudAccounts/AzureServicePrincipalAccountExtensions.cs deleted file mode 100644 index 13344e323..000000000 --- a/source/Calamari.CloudAccounts/AzureServicePrincipalAccountExtensions.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Threading.Tasks; -using Calamari.Common.Plumbing.Logging; -using Microsoft.Identity.Client; -using Microsoft.Rest; -using AzureEnvironmentEnum = Microsoft.Azure.Management.ResourceManager.Fluent.AzureEnvironment; - -namespace Calamari.CloudAccounts -{ - public static class AzureServicePrincipalAccountExtensions - { - public static async Task Credentials(this AzureServicePrincipalAccount account) - { - return new TokenCredentials(await GetAuthorizationToken(account)); - } - - public static Task GetAuthorizationToken(this AzureServicePrincipalAccount account) - { - return GetAuthorizationToken(account.TenantId, account.ClientId, account.GetCredentials, - account.ResourceManagementEndpointBaseUri, account.ActiveDirectoryEndpointBaseUri); - } - - public static async Task GetAuthorizationToken(string tenantId, string applicationId, string password, string managementEndPoint, string activeDirectoryEndPoint) - { - var authClientFactory = new AuthHttpClientFactory(); - - var authContext = GetContextUri(activeDirectoryEndPoint, tenantId); - Log.Verbose($"Authentication Context: {authContext}"); - - var app = ConfidentialClientApplicationBuilder.Create(applicationId) - .WithClientSecret(password) - .WithAuthority(authContext) - .WithHttpClientFactory(authClientFactory) - .Build(); - - var result = await app.AcquireTokenForClient( - new [] { $"{managementEndPoint}/.default" }) - .WithTenantId(tenantId) - .ExecuteAsync() - .ConfigureAwait(false); - - return result.AccessToken; - } - - static string GetContextUri(string activeDirectoryEndPoint, string tenantId) - { - if (!activeDirectoryEndPoint.EndsWith("/")) - { - return $"{activeDirectoryEndPoint}/{tenantId}"; - } - return $"{activeDirectoryEndPoint}{tenantId}"; - } - } -} diff --git a/source/Calamari.CloudAccounts/Calamari.CloudAccounts.csproj b/source/Calamari.CloudAccounts/Calamari.CloudAccounts.csproj index d86a9c838..437288896 100644 --- a/source/Calamari.CloudAccounts/Calamari.CloudAccounts.csproj +++ b/source/Calamari.CloudAccounts/Calamari.CloudAccounts.csproj @@ -17,10 +17,9 @@ + - - - + diff --git a/source/Calamari.CloudAccounts/DefaultVariables.cs b/source/Calamari.CloudAccounts/DefaultVariables.cs index 70f631d27..8236ca60b 100644 --- a/source/Calamari.CloudAccounts/DefaultVariables.cs +++ b/source/Calamari.CloudAccounts/DefaultVariables.cs @@ -2,7 +2,7 @@ namespace Calamari.CloudAccounts { - static class DefaultVariables + public static class DefaultAccountEndpoints { public const string ResourceManagementEndpoint = "https://management.azure.com/"; public const string GraphManagementEndpoint = "https://graph.microsoft.com/"; diff --git a/source/Calamari.CloudAccounts/IAzureAccount.cs b/source/Calamari.CloudAccounts/IAzureAccount.cs index 66cd204ed..e1b0e926d 100644 --- a/source/Calamari.CloudAccounts/IAzureAccount.cs +++ b/source/Calamari.CloudAccounts/IAzureAccount.cs @@ -1,7 +1,7 @@ using System; using System.Threading; using System.Threading.Tasks; -using Microsoft.Azure.Management.Fluent; +using Calamari.Common.Plumbing.Logging; namespace Calamari.CloudAccounts { @@ -16,7 +16,8 @@ public interface IAzureAccount AccountType AccountType { get; } string GetCredentials { get; } - IAzure CreateAzureClient(); + + Task GetAuthorizationToken(ILog log, CancellationToken cancellationToken); } public enum AccountType diff --git a/source/Calamari.Testing/RandomAzureRegion.cs b/source/Calamari.Testing/RandomAzureRegion.cs index ccc064da1..eb2b6fd5e 100644 --- a/source/Calamari.Testing/RandomAzureRegion.cs +++ b/source/Calamari.Testing/RandomAzureRegion.cs @@ -5,24 +5,24 @@ namespace Calamari.Testing { public static class RandomAzureRegion { - static Random random = new Random(); + static readonly Random Random = new(); - static string[] regions = new[] - { + static readonly string[] Regions = { "southeastasia", "centralus", "eastus", "eastus2", "westus", "westus2", - "australiaeast" + "australiaeast", + "australiasoutheast" }; public static string GetRandomRegionWithExclusions(params string[] excludedRegions) { - var possibleRegions = regions.Except(excludedRegions).ToArray(); + var possibleRegions = Regions.Except(excludedRegions).ToArray(); - return possibleRegions[random.Next(0, possibleRegions.Length)]; + return possibleRegions[Random.Next(0, possibleRegions.Length)]; } } } \ No newline at end of file