traefik/vendor/github.com/mesosphere/mesos-dns/records/generator.go
2017-03-09 13:13:02 +01:00

656 lines
18 KiB
Go

// Package records contains functions to generate resource records from
// mesos master states to serve through a dns server
package records
import (
"crypto/sha1"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/mesosphere/mesos-dns/errorutil"
"github.com/mesosphere/mesos-dns/logging"
"github.com/mesosphere/mesos-dns/models"
"github.com/mesosphere/mesos-dns/records/labels"
"github.com/mesosphere/mesos-dns/records/state"
"github.com/tv42/zbase32"
)
// Map host/service name to DNS answer
// REFACTOR - when discoveryinfo is integrated
// Will likely become map[string][]discoveryinfo
// Effectively we're (ab)using the map type as a set
// It used to have the type: rrs map[string][]string
type rrs map[string]map[string]struct{}
func (r rrs) add(name, host string) bool {
if host == "" {
return false
}
v, ok := r[name]
if !ok {
v = make(map[string]struct{})
r[name] = v
} else {
// don't overwrite existing values
_, ok = v[host]
if ok {
return false
}
}
v[host] = struct{}{}
return true
}
func (r rrs) First(name string) (string, bool) {
for host := range r[name] {
return host, true
}
return "", false
}
// Transform the record set into something exportable via the REST API
func (r rrs) ToAXFRResourceRecordSet() models.AXFRResourceRecordSet {
ret := make(models.AXFRResourceRecordSet, len(r))
for host, values := range r {
ret[host] = make([]string, 0, len(values))
for record := range values {
ret[host] = append(ret[host], record)
}
}
return ret
}
type rrsKind string
const (
// A record types
A rrsKind = "A"
// SRV record types
SRV = "SRV"
)
func (kind rrsKind) rrs(rg *RecordGenerator) rrs {
switch kind {
case A:
return rg.As
case SRV:
return rg.SRVs
default:
return nil
}
}
// RecordGenerator contains DNS records and methods to access and manipulate
// them. TODO(kozyraki): Refactor when discovery id is available.
type RecordGenerator struct {
As rrs
SRVs rrs
SlaveIPs map[string]string
EnumData EnumerationData
httpClient http.Client
}
// EnumerableRecord is the lowest level object, and should map 1:1 with DNS records
type EnumerableRecord struct {
Name string `json:"name"`
Host string `json:"host"`
Rtype string `json:"rtype"`
}
// EnumerableTask consists of the records derived from a task
type EnumerableTask struct {
Name string `json:"name"`
ID string `json:"id"`
Records []EnumerableRecord `json:"records"`
}
// EnumerableFramework is consistent of enumerable tasks, and include the name of the framework
type EnumerableFramework struct {
Tasks []*EnumerableTask `json:"tasks"`
Name string `json:"name"`
}
// EnumerationData is the top level container pointing to the
// enumerable frameworks containing enumerable tasks
type EnumerationData struct {
Frameworks []*EnumerableFramework `json:"frameworks"`
}
// NewRecordGenerator returns a RecordGenerator that's been configured with a timeout.
func NewRecordGenerator(httpTimeout time.Duration) *RecordGenerator {
enumData := EnumerationData{
Frameworks: []*EnumerableFramework{},
}
rg := &RecordGenerator{
httpClient: http.Client{Timeout: httpTimeout},
EnumData: enumData,
}
return rg
}
// ParseState retrieves and parses the Mesos master /state.json and converts it
// into DNS records.
func (rg *RecordGenerator) ParseState(c Config, masters ...string) error {
// find master -- return if error
sj, err := rg.FindMaster(masters...)
if err != nil {
logging.Error.Println("no master")
return err
}
if sj.Leader == "" {
logging.Error.Println("Unexpected error")
err = errors.New("empty master")
return err
}
hostSpec := labels.RFC1123
if c.EnforceRFC952 {
hostSpec = labels.RFC952
}
return rg.InsertState(sj, c.Domain, c.SOARname, c.Listener, masters, c.IPSources, hostSpec)
}
// Tries each master and looks for the leader
// if no leader responds it errors
func (rg *RecordGenerator) FindMaster(masters ...string) (state.State, error) {
var sj state.State
var leader string
if len(masters) > 0 {
leader, masters = masters[0], masters[1:]
}
// Check if ZK leader is correct
if leader != "" {
logging.VeryVerbose.Println("Zookeeper says the leader is: ", leader)
ip, port, err := getProto(leader)
if err != nil {
logging.Error.Println(err)
}
if sj, err = rg.loadWrap(ip, port); err == nil && sj.Leader != "" {
return sj, nil
}
logging.Verbose.Println("Warning: Zookeeper is wrong about leader")
if len(masters) == 0 {
return sj, errors.New("no master")
}
logging.Verbose.Println("Warning: falling back to Masters config field: ", masters)
}
// try each listed mesos master before dying
for i, master := range masters {
ip, port, err := getProto(master)
if err != nil {
logging.Error.Println(err)
}
if sj, err = rg.loadWrap(ip, port); err == nil && sj.Leader == "" {
logging.VeryVerbose.Println("Warning: not a leader - trying next one")
if len(masters)-1 == i {
return sj, errors.New("no master")
}
} else {
return sj, nil
}
}
return sj, errors.New("no master")
}
// Loads state.json from mesos master
func (rg *RecordGenerator) loadFromMaster(ip string, port string) (state.State, error) {
// REFACTOR: state.json security
var sj state.State
u := url.URL{
Scheme: "http",
Host: net.JoinHostPort(ip, port),
Path: "/master/state.json",
}
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
logging.Error.Println(err)
return state.State{}, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := rg.httpClient.Do(req)
if err != nil {
logging.Error.Println(err)
return state.State{}, err
}
defer errorutil.Ignore(resp.Body.Close)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
logging.Error.Println(err)
return state.State{}, err
}
err = json.Unmarshal(body, &sj)
if err != nil {
logging.Error.Println(err)
return state.State{}, err
}
return sj, nil
}
// Catches an attempt to load state.json from a mesos master
// attempts can fail from down server or mesos master secondary
// it also reloads from a different master if the master it attempted to
// load from was not the leader
func (rg *RecordGenerator) loadWrap(ip string, port string) (state.State, error) {
var err error
var sj state.State
logging.VeryVerbose.Println("reloading from master " + ip)
sj, err = rg.loadFromMaster(ip, port)
if err != nil {
return state.State{}, err
}
if rip := leaderIP(sj.Leader); rip != ip {
logging.VeryVerbose.Println("Warning: master changed to " + ip)
sj, err = rg.loadFromMaster(rip, port)
return sj, err
}
return sj, nil
}
// hashes a given name using a truncated sha1 hash
// 5 characters extracted from the zbase32 encoded hash provides
// enough entropy to avoid collisions
// zbase32: http://philzimmermann.com/docs/human-oriented-base-32-encoding.txt
// is used to promote human-readable names
func hashString(s string) string {
hash := sha1.Sum([]byte(s))
return zbase32.EncodeToString(hash[:])[:5]
}
// attempt to translate the hostname into an IPv4 address. logs an error if IP
// lookup fails. if an IP address cannot be found, returns the same hostname
// that was given. upon success returns the IP address as a string.
func hostToIP4(hostname string) (string, bool) {
ip := net.ParseIP(hostname)
if ip == nil {
t, err := net.ResolveIPAddr("ip4", hostname)
if err != nil {
logging.Error.Printf("cannot translate hostname %q into an ip4 address", hostname)
return hostname, false
}
ip = t.IP
}
return ip.String(), true
}
// InsertState transforms a StateJSON into RecordGenerator RRs
func (rg *RecordGenerator) InsertState(sj state.State, domain, ns, listener string, masters, ipSources []string, spec labels.Func) error {
rg.SlaveIPs = map[string]string{}
rg.SRVs = rrs{}
rg.As = rrs{}
rg.frameworkRecords(sj, domain, spec)
rg.slaveRecords(sj, domain, spec)
rg.listenerRecord(listener, ns)
rg.masterRecord(domain, masters, sj.Leader)
rg.taskRecords(sj, domain, spec, ipSources)
return nil
}
// frameworkRecords injects A and SRV records into the generator store:
// frameworkname.domain. // resolves to IPs of each framework
// _framework._tcp.frameworkname.domain. // resolves to the driver port and IP of each framework
func (rg *RecordGenerator) frameworkRecords(sj state.State, domain string, spec labels.Func) {
for _, f := range sj.Frameworks {
fname := labels.DomainFrag(f.Name, labels.Sep, spec)
host, port := f.HostPort()
if address, ok := hostToIP4(host); ok {
a := fname + "." + domain + "."
rg.insertRR(a, address, A)
if port != "" {
srvAddress := net.JoinHostPort(a, port)
rg.insertRR("_framework._tcp."+a, srvAddress, SRV)
}
}
}
}
// slaveRecords injects A and SRV records into the generator store:
// slave.domain. // resolves to IPs of all slaves
// _slave._tc.domain. // resolves to the driver port and IP of all slaves
func (rg *RecordGenerator) slaveRecords(sj state.State, domain string, spec labels.Func) {
for _, slave := range sj.Slaves {
address, ok := hostToIP4(slave.PID.Host)
if ok {
a := "slave." + domain + "."
rg.insertRR(a, address, A)
srv := net.JoinHostPort(a, slave.PID.Port)
rg.insertRR("_slave._tcp."+domain+".", srv, SRV)
} else {
logging.VeryVerbose.Printf("string '%q' for slave with id %q is not a valid IP address", address, slave.ID)
address = labels.DomainFrag(address, labels.Sep, spec)
}
rg.SlaveIPs[slave.ID] = address
}
}
// masterRecord injects A and SRV records into the generator store:
// master.domain. // resolves to IPs of all masters
// masterN.domain. // one IP address for each master
// leader.domain. // one IP address for the leading master
//
// The current func implementation makes an assumption about the order of masters:
// it's the order in which you expect the enumerated masterN records to be created.
// This is probably important: if a new leader is elected, you may not want it to
// become master0 simply because it's the leader. You probably want your DNS records
// to change as little as possible. And this func should have the least impact on
// enumeration order, or name/IP mappings - it's just creating the records. So let
// the caller do the work of ordering/sorting (if desired) the masters list if a
// different outcome is desired.
//
// Another consequence of the current overall mesos-dns app implementation is that
// the leader may not even be in the masters list at some point in time. masters is
// really fallback-masters (only consider these to be masters if I can't find a
// leader via ZK). At some point in time, they may not actually be masters any more.
// Consider a cluster of 3 nodes that suffers the loss of a member, and gains a new
// member (VM crashed, was replaced by another VM). And the cycle repeats several
// times. You end up with a set of running masters (and leader) that's different
// than the set of statically configured fallback masters.
//
// So the func tries to index the masters as they're listed and begrudgingly assigns
// the leading master an index out-of-band if it's not actually listed in the masters
// list. There are probably better ways to do it.
func (rg *RecordGenerator) masterRecord(domain string, masters []string, leader string) {
// create records for leader
// A records
h := strings.Split(leader, "@")
if len(h) < 2 {
logging.Error.Println(leader)
return // avoid a panic later
}
leaderAddress := h[1]
ip, port, err := getProto(leaderAddress)
if err != nil {
logging.Error.Println(err)
return
}
arec := "leader." + domain + "."
rg.insertRR(arec, ip, A)
arec = "master." + domain + "."
rg.insertRR(arec, ip, A)
// SRV records
tcp := "_leader._tcp." + domain + "."
udp := "_leader._udp." + domain + "."
host := "leader." + domain + "." + ":" + port
rg.insertRR(tcp, host, SRV)
rg.insertRR(udp, host, SRV)
// if there is a list of masters, insert that as well
addedLeaderMasterN := false
idx := 0
for _, master := range masters {
masterIP, _, err := getProto(master)
if err != nil {
logging.Error.Println(err)
continue
}
// A records (master and masterN)
if master != leaderAddress {
arec := "master." + domain + "."
added := rg.insertRR(arec, masterIP, A)
if !added {
// duplicate master?!
continue
}
}
if master == leaderAddress && addedLeaderMasterN {
// duplicate leader in masters list?!
continue
}
arec := "master" + strconv.Itoa(idx) + "." + domain + "."
rg.insertRR(arec, masterIP, A)
idx++
if master == leaderAddress {
addedLeaderMasterN = true
}
}
// flake: we ended up with a leader that's not in the list of all masters?
if !addedLeaderMasterN {
// only a flake if there were fallback masters configured
if len(masters) > 0 {
logging.Error.Printf("warning: leader %q is not in master list", leader)
}
arec = "master" + strconv.Itoa(idx) + "." + domain + "."
rg.insertRR(arec, ip, A)
}
}
// A record for mesos-dns (the name is listed in SOA replies)
func (rg *RecordGenerator) listenerRecord(listener string, ns string) {
if listener == "0.0.0.0" {
rg.setFromLocal(listener, ns)
} else if listener == "127.0.0.1" {
rg.insertRR(ns, "127.0.0.1", A)
} else {
rg.insertRR(ns, listener, A)
}
}
func (rg *RecordGenerator) taskRecords(sj state.State, domain string, spec labels.Func, ipSources []string) {
for _, f := range sj.Frameworks {
enumerableFramework := &EnumerableFramework{
Name: f.Name,
Tasks: []*EnumerableTask{},
}
rg.EnumData.Frameworks = append(rg.EnumData.Frameworks, enumerableFramework)
for _, task := range f.Tasks {
var ok bool
task.SlaveIP, ok = rg.SlaveIPs[task.SlaveID]
// only do running and discoverable tasks
if ok && (task.State == "TASK_RUNNING") {
rg.taskRecord(task, f, domain, spec, ipSources, enumerableFramework)
}
}
}
}
type context struct {
taskName,
taskID,
slaveID,
taskIP,
slaveIP string
}
func (rg *RecordGenerator) taskRecord(task state.Task, f state.Framework, domain string, spec labels.Func, ipSources []string, enumFW *EnumerableFramework) {
newTask := &EnumerableTask{ID: task.ID, Name: task.Name}
enumFW.Tasks = append(enumFW.Tasks, newTask)
// define context
ctx := context{
spec(task.Name),
hashString(task.ID),
slaveIDTail(task.SlaveID),
task.IP(ipSources...),
task.SlaveIP,
}
// use DiscoveryInfo name if defined instead of task name
if task.HasDiscoveryInfo() {
// LEGACY TODO: REMOVE
ctx.taskName = task.DiscoveryInfo.Name
rg.taskContextRecord(ctx, task, f, domain, spec, newTask)
// LEGACY, TODO: REMOVE
ctx.taskName = spec(task.DiscoveryInfo.Name)
rg.taskContextRecord(ctx, task, f, domain, spec, newTask)
} else {
rg.taskContextRecord(ctx, task, f, domain, spec, newTask)
}
}
func (rg *RecordGenerator) taskContextRecord(ctx context, task state.Task, f state.Framework, domain string, spec labels.Func, enumTask *EnumerableTask) {
fname := labels.DomainFrag(f.Name, labels.Sep, spec)
tail := "." + domain + "."
// insert canonical A records
canonical := ctx.taskName + "-" + ctx.taskID + "-" + ctx.slaveID + "." + fname
arec := ctx.taskName + "." + fname
rg.insertTaskRR(arec+tail, ctx.taskIP, A, enumTask)
rg.insertTaskRR(canonical+tail, ctx.taskIP, A, enumTask)
rg.insertTaskRR(arec+".slave"+tail, ctx.slaveIP, A, enumTask)
rg.insertTaskRR(canonical+".slave"+tail, ctx.slaveIP, A, enumTask)
// recordName generates records for ctx.taskName, given some generation chain
recordName := func(gen chain) { gen("_" + ctx.taskName) }
// asSRV is always the last link in a chain, it must insert RR's
asSRV := func(target string) chain {
return func(records ...string) {
for i := range records {
name := records[i] + tail
rg.insertTaskRR(name, target, SRV, enumTask)
}
}
}
// Add RFC 2782 SRV records
var subdomains []string
if task.HasDiscoveryInfo() {
subdomains = []string{"slave"}
} else {
subdomains = []string{"slave", domainNone}
}
slaveHost := canonical + ".slave" + tail
for _, port := range task.Ports() {
slaveTarget := slaveHost + ":" + port
recordName(withProtocol(protocolNone, fname, spec,
withSubdomains(subdomains, asSRV(slaveTarget))))
}
if !task.HasDiscoveryInfo() {
return
}
for _, port := range task.DiscoveryInfo.Ports.DiscoveryPorts {
target := canonical + tail + ":" + strconv.Itoa(port.Number)
recordName(withProtocol(port.Protocol, fname, spec,
withNamedPort(port.Name, spec, asSRV(target))))
}
}
// A records for each local interface
// If this causes problems you should explicitly set the
// listener address in config.json
func (rg *RecordGenerator) setFromLocal(host string, ns string) {
ifaces, err := net.Interfaces()
if err != nil {
logging.Error.Println(err)
}
// handle err
for _, i := range ifaces {
addrs, err := i.Addrs()
if err != nil {
logging.Error.Println(err)
}
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if ip == nil || ip.IsLoopback() {
continue
}
ip = ip.To4()
if ip == nil {
continue
}
rg.insertRR(ns, ip.String(), A)
}
}
}
// insertRR adds a record to the appropriate record map for the given name/host pair,
// but only if the pair is unique. returns true if added, false otherwise.
// TODO(???): REFACTOR when storage is updated
func (rg *RecordGenerator) insertTaskRR(name, host string, kind rrsKind, enumTask *EnumerableTask) bool {
if rg.insertRR(name, host, kind) {
enumRecord := EnumerableRecord{Name: name, Host: host, Rtype: string(kind)}
enumTask.Records = append(enumTask.Records, enumRecord)
return true
}
return false
}
func (rg *RecordGenerator) insertRR(name, host string, kind rrsKind) (added bool) {
if rrs := kind.rrs(rg); rrs != nil {
if added = rrs.add(name, host); added {
logging.VeryVerbose.Println("[" + string(kind) + "]\t" + name + ": " + host)
}
}
return
}
// leaderIP returns the ip for the mesos master
// input format master@ip:port
func leaderIP(leader string) string {
pair := strings.Split(leader, "@")[1]
return strings.Split(pair, ":")[0]
}
// return the slave number from a Mesos slave id
func slaveIDTail(slaveID string) string {
fields := strings.Split(slaveID, "-")
return strings.ToLower(fields[len(fields)-1])
}
// should be able to accept
// ip:port
// zk://host1:port1,host2:port2,.../path
// zk://username:password@host1:port1,host2:port2,.../path
// file:///path/to/file (where file contains one of the above)
func getProto(pair string) (string, string, error) {
h := strings.SplitN(pair, ":", 2)
if len(h) != 2 {
return "", "", fmt.Errorf("unable to parse proto from %q", pair)
}
return h[0], h[1], nil
}