Skip to content

Commit 7144eb3

Browse files
committed
added draft implementation
1 parent 5ef54e3 commit 7144eb3

File tree

4 files changed

+165
-1
lines changed

4 files changed

+165
-1
lines changed

apidef/oas/operation.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ type Operation struct {
2929
// IgnoreAuthentication ignores authentication on request by allowance.
3030
IgnoreAuthentication *Allowance `bson:"ignoreAuthentication,omitempty" json:"ignoreAuthentication,omitempty"`
3131

32+
// TrafficShaping contains configuration for traffic control and gradual rollouts.
33+
TrafficShaping *TrafficShaping `bson:"trafficShaping,omitempty" json:"trafficShaping,omitempty"`
34+
3235
// Internal makes the endpoint only respond to internal requests.
3336
Internal *Internal `bson:"internal,omitempty" json:"internal,omitempty"`
3437

apidef/oas/traffic_shaping.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package oas
2+
3+
import (
4+
"github.com/TykTechnologies/tyk/apidef"
5+
)
6+
7+
// TrafficShaping contains configuration for traffic control and gradual rollouts
8+
type TrafficShaping struct {
9+
// Enabled activates the traffic shaping functionality
10+
Enabled bool `bson:"enabled" json:"enabled"`
11+
12+
// Percentage of traffic to allow (0-100)
13+
Percentage int `bson:"percentage" json:"percentage"`
14+
15+
// ConsistentRouting configuration
16+
ConsistentRouting *ConsistentRouting `bson:"consistentRouting,omitempty" json:"consistentRouting,omitempty"`
17+
18+
// AlternativeEndpoint for rejected traffic
19+
AlternativeEndpoint string `bson:"alternativeEndpoint,omitempty" json:"alternativeEndpoint,omitempty"`
20+
}
21+
22+
// ConsistentRouting defines how to consistently route requests based on attributes
23+
type ConsistentRouting struct {
24+
// HeaderName to extract the attribute from
25+
HeaderName string `bson:"headerName" json:"headerName"`
26+
27+
// QueryName to extract the attribute from (optional)
28+
QueryName string `bson:"queryName,omitempty" json:"queryName,omitempty"`
29+
}
30+
31+
// Fill populates TrafficShaping from classic API definition
32+
func (t *TrafficShaping) Fill(api apidef.APIDefinition) {
33+
// TODO: Implement if classic API compatibility is needed
34+
t.Enabled = false
35+
t.Percentage = 100
36+
}
37+
38+
// ExtractTo maps TrafficShaping to classic API definition
39+
func (t *TrafficShaping) ExtractTo(api *apidef.APIDefinition) {
40+
// TODO: Implement if classic API compatibility is needed
41+
}
42+
43+
// Validate ensures the configuration is valid
44+
func (t *TrafficShaping) Validate() error {
45+
if !t.Enabled {
46+
return nil
47+
}
48+
49+
if t.Percentage < 0 || t.Percentage > 100 {
50+
return ErrInvalidPercentage
51+
}
52+
53+
return nil
54+
}

gateway/model_apispec.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
package gateway
22

33
import (
4-
"github.com/TykTechnologies/tyk/internal/errors"
54
"net/http"
65
"net/url"
76
"strings"
87
"sync"
98
"time"
109

10+
"github.com/TykTechnologies/tyk/internal/errors"
11+
1112
"github.com/getkin/kin-openapi/routers"
1213

1314
"github.com/TykTechnologies/tyk-pump/analytics"
@@ -139,6 +140,14 @@ func (a *APISpec) injectIntoReqContext(req *http.Request) {
139140
}
140141
}
141142

143+
// GetTykExtension returns the Tyk extension from the OAS definition
144+
func (a *APISpec) GetTykExtension() *oas.XTykAPIGateway {
145+
if !a.IsOAS {
146+
return nil
147+
}
148+
return a.OAS.GetTykExtension()
149+
}
150+
142151
func (a *APISpec) findOperation(r *http.Request) *Operation {
143152
middleware := a.OAS.GetTykMiddleware()
144153
if middleware == nil {

gateway/mw_traffic_shaping.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package gateway
2+
3+
import (
4+
"errors"
5+
"github.com/TykTechnologies/tyk/apidef/oas"
6+
"hash/fnv"
7+
"net/http"
8+
)
9+
10+
var (
11+
ErrTrafficShapingRejected = errors.New("request rejected by traffic shaping rules")
12+
)
13+
14+
// TrafficShapingMiddleware controls traffic based on configured rules
15+
type TrafficShapingMiddleware struct {
16+
*BaseMiddleware
17+
}
18+
19+
func (t *TrafficShapingMiddleware) Name() string {
20+
return "TrafficShapingMiddleware"
21+
}
22+
23+
// EnabledForSpec checks if any operation has traffic shaping enabled
24+
func (t *TrafficShapingMiddleware) EnabledForSpec() bool {
25+
if ext := t.Spec.GetTykExtension(); ext != nil && ext.Middleware != nil {
26+
for _, op := range ext.Middleware.Operations {
27+
if op.TrafficShaping != nil && op.TrafficShaping.Enabled {
28+
return true
29+
}
30+
}
31+
}
32+
return false
33+
}
34+
35+
// ProcessRequest implements the traffic shaping logic
36+
func (t *TrafficShapingMiddleware) ProcessRequest(w http.ResponseWriter, r *http.Request, _ interface{}) (error, int) {
37+
// Find operation config for this request
38+
op := t.Spec.findOperation(r)
39+
if op == nil || op.TrafficShaping == nil || !op.TrafficShaping.Enabled {
40+
return nil, http.StatusOK
41+
}
42+
43+
// Get routing value based on config
44+
routingKey := t.getRoutingValue(r, op.TrafficShaping.ConsistentRouting)
45+
46+
// Check if request should be allowed
47+
if !t.isAllowed(routingKey, op.TrafficShaping.Percentage) {
48+
if altEndpoint := op.TrafficShaping.AlternativeEndpoint; altEndpoint != "" {
49+
http.Redirect(w, r, altEndpoint, http.StatusTemporaryRedirect)
50+
return nil, http.StatusTemporaryRedirect
51+
}
52+
return ErrTrafficShapingRejected, http.StatusTooManyRequests
53+
}
54+
55+
return nil, http.StatusOK
56+
}
57+
58+
// getRoutingValue extracts a consistent routing value from the request
59+
func (t *TrafficShapingMiddleware) getRoutingValue(r *http.Request, routing *oas.ConsistentRouting) string {
60+
if routing == nil {
61+
return r.RemoteAddr // Use IP as default
62+
}
63+
64+
// Try header first
65+
if routing.HeaderName != "" {
66+
if val := r.Header.Get(routing.HeaderName); val != "" {
67+
return val
68+
}
69+
}
70+
71+
// Try query param
72+
if routing.QueryName != "" {
73+
if val := r.URL.Query().Get(routing.QueryName); val != "" {
74+
return val
75+
}
76+
}
77+
78+
return r.RemoteAddr // Fallback to IP
79+
}
80+
81+
// isAllowed determines if the request should be allowed through
82+
func (t *TrafficShapingMiddleware) isAllowed(routingKey string, percentage int) bool {
83+
if percentage >= 100 {
84+
return true
85+
}
86+
if percentage <= 0 {
87+
return false
88+
}
89+
90+
// Hash the routing key for consistent distribution
91+
h := fnv.New32a()
92+
h.Write([]byte(routingKey))
93+
hash := h.Sum32()
94+
95+
// Map to 0-99 range
96+
bucket := hash % 100
97+
return int(bucket) < percentage
98+
}

0 commit comments

Comments
 (0)