mirror of
https://github.com/weaveworks/scope.git
synced 2026-02-14 18:09:59 +00:00
There's no reason to expect anyone is changing their DNS that fast, and the probe queries all endpoints every time it polls so slowing down will reduce effort.
235 lines
5.0 KiB
Go
235 lines
5.0 KiB
Go
package appclient
|
|
|
|
import (
|
|
"net"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/miekg/dns"
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/weaveworks/scope/common/xfer"
|
|
)
|
|
|
|
const (
|
|
dnsPollInterval = 5 * time.Minute
|
|
)
|
|
|
|
// fastStartTicker is a ticker that 'ramps up' from 1 sec to duration.
|
|
func fastStartTicker(duration time.Duration) <-chan time.Time {
|
|
c := make(chan time.Time, 1)
|
|
go func() {
|
|
d := 1 * time.Second
|
|
for {
|
|
time.Sleep(d)
|
|
d = d * 2
|
|
if d > duration {
|
|
d = duration
|
|
}
|
|
|
|
select {
|
|
case c <- time.Now():
|
|
default:
|
|
}
|
|
}
|
|
}()
|
|
return c
|
|
}
|
|
|
|
// Resolver is a thing that can be stopped...
|
|
type Resolver interface {
|
|
Stop()
|
|
}
|
|
|
|
type staticResolver struct {
|
|
ResolverConfig
|
|
|
|
failedResolutions map[string]struct{}
|
|
quit chan struct{}
|
|
}
|
|
|
|
// LookupIP type is used for looking up IPs.
|
|
type LookupIP func(host string) (ips []net.IP, err error)
|
|
|
|
// Target is a parsed representation of the app location.
|
|
type Target struct {
|
|
original string // the original url string
|
|
url *url.URL // the parsed url
|
|
hostname string // the hostname (without port) from the url
|
|
port int // the port, or a sensible default
|
|
}
|
|
|
|
func (t Target) String() string {
|
|
return net.JoinHostPort(t.hostname, strconv.Itoa(t.port))
|
|
}
|
|
|
|
// ResolverConfig is the config for a resolver.
|
|
type ResolverConfig struct {
|
|
Targets []Target
|
|
Set func(string, []url.URL)
|
|
|
|
// Optional
|
|
Lookup LookupIP
|
|
Ticker func(time.Duration) <-chan time.Time
|
|
}
|
|
|
|
// NewResolver periodically resolves the targets, and calls the set
|
|
// function with all the resolved IPs. It explictiy supports targets which
|
|
// resolve to multiple IPs. It uses the supplied DNS server name.
|
|
func NewResolver(config ResolverConfig) (Resolver, error) {
|
|
if config.Lookup == nil {
|
|
config.Lookup = net.LookupIP
|
|
}
|
|
if config.Ticker == nil {
|
|
config.Ticker = fastStartTicker
|
|
}
|
|
r := staticResolver{
|
|
ResolverConfig: config,
|
|
failedResolutions: map[string]struct{}{},
|
|
quit: make(chan struct{}),
|
|
}
|
|
go r.loop()
|
|
return r, nil
|
|
}
|
|
|
|
// LookupUsing produces a LookupIP function for the given DNS server.
|
|
func LookupUsing(dnsServer string) func(host string) (ips []net.IP, err error) {
|
|
client := dns.Client{
|
|
Net: "tcp",
|
|
}
|
|
return func(host string) (ips []net.IP, err error) {
|
|
m := &dns.Msg{}
|
|
m.SetQuestion(dns.Fqdn(host), dns.TypeA)
|
|
in, _, err := client.Exchange(m, dnsServer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := []net.IP{}
|
|
for _, answer := range in.Answer {
|
|
if a, ok := answer.(*dns.A); ok {
|
|
result = append(result, a.A)
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
}
|
|
|
|
func (r staticResolver) loop() {
|
|
r.resolve()
|
|
t := r.Ticker(dnsPollInterval)
|
|
for {
|
|
select {
|
|
case <-t:
|
|
r.resolve()
|
|
case <-r.quit:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r staticResolver) Stop() {
|
|
close(r.quit)
|
|
}
|
|
|
|
// ParseTargets deals with missing information in the targets string, defaulting
|
|
// the scheme, port etc.
|
|
func ParseTargets(urls []string) ([]Target, error) {
|
|
var targets []Target
|
|
for _, u := range urls {
|
|
// naked hostnames (such as "localhost") are interpreted as relative URLs
|
|
// so we add a scheme if u doesn't have one.
|
|
prefixAdded := false
|
|
if !strings.Contains(u, "://") {
|
|
prefixAdded = true
|
|
if strings.HasSuffix(u, ":443") {
|
|
u = "https://" + u
|
|
} else {
|
|
u = "http://" + u
|
|
}
|
|
}
|
|
parsed, err := url.Parse(u)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var hostname string
|
|
var port int
|
|
if strings.Contains(parsed.Host, ":") {
|
|
var portStr string
|
|
hostname, portStr, err = net.SplitHostPort(parsed.Host)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
port, err = strconv.Atoi(portStr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
if prefixAdded {
|
|
port = xfer.AppPort
|
|
} else if strings.HasPrefix(u, "https://") {
|
|
port = 443
|
|
} else {
|
|
port = 80
|
|
}
|
|
hostname = parsed.Host
|
|
}
|
|
targets = append(targets, Target{
|
|
original: u,
|
|
url: parsed,
|
|
hostname: hostname,
|
|
port: port,
|
|
})
|
|
}
|
|
return targets, nil
|
|
}
|
|
|
|
func (r staticResolver) resolve() {
|
|
for _, t := range r.Targets {
|
|
ips := r.resolveOne(t)
|
|
urls := makeURLs(t, ips)
|
|
r.Set(t.hostname, urls)
|
|
}
|
|
}
|
|
|
|
func makeURLs(t Target, ips []string) []url.URL {
|
|
result := []url.URL{}
|
|
for _, ip := range ips {
|
|
u := *t.url
|
|
u.Host = net.JoinHostPort(ip, strconv.Itoa(t.port))
|
|
result = append(result, u)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (r staticResolver) resolveOne(t Target) []string {
|
|
var addrs []net.IP
|
|
if addr := net.ParseIP(t.hostname); addr != nil {
|
|
addrs = []net.IP{addr}
|
|
} else {
|
|
var err error
|
|
addrs, err = r.Lookup(t.hostname)
|
|
if err != nil {
|
|
if _, ok := r.failedResolutions[t.hostname]; !ok {
|
|
log.Warnf("Cannot resolve '%s': %v", t.hostname, err)
|
|
// Only log the error once
|
|
r.failedResolutions[t.hostname] = struct{}{}
|
|
}
|
|
return []string{}
|
|
}
|
|
// Allow logging errors in future resolutions
|
|
delete(r.failedResolutions, t.hostname)
|
|
}
|
|
endpoints := make([]string, 0, len(addrs))
|
|
for _, addr := range addrs {
|
|
// For now, ignore IPv6
|
|
if addr.To4() == nil {
|
|
continue
|
|
}
|
|
endpoints = append(endpoints, addr.String())
|
|
}
|
|
return endpoints
|
|
}
|