Browse Source

proxy: introduce director

The director class drives an httputil.ReverseProxy. This is used when
etcd is deployed in proxy mode.
Brian Waldon 11 years ago
parent
commit
e5a482266f
3 changed files with 136 additions and 1 deletions
  1. 64 0
      proxy/proxy.go
  2. 71 0
      proxy/proxy_test.go
  3. 1 1
      test

+ 64 - 0
proxy/proxy.go

@@ -0,0 +1,64 @@
+package proxy
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+	"net/http/httputil"
+	"net/url"
+)
+
+func NewHandler(endpoints []string) (*httputil.ReverseProxy, error) {
+	d, err := newDirector(endpoints)
+	if err != nil {
+		return nil, err
+	}
+
+	proxy := httputil.ReverseProxy{
+		Director:      d.direct,
+		Transport:     &http.Transport{},
+		FlushInterval: 0,
+	}
+
+	return &proxy, nil
+}
+
+func newDirector(endpoints []string) (*director, error) {
+	if len(endpoints) == 0 {
+		return nil, errors.New("one or more endpoints required")
+	}
+
+	urls := make([]url.URL, len(endpoints))
+	for i, e := range endpoints {
+		u, err := url.Parse(e)
+		if err != nil {
+			return nil, fmt.Errorf("invalid endpoint %q: %v", e, err)
+		}
+
+		if u.Scheme == "" {
+			return nil, fmt.Errorf("invalid endpoint %q: scheme required", e)
+		}
+
+		if u.Host == "" {
+			return nil, fmt.Errorf("invalid endpoint %q: host empty", e)
+		}
+
+		urls[i] = *u
+	}
+
+	d := director{
+		endpoints: urls,
+	}
+
+	return &d, nil
+}
+
+type director struct {
+	endpoints []url.URL
+}
+
+func (d *director) direct(req *http.Request) {
+	choice := d.endpoints[0]
+	req.URL.Scheme = choice.Scheme
+	req.URL.Host = choice.Host
+}

+ 71 - 0
proxy/proxy_test.go

@@ -0,0 +1,71 @@
+package proxy
+
+import (
+	"net/http"
+	"net/url"
+	"reflect"
+	"testing"
+)
+
+func TestNewDirector(t *testing.T) {
+	tests := []struct {
+		good      bool
+		endpoints []string
+	}{
+		{true, []string{"http://192.0.2.8"}},
+		{true, []string{"http://192.0.2.8:8001"}},
+		{true, []string{"http://example.com"}},
+		{true, []string{"http://example.com:8001"}},
+		{true, []string{"http://192.0.2.8:8001", "http://example.com:8002"}},
+
+		{false, []string{"192.0.2.8"}},
+		{false, []string{"192.0.2.8:8001"}},
+		{false, []string{""}},
+	}
+
+	for i, tt := range tests {
+		_, err := newDirector(tt.endpoints)
+		if tt.good != (err == nil) {
+			t.Errorf("#%d: expected success = %t, got err = %v", i, tt.good, err)
+		}
+	}
+}
+
+func TestDirectorDirect(t *testing.T) {
+	d := &director{
+		endpoints: []url.URL{
+			url.URL{
+				Scheme: "http",
+				Host:   "bar.example.com",
+			},
+		},
+	}
+
+	req := &http.Request{
+		Method: "GET",
+		Host:   "foo.example.com",
+		URL: &url.URL{
+			Host: "foo.example.com",
+			Path: "/v2/keys/baz",
+		},
+	}
+
+	d.direct(req)
+
+	want := &http.Request{
+		Method: "GET",
+		// this field must not change
+		Host: "foo.example.com",
+		URL: &url.URL{
+			// the Scheme field is updated per the director's first endpoint
+			Scheme: "http",
+			// the Host field is updated per the director's first endpoint
+			Host: "bar.example.com",
+			Path: "/v2/keys/baz",
+		},
+	}
+
+	if !reflect.DeepEqual(want, req) {
+		t.Fatalf("HTTP request does not match expected criteria: want=%#v got=%#v", want, req)
+	}
+}

+ 1 - 1
test

@@ -14,7 +14,7 @@ COVER=${COVER:-"-cover"}
 
 source ./build
 
-TESTABLE="wal snap etcdserver etcdserver/etcdhttp etcdserver/etcdserverpb functional raft store"
+TESTABLE="wal snap etcdserver etcdserver/etcdhttp etcdserver/etcdserverpb functional proxy raft store"
 FORMATTABLE="$TESTABLE cors.go main.go"
 
 # user has not provided PKG override