diff --git a/apidef/oas/operation.go b/apidef/oas/operation.go index ba8f72a7be8..80dc0381589 100644 --- a/apidef/oas/operation.go +++ b/apidef/oas/operation.go @@ -29,6 +29,9 @@ type Operation struct { // IgnoreAuthentication ignores authentication on request by allowance. IgnoreAuthentication *Allowance `bson:"ignoreAuthentication,omitempty" json:"ignoreAuthentication,omitempty"` + // TrafficShaping contains configuration for traffic control and gradual rollouts. + TrafficShaping *TrafficShaping `bson:"trafficShaping,omitempty" json:"trafficShaping,omitempty"` + // Internal makes the endpoint only respond to internal requests. Internal *Internal `bson:"internal,omitempty" json:"internal,omitempty"` diff --git a/apidef/oas/traffic_shaping.go b/apidef/oas/traffic_shaping.go new file mode 100644 index 00000000000..67e8e77fddf --- /dev/null +++ b/apidef/oas/traffic_shaping.go @@ -0,0 +1,37 @@ +package oas + +import ( + "github.com/TykTechnologies/tyk/apidef" +) + +type TrafficShaping struct { + Enabled bool `bson:"enabled" json:"enabled"` + Percentage int `bson:"percentage" json:"percentage"` + ConsistentRouting *ConsistentRouting `bson:"consistentRouting,omitempty" json:"consistentRouting,omitempty"` + AlternativeEndpoint string `bson:"alternativeEndpoint,omitempty" json:"alternativeEndpoint,omitempty"` +} + +type ConsistentRouting struct { + HeaderName string `bson:"headerName" json:"headerName"` + QueryName string `bson:"queryName,omitempty" json:"queryName,omitempty"` +} + +func (t *TrafficShaping) Fill(api apidef.APIDefinition) { + t.Enabled = false + t.Percentage = 100 +} + +func (t *TrafficShaping) ExtractTo(api *apidef.APIDefinition) { +} + +func (t *TrafficShaping) Validate() error { + if !t.Enabled { + return nil + } + + if t.Percentage < 0 || t.Percentage > 100 { + return ErrInvalidPercentage + } + + return nil +} diff --git a/gateway/model_apispec.go b/gateway/model_apispec.go index a5d7d446b74..2eece22eca8 100644 --- a/gateway/model_apispec.go +++ b/gateway/model_apispec.go @@ -1,13 +1,14 @@ package gateway import ( - "github.com/TykTechnologies/tyk/internal/errors" "net/http" "net/url" "strings" "sync" "time" + "github.com/TykTechnologies/tyk/internal/errors" + "github.com/getkin/kin-openapi/routers" "github.com/TykTechnologies/tyk-pump/analytics" @@ -139,6 +140,20 @@ func (a *APISpec) injectIntoReqContext(req *http.Request) { } } +// GetTykExtension returns the Tyk extension from the OAS definition +func (a *APISpec) GetTykExtension() *oas.XTykAPIGateway { + if !a.IsOAS { + return nil + } +} + +func (a *APISpec) GetTykExtension() *oas.XTykAPIGateway { + if !a.IsOAS { + return nil + } + return a.OAS.GetTykExtension() +} + func (a *APISpec) findOperation(r *http.Request) *Operation { middleware := a.OAS.GetTykMiddleware() if middleware == nil { diff --git a/gateway/mw_traffic_shaping.go b/gateway/mw_traffic_shaping.go new file mode 100644 index 00000000000..27b7759807c --- /dev/null +++ b/gateway/mw_traffic_shaping.go @@ -0,0 +1,80 @@ +package gateway + +import ( + "errors" + "github.com/TykTechnologies/tyk/apidef/oas" + "hash/fnv" + "net/http" +) + +var ErrTrafficShapingRejected = errors.New("request rejected by traffic shaping rules") + +type TrafficShapingMiddleware struct { + *BaseMiddleware +} + +func (t *TrafficShapingMiddleware) Name() string { + return "TrafficShapingMiddleware" +} + +func (t *TrafficShapingMiddleware) EnabledForSpec() bool { + if ext := t.Spec.GetTykExtension(); ext != nil && ext.Middleware != nil { + for _, op := range ext.Middleware.Operations { + if op.TrafficShaping != nil && op.TrafficShaping.Enabled { + return true + } + } + } + return false +} + +func (t *TrafficShapingMiddleware) ProcessRequest(w http.ResponseWriter, r *http.Request, _ interface{}) (error, int) { + op := t.Spec.findOperation(r) + if op == nil || op.TrafficShaping == nil || !op.TrafficShaping.Enabled { + return nil, http.StatusOK + } + + routingKey := t.getRoutingValue(r, op.TrafficShaping.ConsistentRouting) + if !t.isAllowed(routingKey, op.TrafficShaping.Percentage) { + if altEndpoint := op.TrafficShaping.AlternativeEndpoint; altEndpoint != "" { + http.Redirect(w, r, altEndpoint, http.StatusTemporaryRedirect) + return nil, http.StatusTemporaryRedirect + } + return ErrTrafficShapingRejected, http.StatusTooManyRequests + } + + return nil, http.StatusOK +} + +func (t *TrafficShapingMiddleware) getRoutingValue(r *http.Request, routing *oas.ConsistentRouting) string { + if routing == nil { + return r.RemoteAddr + } + + if routing.HeaderName != "" { + if val := r.Header.Get(routing.HeaderName); val != "" { + return val + } + } + + if routing.QueryName != "" { + if val := r.URL.Query().Get(routing.QueryName); val != "" { + return val + } + } + + return r.RemoteAddr +} + +func (t *TrafficShapingMiddleware) isAllowed(routingKey string, percentage int) bool { + if percentage >= 100 { + return true + } + if percentage <= 0 { + return false + } + + h := fnv.New32a() + h.Write([]byte(routingKey)) + return (h.Sum32() % 100) < uint32(percentage) +}