File: DnsSrvServiceEndpointProviderFactory.cs
Web Access
Project: src\src\Microsoft.Extensions.ServiceDiscovery.Dns\Microsoft.Extensions.ServiceDiscovery.Dns.csproj (Microsoft.Extensions.ServiceDiscovery.Dns)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics.CodeAnalysis;
using DnsClient;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
 
namespace Microsoft.Extensions.ServiceDiscovery.Dns;
 
internal sealed partial class DnsSrvServiceEndpointProviderFactory(
    IOptionsMonitor<DnsSrvServiceEndpointProviderOptions> options,
    ILogger<DnsSrvServiceEndpointProvider> logger,
    IDnsQuery dnsClient,
    TimeProvider timeProvider) : IServiceEndpointProviderFactory
{
    private static readonly string s_serviceAccountPath = Path.Combine($"{Path.DirectorySeparatorChar}var", "run", "secrets", "kubernetes.io", "serviceaccount");
    private static readonly string s_serviceAccountNamespacePath = Path.Combine($"{Path.DirectorySeparatorChar}var", "run", "secrets", "kubernetes.io", "serviceaccount", "namespace");
    private static readonly string s_resolveConfPath = Path.Combine($"{Path.DirectorySeparatorChar}etc", "resolv.conf");
    private readonly string? _querySuffix = options.CurrentValue.QuerySuffix ?? GetKubernetesHostDomain();
 
    /// <inheritdoc/>
    public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? provider)
    {
        // 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.
        // Kubernetes DNS spec: https://github.com/kubernetes/dns/blob/master/docs/specification.md
        // SRV records are available for headless services with named ports. 
        // They take the form $"_{portName}._{protocol}.{serviceName}.{namespace}.{suffix}"
        // The suffix (after the service name) can be parsed from /etc/resolv.conf
        // Otherwise, the namespace can be read from /var/run/secrets/kubernetes.io/serviceaccount/namespace and combined with an assumed suffix of "svc.cluster.local".
        // The protocol is assumed to be "tcp".
        // 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".
        if (string.IsNullOrWhiteSpace(_querySuffix))
        {
            DnsServiceEndpointProviderBase.Log.NoDnsSuffixFound(logger, query.ToString()!);
            provider = default;
            return false;
        }
 
        var portName = query.EndpointName ?? "default";
        var srvQuery = $"_{portName}._tcp.{query.ServiceName}.{_querySuffix}";
        provider = new DnsSrvServiceEndpointProvider(query, srvQuery, hostName: query.ServiceName, options, logger, dnsClient, timeProvider);
        return true;
    }
 
    private static string? GetKubernetesHostDomain()
    {
        // Check that we are running in Kubernetes first.
        if (!IsInKubernetesCluster())
        {
            return null;
        }
 
        if (!OperatingSystem.IsLinux())
        {
            return null;
        }
 
        var qualifiedNamespace = ReadQualifiedNamespaceFromResolvConf();
        if (!string.IsNullOrWhiteSpace(qualifiedNamespace))
        {
            return qualifiedNamespace;
        }
 
        var serviceAccountNamespace = ReadNamespaceFromKubernetesServiceAccount();
        if (!string.IsNullOrWhiteSpace(serviceAccountNamespace))
        {
            // The zone is assumed to be "cluster.local"
            return $"{serviceAccountNamespace}.svc.cluster.local";
        }
 
        return null;
    }
 
    private static string? ReadNamespaceFromKubernetesServiceAccount()
    {
        // Read the namespace from the Kubernetes pod's service account.
        if (File.Exists(s_serviceAccountNamespacePath))
        {
            return File.ReadAllText(s_serviceAccountNamespacePath).Trim();
        }
 
        return null;
    }
 
    private static string? ReadQualifiedNamespaceFromResolvConf()
    {
        if (!File.Exists(s_resolveConfPath))
        {
            return default;
        }
 
        // See https://manpages.debian.org/bookworm/manpages/resolv.conf.5.en.html#search for the format of /etc/resolv.conf's search option.
        // In our case, we are interested in determining the domain name.
        var lines = File.ReadAllLines(s_resolveConfPath);
        foreach (var line in lines)
        {
            if (!line.StartsWith("search "))
            {
                continue;
            }
 
            var components = line.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
            if (components.Length > 1)
            {
                return components[1];
            }
        }
 
        return default;
    }
 
    private static bool IsInKubernetesCluster()
    {
        // This logic is based on the Kubernetes C# client logic found here:
        // https://github.com/kubernetes-client/csharp/blob/52c3c00d4c55b28bdb491a219f4967823a83df2d/src/KubernetesClient/KubernetesClientConfiguration.InCluster.cs#L21
        var host = Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_HOST");
        var port = Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_PORT");
        if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(port))
        {
            return false;
        }
 
        var tokenPath = Path.Combine(s_serviceAccountPath, "token");
        if (!File.Exists(tokenPath))
        {
            return false;
        }
 
        var certPath = Path.Combine(s_serviceAccountPath, "ca.crt");
        return File.Exists(certPath);
    }
}