Skip to content

Commit

Permalink
support nofallback and testing canary label.
Browse files Browse the repository at this point in the history
  • Loading branch information
zensh committed May 14, 2020
1 parent 87d4da0 commit 8e4ef57
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 64 deletions.
60 changes: 44 additions & 16 deletions pkg/middlewares/canary/canary.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
"github.com/containous/traefik/v2/pkg/log"
"github.com/containous/traefik/v2/pkg/middlewares"
"github.com/containous/traefik/v2/pkg/middlewares/accesslog"
"github.com/containous/traefik/v2/pkg/tracing"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
)

Expand Down Expand Up @@ -84,7 +84,7 @@ func New(ctx context.Context, next http.Handler, cfg dynamic.Canary, name string

// GetTracingInformation implements Tracable interface
func (c *Canary) GetTracingInformation() (string, ext.SpanKindEnum) {
return c.name, tracing.SpanKindNoneEnum
return c.name, ext.SpanKindRPCClientEnum
}

func (c *Canary) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
Expand All @@ -103,9 +103,15 @@ func (c *Canary) processRequestID(rw http.ResponseWriter, req *http.Request) {
rw.Header().Set(headerXRequestID, requestID)
}

if span := opentracing.SpanFromContext(req.Context()); span != nil {
span.SetTag("component", "Canary")
span.SetTag("x-request-id", requestID)
}

if logData := accesslog.GetLogData(req); logData != nil {
logData.Core["XRequestID"] = requestID
logData.Core["UserAgent"] = req.Header.Get(headerUA)
logData.Core["Referer"] = req.Header.Get("Referer")
}
}

Expand All @@ -119,8 +125,8 @@ func (c *Canary) processCanary(rw http.ResponseWriter, req *http.Request) {
// load user's labels and update to header when work as public gateway.
info.fromHeader(req.Header, false)
if info.label == "" {
if cookie, _ := req.Cookie(headerXCanary); cookie != nil && validLabelReg.MatchString(cookie.Value) {
info.label = cookie.Value
if cookie, _ := req.Cookie(headerXCanary); cookie != nil && cookie.Value != "" {
info.feed(strings.Split(cookie.Value, ","), false)
}
}

Expand Down Expand Up @@ -155,8 +161,10 @@ func (c *Canary) processCanary(rw http.ResponseWriter, req *http.Request) {
type userInfo struct {
UID0 string `json:"uid"`
UID1 string `json:"_userId"`
UID2 string `json:"sub"`
UID3 string `json:"id"`
UID2 string `json:"userId"`
UID3 string `json:"user_id"`
UID4 string `json:"sub"`
UID5 string `json:"id"`
}

func extractUserID(req *http.Request, uidCookies []string) string {
Expand Down Expand Up @@ -221,26 +229,36 @@ func extractUserIDFromBase64(s string) string {
return user.UID2
case user.UID3 != "":
return user.UID3
case user.UID4 != "":
return user.UID4
case user.UID5 != "":
return user.UID5
}
}
}
return ""
}

type canaryHeader struct {
label string
product string
uid string
client string
channel string
app string
version string
label string
product string
uid string
client string
channel string
app string
version string
nofallback bool
testing bool
}

// uid and product will not be extracted
func (ch *canaryHeader) fromHeader(header http.Header, trust bool) {
vals := header.Values(headerXCanary)
for _, v := range vals {
ch.feed(header.Values(headerXCanary), trust)
}

func (ch *canaryHeader) feed(vals []string, trust bool) {
for i, v := range vals {
v = strings.TrimSpace(v)
switch {
case strings.HasPrefix(v, "label="):
ch.label = v[6:]
Expand All @@ -256,8 +274,12 @@ func (ch *canaryHeader) fromHeader(header http.Header, trust bool) {
ch.app = v[4:]
case strings.HasPrefix(v, "version="):
ch.version = v[8:]
case v == "nofallback":
ch.nofallback = true
case v == "testing":
ch.testing = true
default:
if len(vals) == 1 && validLabelReg.MatchString(v) {
if i == 0 && validLabelReg.MatchString(v) {
ch.label = v
}
}
Expand Down Expand Up @@ -288,4 +310,10 @@ func (ch *canaryHeader) intoHeader(header http.Header) {
if ch.version != "" {
header.Add(headerXCanary, fmt.Sprintf("version=%s", ch.version))
}
if ch.nofallback {
header.Add(headerXCanary, "nofallback")
}
if ch.testing {
header.Add(headerXCanary, "testing")
}
}
54 changes: 46 additions & 8 deletions pkg/middlewares/canary/canary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ func TestCanaryHeader(t *testing.T) {
h.Add(headerXCanary, fmt.Sprintf("uid=%s", "uid"))
h.Add(headerXCanary, fmt.Sprintf("product=%s", "product"))
h.Add(headerXCanary, fmt.Sprintf("ip=%s", "ip"))
h.Add(headerXCanary, "nofallback")
h.Add(headerXCanary, "testing")
ch.fromHeader(h, false)
a.Equal("label", ch.label)
a.Equal("", ch.product)
Expand All @@ -62,6 +64,8 @@ func TestCanaryHeader(t *testing.T) {
a.Equal("channel", ch.channel)
a.Equal("app", ch.app)
a.Equal("version", ch.version)
a.True(ch.nofallback)
a.True(ch.testing)
})

t.Run("intoHeader should work", func(t *testing.T) {
Expand All @@ -87,17 +91,19 @@ func TestCanaryHeader(t *testing.T) {
a.Equal(*ch, *chn)

ch = &canaryHeader{
label: "label",
product: "product",
uid: "uid",
client: "client",
channel: "channel",
app: "app",
version: "version",
label: "label",
product: "product",
uid: "uid",
client: "client",
channel: "channel",
app: "app",
version: "version",
nofallback: true,
testing: true,
}
h = http.Header{}
ch.intoHeader(h)
a.Equal(7, len(h.Values(headerXCanary)))
a.Equal(9, len(h.Values(headerXCanary)))

chn = &canaryHeader{}
chn.fromHeader(h, true)
Expand Down Expand Up @@ -209,6 +215,38 @@ func TestCanary(t *testing.T) {
ch.fromHeader(req.Header, true)
a.Equal("beta", ch.label)
a.Equal("Urbs", ch.product)
a.False(ch.nofallback)
a.False(ch.testing)
ch = &canaryHeader{}
ch.fromHeader(rw.Header(), true)
a.Equal("", ch.label)
a.Equal("", ch.product)

req = httptest.NewRequest("GET", "http://example.com/foo", nil)
rw = httptest.NewRecorder()
req.AddCookie(&http.Cookie{Name: headerXCanary, Value: "beta, nofallback,testing "})
c.processCanary(rw, req)
ch = &canaryHeader{}
ch.fromHeader(req.Header, true)
a.Equal("beta", ch.label)
a.Equal("Urbs", ch.product)
a.True(ch.nofallback)
a.True(ch.testing)
ch = &canaryHeader{}
ch.fromHeader(rw.Header(), true)
a.Equal("", ch.label)
a.Equal("", ch.product)

req = httptest.NewRequest("GET", "http://example.com/foo", nil)
rw = httptest.NewRecorder()
req.AddCookie(&http.Cookie{Name: headerXCanary, Value: "label=beta,nofallback,testing"})
c.processCanary(rw, req)
ch = &canaryHeader{}
ch.fromHeader(req.Header, true)
a.Equal("beta", ch.label)
a.Equal("Urbs", ch.product)
a.True(ch.nofallback)
a.True(ch.testing)
ch = &canaryHeader{}
ch.fromHeader(rw.Header(), true)
a.Equal("", ch.label)
Expand Down
8 changes: 8 additions & 0 deletions pkg/middlewares/canary/label.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/containous/traefik/v2/pkg/config/dynamic"
"github.com/containous/traefik/v2/pkg/log"
"github.com/opentracing/opentracing-go"
)

var storesMu sync.Mutex
Expand Down Expand Up @@ -119,10 +120,17 @@ func (ls *LabelStore) MustLoadLabels(ctx context.Context, uid, requestID string)

e.mu.Lock()
defer e.mu.Unlock()
fetchLabels := false

if e.value == nil || e.expireAt.Before(now) {
labels, ts := ls.mustFetchLabels(ctx, uid, requestID)
e.value = labels
e.expireAt = time.Unix(ts, 0).Add(ls.expiration)
fetchLabels = true
}

if span := opentracing.SpanFromContext(ctx); span != nil {
span.SetTag("fetched-labels", fetchLabels)
}

return e.value
Expand Down
19 changes: 0 additions & 19 deletions pkg/middlewares/canary/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@ import (
"time"

"github.com/containous/traefik/v2/pkg/log"
"github.com/containous/traefik/v2/pkg/tracing"
"github.com/containous/traefik/v2/pkg/version"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
)

func init() {
Expand Down Expand Up @@ -94,19 +91,6 @@ func getUserLabels(ctx context.Context, api, xRequestID string) (*labelsRes, err
return nil, err
}

var sp opentracing.Span
if tr, _ := tracing.FromContext(req.Context()); tr != nil {
opParts := []string{"label"}
span, re, finish := tr.StartSpanf(req, ext.SpanKindRPCClientEnum, "canary", opParts, "/")
sp = span
defer finish()

span.SetTag(headerXRequestID, xRequestID)
ext.HTTPUrl.Set(span, re.URL.String())
tracing.InjectRequestHeaders(re)
req = re
}

req.Header.Set(headerUA, userAgent)
req.Header.Set(headerXRequestID, xRequestID)
resp, err := client.Do(req)
Expand All @@ -120,9 +104,6 @@ func getUserLabels(ctx context.Context, api, xRequestID string) (*labelsRes, err
}

hc.Reset()
if sp != nil {
tracing.LogResponseCode(sp, resp.StatusCode)
}

respBody, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
Expand Down
26 changes: 18 additions & 8 deletions pkg/server/service/loadbalancer/lrr/lrr.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ func New(defaultServiceName string, handler http.Handler) *Balancer {

type sliceHandler []*namedHandler

func (s sliceHandler) Match(name string) *namedHandler {
func (s sliceHandler) Match(name string, fallback bool) *namedHandler {
for _, handler := range s {
if strings.HasPrefix(name, handler.name) {
if fallback && strings.HasPrefix(name, handler.name) {
return handler
} else if name == handler.name {
return handler
}
}
Expand All @@ -50,22 +52,30 @@ type Balancer struct {

func (b *Balancer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// X-Canary: beta
// X-Canary: label=beta; product=urbs; uid=5c4057f0be825b390667abee; ...
label := req.Header.Get(labelKey)
if label != "" && strings.HasPrefix(label, "label=") {
label = label[6:]
// X-Canary: label=beta; product=urbs; uid=5c4057f0be825b390667abee; nofallback ...
label := ""
fallback := true
for i, v := range req.Header.Values(labelKey) {
switch {
case strings.HasPrefix(v, "label="):
label = v[6:]
case v == "nofallback":
fallback = false
case i == 0:
label = v
}
}

name := b.serviceName
if label != "" {
name = fmt.Sprintf("%s-%s", name, label)
if handler := b.handlers.Match(name); handler != nil {
if handler := b.handlers.Match(name, fallback); handler != nil {
handler.ServeHTTP(w, req)
return
}
}

if b.defaultHandler != nil {
if b.defaultHandler != nil && (fallback || label == "") {
b.defaultHandler.ServeHTTP(w, req)
return
}
Expand Down
35 changes: 22 additions & 13 deletions pkg/server/service/loadbalancer/lrr/lrr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,27 +63,36 @@ func TestLRRBalancer(t *testing.T) {
r string
}
list := []ir{
ir{"lrr", "lrr"},
ir{"lrr-api", "lrr-api"},
ir{"lrr-api-stable", "lrr-api-stable"},
ir{"lrr-api-canary", "lrr-api-canary"},
ir{"lrr-api-canary-v2", "lrr-api-canary-v2"},
ir{"lrr-api-canary-v1", "lrr-api-canary-v1"},
ir{"lrr-api-canary-v3", "lrr-api-canary"},
ir{"lrr-api-canary-v1-beta1", "lrr-api-canary-v1"},
ir{"lrr-api-dev", "lrr-api"},
ir{"lrr-web", "lrr"},
{"lrr", "lrr"},
{"lrr-api", "lrr-api"},
{"lrr-api-stable", "lrr-api-stable"},
{"lrr-api-canary", "lrr-api-canary"},
{"lrr-api-canary-v2", "lrr-api-canary-v2"},
{"lrr-api-canary-v1", "lrr-api-canary-v1"},
{"lrr-api-canary-v3", "lrr-api-canary"},
{"lrr-api-canary-v1-beta1", "lrr-api-canary-v1"},
{"lrr-api-dev", "lrr-api"},
{"lrr-web", "lrr"},
}

for _, ele := range list {
h := s.Match(ele.i)
h := s.Match(ele.i, true)
a.Equal(ele.r, h.name)
}

h := s.Match("api")
h := s.Match("lrr", false)
a.NotNil(h)

h = s.Match("lrr-api", false)
a.NotNil(h)

h = s.Match("lrr-api-canary-v1-beta1", false)
a.Nil(h)

h = s.Match("api", true)
a.Nil(h)

h = s.Match("lr")
h = s.Match("lr", true)
a.Nil(h)
})
}

0 comments on commit 8e4ef57

Please sign in to comment.