dc16de5d |
package dns
import ( |
aa0cae0b |
"encoding/json" |
dc16de5d |
"fmt" |
eb5c7449 |
"hash/fnv"
"net" |
dc16de5d |
"strings"
|
8201efbe |
etcd "github.com/coreos/etcd/client" |
eb5c7449 |
"github.com/golang/glog"
|
83c702b4 |
kapi "k8s.io/kubernetes/pkg/api" |
aa0cae0b |
kendpoints "k8s.io/kubernetes/pkg/api/endpoints" |
83c702b4 |
"k8s.io/kubernetes/pkg/api/errors" |
97e6f1de |
kcoreclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/unversioned" |
aa0cae0b |
"k8s.io/kubernetes/pkg/util/validation" |
dc16de5d |
"github.com/skynetservices/skydns/msg"
"github.com/skynetservices/skydns/server"
)
// ServiceResolver is a SkyDNS backend that will serve lookups for DNS entries
// based on Kubernetes service entries. The default DNS name for each service
// will be `<name>.<namespace>.<base>` where base can be an arbitrary depth
// DNS suffix. Queries not recognized within this base will return an error.
type ServiceResolver struct { |
83e2f09a |
config *server.Config
accessor ServiceAccessor |
97e6f1de |
endpoints kcoreclient.EndpointsGetter |
83e2f09a |
base string
fallback FallbackFunc |
dc16de5d |
}
// ServiceResolver implements server.Backend
var _ server.Backend = &ServiceResolver{}
|
cf96d56f |
// TODO: abstract in upstream SkyDNS |
8201efbe |
var errNoSuchName = etcd.Error{Code: etcd.ErrorCodeKeyNotFound} |
cf96d56f |
|
83e2f09a |
type FallbackFunc func(name string, exact bool) (string, bool)
|
dc16de5d |
// NewServiceResolver creates an object that will return DNS record entries for
// SkyDNS based on service names. |
97e6f1de |
func NewServiceResolver(config *server.Config, accessor ServiceAccessor, endpoints kcoreclient.EndpointsGetter, fn FallbackFunc) *ServiceResolver { |
dc16de5d |
domain := config.Domain
if !strings.HasSuffix(domain, ".") {
domain = domain + "."
}
return &ServiceResolver{ |
83e2f09a |
config: config,
accessor: accessor,
endpoints: endpoints,
base: domain,
fallback: fn, |
dc16de5d |
}
}
// Records implements the SkyDNS Backend interface and returns standard records for
// a name. |
9f79fd3a |
// |
eb5c7449 |
// The standard pattern is <prefix>.<service_name>.<namespace>.(svc|endpoints|pod).<base> |
9f79fd3a |
//
// * prefix may be any series of prefix values |
eb5c7449 |
// * _endpoints is a special prefix that returns the same as <service_name>.<namespace>.svc.<base> |
9f79fd3a |
// * service_name and namespace must locate a real service |
eb5c7449 |
// * unless a fallback is defined, in which case the fallback name will be looked up |
e0c8f93b |
// * svc indicates standard service rules apply (clusterIP or endpoints as A records)
// * reverse lookup of IP is only possible for clusterIP |
9f79fd3a |
// * SRV records are returned for each host+port combination as:
// _<port_name>._<port_protocol>.<dns>
// _<port_name>.<endpoint_id>.<dns>
// * endpoints always returns each individual endpoint as A records |
eb5c7449 |
// * SRV records for endpoints are similar to SVC, but are prefixed with a single label
// that is a hash of the endpoint IP
// * pods is of the form <IP_with_dashes>.<namespace>.pod.<base> and resolves to <IP> |
9f79fd3a |
// |
eb5c7449 |
func (b *ServiceResolver) Records(dnsName string, exact bool) ([]msg.Service, error) {
if !strings.HasSuffix(dnsName, b.base) { |
cf96d56f |
return nil, errNoSuchName |
dc16de5d |
} |
eb5c7449 |
prefix := strings.Trim(strings.TrimSuffix(dnsName, b.base), ".") |
dc16de5d |
segments := strings.Split(prefix, ".") |
9f79fd3a |
for i, j := 0, len(segments)-1; i < j; i, j = i+1, j-1 {
segments[i], segments[j] = segments[j], segments[i]
}
if len(segments) == 0 { |
cf96d56f |
return nil, errNoSuchName |
9f79fd3a |
} |
eb5c7449 |
glog.V(4).Infof("Answering query %s:%t", dnsName, exact)
switch base := segments[0]; base {
case "pod":
if len(segments) != 3 { |
cf96d56f |
return nil, errNoSuchName |
eb5c7449 |
}
namespace, encodedIP := segments[1], segments[2]
ip := convertDashIPToIP(encodedIP)
if net.ParseIP(ip) == nil { |
cf96d56f |
return nil, errNoSuchName |
eb5c7449 |
}
return []msg.Service{
{
Host: ip,
Port: 0,
Priority: 10,
Weight: 10,
Ttl: 30,
Key: msg.Path(buildDNSName(b.base, "pod", namespace, getHash(ip))),
},
}, nil |
9f79fd3a |
case "svc", "endpoints":
if len(segments) < 3 { |
cf96d56f |
return nil, errNoSuchName |
9f79fd3a |
}
namespace, name := segments[1], segments[2]
svc, err := b.accessor.Services(namespace).Get(name) |
dc16de5d |
if err != nil { |
83e2f09a |
if errors.IsNotFound(err) && b.fallback != nil {
if fallback, ok := b.fallback(prefix, exact); ok {
return b.Records(fallback+b.base, exact)
} |
cf96d56f |
return nil, errNoSuchName |
83e2f09a |
} |
cf96d56f |
return nil, errNoSuchName |
dc16de5d |
} |
9f79fd3a |
|
e0c8f93b |
// no clusterIP and not headless, no DNS |
a3f4e254 |
if len(svc.Spec.ClusterIP) == 0 && svc.Spec.Type != kapi.ServiceTypeExternalName { |
cf96d56f |
return nil, errNoSuchName |
9f79fd3a |
}
|
eb5c7449 |
subdomain := buildDNSName(b.base, base, namespace, name)
endpointPrefix := base == "endpoints"
retrieveEndpoints := endpointPrefix || (len(segments) > 3 && segments[3] == "_endpoints") |
b6d35b13 |
|
aa0cae0b |
includePorts := len(segments) > 3 && hasAllPrefixedSegments(segments[3:], "_") && segments[3] != "_endpoints"
|
9f79fd3a |
// if has a portal IP and looking at svc |
d3282b30 |
if svc.Spec.ClusterIP != kapi.ClusterIPNone && !retrieveEndpoints { |
a3f4e254 |
hostValue := svc.Spec.ClusterIP |
f5843492 |
targetStripValue := 2 |
a3f4e254 |
if svc.Spec.Type == kapi.ServiceTypeExternalName {
hostValue = svc.Spec.ExternalName |
f5843492 |
targetStripValue = 0 |
a3f4e254 |
}
|
eb5c7449 |
defaultService := msg.Service{ |
a3f4e254 |
Host: hostValue, |
eb5c7449 |
Port: 0,
Priority: 10,
Weight: 10,
Ttl: 30, |
83e2f09a |
} |
eb5c7449 |
defaultHash := getHash(defaultService.Host)
defaultName := buildDNSName(subdomain, defaultHash)
defaultService.Key = msg.Path(defaultName) |
9f79fd3a |
|
aa0cae0b |
if len(svc.Spec.Ports) == 0 || !includePorts { |
a3f4e254 |
glog.V(4).Infof("Answered %s:%t with %#v", dnsName, exact, defaultService) |
eb5c7449 |
return []msg.Service{defaultService}, nil
} |
9f79fd3a |
|
eb5c7449 |
services := []msg.Service{} |
aa0cae0b |
protocolMatch, portMatch := segments[3], "*"
if len(segments) > 4 {
portMatch = segments[4]
}
for _, p := range svc.Spec.Ports {
portSegment, protocolSegment, ok := matchesPortAndProtocol(p.Name, string(p.Protocol), portMatch, protocolMatch)
if !ok {
continue |
eb5c7449 |
} |
aa0cae0b |
port := p.Port
if port == 0 {
port = int32(p.TargetPort.IntVal)
}
keyName := buildDNSName(defaultName, protocolSegment, portSegment)
services = append(services,
msg.Service{ |
a3f4e254 |
Host: hostValue, |
aa0cae0b |
Port: int(port),
Priority: 10,
Weight: 10,
Ttl: 30,
|
f5843492 |
TargetStrip: targetStripValue, |
aa0cae0b |
Key: msg.Path(keyName),
},
) |
eb5c7449 |
}
if len(services) == 0 {
services = append(services, defaultService) |
83e2f09a |
} |
eb5c7449 |
glog.V(4).Infof("Answered %s:%t with %#v", dnsName, exact, services) |
83e2f09a |
return services, nil |
dc16de5d |
} |
9f79fd3a |
// return endpoints
endpoints, err := b.endpoints.Endpoints(namespace).Get(name)
if err != nil { |
cf96d56f |
return nil, errNoSuchName |
9f79fd3a |
} |
eb5c7449 |
|
aa0cae0b |
hostnameMappings := noHostnameMappings
if savedHostnames := endpoints.Annotations[kendpoints.PodHostnamesAnnotation]; len(savedHostnames) > 0 {
mapped := make(map[string]kendpoints.HostRecord)
if err = json.Unmarshal([]byte(savedHostnames), &mapped); err == nil {
hostnameMappings = mapped
}
}
matchHostname := len(segments) > 3 && !hasAllPrefixedSegments(segments[3:4], "_")
|
9f79fd3a |
services := make([]msg.Service, 0, len(endpoints.Subsets)*4)
for _, s := range endpoints.Subsets {
for _, a := range s.Addresses { |
eb5c7449 |
defaultService := msg.Service{
Host: a.IP,
Port: 0,
Priority: 10,
Weight: 10,
Ttl: 30, |
9f79fd3a |
} |
aa0cae0b |
var endpointName string
if hostname, ok := getHostname(&a, hostnameMappings); ok {
endpointName = hostname
} else {
endpointName = getHash(defaultService.Host)
}
if matchHostname && endpointName != segments[3] {
continue
}
defaultName := buildDNSName(subdomain, endpointName) |
eb5c7449 |
defaultService.Key = msg.Path(defaultName)
|
aa0cae0b |
if !includePorts {
services = append(services, defaultService)
continue
}
protocolMatch, portMatch := segments[3], "*"
if len(segments) > 4 {
portMatch = segments[4]
} |
9f79fd3a |
for _, p := range s.Ports { |
aa0cae0b |
portSegment, protocolSegment, ok := matchesPortAndProtocol(p.Name, string(p.Protocol), portMatch, protocolMatch)
if !ok || p.Port == 0 { |
9f79fd3a |
continue
} |
aa0cae0b |
keyName := buildDNSName(defaultName, protocolSegment, portSegment) |
9f79fd3a |
services = append(services, msg.Service{ |
eb5c7449 |
Host: a.IP, |
aa0cae0b |
Port: int(p.Port), |
9f79fd3a |
Priority: 10,
Weight: 10,
Ttl: 30,
|
aa0cae0b |
TargetStrip: 2,
|
eb5c7449 |
Key: msg.Path(keyName), |
9f79fd3a |
})
}
} |
dc16de5d |
} |
eb5c7449 |
glog.V(4).Infof("Answered %s:%t with %#v", dnsName, exact, services) |
9f79fd3a |
return services, nil |
dc16de5d |
} |
cf96d56f |
return nil, errNoSuchName |
dc16de5d |
}
// ReverseRecord implements the SkyDNS Backend interface and returns standard records for
// a name.
func (b *ServiceResolver) ReverseRecord(name string) (*msg.Service, error) { |
e0c8f93b |
clusterIP, ok := extractIP(name) |
dc16de5d |
if !ok {
return nil, fmt.Errorf("does not support reverse lookup with %s", name)
}
|
e0c8f93b |
svc, err := b.accessor.ServiceByClusterIP(clusterIP) |
dc16de5d |
if err != nil {
return nil, err
} |
bd38defa |
port := 0
if len(svc.Spec.Ports) > 0 { |
37b6c5f1 |
port = int(svc.Spec.Ports[0].Port) |
bd38defa |
} |
eb5c7449 |
hostName := buildDNSName(b.base, "svc", svc.Namespace, svc.Name) |
dc16de5d |
return &msg.Service{ |
eb5c7449 |
Host: hostName, |
bd38defa |
Port: port, |
dc16de5d |
Priority: 10,
Weight: 10,
Ttl: 30,
|
eb5c7449 |
Key: msg.Path(name), |
dc16de5d |
}, nil
}
// arpaSuffix is the standard suffix for PTR IP reverse lookups.
const arpaSuffix = ".in-addr.arpa."
|
aa0cae0b |
func matchesPortAndProtocol(name, protocol, matchPortSegment, matchProtocolSegment string) (portSegment string, protocolSegment string, match bool) {
if len(name) == 0 {
return "", "", false
}
portSegment = "_" + name
if portSegment != matchPortSegment && matchPortSegment != "*" {
return "", "", false
}
protocolSegment = "_" + strings.ToLower(string(protocol))
if protocolSegment != matchProtocolSegment && matchProtocolSegment != "*" {
return "", "", false
}
return portSegment, protocolSegment, true
}
|
dc16de5d |
// extractIP turns a standard PTR reverse record lookup name
// into an IP address
func extractIP(reverseName string) (string, bool) {
if !strings.HasSuffix(reverseName, arpaSuffix) {
return "", false
}
search := strings.TrimSuffix(reverseName, arpaSuffix)
// reverse the segments and then combine them
segments := strings.Split(search, ".")
for i := 0; i < len(segments)/2; i++ {
j := len(segments) - i - 1
segments[i], segments[j] = segments[j], segments[i]
}
return strings.Join(segments, "."), true
} |
eb5c7449 |
// buildDNSName reverses the labels order and joins them with dots.
func buildDNSName(labels ...string) string {
var res string
for _, label := range labels {
if len(res) == 0 {
res = label
} else {
res = fmt.Sprintf("%s.%s", label, res)
}
}
return res
}
|
aa0cae0b |
// getHostname returns true if the provided address has a hostname, or false otherwise.
func getHostname(address *kapi.EndpointAddress, podHostnames map[string]kendpoints.HostRecord) (string, bool) {
if len(address.Hostname) > 0 {
return address.Hostname, true
}
if hostRecord, exists := podHostnames[address.IP]; exists && len(validation.IsDNS1123Label(hostRecord.HostName)) == 0 {
return hostRecord.HostName, true
}
return "", false
}
|
eb5c7449 |
// return a hash for the key name
func getHash(text string) string {
h := fnv.New32a()
h.Write([]byte(text))
return fmt.Sprintf("%x", h.Sum32())
}
// convertDashIPToIP takes an encoded IP (with dashes) and replaces them with
// dots.
func convertDashIPToIP(ip string) string {
return strings.Join(strings.Split(ip, "-"), ".")
} |
aa0cae0b |
// hasAllPrefixedSegments returns true if all provided segments have the given prefix.
func hasAllPrefixedSegments(segments []string, prefix string) bool {
for _, s := range segments {
if !strings.HasPrefix(s, prefix) {
return false
}
}
return true
}
var noHostnameMappings = map[string]kendpoints.HostRecord{} |