Allow user to specify URLs on the command line, and use that to allow per-target tokens. (#1901)

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
This commit is contained in:
Tom Wilkie
2016-10-05 10:59:56 -07:00
committed by GitHub
parent 0cc794248b
commit 2a00fd2d78
12 changed files with 365 additions and 139 deletions

View File

@@ -4,6 +4,7 @@ import (
"net"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
@@ -43,7 +44,8 @@ func TestControl(t *testing.T) {
Value: "foo",
}
})
client, err := appclient.NewAppClient(probeConfig, ip+":"+port, ip+":"+port, controlHandler)
url := url.URL{Scheme: "http", Host: ip + ":" + port}
client, err := appclient.NewAppClient(probeConfig, ip+":"+port, url, controlHandler)
if err != nil {
t.Fatal(err)
}

View File

@@ -6,6 +6,7 @@ import (
"net"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
@@ -83,7 +84,8 @@ func TestPipeClose(t *testing.T) {
probeConfig := appclient.ProbeConfig{
ProbeID: "foo",
}
client, err := appclient.NewAppClient(probeConfig, ip+":"+port, ip+":"+port, nil)
url := url.URL{Scheme: "http", Host: ip + ":" + port}
client, err := appclient.NewAppClient(probeConfig, ip+":"+port, url, nil)
if err != nil {
t.Fatal(err)
}

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"io/ioutil"
"log"
"net/url"
"time"
"github.com/ugorji/go/codec"
@@ -45,11 +46,16 @@ func main() {
}
}
url, err := url.Parse(fmt.Sprintf("http://%s", *publish))
if err != nil {
log.Fatal(err)
}
client, err := appclient.NewAppClient(appclient.ProbeConfig{
Token: *publishToken,
ProbeID: *publishID,
Insecure: false,
}, *publish, *publish, nil)
}, *publish, *url, nil)
if err != nil {
log.Fatal(err)
}

View File

@@ -7,6 +7,7 @@ import (
"net"
"net/http"
"net/rpc"
"net/url"
"sync"
"time"
@@ -14,7 +15,6 @@ import (
"github.com/gorilla/websocket"
"github.com/ugorji/go/codec"
"github.com/weaveworks/scope/common/sanitize"
"github.com/weaveworks/scope/common/xfer"
)
@@ -41,10 +41,11 @@ type appClient struct {
quit chan struct{}
mtx sync.Mutex
target string
client http.Client
wsDialer websocket.Dialer
appID string
hostname string
target url.URL
// Track all the background goroutines, ensure they all stop
backgroundWait sync.WaitGroup
@@ -61,7 +62,7 @@ type appClient struct {
}
// NewAppClient makes a new appClient.
func NewAppClient(pc ProbeConfig, hostname, target string, control xfer.ControlHandler) (AppClient, error) {
func NewAppClient(pc ProbeConfig, hostname string, target url.URL, control xfer.ControlHandler) (AppClient, error) {
httpTransport, err := pc.getHTTPTransport(hostname)
if err != nil {
return nil, err
@@ -74,6 +75,7 @@ func NewAppClient(pc ProbeConfig, hostname, target string, control xfer.ControlH
return &appClient{
ProbeConfig: pc,
quit: make(chan struct{}),
hostname: hostname,
target: target,
client: http.Client{
Transport: httpTransport,
@@ -88,6 +90,20 @@ func NewAppClient(pc ProbeConfig, hostname, target string, control xfer.ControlH
}, nil
}
func (c *appClient) url(path string) string {
return c.target.String() + path
}
func (c *appClient) wsURL(path string) string {
output := c.target //copy the url
if output.Scheme == "https" {
output.Scheme = "wss"
} else {
output.Scheme = "ws"
}
return output.String() + path
}
func (c *appClient) hasQuit() bool {
select {
case <-c.quit:
@@ -150,7 +166,7 @@ func (c *appClient) Stop() {
// Details fetches the details (version, id) of the app.
func (c *appClient) Details() (xfer.Details, error) {
result := xfer.Details{}
req, err := c.ProbeConfig.authorizedRequest("GET", sanitize.URL("", 0, "/api")(c.target), nil)
req, err := c.ProbeConfig.authorizedRequest("GET", c.url("/api"), nil)
if err != nil {
return result, err
}
@@ -184,7 +200,7 @@ func (c *appClient) doWithBackoff(msg string, f func() (bool, error)) {
continue
}
log.Errorf("Error doing %s for %s, backing off %s: %v", msg, c.target, backoff, err)
log.Errorf("Error doing %s for %s, backing off %s: %v", msg, c.hostname, backoff, err)
select {
case <-time.After(backoff):
case <-c.quit:
@@ -200,7 +216,7 @@ func (c *appClient) doWithBackoff(msg string, f func() (bool, error)) {
func (c *appClient) controlConnection() (bool, error) {
headers := http.Header{}
c.ProbeConfig.authorizeHeaders(headers)
url := sanitize.URL("ws://", 0, "/api/control/ws")(c.target)
url := c.wsURL("/api/control/ws")
conn, _, err := xfer.DialWS(&c.wsDialer, url, headers)
if err != nil {
return false, err
@@ -232,14 +248,14 @@ func (c *appClient) controlConnection() (bool, error) {
func (c *appClient) ControlConnection() {
go func() {
log.Infof("Control connection to %s starting", c.target)
defer log.Infof("Control connection to %s exiting", c.target)
log.Infof("Control connection to %s starting", c.hostname)
defer log.Infof("Control connection to %s exiting", c.hostname)
c.doWithBackoff("controls", c.controlConnection)
}()
}
func (c *appClient) publish(r io.Reader) error {
url := sanitize.URL("", 0, "/api/report")(c.target)
url := c.url("/api/report")
req, err := c.ProbeConfig.authorizedRequest("POST", url, r)
if err != nil {
return err
@@ -266,8 +282,8 @@ func (c *appClient) publish(r io.Reader) error {
func (c *appClient) startPublishing() {
go func() {
log.Infof("Publish loop for %s starting", c.target)
defer log.Infof("Publish loop for %s exiting", c.target)
log.Infof("Publish loop for %s starting", c.hostname)
defer log.Infof("Publish loop for %s exiting", c.hostname)
c.doWithBackoff("publish", func() (bool, error) {
r := <-c.readers
if r == nil {
@@ -285,7 +301,7 @@ func (c *appClient) Publish(r io.Reader) error {
select {
case c.readers <- r:
default:
log.Errorf("Dropping report to %s", c.target)
log.Errorf("Dropping report to %s", c.hostname)
}
return nil
}
@@ -293,7 +309,7 @@ func (c *appClient) Publish(r io.Reader) error {
func (c *appClient) pipeConnection(id string, pipe xfer.Pipe) (bool, error) {
headers := http.Header{}
c.ProbeConfig.authorizeHeaders(headers)
url := sanitize.URL("ws://", 0, fmt.Sprintf("/api/pipe/%s/probe", id))(c.target)
url := c.wsURL(fmt.Sprintf("/api/pipe/%s/probe", id))
conn, resp, err := xfer.DialWS(&c.wsDialer, url, headers)
if resp != nil && resp.StatusCode == http.StatusNotFound {
// Special handling - 404 means the app/user has closed the pipe
@@ -319,8 +335,8 @@ func (c *appClient) pipeConnection(id string, pipe xfer.Pipe) (bool, error) {
func (c *appClient) PipeConnection(id string, pipe xfer.Pipe) {
go func() {
log.Infof("Pipe %s connection to %s starting", id, c.target)
defer log.Infof("Pipe %s connection to %s exiting", id, c.target)
log.Infof("Pipe %s connection to %s starting", id, c.hostname)
defer log.Infof("Pipe %s connection to %s exiting", id, c.hostname)
c.doWithBackoff(id, func() (bool, error) {
return c.pipeConnection(id, pipe)
})
@@ -329,7 +345,7 @@ func (c *appClient) PipeConnection(id string, pipe xfer.Pipe) {
// PipeClose closes the given pipe id on the app.
func (c *appClient) PipeClose(id string) error {
url := sanitize.URL("", 0, fmt.Sprintf("/api/pipe/%s", id))(c.target)
url := c.url(fmt.Sprintf("/api/pipe/%s", id))
req, err := c.ProbeConfig.authorizedRequest("DELETE", url, nil)
if err != nil {
return err

View File

@@ -109,7 +109,11 @@ func TestAppClientPublish(t *testing.T) {
Insecure: false,
}
p, err := NewAppClient(pc, u.Host, s.URL, nil)
url, err := url.Parse(s.URL)
if err != nil {
t.Fatal(err)
}
p, err := NewAppClient(pc, u.Host, *url, nil)
if err != nil {
t.Fatal(err)
}
@@ -158,7 +162,11 @@ func TestAppClientDetails(t *testing.T) {
ProbeID: "",
Insecure: false,
}
p, err := NewAppClient(pc, u.Host, s.URL, nil)
url, err := url.Parse(s.URL)
if err != nil {
t.Fatal(err)
}
p, err := NewAppClient(pc, u.Host, *url, nil)
if err != nil {
t.Fatal(err)
}
@@ -203,7 +211,11 @@ func TestStop(t *testing.T) {
Insecure: false,
}
p, err := NewAppClient(pc, u.Host, s.URL, nil)
url, err := url.Parse(s.URL)
if err != nil {
t.Fatal(err)
}
p, err := NewAppClient(pc, u.Host, *url, nil)
if err != nil {
t.Fatal(err)
}

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"io"
"io/ioutil"
"net/url"
"strings"
"sync"
@@ -18,7 +19,7 @@ import (
const maxConcurrentGET = 10
// ClientFactory is a thing thats makes AppClients
type ClientFactory func(string, string) (AppClient, error)
type ClientFactory func(string, url.URL) (AppClient, error)
type multiClient struct {
clientFactory ClientFactory
@@ -46,7 +47,7 @@ type Publisher interface {
// MultiAppClient maintains a set of upstream apps, and ensures we have an
// AppClient for each one.
type MultiAppClient interface {
Set(hostname string, endpoints []string)
Set(hostname string, urls []url.URL)
PipeConnection(appID, pipeID string, pipe xfer.Pipe) error
PipeClose(appID, pipeID string) error
Stop()
@@ -67,17 +68,17 @@ func NewMultiAppClient(clientFactory ClientFactory, noControls bool) MultiAppCli
}
// Set the list of endpoints for the given hostname.
func (c *multiClient) Set(hostname string, endpoints []string) {
func (c *multiClient) Set(hostname string, urls []url.URL) {
wg := sync.WaitGroup{}
wg.Add(len(endpoints))
clients := make(chan clientTuple, len(endpoints))
for _, endpoint := range endpoints {
go func(endpoint string) {
wg.Add(len(urls))
clients := make(chan clientTuple, len(urls))
for _, u := range urls {
go func(u url.URL) {
c.sema.acquire()
defer c.sema.release()
defer wg.Done()
client, err := c.clientFactory(hostname, endpoint)
client, err := c.clientFactory(hostname, u)
if err != nil {
log.Errorf("Error creating new app client: %v", err)
return
@@ -90,7 +91,7 @@ func (c *multiClient) Set(hostname string, endpoints []string) {
}
clients <- clientTuple{details, client}
}(endpoint)
}(u)
}
wg.Wait()

View File

@@ -3,6 +3,7 @@ package appclient_test
import (
"bytes"
"io"
"net/url"
"runtime"
"testing"
@@ -42,8 +43,8 @@ var (
a2 = &mockClient{id: "2"} // hostname a, app id 2
b2 = &mockClient{id: "2"} // hostname b, app id 2 (duplicate)
b3 = &mockClient{id: "3"} // hostname b, app id 3
factory = func(hostname, target string) (appclient.AppClient, error) {
switch target {
factory = func(hostname string, url url.URL) (appclient.AppClient, error) {
switch url.Host {
case "a1":
return a1, nil
case "a2":
@@ -53,7 +54,7 @@ var (
case "b3":
return b3, nil
}
panic(target)
panic(url.Host)
}
)
@@ -71,20 +72,20 @@ func TestMultiClient(t *testing.T) {
defer mp.Stop()
// Add two hostnames with overlapping apps, check we don't add the same app twice
mp.Set("a", []string{"a1", "a2"})
mp.Set("b", []string{"b2", "b3"})
mp.Set("a", []url.URL{{Host: "a1"}, {Host: "a2"}})
mp.Set("b", []url.URL{{Host: "b2"}, {Host: "b3"}})
expect(a1.count, 1)
expect(a2.count+b2.count, 1)
expect(b3.count, 1)
// Now drop the overlap, check we don't remove the app
mp.Set("b", []string{"b3"})
mp.Set("b", []url.URL{{Host: "b3"}})
expect(a1.count, 1)
expect(a2.count+b2.count, 1)
expect(b3.count, 1)
// Now check we remove apps
mp.Set("b", []string{})
mp.Set("b", []url.URL{})
expect(b3.stopped, 1)
}
@@ -94,8 +95,8 @@ func TestMultiClientPublish(t *testing.T) {
sum := func() int { return a1.publish + a2.publish + b2.publish + b3.publish }
mp.Set("a", []string{"a1", "a2"})
mp.Set("b", []string{"b2", "b3"})
mp.Set("a", []url.URL{{Host: "a1"}, {Host: "a2"}})
mp.Set("b", []url.URL{{Host: "b2"}, {Host: "b3"}})
for i := 1; i < 10; i++ {
if err := mp.Publish(&bytes.Buffer{}); err != nil {

View File

@@ -5,7 +5,6 @@ import (
"crypto/x509"
"fmt"
"io"
"net"
"net/http"
"github.com/certifi/gocertifi"
@@ -51,15 +50,10 @@ func (pc ProbeConfig) getHTTPTransport(hostname string) (*http.Transport, error)
}, nil
}
host, _, err := net.SplitHostPort(hostname)
if err != nil {
return nil, err
}
return &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: certPool,
ServerName: host,
ServerName: hostname,
},
}, nil
}

View File

@@ -2,6 +2,7 @@ package appclient
import (
"net"
"net/url"
"strconv"
"strings"
"time"
@@ -16,10 +17,6 @@ const (
dnsPollInterval = 10 * time.Second
)
var (
tick = fastStartTicker
)
// 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)
@@ -41,41 +38,60 @@ func fastStartTicker(duration time.Duration) <-chan time.Time {
return c
}
type setter func(string, []string)
// Resolver is a thing that can be stopped...
type Resolver interface {
Stop()
}
type staticResolver struct {
setters []setter
targets []target
ResolverConfig
failedResolutions map[string]struct{}
quit chan struct{}
lookup LookupIP
}
// LookupIP type is used for looking up IPs.
type LookupIP func(host string) (ips []net.IP, err error)
type target struct{ host, port string }
// 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.host, t.port) }
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(targets []string, lookup LookupIP, setters ...setter) Resolver {
func NewResolver(config ResolverConfig) (Resolver, error) {
if config.Lookup == nil {
config.Lookup = net.LookupIP
}
if config.Ticker == nil {
config.Ticker = fastStartTicker
}
r := staticResolver{
targets: prepare(targets),
setters: setters,
ResolverConfig: config,
failedResolutions: map[string]struct{}{},
quit: make(chan struct{}),
lookup: lookup,
}
go r.loop()
return r
return r, nil
}
// LookupUsing produces a LookupIP function for the given DNS server.
@@ -102,7 +118,7 @@ func LookupUsing(dnsServer string) func(host string) (ips []net.IP, err error) {
func (r staticResolver) loop() {
r.resolve()
t := tick(dnsPollInterval)
t := r.Ticker(dnsPollInterval)
for {
select {
case <-t:
@@ -117,58 +133,94 @@ func (r staticResolver) Stop() {
close(r.quit)
}
func prepare(strs []string) []target {
var targets []target
for _, s := range strs {
var host, port string
if strings.Contains(s, ":") {
var err error
host, port, err = net.SplitHostPort(s)
// 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 {
log.Errorf("invalid address %s: %v", s, err)
continue
return nil, err
}
port, err = strconv.Atoi(portStr)
if err != nil {
return nil, err
}
} else {
host, port = s, strconv.Itoa(xfer.AppPort)
if prefixAdded {
port = xfer.AppPort
} else if strings.HasPrefix(u, "https://") {
port = 443
} else {
port = 80
}
hostname = parsed.Host
}
targets = append(targets, target{host, port})
targets = append(targets, Target{
original: u,
url: parsed,
hostname: hostname,
port: port,
})
}
return targets
return targets, nil
}
func (r staticResolver) resolve() {
for t, endpoints := range r.resolveMany(r.targets) {
for _, setter := range r.setters {
setter(t.String(), endpoints)
}
for _, t := range r.Targets {
ips := r.resolveOne(t)
urls := makeURLs(t, ips)
r.Set(t.hostname, urls)
}
}
func (r staticResolver) resolveMany(targets []target) map[target][]string {
result := map[target][]string{}
for _, t := range targets {
result[t] = r.resolveOne(t)
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 {
func (r staticResolver) resolveOne(t Target) []string {
var addrs []net.IP
if addr := net.ParseIP(t.host); addr != nil {
if addr := net.ParseIP(t.hostname); addr != nil {
addrs = []net.IP{addr}
} else {
var err error
addrs, err = r.lookup(t.host)
addrs, err = r.Lookup(t.hostname)
if err != nil {
if _, ok := r.failedResolutions[t.host]; !ok {
log.Warnf("Cannot resolve %s: %v", t.host, err)
if _, ok := r.failedResolutions[t.hostname]; !ok {
log.Warnf("Cannot resolve '%s': %v", t.hostname, err)
// Only log the error once
r.failedResolutions[t.host] = struct{}{}
r.failedResolutions[t.hostname] = struct{}{}
}
return []string{}
}
// Allow logging errors in future resolutions
delete(r.failedResolutions, t.host)
delete(r.failedResolutions, t.hostname)
}
endpoints := make([]string, 0, len(addrs))
for _, addr := range addrs {
@@ -176,7 +228,7 @@ func (r staticResolver) resolveOne(t target) []string {
if addr.To4() == nil {
continue
}
endpoints = append(endpoints, net.JoinHostPort(addr.String(), t.port))
endpoints = append(endpoints, addr.String())
}
return endpoints
}

View File

@@ -3,19 +3,97 @@ package appclient
import (
"fmt"
"net"
"net/url"
"runtime"
"sync"
"testing"
"time"
"github.com/weaveworks/scope/common/xfer"
"github.com/weaveworks/scope/test"
)
func TestResolver(t *testing.T) {
oldTick := tick
defer func() { tick = oldTick }()
func TestResolverCases(t *testing.T) {
c := make(chan time.Time)
tick = func(_ time.Duration) <-chan time.Time { return c }
ticker := func(_ time.Duration) <-chan time.Time { return c }
ips := map[string][]net.IP{
"foo": {net.IPv4(192, 168, 0, 1)},
"bar": {net.IPv4(192, 168, 0, 2), net.IPv4(192, 168, 0, 3)},
}
lookupIP := func(host string) ([]net.IP, error) {
addrs, ok := ips[host]
if !ok {
return nil, fmt.Errorf("Not found")
}
return addrs, nil
}
testResolver := func(target string, expected []url.URL) {
mtx := sync.Mutex{}
found := map[url.URL]struct{}{}
set := func(target string, urls []url.URL) {
mtx.Lock()
defer mtx.Unlock()
for _, url := range urls {
found[url] = struct{}{}
}
}
targets, err := ParseTargets([]string{target})
if err != nil {
t.Fatal(err)
}
r, err := NewResolver(ResolverConfig{
Targets: targets,
Lookup: lookupIP,
Set: set,
Ticker: ticker,
})
if err != nil {
t.Fatal(err)
}
defer r.Stop()
c <- time.Now()
test.Poll(t, 200*time.Millisecond, expected, func() interface{} {
mtx.Lock()
defer mtx.Unlock()
have := []url.URL{}
for url := range found {
have = append(have, url)
}
return have
})
}
for _, tc := range []struct {
in string
expected []url.URL
}{
{"foo", []url.URL{{Scheme: "http", Host: "192.168.0.1:4040"}}},
{"foo:80", []url.URL{{Scheme: "http", Host: "192.168.0.1:80"}}},
{"foo:443", []url.URL{{Scheme: "https", Host: "192.168.0.1:443"}}},
{"foo:1234", []url.URL{{Scheme: "http", Host: "192.168.0.1:1234"}}},
{"http://foo", []url.URL{{Scheme: "http", Host: "192.168.0.1:80"}}},
{"http://foo:80", []url.URL{{Scheme: "http", Host: "192.168.0.1:80"}}},
{"http://foo:443", []url.URL{{Scheme: "http", Host: "192.168.0.1:443"}}},
{"http://foo:1234", []url.URL{{Scheme: "http", Host: "192.168.0.1:1234"}}},
{"https://foo", []url.URL{{Scheme: "https", Host: "192.168.0.1:443"}}},
{"https://foo:80", []url.URL{{Scheme: "https", Host: "192.168.0.1:80"}}},
{"https://foo:443", []url.URL{{Scheme: "https", Host: "192.168.0.1:443"}}},
{"https://foo:1234", []url.URL{{Scheme: "https", Host: "192.168.0.1:1234"}}},
{"user:pass@foo", []url.URL{{Scheme: "http", Host: "192.168.0.1:4040", User: url.UserPassword("user", "pass")}}},
{"bar", []url.URL{{Scheme: "http", Host: "192.168.0.2:4040"}, {Scheme: "http", Host: "192.168.0.3:4040"}}},
} {
testResolver(tc.in, tc.expected)
}
}
func TestResolver(t *testing.T) {
c := make(chan time.Time)
ticker := func(_ time.Duration) <-chan time.Time { return c }
ipsLock := sync.Mutex{}
ips := map[string][]net.IP{}
@@ -37,17 +115,30 @@ func TestResolver(t *testing.T) {
port := ":80"
ip1 := "192.168.0.1"
ip2 := "192.168.0.10"
sets := make(chan string)
set := func(target string, endpoints []string) {
sets := make(chan url.URL)
set := func(target string, endpoints []url.URL) {
for _, endpoint := range endpoints {
sets <- endpoint
}
}
r := NewResolver([]string{"symbolic.name" + port, "namewithnoport", ip1 + port, ip2}, lookupIP, set)
targets, err := ParseTargets([]string{"symbolic.name" + port, "namewithnoport", ip1 + port, ip2})
if err != nil {
t.Fatal(err)
}
assertAdd := func(want ...string) {
remaining := map[string]struct{}{}
r, err := NewResolver(ResolverConfig{
Targets: targets,
Lookup: lookupIP,
Set: set,
Ticker: ticker,
})
if err != nil {
t.Fatal(err)
}
assertAdd := func(want ...url.URL) {
remaining := map[url.URL]struct{}{}
for _, s := range want {
remaining[s] = struct{}{}
}
@@ -56,10 +147,10 @@ func TestResolver(t *testing.T) {
select {
case s := <-sets:
if _, ok := remaining[s]; ok {
t.Logf("line %d: got %q OK", line, s)
t.Logf("line %d: got %v OK", line, s)
delete(remaining, s)
} else {
t.Errorf("line %d: got unexpected %q", line, s)
t.Errorf("line %d: got unexpected %v", line, s)
}
case <-time.After(100 * time.Millisecond):
t.Fatalf("line %d: didn't get the adds in time", line)
@@ -68,22 +159,36 @@ func TestResolver(t *testing.T) {
}
// Initial resolve should just give us IPs
assertAdd(ip1+port, fmt.Sprintf("%s:%d", ip2, xfer.AppPort))
assertAdd(
url.URL{Scheme: "http", Host: ip1 + port},
url.URL{Scheme: "http", Host: fmt.Sprintf("%s:%d", ip2, xfer.AppPort)},
)
// Trigger another resolve with a tick; again,
// just want ips.
c <- time.Now()
assertAdd(ip1+port, fmt.Sprintf("%s:%d", ip2, xfer.AppPort))
assertAdd(
url.URL{Scheme: "http", Host: ip1 + port},
url.URL{Scheme: "http", Host: fmt.Sprintf("%s:%d", ip2, xfer.AppPort)},
)
ip3 := "1.2.3.4"
updateIPs("symbolic.name", makeIPs(ip3))
c <- time.Now() // trigger a resolve
assertAdd(ip3+port, ip1+port, fmt.Sprintf("%s:%d", ip2, xfer.AppPort))
assertAdd(
url.URL{Scheme: "http", Host: ip3 + port},
url.URL{Scheme: "http", Host: ip1 + port}, url.URL{Scheme: "http", Host: fmt.Sprintf("%s:%d", ip2, xfer.AppPort)},
)
ip4 := "10.10.10.10"
updateIPs("symbolic.name", makeIPs(ip3, ip4))
c <- time.Now() // trigger another resolve, this time with 2 adds
assertAdd(ip3+port, ip4+port, ip1+port, fmt.Sprintf("%s:%d", ip2, xfer.AppPort))
assertAdd(
url.URL{Scheme: "http", Host: ip3 + port},
url.URL{Scheme: "http", Host: ip4 + port},
url.URL{Scheme: "http", Host: ip1 + port},
url.URL{Scheme: "http", Host: fmt.Sprintf("%s:%d", ip2, xfer.AppPort)},
)
done := make(chan struct{})
go func() { r.Stop(); close(done) }()

View File

@@ -14,6 +14,7 @@ import (
"github.com/weaveworks/scope/app"
"github.com/weaveworks/scope/common/xfer"
"github.com/weaveworks/scope/probe/appclient"
"github.com/weaveworks/scope/probe/kubernetes"
"github.com/weaveworks/weave/common"
)
@@ -278,12 +279,34 @@ func main() {
// Special case for #1191, check listen address is well formed
_, _, err := net.SplitHostPort(flags.app.listen)
if err != nil {
log.Errorf("Invalid value for -app.http.address: %v", err)
log.Fatalf("Invalid value for -app.http.address: %v", err)
}
if flags.probe.httpListen != "" {
_, _, err := net.SplitHostPort(flags.probe.httpListen)
if err != nil {
log.Errorf("Invalid value for -app.http.address: %v", err)
log.Fatalf("Invalid value for -app.http.address: %v", err)
}
}
// Special case probe push address parsing
targets := []appclient.Target{}
if mode == "probe" || dryRun {
args := []string{}
if flags.probe.token != "" {
// service mode
if len(flag.Args()) == 0 {
args = append(args, defaultServiceHost)
}
} else if !flags.probe.noApp {
args = append(args, fmt.Sprintf("localhost:%d", xfer.AppPort))
}
args = append(args, flag.Args()...)
if !dryRun {
log.Infof("publishing to: %s", strings.Join(args, ", "))
}
targets, err = appclient.ParseTargets(args)
if err != nil {
log.Fatalf("Invalid targets: %v", err)
}
}
@@ -295,7 +318,7 @@ func main() {
case "app":
appMain(flags.app)
case "probe":
probeMain(flags.probe)
probeMain(flags.probe, targets)
case "version":
fmt.Println("Weave Scope version", version)
case "help":

View File

@@ -1,15 +1,13 @@
package main
import (
"flag"
"fmt"
"math/rand"
"net"
"net/http"
_ "net/http/pprof"
"net/url"
"os"
"strconv"
"strings"
"time"
log "github.com/Sirupsen/logrus"
@@ -38,7 +36,7 @@ import (
const (
versionCheckPeriod = 6 * time.Hour
defaultServiceHost = "cloud.weave.works:443"
defaultServiceHost = "https://cloud.weave.works:443"
)
var pluginAPIVersion = "1"
@@ -65,7 +63,7 @@ func check(flags map[string]string) {
}
// Main runs the probe
func probeMain(flags probeFlags) {
func probeMain(flags probeFlags, targets []appclient.Target) {
setLogLevel(flags.logLevel)
setLogFormatter(flags.logPrefix)
@@ -94,28 +92,21 @@ func probeMain(flags probeFlags) {
}
go check(checkpointFlags)
var targets = []string{}
if flags.token != "" {
// service mode
if len(flag.Args()) == 0 {
targets = append(targets, defaultServiceHost)
}
} else if !flags.noApp {
targets = append(targets, fmt.Sprintf("localhost:%d", xfer.AppPort))
}
targets = append(targets, flag.Args()...)
log.Infof("publishing to: %s", strings.Join(targets, ", "))
probeConfig := appclient.ProbeConfig{
Token: flags.token,
ProbeVersion: version,
ProbeID: probeID,
Insecure: flags.insecure,
}
handlerRegistry := controls.NewDefaultHandlerRegistry()
clientFactory := func(hostname, endpoint string) (appclient.AppClient, error) {
clientFactory := func(hostname string, url url.URL) (appclient.AppClient, error) {
token := flags.token
if url.User != nil {
token = url.User.Username()
url.User = nil // erase credentials, as we use a special header
}
probeConfig := appclient.ProbeConfig{
Token: token,
ProbeVersion: version,
ProbeID: probeID,
Insecure: flags.insecure,
}
return appclient.NewAppClient(
probeConfig, hostname, endpoint,
probeConfig, hostname, url,
xfer.ControlHandlerFunc(handlerRegistry.HandleControlRequest),
)
}
@@ -126,7 +117,15 @@ func probeMain(flags probeFlags) {
if flags.resolver != "" {
dnsLookupFn = appclient.LookupUsing(flags.resolver)
}
resolver := appclient.NewResolver(targets, dnsLookupFn, clients.Set)
resolver, err := appclient.NewResolver(appclient.ResolverConfig{
Targets: targets,
Lookup: dnsLookupFn,
Set: clients.Set,
})
if err != nil {
log.Fatalf("Failed to create resolver: %v", err)
return
}
defer resolver.Stop()
p := probe.New(flags.spyInterval, flags.publishInterval, clients, flags.noControls)
@@ -210,8 +209,21 @@ func probeMain(flags probeFlags) {
log.Println("Error getting docker bridge ip:", err)
} else {
weaveDNSLookup := appclient.LookupUsing(dockerBridgeIP + ":53")
weaveResolver := appclient.NewResolver([]string{flags.weaveHostname}, weaveDNSLookup, clients.Set)
defer weaveResolver.Stop()
weaveTargets, err := appclient.ParseTargets([]string{flags.weaveHostname})
if err != nil {
log.Errorf("Failed to parse weave targets: %v", err)
} else {
weaveResolver, err := appclient.NewResolver(appclient.ResolverConfig{
Targets: weaveTargets,
Lookup: weaveDNSLookup,
Set: clients.Set,
})
if err != nil {
log.Errorf("Failed to create weave resolver: %v", err)
} else {
defer weaveResolver.Stop()
}
}
}
}
@@ -235,7 +247,7 @@ func probeMain(flags probeFlags) {
if flags.httpListen != "" {
go func() {
log.Infof("Profiling data being exported to %s", flags.httpListen)
log.Infof("go tool pprof http://%s/debug/pprof/{profile,heap,block}", flags.httpListen)
log.Infof("go tool proof http://%s/debug/pprof/{profile,heap,block}", flags.httpListen)
log.Infof("Profiling endpoint %s terminated: %v", flags.httpListen, http.ListenAndServe(flags.httpListen, nil))
}()
}