From e1e3d44b9a859366aa36da7baff5683bf8988b9e Mon Sep 17 00:00:00 2001 From: David Boike Date: Wed, 9 Apr 2025 17:23:22 -0500 Subject: [PATCH 1/4] Support ASB Geo-Recovery alias for querying queue names --- src/Directory.Packages.props | 1 + src/ServiceControl.Transports.ASBS/AzureQuery.cs | 15 ++++++++++++++- .../ServiceControl.Transports.ASBS.csproj | 1 + 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index d8ed8b67d5..dca2126a97 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -12,6 +12,7 @@ + diff --git a/src/ServiceControl.Transports.ASBS/AzureQuery.cs b/src/ServiceControl.Transports.ASBS/AzureQuery.cs index 42b6c37ca4..b66b53e246 100644 --- a/src/ServiceControl.Transports.ASBS/AzureQuery.cs +++ b/src/ServiceControl.Transports.ASBS/AzureQuery.cs @@ -18,6 +18,8 @@ namespace ServiceControl.Transports.ASBS; using Azure.ResourceManager.Resources; using Azure.ResourceManager.ServiceBus; using BrokerThroughput; +using DnsClient; +using DnsClient.Protocol; using Microsoft.Extensions.Logging; public class AzureQuery(ILogger logger, TimeProvider timeProvider, TransportSettings transportSettings) @@ -263,6 +265,17 @@ async Task> GetMetrics(string queueName, DateOnly sta public override async IAsyncEnumerable GetQueueNames( [EnumeratorCancellation] CancellationToken cancellationToken = default) { + var validNamespaces = new HashSet(StringComparer.OrdinalIgnoreCase) { serviceBusName }; + + var dnsLookup = new LookupClient(); + var dnsResult = await dnsLookup.QueryAsync($"{serviceBusName}.servicebus.windows.net", QueryType.CNAME, cancellationToken: cancellationToken); + var domain = (dnsResult.Answers.FirstOrDefault() as CNameRecord)?.CanonicalName.Value; + if (domain is not null && domain.EndsWith(".servicebus.windows.net.")) + { + var otherName = domain.Split('.').First(); + validNamespaces.Add(otherName); + } + SubscriptionResource? subscription = await armClient!.GetDefaultSubscriptionAsync(cancellationToken); var namespaces = subscription.GetServiceBusNamespacesAsync(cancellationToken); @@ -270,7 +283,7 @@ public override async IAsyncEnumerable GetQueueNames( await foreach (var serviceBusNamespaceResource in namespaces.WithCancellation( cancellationToken)) { - if (serviceBusNamespaceResource.Data.Name == serviceBusName) + if (validNamespaces.Contains(serviceBusNamespaceResource.Data.Name)) { resourceId = serviceBusNamespaceResource.Id; await foreach (var queue in serviceBusNamespaceResource.GetServiceBusQueues() diff --git a/src/ServiceControl.Transports.ASBS/ServiceControl.Transports.ASBS.csproj b/src/ServiceControl.Transports.ASBS/ServiceControl.Transports.ASBS.csproj index 540231446d..ef7f0fd2f5 100644 --- a/src/ServiceControl.Transports.ASBS/ServiceControl.Transports.ASBS.csproj +++ b/src/ServiceControl.Transports.ASBS/ServiceControl.Transports.ASBS.csproj @@ -14,6 +14,7 @@ + From 5023cd940975acc772a532312365e3b71c4d7261 Mon Sep 17 00:00:00 2001 From: David Boike Date: Thu, 10 Apr 2025 11:30:19 -0500 Subject: [PATCH 2/4] Account for other regional clouds --- .../AzureQuery.cs | 54 +++++++++++++------ 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/src/ServiceControl.Transports.ASBS/AzureQuery.cs b/src/ServiceControl.Transports.ASBS/AzureQuery.cs index b66b53e246..c5357385b7 100644 --- a/src/ServiceControl.Transports.ASBS/AzureQuery.cs +++ b/src/ServiceControl.Transports.ASBS/AzureQuery.cs @@ -29,6 +29,8 @@ public class AzureQuery(ILogger logger, TimeProvider timeProvider, T MetricsQueryClient? client; ArmClient? armClient; string? resourceId; + ArmEnvironment armEnvironment; + MetricsQueryAudience metricsQueryAudience; protected override void InitializeCore(ReadOnlyDictionary settings) { @@ -101,11 +103,11 @@ protected override void InitializeCore(ReadOnlyDictionary settin Diagnostics.AppendLine("Client secret set"); } - (ArmEnvironment armEnvironment, MetricsQueryAudience metricsQueryAudience) environment = GetEnvironment(); + (armEnvironment, metricsQueryAudience) = GetEnvironment(); if (managementUrl == null) { - Diagnostics.AppendLine($"Management Url not set, defaulted to \"{environment.armEnvironment.Endpoint}\""); + Diagnostics.AppendLine($"Management Url not set, defaulted to \"{armEnvironment.Endpoint}\""); } else { @@ -128,10 +130,10 @@ protected override void InitializeCore(ReadOnlyDictionary settin clientCredentials = new ClientSecretCredential(tenantId, clientId, clientSecret); } - client = new MetricsQueryClient(environment.armEnvironment.Endpoint, clientCredentials, + client = new MetricsQueryClient(armEnvironment.Endpoint, clientCredentials, new MetricsQueryClientOptions { - Audience = environment.metricsQueryAudience, + Audience = metricsQueryAudience, Transport = new HttpClientTransport( new HttpClient(new SocketsHttpHandler { @@ -141,7 +143,7 @@ protected override void InitializeCore(ReadOnlyDictionary settin armClient = new ArmClient(clientCredentials, subscriptionId, new ArmClientOptions { - Environment = environment.armEnvironment, + Environment = armEnvironment, Transport = new HttpClientTransport( new HttpClient(new SocketsHttpHandler { @@ -265,16 +267,7 @@ async Task> GetMetrics(string queueName, DateOnly sta public override async IAsyncEnumerable GetQueueNames( [EnumeratorCancellation] CancellationToken cancellationToken = default) { - var validNamespaces = new HashSet(StringComparer.OrdinalIgnoreCase) { serviceBusName }; - - var dnsLookup = new LookupClient(); - var dnsResult = await dnsLookup.QueryAsync($"{serviceBusName}.servicebus.windows.net", QueryType.CNAME, cancellationToken: cancellationToken); - var domain = (dnsResult.Answers.FirstOrDefault() as CNameRecord)?.CanonicalName.Value; - if (domain is not null && domain.EndsWith(".servicebus.windows.net.")) - { - var otherName = domain.Split('.').First(); - validNamespaces.Add(otherName); - } + var validNamespaces = await GetValidNamespaceNames(cancellationToken); SubscriptionResource? subscription = await armClient!.GetDefaultSubscriptionAsync(cancellationToken); var namespaces = @@ -299,6 +292,37 @@ public override async IAsyncEnumerable GetQueueNames( throw new Exception($"Could not find a ServiceBus named \"{serviceBusName}\""); } + async Task> GetValidNamespaceNames(CancellationToken cancellationToken = default) + { + var validNamespaces = new HashSet(StringComparer.OrdinalIgnoreCase) { serviceBusName }; + + // ArmEnvironment Audience Values: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/resourcemanager/Azure.ResourceManager/src/ArmEnvironment.cs + // Service Bus Domains: https://learn.microsoft.com/en-us/rest/api/servicebus/ + var serviceBusCloudDomain = armEnvironment.Audience switch + { + "https://management.usgovcloudapi.net" => "servicebus.usgovcloudapi.net", + "https://management.microsoftazure.de" => "servicebus.cloudapi.de", + "https://management.chinacloudapi.cn" => "servicebus.chinacloudapi.cn", + _ => "servicebus.windows.net" + }; + + var queryDomain = $"{serviceBusName}.{serviceBusCloudDomain}"; + var validDomainTail = $".{serviceBusCloudDomain}."; + + var dnsLookup = new LookupClient(); + var dnsResult = await dnsLookup.QueryAsync(queryDomain, QueryType.CNAME, cancellationToken: cancellationToken); + var domain = (dnsResult.Answers.FirstOrDefault() as CNameRecord)?.CanonicalName.Value; + if (domain is not null && domain.EndsWith(validDomainTail)) + { + // In some cases, like private networking access, result might be something like `namespacename.private` with a dot in the middle + // which is not a big deal because that will not actually match a namespace name in metrics + var otherName = domain[..^validDomainTail.Length]; + validNamespaces.Add(otherName); + } + + return validNamespaces; + } + public override string SanitizedEndpointNameCleanser(string endpointName) => endpointName.ToLower(); public override KeyDescriptionPair[] Settings => From 22c0f17e912b2222f0ef24d52f527707f9b41c05 Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Thu, 10 Apr 2025 14:16:51 -0400 Subject: [PATCH 3/4] Tweaks --- src/ServiceControl.Transports.ASBS/AzureQuery.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ServiceControl.Transports.ASBS/AzureQuery.cs b/src/ServiceControl.Transports.ASBS/AzureQuery.cs index c5357385b7..c52d485c17 100644 --- a/src/ServiceControl.Transports.ASBS/AzureQuery.cs +++ b/src/ServiceControl.Transports.ASBS/AzureQuery.cs @@ -30,7 +30,6 @@ public class AzureQuery(ILogger logger, TimeProvider timeProvider, T ArmClient? armClient; string? resourceId; ArmEnvironment armEnvironment; - MetricsQueryAudience metricsQueryAudience; protected override void InitializeCore(ReadOnlyDictionary settings) { @@ -103,7 +102,7 @@ protected override void InitializeCore(ReadOnlyDictionary settin Diagnostics.AppendLine("Client secret set"); } - (armEnvironment, metricsQueryAudience) = GetEnvironment(); + (armEnvironment, var metricsQueryAudience) = GetEnvironment(); if (managementUrl == null) { From e380bfa0afe5e13e0add28059c5007d406b6e76e Mon Sep 17 00:00:00 2001 From: David Boike Date: Thu, 10 Apr 2025 14:14:05 -0500 Subject: [PATCH 4/4] Use a dictionary on ArmEnvironment since it is IEquatable --- .../AzureQuery.cs | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/ServiceControl.Transports.ASBS/AzureQuery.cs b/src/ServiceControl.Transports.ASBS/AzureQuery.cs index c52d485c17..041b2bc246 100644 --- a/src/ServiceControl.Transports.ASBS/AzureQuery.cs +++ b/src/ServiceControl.Transports.ASBS/AzureQuery.cs @@ -291,19 +291,25 @@ public override async IAsyncEnumerable GetQueueNames( throw new Exception($"Could not find a ServiceBus named \"{serviceBusName}\""); } + // ArmEnvironment Audience Values: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/resourcemanager/Azure.ResourceManager/src/ArmEnvironment.cs + // Service Bus Domains: https://learn.microsoft.com/en-us/rest/api/servicebus/ + static readonly Dictionary ServiceBusDomains = new() + { + { ArmEnvironment.AzurePublicCloud, "servicebus.windows.net" }, + { ArmEnvironment.AzureGovernment, "servicebus.usgovcloudapi.net" }, + { ArmEnvironment.AzureGermany, "servicebus.cloudapi.de" }, + { ArmEnvironment.AzureChina, "servicebus.chinacloudapi.cn" }, + }; + async Task> GetValidNamespaceNames(CancellationToken cancellationToken = default) { var validNamespaces = new HashSet(StringComparer.OrdinalIgnoreCase) { serviceBusName }; - // ArmEnvironment Audience Values: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/resourcemanager/Azure.ResourceManager/src/ArmEnvironment.cs - // Service Bus Domains: https://learn.microsoft.com/en-us/rest/api/servicebus/ - var serviceBusCloudDomain = armEnvironment.Audience switch + if (!ServiceBusDomains.TryGetValue(armEnvironment, out var serviceBusCloudDomain)) { - "https://management.usgovcloudapi.net" => "servicebus.usgovcloudapi.net", - "https://management.microsoftazure.de" => "servicebus.cloudapi.de", - "https://management.chinacloudapi.cn" => "servicebus.chinacloudapi.cn", - _ => "servicebus.windows.net" - }; + // Worst case: the DNS lookup finds nothing additional to match + serviceBusCloudDomain = "servicebus.windows.net"; + } var queryDomain = $"{serviceBusName}.{serviceBusCloudDomain}"; var validDomainTail = $".{serviceBusCloudDomain}.";