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..041b2bc246 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) @@ -27,6 +29,7 @@ public class AzureQuery(ILogger logger, TimeProvider timeProvider, T MetricsQueryClient? client; ArmClient? armClient; string? resourceId; + ArmEnvironment armEnvironment; protected override void InitializeCore(ReadOnlyDictionary settings) { @@ -99,11 +102,11 @@ protected override void InitializeCore(ReadOnlyDictionary settin Diagnostics.AppendLine("Client secret set"); } - (ArmEnvironment armEnvironment, MetricsQueryAudience metricsQueryAudience) environment = GetEnvironment(); + (armEnvironment, var 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 { @@ -126,10 +129,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 { @@ -139,7 +142,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 { @@ -263,6 +266,8 @@ async Task> GetMetrics(string queueName, DateOnly sta public override async IAsyncEnumerable GetQueueNames( [EnumeratorCancellation] CancellationToken cancellationToken = default) { + var validNamespaces = await GetValidNamespaceNames(cancellationToken); + SubscriptionResource? subscription = await armClient!.GetDefaultSubscriptionAsync(cancellationToken); var namespaces = subscription.GetServiceBusNamespacesAsync(cancellationToken); @@ -270,7 +275,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() @@ -286,6 +291,43 @@ 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 }; + + if (!ServiceBusDomains.TryGetValue(armEnvironment, out var serviceBusCloudDomain)) + { + // Worst case: the DNS lookup finds nothing additional to match + serviceBusCloudDomain = "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 => 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 @@ +