pkg/dns/serviceresolver.go
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{}