diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index c5f51af..fcab44a 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -1,7 +1,10 @@ { - "ImportPath": "github.com/tonistiigi/dnsdock", + "ImportPath": "github.com/aacebedo/dnsdock", "GoVersion": "go1.6", "GodepVersion": "v74", + "Packages": [ + "./..." + ], "Deps": [ { "ImportPath": "github.com/Microsoft/go-winio", @@ -13,14 +16,26 @@ "Comment": "v0.10.0-19-gf3cfb45", "Rev": "f3cfb454f4c209e6668c95216c4744b8fddb2356" }, + { + "ImportPath": "github.com/alecthomas/template", + "Rev": "a0175ee3bccc567396460bf5acd36800cb10c49c" + }, + { + "ImportPath": "github.com/alecthomas/template/parse", + "Rev": "a0175ee3bccc567396460bf5acd36800cb10c49c" + }, + { + "ImportPath": "github.com/alecthomas/units", + "Rev": "2efee857e7cfd4f3d0138cc3cbb1b4966962b93a" + }, { "ImportPath": "github.com/docker/distribution/digest", - "Comment": "v2.5.0-rc.1-22-g4e17ab5", + "Comment": "docs-v2.4.1-2016-06-28-6-g4e17ab5", "Rev": "4e17ab5d319ac5b70b2769442947567a83386fbc" }, { "ImportPath": "github.com/docker/distribution/reference", - "Comment": "v2.5.0-rc.1-22-g4e17ab5", + "Comment": "docs-v2.4.1-2016-06-28-6-g4e17ab5", "Rev": "4e17ab5d319ac5b70b2769442947567a83386fbc" }, { @@ -100,22 +115,22 @@ }, { "ImportPath": "github.com/docker/go-connections/nat", - "Comment": "v0.2.0-8-g990a1a1", + "Comment": "v0.2.1", "Rev": "990a1a1a70b0da4c4cb70e117971a4f0babfbf1a" }, { "ImportPath": "github.com/docker/go-connections/sockets", - "Comment": "v0.2.0-8-g990a1a1", + "Comment": "v0.2.1", "Rev": "990a1a1a70b0da4c4cb70e117971a4f0babfbf1a" }, { "ImportPath": "github.com/docker/go-connections/tlsconfig", - "Comment": "v0.2.0-8-g990a1a1", + "Comment": "v0.2.1", "Rev": "990a1a1a70b0da4c4cb70e117971a4f0babfbf1a" }, { "ImportPath": "github.com/docker/go-units", - "Comment": "v0.1.0-23-g5d2041e", + "Comment": "v0.3.0", "Rev": "5d2041e26a699eaca682e2ea41c8f891e1060444" }, { @@ -132,6 +147,11 @@ "ImportPath": "github.com/miekg/dns", "Rev": "5d001d020961ae1c184f9f8152fdc73810481677" }, + { + "ImportPath": "github.com/op/go-logging", + "Comment": "v1-7-g970db52", + "Rev": "970db520ece77730c7e4724c61121037378659d9" + }, { "ImportPath": "github.com/opencontainers/runc/libcontainer/user", "Comment": "v1.0.0-rc1-50-g7221e38", @@ -156,6 +176,11 @@ { "ImportPath": "golang.org/x/sys/windows", "Rev": "62bee037599929a6e9146f29d10dd5208c43507d" + }, + { + "ImportPath": "gopkg.in/alecthomas/kingpin.v2", + "Comment": "v2.2.3", + "Rev": "e9044be3ab2a8e11d4e1f418d12f0790d57e8d70" } ] } diff --git a/src/core/cmdline.go b/src/core/cmdline.go new file mode 100644 index 0000000..28bf585 --- /dev/null +++ b/src/core/cmdline.go @@ -0,0 +1,65 @@ +/* cmdline.go + * + * Copyright (C) 2016 Alexandre ACEBEDO + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +package core + +import ( + "fmt" + "gopkg.in/alecthomas/kingpin.v2" + "github.com/aacebedo/dnsdock/src/utils" + "strconv" +) + + +const ( + // VERSION dnsdock version + VERSION = "1.16.0" +) + +// CommandLine structure handling parameter parsing +type CommandLine struct{} + +// ParseParameters Parse parameters +func (cmdline *CommandLine) ParseParameters(rawParams []string) (res *utils.Config, err error) { + res = utils.NewConfig() + + app := kingpin.New("dnsdock", "Automatic DNS for docker containers.") + app.Version(VERSION) + app.HelpFlag.Short('h') + nameservers := app.Flag("nameserver", "Comma separated list of DNS server(s) for unmatched requests").Strings() + dns := app.Flag("dns", "Listen DNS requests on this address").Default(res.DnsAddr).Short('d').String() + http := app.Flag("http", "Listen HTTP requests on this address").Default(res.HttpAddr).Short('t').String() + domain := app.Flag("domain", "Domain that is appended to all requests").Default(res.Domain.String()).String() + environment := app.Flag("environment", "Optional context before domain suffix").Default("").String() + docker := app.Flag("docker", "Path to the docker socket").Default(res.DockerHost).String() + tlsverify := app.Flag("tlsverify", "Enable mTLS when connecting to docker").Default(strconv.FormatBool(res.TlsVerify)).Bool() + tlscacert := app.Flag("tlscacert", "Path to CA certificate").Default(res.TlsCaCert).String() + tlscert := app.Flag("tlscert", "Path to Client certificate").Default(res.TlsCert).String() + tlskey := app.Flag("tlskey", "Path to client certificate private key").Default(res.TlsKey).String() + ttl := app.Flag("ttl", "TTL for matched requests").Default(strconv.FormatInt(int64(res.Ttl), 10)).Int() + createAlias := app.Flag("alias", "Automatically create an alias with just the container name.").Default(strconv.FormatBool(res.CreateAlias)).Bool() + verbose := app.Flag("verbose", "Verbose mode.").Default(strconv.FormatBool(res.Verbose)).Short('v').Bool() + quiet := app.Flag("quiet", "Quiet mode.").Default(strconv.FormatBool(res.Quiet)).Short('q').Bool() + + kingpin.MustParse(app.Parse(rawParams)) + + res.Verbose = *verbose + res.Quiet = *quiet + res.Nameserver = *nameservers + res.DnsAddr = *dns + res.HttpAddr = *http + res.Domain = utils.NewDomain(fmt.Sprintf("%s.%s", *environment, *domain)) + res.DockerHost = *docker + res.TlsVerify = *tlsverify + res.TlsCaCert = *tlscacert + res.TlsCert = *tlscert + res.TlsKey = *tlskey + res.Ttl = *ttl + res.CreateAlias = *createAlias + return +} diff --git a/src/docker.go b/src/core/docker.go similarity index 72% rename from src/docker.go rename to src/core/docker.go index 345aa97..11cf624 100644 --- a/src/docker.go +++ b/src/core/docker.go @@ -1,4 +1,12 @@ -package main +/* docker.go + * + * Copyright (C) 2016 Alexandre ACEBEDO + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +package core import ( "crypto/tls" @@ -6,29 +14,28 @@ import ( "github.com/docker/engine-api/client" "github.com/docker/engine-api/types" eventtypes "github.com/docker/engine-api/types/events" - "github.com/docker/engine-api/types/network" "github.com/vdemeester/docker-events" - "log" + "github.com/aacebedo/dnsdock/src/utils" + "github.com/aacebedo/dnsdock/src/servers" "net" "regexp" "strconv" "strings" - "golang.org/x/net/context" ) // DockerManager is the entrypoint to the docker daemon type DockerManager struct { - config *Config - list ServiceListProvider + config *utils.Config + list servers.ServiceListProvider client *client.Client cancel context.CancelFunc } // NewDockerManager creates a new DockerManager -func NewDockerManager(c *Config, list ServiceListProvider, tlsConfig *tls.Config) (*DockerManager, error) { +func NewDockerManager(c *utils.Config, list servers.ServiceListProvider, tlsConfig *tls.Config) (*DockerManager, error) { defaultHeaders := map[string]string{"User-Agent": "engine-api-cli-1.0"} - dclient, err := client.NewClient(c.dockerHost, "v1.22", nil, defaultHeaders) + dclient, err := client.NewClient(c.DockerHost, "v1.22", nil, defaultHeaders) if err != nil { return nil, err @@ -42,17 +49,17 @@ func (d *DockerManager) Start() error { ctx, cancel := context.WithCancel(context.Background()) d.cancel = cancel startHandler := func(m eventtypes.Message) { - log.Printf("Started container '%s'", m.ID) + logger.Debugf("Started container '%s'", m.ID) service, err := d.getService(m.ID) if err != nil { - log.Println(err) + logger.Errorf("%s", err) } else { d.list.AddService(m.ID, *service) } } stopHandler := func(m eventtypes.Message) { - log.Printf("Stopped container '%s'", m.ID) + logger.Debugf("Stopped container '%s'", m.ID) d.list.RemoveService(m.ID) } @@ -60,11 +67,11 @@ func (d *DockerManager) Start() error { oldName, ok := m.Actor.Attributes["oldName"] name, ok2 := m.Actor.Attributes["oldName"] if ok && ok2 { - log.Printf("Renamed container '%s' into '%s'", oldName, name) + logger.Debugf("Renamed container '%s' into '%s'", oldName, name) d.list.RemoveService(oldName) service, err := d.getService(m.ID) if err != nil { - log.Println(err) + logger.Errorf("%s", err) } else { d.list.AddService(m.ID, *service) } @@ -88,7 +95,7 @@ func (d *DockerManager) Start() error { for _, container := range containers { service, err := d.getService(container.ID) if err != nil { - log.Println(err) + logger.Errorf("%s", err) continue } d.list.AddService(container.ID, *service) @@ -102,33 +109,32 @@ func (d *DockerManager) Stop() { d.cancel() } -func (d *DockerManager) getService(id string) (*Service, error) { +func (d *DockerManager) getService(id string) (*servers.Service, error) { desc, err := d.client.ContainerInspect(context.Background(), id) if err != nil { return nil, err } - service := NewService() + service := servers.NewService() service.Aliases = make([]string, 0) service.Image = getImageName(desc.Config.Image) if imageNameIsSHA(service.Image, desc.Image) { - log.Println("Warning: Can't route ", id[:10], ", image", service.Image, "is not a tag.") + logger.Warningf("Warning: Can't route %s, image %s is not a tag.", id[:10], service.Image) service.Image = "" } service.Name = cleanContainerName(desc.Name) + switch len(desc.NetworkSettings.Networks) { case 0: - log.Println("Warning, no IP address found for container ", desc.Name) + logger.Warningf("Warning, no IP address found for container '%s' ", desc.Name) default: - v := make([]*network.EndpointSettings, 0, len(desc.NetworkSettings.Networks)) for _, value := range desc.NetworkSettings.Networks { - v = append(v, value) + ip := net.ParseIP(value.IPAddress) + if ip != nil { + service.IPs = append(service.IPs,ip) + } } - if len(v) > 1 { - log.Println("Warning, Multiple IP address found for container ", desc.Name, ". Only the first address will be used") - } - service.IP = net.ParseIP(v[0].IPAddress) } service = overrideFromLabels(service, desc.Config.Labels) @@ -137,7 +143,7 @@ func (d *DockerManager) getService(id string) (*Service, error) { return nil, errors.New("Skipping " + id) } - if d.config.createAlias { + if d.config.CreateAlias { service.Aliases = append(service.Aliases, service.Name) } return service, nil @@ -183,7 +189,7 @@ func splitEnv(in []string) (out map[string]string) { return } -func overrideFromLabels(in *Service, labels map[string]string) (out *Service) { +func overrideFromLabels(in *servers.Service, labels map[string]string) (out *servers.Service) { var region string for k, v := range labels { if k == "com.dnsdock.ignore" { @@ -223,9 +229,20 @@ func overrideFromLabels(in *Service, labels map[string]string) (out *Service) { if k == "com.dnsdock.ip_addr" { ipAddr := net.ParseIP(v) if ipAddr != nil { - in.IP = ipAddr + in.IPs = in.IPs[:0] + in.IPs = append(in.IPs, ipAddr) } } + + if k == "com.dnsdock.prefix" { + addrs := make([]net.IP, 0) + for _, value := range in.IPs { + if strings.HasPrefix(value.String(), v) { + addrs = append(addrs, value) + } + } + in.IPs = addrs + } } if len(region) > 0 { @@ -235,7 +252,7 @@ func overrideFromLabels(in *Service, labels map[string]string) (out *Service) { return } -func overrideFromEnv(in *Service, env map[string]string) (out *Service) { +func overrideFromEnv(in *servers.Service, env map[string]string) (out *servers.Service) { var region string for k, v := range env { if k == "DNSDOCK_IGNORE" || k == "SERVICE_IGNORE" { @@ -275,9 +292,20 @@ func overrideFromEnv(in *Service, env map[string]string) (out *Service) { if k == "DNSDOCK_IPADDRESS" { ipAddr := net.ParseIP(v) if ipAddr != nil { - in.IP = ipAddr + in.IPs = in.IPs[:0] + in.IPs = append(in.IPs, ipAddr) } } + + if k == "DNSDOCK_PREFIX" { + addrs := make([]net.IP, 0) + for _, value := range in.IPs { + if strings.HasPrefix(value.String(), v) { + addrs = append(addrs, value) + } + } + in.IPs = addrs + } } if len(region) > 0 { diff --git a/src/docker_test.go b/src/core/docker_test.go similarity index 88% rename from src/docker_test.go rename to src/core/docker_test.go index ea69793..f6b1d2a 100644 --- a/src/docker_test.go +++ b/src/core/docker_test.go @@ -1,8 +1,17 @@ -package main +/* docker_test.go + * + * Copyright (C) 2016 Alexandre ACEBEDO + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +package core import ( "reflect" "testing" + "github.com/aacebedo/dnsdock/src/servers" ) func TestGetImageName(t *testing.T) { @@ -68,8 +77,8 @@ func TestSplitEnv(t *testing.T) { } func TestOverrideFromEnv(t *testing.T) { - getService := func() *Service { - service := NewService() + getService := func() *servers.Service { + service := servers.NewService() service.Name = "myfoo" service.Image = "mybar" return service diff --git a/src/core/loggers.go b/src/core/loggers.go new file mode 100644 index 0000000..257aa02 --- /dev/null +++ b/src/core/loggers.go @@ -0,0 +1,15 @@ +/* loggers.go + * + * Copyright (C) 2016 Alexandre ACEBEDO + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +package core + +import ( + "github.com/op/go-logging" +) + +var logger = logging.MustGetLogger("dnsdock.core") diff --git a/src/main.go b/src/main.go index 73fcdf7..4ba9b12 100644 --- a/src/main.go +++ b/src/main.go @@ -1,94 +1,85 @@ +/* dnsdock.go + * + * Copyright (C) 2016 Alexandre ACEBEDO + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + package main import ( "crypto/tls" "crypto/x509" - "flag" - "fmt" "io/ioutil" - "log" "os" + "github.com/aacebedo/dnsdock/src/core" + "github.com/aacebedo/dnsdock/src/utils" + "github.com/aacebedo/dnsdock/src/servers" + "github.com/op/go-logging" ) -var version string +var logger = logging.MustGetLogger("dnsdock.main") func main() { - help := flag.Bool("help", false, "Show this message") - - config := NewConfig() - - flag.Var(&config.nameserver, "nameserver", "Comma separated list of DNS server(s) for unmatched requests") - flag.StringVar(&config.dnsAddr, "dns", config.dnsAddr, "Listen DNS requests on this address") - flag.StringVar(&config.httpAddr, "http", config.httpAddr, "Listen HTTP requests on this address") - domain := flag.String("domain", config.domain.String(), "Domain that is appended to all requests") - environment := flag.String("environment", "", "Optional context before domain suffix") - flag.StringVar(&config.dockerHost, "docker", config.dockerHost, "Path to the docker socket") - flag.BoolVar(&config.tlsVerify, "tlsverify", false, "Enable mTLS when connecting to docker") - flag.StringVar(&config.tlsCaCert, "tlscacert", config.tlsCaCert, "Path to CA certificate") - flag.StringVar(&config.tlsCert, "tlscert", config.tlsCert, "Path to client certificate") - flag.StringVar(&config.tlsKey, "tlskey", config.tlsKey, "Path to client certificate private key") - flag.BoolVar(&config.verbose, "verbose", true, "Verbose output") - flag.IntVar(&config.ttl, "ttl", config.ttl, "TTL for matched requests") - flag.BoolVar(&config.createAlias, "create-alias", config.createAlias, "Automatically create an alias with just the container name.") - var showVersion bool - if len(version) > 0 { - flag.BoolVar(&showVersion, "version", false, "Show application version") + var cmdLine core.CommandLine + config, err := cmdLine.ParseParameters(os.Args[1:]) + if err != nil { + logger.Fatalf(err.Error()) } - - flag.Parse() - - if showVersion { - fmt.Println("dnsdock", version) - return + verbosity := 0 + if config.Quiet == false { + if config.Verbose == false { + verbosity = 1 + } else { + verbosity = 2 + } } - - if *help { - fmt.Fprintf(os.Stderr, "Usage: %s [options]\n", os.Args[0]) - flag.PrintDefaults() - return + err = utils.InitLoggers(verbosity) + if err != nil { + logger.Fatalf("Unable to initialize loggers! %s", err.Error()) } - - config.domain = NewDomain(*environment + "." + *domain) - - dnsServer := NewDNSServer(config) + + dnsServer := servers.NewDNSServer(config) var tlsConfig *tls.Config - if config.tlsVerify { - clientCert, err := tls.LoadX509KeyPair(config.tlsCert, config.tlsKey) + if config.TlsVerify { + clientCert, err := tls.LoadX509KeyPair(config.TlsCert, config.TlsKey) if err != nil { - log.Fatal(err) + logger.Fatalf("Error: '%s'", err) } tlsConfig = &tls.Config{ MinVersion: tls.VersionTLS12, Certificates: []tls.Certificate{clientCert}, } - pemData, err := ioutil.ReadFile(config.tlsCaCert) + pemData, err := ioutil.ReadFile(config.TlsCaCert) if err == nil { rootCert := x509.NewCertPool() rootCert.AppendCertsFromPEM(pemData) tlsConfig.RootCAs = rootCert } else { - log.Print(err) + logger.Fatalf("Error: '%s'", err) } } - docker, err := NewDockerManager(config, dnsServer, tlsConfig) + + docker, err := core.NewDockerManager(config, dnsServer, tlsConfig) if err != nil { - log.Fatal(err) + logger.Fatalf("Error: '%s'", err) } if err := docker.Start(); err != nil { - log.Fatal(err) + logger.Fatalf("Error: '%s'", err) } - - httpServer := NewHTTPServer(config, dnsServer) + + httpServer := servers.NewHTTPServer(config, dnsServer) go func() { if err := httpServer.Start(); err != nil { - log.Fatal(err) + logger.Fatalf("Error: '%s'", err) } }() if err := dnsServer.Start(); err != nil { - log.Fatal(err) + logger.Fatalf("Error: '%s'", err) } } diff --git a/src/dnsserver.go b/src/servers/dnsserver.go similarity index 78% rename from src/dnsserver.go rename to src/servers/dnsserver.go index db5bb97..24252e5 100644 --- a/src/dnsserver.go +++ b/src/servers/dnsserver.go @@ -1,14 +1,21 @@ -package main +/* dnsserver.go + * + * Copyright (C) 2016 Alexandre ACEBEDO + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +package servers import ( "errors" - "log" "net" "regexp" "strings" "sync" "time" - + "github.com/aacebedo/dnsdock/src/utils" "github.com/miekg/dns" ) @@ -16,7 +23,7 @@ import ( type Service struct { Name string Image string - IP net.IP + IPs []net.IP TTL int Aliases []string } @@ -37,7 +44,7 @@ type ServiceListProvider interface { // DNSServer represents a DNS server type DNSServer struct { - config *Config + config *utils.Config server *dns.Server mux *dns.ServeMux services map[string]*Service @@ -45,23 +52,21 @@ type DNSServer struct { } // NewDNSServer create a new DNSServer -func NewDNSServer(c *Config) *DNSServer { +func NewDNSServer(c *utils.Config) *DNSServer { s := &DNSServer{ config: c, services: make(map[string]*Service), lock: &sync.RWMutex{}, } - if s.config.verbose { - log.Println("Handling DNS requests for " + c.domain.String() + ".") - } + logger.Debugf("Handling DNS requests for '%s'.", c.Domain.String()) s.mux = dns.NewServeMux() - s.mux.HandleFunc(c.domain.String()+".", s.handleRequest) + s.mux.HandleFunc(c.Domain.String()+".", s.handleRequest) s.mux.HandleFunc("in-addr.arpa.", s.handleReverseRequest) s.mux.HandleFunc(".", s.handleForward) - s.server = &dns.Server{Addr: c.dnsAddr, Net: "udp", Handler: s.mux} + s.server = &dns.Server{Addr: c.DnsAddr, Net: "udp", Handler: s.mux} return s } @@ -78,22 +83,22 @@ func (s *DNSServer) Stop() { // AddService adds a new container and thus new DNS records func (s *DNSServer) AddService(id string, service Service) { - defer s.lock.Unlock() - s.lock.Lock() - - id = s.getExpandedID(id) - s.services[id] = &service - - if s.config.verbose { - log.Println("Added service:", id, service) - } - - for _, alias := range service.Aliases { - if s.config.verbose { - log.Println("Handling DNS requests for " + alias + ".") - } - s.mux.HandleFunc(alias+".", s.handleRequest) - } + if len(service.IPs) > 0 { + defer s.lock.Unlock() + s.lock.Lock() + + id = s.getExpandedID(id) + s.services[id] = &service + + logger.Debugf("Added service: '%s': %s.", id, service) + + for _, alias := range service.Aliases { + logger.Debugf("Handling DNS requests for '%s'.", alias) + s.mux.HandleFunc(alias+".", s.handleRequest) + } + } else { + logger.Warningf("Service '%s' ignored: No IP provided:", id, id) + } } // RemoveService removes a new container and thus DNS records @@ -112,9 +117,7 @@ func (s *DNSServer) RemoveService(id string) error { delete(s.services, id) - if s.config.verbose { - log.Println("Stopped service:", id) - } + logger.Debugf("Stopped service '%s'", id) return nil } @@ -151,9 +154,9 @@ func (s *DNSServer) listDomains(service *Service) chan string { go func() { if service.Image == "" { - c <- service.Name + "." + s.config.domain.String() + "." + c <- service.Name + "." + s.config.Domain.String() + "." } else { - domain := service.Image + "." + s.config.domain.String() + "." + domain := service.Image + "." + s.config.Domain.String() + "." c <- service.Name + "." + domain c <- domain @@ -170,27 +173,25 @@ func (s *DNSServer) listDomains(service *Service) chan string { } func (s *DNSServer) handleForward(w dns.ResponseWriter, r *dns.Msg) { - if s.config.verbose { - log.Println("Using DNS forwarding for " + r.Question[0].Name) - log.Println("Forwarding DNS nameservers: " + s.config.nameserver.String()) - } + + logger.Debugf("Using DNS forwarding for '%s'",r.Question[0].Name) + logger.Debugf("Forwarding DNS nameservers: %s",s.config.Nameserver.String()) + // Otherwise just forward the request to another server c := new(dns.Client) - // look at each nameserver, stop on success - for i := range s.config.nameserver { - if s.config.verbose { - log.Println("Using nameserver " + s.config.nameserver[i]) - } + // look at each Nameserver, stop on success + for i := range s.config.Nameserver { + logger.Debugf("Using Nameserver %s", s.config.Nameserver[i]) - in, _, err := c.Exchange(r, s.config.nameserver[i]) + in, _, err := c.Exchange(r, s.config.Nameserver[i]) if err == nil { w.WriteMsg(in) return } - if i == (len(s.config.nameserver) - 1) { - log.Println("Error forwarding DNS: " + err.Error() + ": fatal, no more nameservers to try") + if i == (len(s.config.Nameserver) - 1) { + logger.Fatalf("DNS fowarding for '%s' failed: no more nameservers to try", err.Error()) // Send failure reply m := new(dns.Msg) @@ -200,7 +201,7 @@ func (s *DNSServer) handleForward(w dns.ResponseWriter, r *dns.Msg) { w.WriteMsg(m) } else { - log.Println("Error forwarding DNS: " + err.Error() + ": trying next nameserver...") + logger.Errorf("DNS fowarding for '%s' failed: trying next Nameserver...", err.Error()) } } } @@ -212,7 +213,7 @@ func (s *DNSServer) makeServiceA(n string, service *Service) dns.RR { if service.TTL != -1 { ttl = service.TTL } else { - ttl = s.config.ttl + ttl = s.config.Ttl } rr.Hdr = dns.RR_Header{ @@ -222,8 +223,15 @@ func (s *DNSServer) makeServiceA(n string, service *Service) dns.RR { Ttl: uint32(ttl), } - rr.A = service.IP - + if len(service.IPs) != 0 { + if len(service.IPs) > 1 { + logger.Warningf("Multiple IP address found for container '%s'. Only the first address will be used", service.Name) + } + rr.A = service.IPs[0] + } else { + logger.Errorf("No valid IP address found for container '%s' ", service.Name) + } + return rr } @@ -234,7 +242,7 @@ func (s *DNSServer) makeServiceMX(n string, service *Service) dns.RR { if service.TTL != -1 { ttl = service.TTL } else { - ttl = s.config.ttl + ttl = s.config.Ttl } rr.Hdr = dns.RR_Header{ @@ -276,9 +284,7 @@ func (s *DNSServer) handleRequest(w dns.ResponseWriter, r *dns.Msg) { query = query[:len(query)-1] } - if s.config.verbose { - log.Println("DNS request for query " + query + " from remote " + w.RemoteAddr().String()) - } + logger.Debugf("DNS request for query '%s' from remote '%s'", w.RemoteAddr().String(), w.RemoteAddr()) for service := range s.queryServices(query) { var rr dns.RR @@ -297,9 +303,8 @@ func (s *DNSServer) handleRequest(w dns.ResponseWriter, r *dns.Msg) { return } - if s.config.verbose { - log.Println("DNS record found for " + query) - } + logger.Debugf("DNS record found for query '%s'",query) + m.Answer = append(m.Answer, rr) } @@ -307,9 +312,7 @@ func (s *DNSServer) handleRequest(w dns.ResponseWriter, r *dns.Msg) { if len(m.Answer) == 0 { m.Ns = s.createSOA() m.SetRcode(r, dns.RcodeNameError) // NXDOMAIN - if s.config.verbose { - log.Println("No DNS record found for " + query) - } + logger.Debugf("No DNS record found for query '%s'",query) } w.WriteMsg(m) @@ -346,7 +349,7 @@ func (s *DNSServer) handleReverseRequest(w dns.ResponseWriter, r *dns.Msg) { if service.TTL != -1 { ttl = service.TTL } else { - ttl = s.config.ttl + ttl = s.config.Ttl } for domain := range s.listDomains(service) { @@ -382,7 +385,7 @@ func (s *DNSServer) queryIP(query string) chan *Service { s.lock.RLock() for _, service := range s.services { - if service.IP.String() == ip { + if service.IPs[0].String() == ip { c <- service } } @@ -414,7 +417,7 @@ func (s *DNSServer) queryServices(query string) chan *Service { test = append(test, strings.Split(service.Image, ".")...) } - test = append(test, s.config.domain...) + test = append(test, s.config.Domain...) if isPrefixQuery(query, test) { c <- service @@ -466,20 +469,20 @@ func (s *DNSServer) getExpandedID(in string) (out string) { // for a long time. The other defaults left as is(skydns source) because they // do not have an use case in this situation. func (s *DNSServer) createSOA() []dns.RR { - dom := dns.Fqdn(s.config.domain.String() + ".") + dom := dns.Fqdn(s.config.Domain.String() + ".") soa := &dns.SOA{ Hdr: dns.RR_Header{ Name: dom, Rrtype: dns.TypeSOA, Class: dns.ClassINET, - Ttl: uint32(s.config.ttl)}, + Ttl: uint32(s.config.Ttl)}, Ns: "dnsdock." + dom, Mbox: "dnsdock.dnsdock." + dom, Serial: uint32(time.Now().Truncate(time.Hour).Unix()), Refresh: 28800, Retry: 7200, Expire: 604800, - Minttl: uint32(s.config.ttl), + Minttl: uint32(s.config.Ttl), } return []dns.RR{soa} } diff --git a/src/dnsserver_test.go b/src/servers/dnsserver_test.go similarity index 78% rename from src/dnsserver_test.go rename to src/servers/dnsserver_test.go index 0151753..e906413 100644 --- a/src/dnsserver_test.go +++ b/src/servers/dnsserver_test.go @@ -1,19 +1,27 @@ -package main +/* dnsserver_test.go + * + * Copyright (C) 2016 Alexandre ACEBEDO + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +package servers import ( "net" "strings" "testing" "time" - + "github.com/aacebedo/dnsdock/src/utils" "github.com/miekg/dns" ) func TestDNSResponse(t *testing.T) { const TestAddr = "127.0.0.1:9953" - config := NewConfig() - config.dnsAddr = TestAddr + config := utils.NewConfig() + config.DnsAddr = TestAddr server := NewDNSServer(config) go server.Start() @@ -21,10 +29,10 @@ func TestDNSResponse(t *testing.T) { // Allow some time for server to start time.Sleep(250 * time.Millisecond) - server.AddService("foo", Service{Name: "foo", Image: "bar", IP: net.ParseIP("127.0.0.1")}) - server.AddService("baz", Service{Name: "baz", Image: "bar", IP: net.ParseIP("127.0.0.1"), TTL: -1}) - server.AddService("biz", Service{Name: "hey", Image: "", IP: net.ParseIP("127.0.0.4")}) - server.AddService("joe", Service{Name: "joe", Image: "", IP: net.ParseIP("127.0.0.5"), Aliases: []string{"lala.docker", "super-alias", "alias.domain"}}) + server.AddService("foo", Service{Name: "foo", Image: "bar", IPs: []net.IP {net.ParseIP("127.0.0.1")}}) + server.AddService("baz", Service{Name: "baz", Image: "bar", IPs: []net.IP {net.ParseIP("127.0.0.1")}, TTL: -1}) + server.AddService("biz", Service{Name: "hey", Image: "", IPs: []net.IP {net.ParseIP("127.0.0.4")}}) + server.AddService("joe", Service{Name: "joe", Image: "", IPs: []net.IP {net.ParseIP("127.0.0.5")}, Aliases: []string{"lala.docker", "super-alias", "alias.domain"}}) var inputs = []struct { query string @@ -100,13 +108,13 @@ func TestDNSResponse(t *testing.T) { } func TestServiceManagement(t *testing.T) { - list := ServiceListProvider(NewDNSServer(NewConfig())) + list := ServiceListProvider(NewDNSServer(utils.NewConfig())) if len(list.GetAllServices()) != 0 { t.Error("Initial service count should be 0.") } - A := Service{Name: "bar"} + A := Service{Name: "bar", IPs: []net.IP {net.ParseIP("127.0.0.1")}} list.AddService("foo", A) if len(list.GetAllServices()) != 1 { @@ -130,7 +138,7 @@ func TestServiceManagement(t *testing.T) { t.Error("Request to boo should have failed") } - list.AddService("boo", Service{Name: "boo"}) + list.AddService("boo", Service{Name: "boo", IPs: []net.IP {net.ParseIP("127.0.0.1")}}) all := list.GetAllServices() @@ -160,7 +168,7 @@ func TestServiceManagement(t *testing.T) { t.Error("Item count after remove should be 1") } - list.AddService("416261e74515b7dd1dbd55f35e8625b063044f6ddf74907269e07e9f142bc0df", Service{Name: "mysql"}) + list.AddService("416261e74515b7dd1dbd55f35e8625b063044f6ddf74907269e07e9f142bc0df", Service{Name: "mysql", IPs: []net.IP {net.ParseIP("127.0.0.1")}}) if s1, _ = list.GetService("416261"); s1.Name != "mysql" { t.Error("Container can't be found by prefix") @@ -178,12 +186,12 @@ func TestServiceManagement(t *testing.T) { } func TestDNSRequestMatch(t *testing.T) { - server := NewDNSServer(NewConfig()) + server := NewDNSServer(utils.NewConfig()) - server.AddService("foo", Service{Name: "foo", Image: "bar"}) - server.AddService("baz", Service{Name: "baz", Image: "bar"}) - server.AddService("abc", Service{Name: "def", Image: "ghi"}) - server.AddService("qux", Service{Name: "qux", Image: ""}) + server.AddService("foo", Service{Name: "foo", Image: "bar", IPs: []net.IP {net.ParseIP("127.0.0.1")}}) + server.AddService("baz", Service{Name: "baz", Image: "bar", IPs: []net.IP {net.ParseIP("127.0.0.1")}}) + server.AddService("abc", Service{Name: "def", Image: "ghi", IPs: []net.IP {net.ParseIP("127.0.0.1")}}) + server.AddService("qux", Service{Name: "qux", Image: "", IPs: []net.IP {net.ParseIP("127.0.0.1")}}) inputs := []struct { query, domain string @@ -209,7 +217,7 @@ func TestDNSRequestMatch(t *testing.T) { } for _, input := range inputs { - server.config.domain = NewDomain(input.domain) + server.config.Domain = utils.NewDomain(input.domain) t.Log(input.query, input.domain) @@ -225,12 +233,12 @@ func TestDNSRequestMatch(t *testing.T) { } func TestDNSRequestMatchNamesWithDots(t *testing.T) { - server := NewDNSServer(NewConfig()) + server := NewDNSServer(utils.NewConfig()) - server.AddService("boo", Service{Name: "foo.boo", Image: "bar.zar"}) - server.AddService("baz", Service{Name: "baz", Image: "bar.zar"}) - server.AddService("abc", Service{Name: "bar", Image: "zar"}) - server.AddService("qux", Service{Name: "qux.quu", Image: ""}) + server.AddService("boo", Service{Name: "foo.boo", Image: "bar.zar", IPs: []net.IP {net.ParseIP("127.0.0.1")}}) + server.AddService("baz", Service{Name: "baz", Image: "bar.zar", IPs: []net.IP {net.ParseIP("127.0.0.1")}}) + server.AddService("abc", Service{Name: "bar", Image: "zar", IPs: []net.IP {net.ParseIP("127.0.0.1")}}) + server.AddService("qux", Service{Name: "qux.quu", Image: "", IPs: []net.IP {net.ParseIP("127.0.0.1")}}) inputs := []struct { query, domain string @@ -253,7 +261,7 @@ func TestDNSRequestMatchNamesWithDots(t *testing.T) { } for _, input := range inputs { - server.config.domain = NewDomain(input.domain) + server.config.Domain = utils.NewDomain(input.domain) t.Log(input.query, input.domain) actual := 0 @@ -268,7 +276,7 @@ func TestDNSRequestMatchNamesWithDots(t *testing.T) { } func TestgetExpandedID(t *testing.T) { - server := NewDNSServer(NewConfig()) + server := NewDNSServer(utils.NewConfig()) server.AddService("416261e74515b7dd1dbd55f35e8625b063044f6ddf74907269e07e9f142bc0df", Service{}) server.AddService("316261e74515b7dd1dbd55f35e8625b063044f6ddf74907269e07e9f14nothex", Service{}) diff --git a/src/http.go b/src/servers/http.go similarity index 83% rename from src/http.go rename to src/servers/http.go index 6f41675..c312f2f 100644 --- a/src/http.go +++ b/src/servers/http.go @@ -1,21 +1,29 @@ -package main +/* http.go + * + * Copyright (C) 2016 Alexandre ACEBEDO + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +package servers import ( "encoding/json" "github.com/gorilla/mux" - "log" "net/http" + "github.com/aacebedo/dnsdock/src/utils" ) // HTTPServer represents the http endpoint type HTTPServer struct { - config *Config + config *utils.Config list ServiceListProvider server *http.Server } // NewHTTPServer create a new http endpoint -func NewHTTPServer(c *Config, list ServiceListProvider) *HTTPServer { +func NewHTTPServer(c *utils.Config, list ServiceListProvider) *HTTPServer { s := &HTTPServer{ config: c, list: list, @@ -30,7 +38,7 @@ func NewHTTPServer(c *Config, list ServiceListProvider) *HTTPServer { router.HandleFunc("/set/ttl", s.setTTL).Methods("PUT") - s.server = &http.Server{Addr: c.httpAddr, Handler: router} + s.server = &http.Server{Addr: c.HttpAddr, Handler: router} return s } @@ -45,16 +53,15 @@ func (s *HTTPServer) getServices(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "application/json; charset=UTF-8") if err := json.NewEncoder(w).Encode(s.list.GetAllServices()); err != nil { - log.Println("Error encoding: ", err) + logger.Errorf("Encoding error: %s", err) http.Error(w, err.Error(), http.StatusInternalServerError) } } func (s *HTTPServer) getService(w http.ResponseWriter, req *http.Request) { vars := mux.Vars(req) - - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + id, ok := vars["id"] if !ok { http.Error(w, "ID required", http.StatusBadRequest) @@ -68,7 +75,7 @@ func (s *HTTPServer) getService(w http.ResponseWriter, req *http.Request) { } if err := json.NewEncoder(w).Encode(service); err != nil { - log.Println("Error: ", err) + logger.Errorf("Encoding error: %s", err) http.Error(w, err.Error(), http.StatusInternalServerError) } } @@ -84,7 +91,7 @@ func (s *HTTPServer) addService(w http.ResponseWriter, req *http.Request) { service := NewService() if err := json.NewDecoder(req.Body).Decode(&service); err != nil { - log.Println("Error decoding JSON: ", err) + logger.Errorf("JSON decoding error: %s", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -99,7 +106,7 @@ func (s *HTTPServer) addService(w http.ResponseWriter, req *http.Request) { return } - if service.IP == nil { + if len(service.IPs) == 0 || service.IPs[0] == nil { http.Error(w, "Property \"ip\" is required", http.StatusInternalServerError) return } @@ -139,7 +146,7 @@ func (s *HTTPServer) updateService(w http.ResponseWriter, req *http.Request) { var input map[string]interface{} if err := json.NewDecoder(req.Body).Decode(&input); err != nil { - log.Println("Error decoding JSON: ", err) + logger.Errorf("JSON decoding error: %s", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -177,11 +184,11 @@ func (s *HTTPServer) updateService(w http.ResponseWriter, req *http.Request) { func (s *HTTPServer) setTTL(w http.ResponseWriter, req *http.Request) { var value int if err := json.NewDecoder(req.Body).Decode(&value); err != nil { - log.Println("Error decoding value: ", err) + logger.Errorf("Decoding error: %s", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } - s.config.ttl = value + s.config.Ttl = value } diff --git a/src/http_test.go b/src/servers/http_test.go similarity index 62% rename from src/http_test.go rename to src/servers/http_test.go index a2243c1..34422ab 100644 --- a/src/http_test.go +++ b/src/servers/http_test.go @@ -1,4 +1,12 @@ -package main +/* http_test.go + * + * Copyright (C) 2016 Alexandre ACEBEDO + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +package servers import ( "io/ioutil" @@ -6,13 +14,14 @@ import ( "strings" "testing" "time" + "github.com/aacebedo/dnsdock/src/utils" ) func TestServiceRequests(t *testing.T) { const TestAddr = "127.0.0.1:9980" - config := NewConfig() - config.httpAddr = TestAddr + config := utils.NewConfig() + config.HttpAddr = TestAddr server := NewHTTPServer(config, NewDNSServer(config)) go server.Start() @@ -27,14 +36,14 @@ func TestServiceRequests(t *testing.T) { {"GET", "/services", "", "{}", 200}, {"GET", "/services/foo", "", "", 404}, {"PUT", "/services/foo", `{"name": "foo"}`, "", 500}, - {"PUT", "/services/foo", `{"name": "foo", "image": "bar", "ip": "127.0.0.1", "aliases": ["foo.docker"]}`, "", 200}, - {"GET", "/services/foo", "", `{"Name":"foo","Image":"bar","IP":"127.0.0.1","TTL":-1,"Aliases":["foo.docker"]}`, 200}, - {"PUT", "/services/boo", `{"name": "baz", "image": "bar", "ip": "127.0.0.2"}`, "", 200}, - {"GET", "/services", "", `{"boo":{"Name":"baz","Image":"bar","IP":"127.0.0.2","TTL":-1,"Aliases":null},"foo":{"Name":"foo","Image":"bar","IP":"127.0.0.1","TTL":-1,"Aliases":["foo.docker"]}}`, 200}, + {"PUT", "/services/foo", `{"name": "foo", "image": "bar", "ips": ["127.0.0.1"], "aliases": ["foo.docker"]}`, "", 200}, + {"GET", "/services/foo", "", `{"Name":"foo","Image":"bar","IPs":["127.0.0.1"],"TTL":-1,"Aliases":["foo.docker"]}`, 200}, + {"PUT", "/services/boo", `{"name": "baz", "image": "bar", "ips": ["127.0.0.2"]}`, "", 200}, + {"GET", "/services", "", `{"boo":{"Name":"baz","Image":"bar","IPs":["127.0.0.2"],"TTL":-1,"Aliases":null},"foo":{"Name":"foo","Image":"bar","IPs":["127.0.0.1"],"TTL":-1,"Aliases":["foo.docker"]}}`, 200}, {"PATCH", "/services/boo", `{"name": "bar", "ttl": 20, "image": "bar"}`, "", 200}, - {"GET", "/services/boo", "", `{"Name":"bar","Image":"bar","IP":"127.0.0.2","TTL":20,"Aliases":null}`, 200}, + {"GET", "/services/boo", "", `{"Name":"bar","Image":"bar","IPs":["127.0.0.2"],"TTL":20,"Aliases":null}`, 200}, {"DELETE", "/services/foo", ``, "", 200}, - {"GET", "/services", "", `{"boo":{"Name":"bar","Image":"bar","IP":"127.0.0.2","TTL":20,"Aliases":null}}`, 200}, + {"GET", "/services", "", `{"boo":{"Name":"bar","Image":"bar","IPs":["127.0.0.2"],"TTL":20,"Aliases":null}}`, 200}, } for _, input := range tests { @@ -68,7 +77,7 @@ func TestServiceRequests(t *testing.T) { } t.Log("Test TTL setter") - if config.ttl != 0 { + if config.Ttl != 0 { t.Error("Default TTL is not 0") } req, err := http.NewRequest("PUT", "http://"+TestAddr+"/set/ttl", strings.NewReader("12")) @@ -79,7 +88,7 @@ func TestServiceRequests(t *testing.T) { if err != nil { t.Error(err) } - if config.ttl != 12 { - t.Error("TTL not updated. Expected: 12 Got:", config.ttl) + if config.Ttl != 12 { + t.Error("TTL not updated. Expected: 12 Got:", config.Ttl) } } diff --git a/src/servers/loggers.go b/src/servers/loggers.go new file mode 100644 index 0000000..15a9116 --- /dev/null +++ b/src/servers/loggers.go @@ -0,0 +1,15 @@ +/* loggers.go + * + * Copyright (C) 2016 Alexandre ACEBEDO + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +package servers + +import ( + "github.com/op/go-logging" +) + +var logger = logging.MustGetLogger("dnsdock.servers") diff --git a/src/config.go b/src/utils/config.go similarity index 60% rename from src/config.go rename to src/utils/config.go index 467f02e..c381a78 100644 --- a/src/config.go +++ b/src/utils/config.go @@ -1,4 +1,12 @@ -package main +/* config.go + * + * Copyright (C) 2016 Alexandre ACEBEDO + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +package utils import ( "os" @@ -44,18 +52,19 @@ func (n *nameservers) Set(value string) error { // Config contains DNSDock configuration type Config struct { - nameserver nameservers - dnsAddr string - domain Domain - dockerHost string - tlsVerify bool - tlsCaCert string - tlsCert string - tlsKey string - verbose bool - httpAddr string - ttl int - createAlias bool + Nameserver nameservers + DnsAddr string + Domain Domain + DockerHost string + TlsVerify bool + TlsCaCert string + TlsCert string + TlsKey string + HttpAddr string + Ttl int + CreateAlias bool + Verbose bool + Quiet bool } // NewConfig creates a new config @@ -71,16 +80,18 @@ func NewConfig() *Config { } return &Config{ - nameserver: nameservers{"8.8.8.8:53"}, - dnsAddr: ":53", - domain: NewDomain("docker"), - dockerHost: dockerHost, - httpAddr: ":80", - createAlias: false, - tlsVerify: tlsVerify, - tlsCaCert: dockerCerts + "/ca.pem", - tlsCert: dockerCerts + "/cert.pem", - tlsKey: dockerCerts + "/key.pem", + Nameserver: nameservers{"8.8.8.8:53"}, + DnsAddr: ":53", + Domain: NewDomain("docker"), + DockerHost: dockerHost, + HttpAddr: ":80", + CreateAlias: false, + TlsVerify: tlsVerify, + TlsCaCert: dockerCerts + "/ca.pem", + TlsCert: dockerCerts + "/cert.pem", + TlsKey: dockerCerts + "/key.pem", + Verbose: false, + Quiet: false, } } diff --git a/src/config_test.go b/src/utils/config_test.go similarity index 68% rename from src/config_test.go rename to src/utils/config_test.go index b6224bd..e60fc4b 100644 --- a/src/config_test.go +++ b/src/utils/config_test.go @@ -1,4 +1,12 @@ -package main +/* config_test.go + * + * Copyright (C) 2016 Alexandre ACEBEDO + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +package utils import ( "testing" diff --git a/src/utils/loggers.go b/src/utils/loggers.go new file mode 100644 index 0000000..2437680 --- /dev/null +++ b/src/utils/loggers.go @@ -0,0 +1,45 @@ +/* loggers.go + * + * Copyright (C) 2016 Alexandre ACEBEDO + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +package utils + +import ( + "os" + "io/ioutil" + "github.com/op/go-logging" +) + +// InitLoggers initialize loggers +func InitLoggers(verbosity int) (err error) { + var format logging.Formatter + + var backend logging.Backend + + switch { + case verbosity == 0 : + backend = logging.NewLogBackend(ioutil.Discard, "", 0) + case verbosity >= 1 : + backend = logging.NewLogBackend(os.Stdout, "", 0) + } + + format = logging.MustStringFormatter(`%{color}%{time:15:04:05.000} | %{level:.10s} ▶%{color:reset} %{message}`) + + formatter := logging.NewBackendFormatter(backend, format) + leveledBackend := logging.AddModuleLevel(formatter) + + switch { + case verbosity == 1 : + leveledBackend.SetLevel(logging.INFO, "") + case verbosity >= 2 : + leveledBackend.SetLevel(logging.DEBUG, "") + } + + logging.SetBackend(leveledBackend) + return +} +