Skip to content

Aspir8 and resolving service endpoints in kubernetes with DNS SRV #9913

Open
@rzikm

Description

@rzikm

The Readme for Microsoft.Extensions.ServiceDiscovery.Dns mentions

When deploying to Kubernetes, the DNS SRV service endpoint provider can be used to resolve endpoints. For example, the following resource definition will result in a DNS SRV record being created for an endpoint named "default" and an endpoint named "dashboard", both on the service named "basket".

apiVersion: v1
kind: Service
metadata:
  name: basket
spec:
  selector:
    name: basket-service
  clusterIP: None
  ports:
  - name: default
    port: 8080
  - name: dashboard
    port: 8888

However, when using Aspir8 to generate kubernetes deployments (for example in the TestShop project), I get

---
apiVersion: v1
kind: Service
metadata:
  name: basketservice
spec:
  type: ClusterIP
  selector:
    app: basketservice
  ports:
    - name: http
      port: 8080
      targetPort: 8080
    - name: https
      port: 8443
      targetPort: 8443

Notice that the name of the 8080 port is http instead of default, meaning that DNS SRV resolution will attempt to resolve _default._tcp._basketservice and fail (because there is no "default" port). So the options app developer has are either:

  • manually fixup the generated yaml - and risk it being overwritten on next aspirate generate invocation
  • fix all references in code to http://_http.basketservice - which does not work well if I want to do local deployment via dotnet run in AppHost folder.

Furthermore, reading the comments and code:

// 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, resolver, timeProvider);
return true;
}

Specifically note the last sentece: "If serviceName parses as a URI, we use scheme as the port name, otherwise "default""

Bu there is no corresponding code in ServiceEndpointQuery parsing code

public static bool TryParse(string input, [NotNullWhen(true)] out ServiceEndpointQuery? query)
{
ArgumentException.ThrowIfNullOrEmpty(input);
bool hasScheme;
if (!input.Contains("://", StringComparison.InvariantCulture)
&& Uri.TryCreate($"fakescheme://{input}", default, out var uri))
{
hasScheme = false;
}
else if (Uri.TryCreate(input, default, out uri))
{
hasScheme = true;
}
else
{
query = null;
return false;
}
var uriHost = uri.Host;
var segmentSeparatorIndex = uriHost.IndexOf('.');
string host;
string? endpointName = null;
if (uriHost.StartsWith('_') && segmentSeparatorIndex > 1 && uriHost[^1] != '.')
{
endpointName = uriHost[1..segmentSeparatorIndex];
// Skip the endpoint name, including its prefix ('_') and suffix ('.').
host = uriHost[(segmentSeparatorIndex + 1)..];
}
else
{
host = uriHost;
}
// Allow multiple schemes to be separated by a '+', eg. "https+http://host:port".
var schemes = hasScheme ? uri.Scheme.Split('+') : [];
query = new(input, schemes, host, endpointName);
return true;
}

Given the comments in the first code snippet, I would expect referencing http://basketservice to lead to resolving via SRV query for _http._tcp.basketservice. On the other hand, it is not so clear what should happen with http+https://basketservice.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions