Skip to content

Commit d38335a

Browse files
DavidBoikebording
andauthored
Support Azure Service Bus Geo-Recovery alias for querying queue names (#4938)
* Support ASB Geo-Recovery alias for querying queue names * Account for other regional clouds * Tweaks * Use a dictionary on ArmEnvironment since it is IEquatable --------- Co-authored-by: Brandon Ording <[email protected]>
1 parent 9b09977 commit d38335a

File tree

3 files changed

+50
-6
lines changed

3 files changed

+50
-6
lines changed

src/Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<PackageVersion Include="Azure.ResourceManager.ServiceBus" Version="1.1.0" />
1313
<PackageVersion Include="ByteSize" Version="2.1.2" />
1414
<PackageVersion Include="Caliburn.Micro" Version="4.0.230" />
15+
<PackageVersion Include="DnsClient" Version="1.8.0" />
1516
<PackageVersion Include="FluentValidation" Version="11.11.0" />
1617
<PackageVersion Include="Fody" Version="6.9.1" />
1718
<PackageVersion Include="GitHubActionsTestLogger" Version="2.4.1" />

src/ServiceControl.Transports.ASBS/AzureQuery.cs

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ namespace ServiceControl.Transports.ASBS;
1818
using Azure.ResourceManager.Resources;
1919
using Azure.ResourceManager.ServiceBus;
2020
using BrokerThroughput;
21+
using DnsClient;
22+
using DnsClient.Protocol;
2123
using Microsoft.Extensions.Logging;
2224

2325
public class AzureQuery(ILogger<AzureQuery> logger, TimeProvider timeProvider, TransportSettings transportSettings)
@@ -27,6 +29,7 @@ public class AzureQuery(ILogger<AzureQuery> logger, TimeProvider timeProvider, T
2729
MetricsQueryClient? client;
2830
ArmClient? armClient;
2931
string? resourceId;
32+
ArmEnvironment armEnvironment;
3033

3134
protected override void InitializeCore(ReadOnlyDictionary<string, string> settings)
3235
{
@@ -99,11 +102,11 @@ protected override void InitializeCore(ReadOnlyDictionary<string, string> settin
99102
Diagnostics.AppendLine("Client secret set");
100103
}
101104

102-
(ArmEnvironment armEnvironment, MetricsQueryAudience metricsQueryAudience) environment = GetEnvironment();
105+
(armEnvironment, var metricsQueryAudience) = GetEnvironment();
103106

104107
if (managementUrl == null)
105108
{
106-
Diagnostics.AppendLine($"Management Url not set, defaulted to \"{environment.armEnvironment.Endpoint}\"");
109+
Diagnostics.AppendLine($"Management Url not set, defaulted to \"{armEnvironment.Endpoint}\"");
107110
}
108111
else
109112
{
@@ -126,10 +129,10 @@ protected override void InitializeCore(ReadOnlyDictionary<string, string> settin
126129
clientCredentials = new ClientSecretCredential(tenantId, clientId, clientSecret);
127130
}
128131

129-
client = new MetricsQueryClient(environment.armEnvironment.Endpoint, clientCredentials,
132+
client = new MetricsQueryClient(armEnvironment.Endpoint, clientCredentials,
130133
new MetricsQueryClientOptions
131134
{
132-
Audience = environment.metricsQueryAudience,
135+
Audience = metricsQueryAudience,
133136
Transport = new HttpClientTransport(
134137
new HttpClient(new SocketsHttpHandler
135138
{
@@ -139,7 +142,7 @@ protected override void InitializeCore(ReadOnlyDictionary<string, string> settin
139142
armClient = new ArmClient(clientCredentials, subscriptionId,
140143
new ArmClientOptions
141144
{
142-
Environment = environment.armEnvironment,
145+
Environment = armEnvironment,
143146
Transport = new HttpClientTransport(
144147
new HttpClient(new SocketsHttpHandler
145148
{
@@ -263,14 +266,16 @@ async Task<IReadOnlyList<MetricValue>> GetMetrics(string queueName, DateOnly sta
263266
public override async IAsyncEnumerable<IBrokerQueue> GetQueueNames(
264267
[EnumeratorCancellation] CancellationToken cancellationToken = default)
265268
{
269+
var validNamespaces = await GetValidNamespaceNames(cancellationToken);
270+
266271
SubscriptionResource? subscription = await armClient!.GetDefaultSubscriptionAsync(cancellationToken);
267272
var namespaces =
268273
subscription.GetServiceBusNamespacesAsync(cancellationToken);
269274

270275
await foreach (var serviceBusNamespaceResource in namespaces.WithCancellation(
271276
cancellationToken))
272277
{
273-
if (serviceBusNamespaceResource.Data.Name == serviceBusName)
278+
if (validNamespaces.Contains(serviceBusNamespaceResource.Data.Name))
274279
{
275280
resourceId = serviceBusNamespaceResource.Id;
276281
await foreach (var queue in serviceBusNamespaceResource.GetServiceBusQueues()
@@ -286,6 +291,43 @@ public override async IAsyncEnumerable<IBrokerQueue> GetQueueNames(
286291
throw new Exception($"Could not find a ServiceBus named \"{serviceBusName}\"");
287292
}
288293

294+
// ArmEnvironment Audience Values: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/resourcemanager/Azure.ResourceManager/src/ArmEnvironment.cs
295+
// Service Bus Domains: https://learn.microsoft.com/en-us/rest/api/servicebus/
296+
static readonly Dictionary<ArmEnvironment, string> ServiceBusDomains = new()
297+
{
298+
{ ArmEnvironment.AzurePublicCloud, "servicebus.windows.net" },
299+
{ ArmEnvironment.AzureGovernment, "servicebus.usgovcloudapi.net" },
300+
{ ArmEnvironment.AzureGermany, "servicebus.cloudapi.de" },
301+
{ ArmEnvironment.AzureChina, "servicebus.chinacloudapi.cn" },
302+
};
303+
304+
async Task<HashSet<string>> GetValidNamespaceNames(CancellationToken cancellationToken = default)
305+
{
306+
var validNamespaces = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { serviceBusName };
307+
308+
if (!ServiceBusDomains.TryGetValue(armEnvironment, out var serviceBusCloudDomain))
309+
{
310+
// Worst case: the DNS lookup finds nothing additional to match
311+
serviceBusCloudDomain = "servicebus.windows.net";
312+
}
313+
314+
var queryDomain = $"{serviceBusName}.{serviceBusCloudDomain}";
315+
var validDomainTail = $".{serviceBusCloudDomain}.";
316+
317+
var dnsLookup = new LookupClient();
318+
var dnsResult = await dnsLookup.QueryAsync(queryDomain, QueryType.CNAME, cancellationToken: cancellationToken);
319+
var domain = (dnsResult.Answers.FirstOrDefault() as CNameRecord)?.CanonicalName.Value;
320+
if (domain is not null && domain.EndsWith(validDomainTail))
321+
{
322+
// In some cases, like private networking access, result might be something like `namespacename.private` with a dot in the middle
323+
// which is not a big deal because that will not actually match a namespace name in metrics
324+
var otherName = domain[..^validDomainTail.Length];
325+
validNamespaces.Add(otherName);
326+
}
327+
328+
return validNamespaces;
329+
}
330+
289331
public override string SanitizedEndpointNameCleanser(string endpointName) => endpointName.ToLower();
290332

291333
public override KeyDescriptionPair[] Settings =>

src/ServiceControl.Transports.ASBS/ServiceControl.Transports.ASBS.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<PackageReference Include="Azure.Identity" />
1515
<PackageReference Include="Azure.Monitor.Query" />
1616
<PackageReference Include="Azure.ResourceManager.ServiceBus" />
17+
<PackageReference Include="DnsClient" />
1718
<PackageReference Include="NServiceBus.CustomChecks" />
1819
<PackageReference Include="NServiceBus.Transport.AzureServiceBus" />
1920
</ItemGroup>

0 commit comments

Comments
 (0)