Skip to content

Commit 772c874

Browse files
committed
Extend service discovery to support Consul-based DNS lookups:
- Enable specifying how to construct the DNS SRV query - Enable adding the Consul DNS server host/port
1 parent 1eb963e commit 772c874

File tree

12 files changed

+92
-47
lines changed

12 files changed

+92
-47
lines changed

src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ internal sealed partial class DnsSrvServiceEndpointProviderFactory(
2222
/// <inheritdoc/>
2323
public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? provider)
2424
{
25+
var optionsValue = options.CurrentValue;
26+
2527
// If a default namespace is not specified, then this provider will attempt to infer the namespace from the service name, but only when running inside Kubernetes.
2628
// Kubernetes DNS spec: https://github.com/kubernetes/dns/blob/master/docs/specification.md
2729
// SRV records are available for headless services with named ports.
@@ -30,15 +32,16 @@ public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] ou
3032
// Otherwise, the namespace can be read from /var/run/secrets/kubernetes.io/serviceaccount/namespace and combined with an assumed suffix of "svc.cluster.local".
3133
// The protocol is assumed to be "tcp".
3234
// The portName is the name of the port in the service definition. If the serviceName parses as a URI, we use the scheme as the port name, otherwise "default".
33-
if (string.IsNullOrWhiteSpace(_querySuffix))
35+
if (optionsValue.GetQueryText == null && string.IsNullOrWhiteSpace(_querySuffix))
3436
{
3537
DnsServiceEndpointProviderBase.Log.NoDnsSuffixFound(logger, query.ToString()!);
3638
provider = default;
3739
return false;
3840
}
3941

40-
var portName = query.EndpointName ?? "default";
41-
var srvQuery = $"_{portName}._tcp.{query.ServiceName}.{_querySuffix}";
42+
var srvQuery = optionsValue.GetQueryText != null
43+
? optionsValue.GetQueryText(query)
44+
: optionsValue.GetDefaultQueryText(query);
4245
provider = new DnsSrvServiceEndpointProvider(query, srvQuery, hostName: query.ServiceName, options, logger, resolver, timeProvider);
4346
return true;
4447
}

src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderOptions.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using Microsoft.Extensions.ServiceDiscovery.Dns.Resolver;
5+
46
namespace Microsoft.Extensions.ServiceDiscovery.Dns;
57

68
/// <summary>
@@ -28,6 +30,11 @@ public class DnsSrvServiceEndpointProviderOptions
2830
/// </summary>
2931
public double RetryBackOffFactor { get; set; } = 2;
3032

33+
/// <summary>
34+
/// Gets or sets the options used to configure the resolver's behavior.
35+
/// </summary>
36+
public ResolverOptions ResolverOptions { get; set; } = new();
37+
3138
/// <summary>
3239
/// Gets or sets the default DNS query suffix for services resolved via this provider.
3340
/// </summary>
@@ -36,8 +43,19 @@ public class DnsSrvServiceEndpointProviderOptions
3643
/// </remarks>
3744
public string? QuerySuffix { get; set; }
3845

46+
/// <summary>
47+
/// Gets or sets a delegate that generates a DNS SRV query from a specified <see cref="ServiceEndpointQuery"/> instance.
48+
/// </summary>
49+
public Func<ServiceEndpointQuery, string>? GetQueryText { get; set; }
50+
3951
/// <summary>
4052
/// Gets or sets a delegate used to determine whether to apply host name metadata to each resolved endpoint. Defaults to <c>false</c>.
4153
/// </summary>
4254
public Func<ServiceEndpoint, bool> ShouldApplyHostNameMetadata { get; set; } = _ => false;
55+
56+
internal string GetDefaultQueryText(ServiceEndpointQuery query)
57+
{
58+
var portName = query.EndpointName ?? "default";
59+
return $"_{portName}._tcp.{query.ServiceName}.{QuerySuffix}";
60+
}
4361
}

src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.cs

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
using System.Net.Sockets;
1010
using System.Runtime.InteropServices;
1111
using System.Security.Cryptography;
12+
using Microsoft.Extensions.DependencyInjection;
1213
using Microsoft.Extensions.Logging;
1314
using Microsoft.Extensions.Logging.Abstractions;
15+
using Microsoft.Extensions.Options;
1416

1517
namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver;
1618

@@ -28,16 +30,26 @@ internal sealed partial class DnsResolver : IDnsResolver, IDisposable
2830
private readonly TimeProvider _timeProvider;
2931
private readonly ILogger<DnsResolver> _logger;
3032

31-
public DnsResolver(TimeProvider timeProvider, ILogger<DnsResolver> logger) : this(timeProvider, logger, OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() ? ResolvConf.GetOptions() : NetworkInfo.GetOptions())
32-
{
33-
}
34-
35-
internal DnsResolver(TimeProvider timeProvider, ILogger<DnsResolver> logger, ResolverOptions options)
33+
public DnsResolver(TimeProvider timeProvider, ILogger<DnsResolver> logger, ResolverOptions options)
3634
{
3735
_timeProvider = timeProvider;
3836
_logger = logger;
3937
_options = options;
40-
Debug.Assert(_options.Servers.Count > 0);
38+
39+
if (_options.Servers.Count == 0)
40+
{
41+
foreach (var server in OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()
42+
? ResolvConf.GetServers()
43+
: NetworkInfo.GetServers())
44+
{
45+
_options.Servers.Add(server);
46+
}
47+
48+
if (_options.Servers.Count == 0)
49+
{
50+
throw new ArgumentException("At least one DNS server is required.", nameof(options));
51+
}
52+
}
4153

4254
if (options.Timeout != Timeout.InfiniteTimeSpan)
4355
{
@@ -50,14 +62,24 @@ internal DnsResolver(ResolverOptions options) : this(TimeProvider.System, NullLo
5062
{
5163
}
5264

53-
internal DnsResolver(IEnumerable<IPEndPoint> servers) : this(new ResolverOptions(servers.ToArray()))
65+
internal DnsResolver(IEnumerable<IPEndPoint> servers) : this(new ResolverOptions { Servers = servers.ToArray() })
5466
{
5567
}
5668

57-
internal DnsResolver(IPEndPoint server) : this(new ResolverOptions(server))
69+
internal DnsResolver(IPEndPoint server) : this(new ResolverOptions { Servers = [server] })
5870
{
5971
}
6072

73+
internal static DnsResolver Create(IServiceProvider serviceProvider)
74+
{
75+
ArgumentNullException.ThrowIfNull(serviceProvider);
76+
77+
var resolverOptions = serviceProvider.GetRequiredService<IOptions<DnsSrvServiceEndpointProviderOptions>>().Value.ResolverOptions;
78+
var timeProvider = serviceProvider.GetRequiredService<TimeProvider>();
79+
var logger = serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger<DnsResolver>();
80+
return new DnsResolver(timeProvider, logger, resolverOptions);
81+
}
82+
6183
public ValueTask<ServiceResult[]> ResolveServiceAsync(string name, CancellationToken cancellationToken = default)
6284
{
6385
ObjectDisposedException.ThrowIf(_disposed, this);

src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/NetworkInfo.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver;
88

99
internal static class NetworkInfo
1010
{
11-
// basic option to get DNS serves via NetworkInfo. We may get it directly later via proper APIs.
12-
public static ResolverOptions GetOptions()
11+
// basic option to get DNS servers via NetworkInfo. We may get it directly later via proper APIs.
12+
public static IList<IPEndPoint> GetServers()
1313
{
1414
List<IPEndPoint> servers = new List<IPEndPoint>();
1515

@@ -31,6 +31,6 @@ public static ResolverOptions GetOptions()
3131
}
3232
}
3333

34-
return new ResolverOptions(servers);
34+
return servers;
3535
}
3636
}

src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolvConf.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ internal static class ResolvConf
1010
{
1111
[SupportedOSPlatform("linux")]
1212
[SupportedOSPlatform("osx")]
13-
public static ResolverOptions GetOptions()
13+
public static IList<IPEndPoint> GetServers()
1414
{
15-
return GetOptions(new StreamReader("/etc/resolv.conf"));
15+
return GetServers(new StreamReader("/etc/resolv.conf"));
1616
}
1717

18-
public static ResolverOptions GetOptions(TextReader reader)
18+
public static IList<IPEndPoint> GetServers(TextReader reader)
1919
{
2020
List<IPEndPoint> serverList = new();
2121

@@ -40,9 +40,9 @@ public static ResolverOptions GetOptions(TextReader reader)
4040
if (serverList.Count == 0)
4141
{
4242
// If no nameservers are configured, fall back to the default behavior of using the system resolver configuration.
43-
return NetworkInfo.GetOptions();
43+
return NetworkInfo.GetServers();
4444
}
4545

46-
return new ResolverOptions(serverList);
46+
return serverList;
4747
}
4848
}

src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolverOptions.cs

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,26 @@
55

66
namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver;
77

8-
internal sealed class ResolverOptions
8+
/// <summary>
9+
/// Provides configuration options for DNS resolution, including server endpoints, retry attempts, and timeout settings.
10+
/// </summary>
11+
public class ResolverOptions
912
{
10-
public IReadOnlyList<IPEndPoint> Servers;
11-
public int Attempts = 2;
12-
public TimeSpan Timeout = TimeSpan.FromSeconds(3);
13+
/// <summary>
14+
/// Gets or sets the collection of server endpoints used for network connections.
15+
/// </summary>
16+
public IList<IPEndPoint> Servers { get; set; } = new List<IPEndPoint>();
1317

14-
// override for testing purposes
15-
internal Func<Memory<byte>, int, int>? _transportOverride;
16-
17-
public ResolverOptions(IReadOnlyList<IPEndPoint> servers)
18-
{
19-
if (servers.Count == 0)
20-
{
21-
throw new ArgumentException("At least one DNS server is required.", nameof(servers));
22-
}
18+
/// <summary>
19+
/// Gets or sets the number of allowed attempts for the operation.
20+
/// </summary>
21+
public int Attempts { get; set; } = 2;
2322

24-
Servers = servers;
25-
}
23+
/// <summary>
24+
/// Gets or sets the maximum duration to wait for an operation to complete before timing out.
25+
/// </summary>
26+
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(3);
2627

27-
public ResolverOptions(IPEndPoint server)
28-
{
29-
Servers = new IPEndPoint[] { server };
30-
}
28+
// override for testing purposes
29+
internal Func<Memory<byte>, int, int>? _transportOverride;
3130
}

src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using Microsoft.Extensions.DependencyInjection;
55
using Microsoft.Extensions.DependencyInjection.Extensions;
6+
using Microsoft.Extensions.Options;
67
using Microsoft.Extensions.ServiceDiscovery;
78
using Microsoft.Extensions.ServiceDiscovery.Dns;
89
using Microsoft.Extensions.ServiceDiscovery.Dns.Resolver;
@@ -49,7 +50,7 @@ public static IServiceCollection AddDnsSrvServiceEndpointProvider(this IServiceC
4950

5051
if (!GetDnsClientFallbackFlag())
5152
{
52-
services.TryAddSingleton<IDnsResolver, DnsResolver>();
53+
services.TryAddSingleton<IDnsResolver>(DnsResolver.Create);
5354
}
5455
else
5556
{

test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/DnsResponseFuzzer.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ public void FuzzTarget(ReadOnlySpan<byte> data)
1919
if (_resolver == null)
2020
{
2121
_buffer = new byte[4096];
22-
_resolver = new DnsResolver(new ResolverOptions(new IPEndPoint(IPAddress.Loopback, 53))
22+
_resolver = new DnsResolver(new ResolverOptions
2323
{
24+
Servers = [new IPEndPoint(IPAddress.Loopback, 53)],
2425
Timeout = TimeSpan.FromSeconds(5),
2526
Attempts = 1,
2627
_transportOverride = (buffer, length) =>
@@ -41,4 +42,4 @@ public void FuzzTarget(ReadOnlySpan<byte> data)
4142
Debug.Assert(task.IsCompleted, "Task should be completed synchronously");
4243
task.GetAwaiter().GetResult();
4344
}
44-
}
45+
}

test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndpointResolverTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public async Task ResolveServiceEndpoint_Dns_MultiShot()
1717
var timeProvider = new FakeTimeProvider();
1818
var services = new ServiceCollection()
1919
.AddSingleton<TimeProvider>(timeProvider)
20-
.AddSingleton<IDnsResolver, DnsResolver>()
20+
.AddSingleton<IDnsResolver>(DnsResolver.Create)
2121
.AddServiceDiscoveryCore()
2222
.AddDnsServiceEndpointProvider(o => o.DefaultRefreshPeriod = TimeSpan.FromSeconds(30))
2323
.BuildServiceProvider();

test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsTestBase.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ public LoopbackDnsTestBase(ITestOutputHelper output)
2626
Output = output;
2727
DnsServer = new();
2828
TimeProvider = new();
29-
Options = new([DnsServer.DnsEndPoint])
29+
Options = new()
3030
{
31+
Servers = [DnsServer.DnsEndPoint],
3132
Timeout = TimeSpan.FromSeconds(5),
3233
Attempts = 1,
3334
};

0 commit comments

Comments
 (0)