feat(kit/feature): Add HTTP request proxy.

pull/17970/head
Brett Buddin 2020-05-06 11:14:27 -04:00
parent 9bde937c54
commit 73ae2122aa
No known key found for this signature in database
GPG Key ID: C51265E441C4C5AC
2 changed files with 202 additions and 0 deletions

65
kit/feature/http_proxy.go Normal file
View File

@ -0,0 +1,65 @@
package feature
import (
"context"
"net/http"
"net/http/httputil"
"net/url"
"go.uber.org/zap"
)
// HTTPProxy is an HTTP proxy that's guided by a feature flag. If the feature flag
// presented to it is enabled, it will perform the proxying behavior. Otherwise
// it will be a no-op.
type HTTPProxy struct {
proxy *httputil.ReverseProxy
logger *zap.Logger
enabler ProxyEnabler
}
// NewHTTPProxy returns a new Proxy.
func NewHTTPProxy(dest *url.URL, logger *zap.Logger, enabler ProxyEnabler) *HTTPProxy {
return &HTTPProxy{
proxy: newReverseProxy(dest, enabler.Key()),
logger: logger,
enabler: enabler,
}
}
// Do performs the proxying. It returns whether or not the request was proxied.
func (p *HTTPProxy) Do(w http.ResponseWriter, r *http.Request) bool {
if p.enabler.Enabled(r.Context()) {
p.proxy.ServeHTTP(w, r)
return true
}
return false
}
const (
// headerProxyFlag is the HTTP header for enriching the request and response
// with the feature flag key that precipitated the proxying behavior.
headerProxyFlag = "X-Proxy-Flag"
)
// newReverseProxy creates a new single-host reverse proxy.
func newReverseProxy(dest *url.URL, enablerKey string) *httputil.ReverseProxy {
proxy := httputil.NewSingleHostReverseProxy(dest)
defaultDirector := proxy.Director
proxy.Director = func(r *http.Request) {
defaultDirector(r)
r.Header.Set(headerProxyFlag, enablerKey)
}
proxy.ModifyResponse = func(r *http.Response) error {
r.Header.Set(headerProxyFlag, enablerKey)
return nil
}
return proxy
}
// ProxyEnabler is a boolean feature flag.
type ProxyEnabler interface {
Key() string
Enabled(ctx context.Context, fs ...Flagger) bool
}

View File

@ -0,0 +1,137 @@
package feature
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"go.uber.org/zap"
"go.uber.org/zap/zaptest"
)
const (
destBody = "hello from destination"
srcBody = "hello from source"
flagKey = "fancy-feature"
)
func TestHTTPProxy_Proxying(t *testing.T) {
en := enabler{key: flagKey, state: true}
logger := zaptest.NewLogger(t)
resp, err := testHTTPProxy(logger, en)
if err != nil {
t.Error(err)
}
proxyFlag := resp.Header.Get("X-Proxy-Flag")
if proxyFlag != flagKey {
t.Error("X-Proxy-Flag header not populated")
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Error(err)
}
bodyStr := string(body)
if bodyStr != destBody {
t.Errorf("expected body of destination handler, but got: %q", bodyStr)
}
}
func TestHTTPProxy_DefaultBehavior(t *testing.T) {
en := enabler{key: flagKey, state: false}
logger := zaptest.NewLogger(t)
resp, err := testHTTPProxy(logger, en)
if err != nil {
t.Error(err)
}
proxyFlag := resp.Header.Get("X-Proxy-Flag")
if proxyFlag != "" {
t.Error("X-Proxy-Flag header populated")
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Error(err)
}
bodyStr := string(body)
if bodyStr != srcBody {
t.Errorf("expected body of source handler, but got: %q", bodyStr)
}
}
func TestHTTPProxy_RequestHeader(t *testing.T) {
h := func(w http.ResponseWriter, r *http.Request) {
proxyFlag := r.Header.Get("X-Proxy-Flag")
if proxyFlag != flagKey {
t.Error("expected X-Proxy-Flag to contain feature flag key")
}
}
s := httptest.NewServer(http.HandlerFunc(h))
defer s.Close()
sURL, err := url.Parse(s.URL)
if err != nil {
t.Error(err)
}
logger := zaptest.NewLogger(t)
en := enabler{key: flagKey, state: true}
proxy := NewHTTPProxy(sURL, logger, en)
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "http://example.com/foo", nil)
srcHandler(proxy)(w, r)
}
func testHTTPProxy(logger *zap.Logger, enabler ProxyEnabler) (*http.Response, error) {
s := httptest.NewServer(http.HandlerFunc(destHandler))
defer s.Close()
sURL, err := url.Parse(s.URL)
if err != nil {
return nil, err
}
proxy := NewHTTPProxy(sURL, logger, enabler)
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "http://example.com/foo", nil)
srcHandler(proxy)(w, r)
return w.Result(), nil
}
func destHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, destBody)
}
func srcHandler(proxy *HTTPProxy) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if proxy.Do(w, r) {
return
}
fmt.Fprint(w, srcBody)
}
}
type enabler struct {
key string
state bool
}
func (e enabler) Key() string {
return e.key
}
func (e enabler) Enabled(context.Context, ...Flagger) bool {
return e.state
}