mirror of
https://github.com/weaveworks/scope.git
synced 2026-03-03 18:20:27 +00:00
Also: - Parse targets on startup and catch badly formed ones before Scope can start. - If no port is specified, use default port for scheme; if no scheme is specificed, use 4040. - Use username as probe token
235 lines
5.0 KiB
Go
235 lines
5.0 KiB
Go
package appclient
|
|
|
|
import (
|
|
"net"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
log "github.com/Sirupsen/logrus"
|
|
"github.com/miekg/dns"
|
|
|
|
"github.com/weaveworks/scope/common/xfer"
|
|
)
|
|
|
|
const (
|
|
dnsPollInterval = 10 * time.Second
|
|
)
|
|
|
|
// 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
|
|
}
|