Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apidef/oas/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`

Expand Down
37 changes: 37 additions & 0 deletions apidef/oas/traffic_shaping.go
Original file line number Diff line number Diff line change
@@ -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
}
17 changes: 16 additions & 1 deletion gateway/model_apispec.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
80 changes: 80 additions & 0 deletions gateway/mw_traffic_shaping.go
Original file line number Diff line number Diff line change
@@ -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)
}