Skip to content

Commit

Permalink
RateLimitFilter: Gateway API to Xds IR translaton (#933)
Browse files Browse the repository at this point in the history
* RateLimitFilter: Gateway API to Xds IR translaton

* Also rm'ed `ResetConditon` and instead set `Accepted=False` for
negative cases for ExtensionFilter cases

Relates to #670

Signed-off-by: Arko Dasgupta <arko@tetrate.io>
  • Loading branch information
arkodg authored Jan 24, 2023
1 parent 96b34e6 commit 2a03c58
Show file tree
Hide file tree
Showing 12 changed files with 456 additions and 54 deletions.
131 changes: 96 additions & 35 deletions internal/gatewayapi/filters.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/gateway-api/apis/v1beta1"

egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
"github.com/envoyproxy/gateway/internal/ir"
)

Expand Down Expand Up @@ -55,6 +56,7 @@ type HTTPFilterChains struct {
Mirrors []*ir.RouteDestination

RequestAuthentication *ir.RequestAuthentication
RateLimit *ir.RateLimit
}

// ProcessHTTPFilters translate gateway api http filters to IRs.
Expand Down Expand Up @@ -622,50 +624,90 @@ func (t *Translator) processExtensionRefHTTPFilter(filter v1beta1.HTTPRouteFilte
return
}

// Set negative status condition and return early if the no AuthenticationFilters exist.
if len(resources.AuthenticationFilters) == 0 {
errMsg := fmt.Sprintf("Reference not found for filter type: %v", filter.Type)
filterContext.ParentRef.ResetConditions(filterContext.HTTPRoute)
filterContext.ParentRef.SetCondition(filterContext.HTTPRoute,
v1beta1.RouteConditionResolvedRefs,
metav1.ConditionFalse,
v1beta1.RouteReasonBackendNotFound,
errMsg,
)
filterContext.DirectResponse = &ir.DirectResponse{
Body: &errMsg,
StatusCode: 500,
// Set the filter context and return early if a matching AuthenticationFilter is found.
if string(filter.ExtensionRef.Kind) == egv1a1.KindAuthenticationFilter {
for _, authenFilter := range resources.AuthenticationFilters {
if authenFilter.Namespace == filterContext.HTTPRoute.Namespace &&
authenFilter.Name == string(extFilter.Name) {
filterContext.HTTPFilterChains.RequestAuthentication = &ir.RequestAuthentication{
JWT: &ir.JwtRequestAuthentication{
Providers: authenFilter.Spec.JwtProviders,
},
}
return
}
}
return
}

// Set the filter context and return early if a matching AuthenticationFilter is found.
for _, authenFilter := range resources.AuthenticationFilters {
if authenFilter.Namespace == filterContext.HTTPRoute.Namespace &&
authenFilter.Name == string(extFilter.Name) {
filterContext.HTTPFilterChains.RequestAuthentication = &ir.RequestAuthentication{
JWT: &ir.JwtRequestAuthentication{
Providers: authenFilter.Spec.JwtProviders,
},
// Set the filter context and return early if a matching RateLimitFilter is found.
if string(filter.ExtensionRef.Kind) == egv1a1.KindRateLimitFilter {
for _, rateLimitFilter := range resources.RateLimitFilters {
if rateLimitFilter.Namespace == filterContext.HTTPRoute.Namespace &&
rateLimitFilter.Name == string(extFilter.Name) {
if rateLimitFilter.Spec.Global == nil {
errMsg := fmt.Sprintf("Global configuration empty for RateLimitFilter: %s/%s", filterContext.HTTPRoute.Namespace,
filter.ExtensionRef.Name)
t.processUnresolvedHTTPFilter(errMsg, filterContext)
return
}
rateLimit := &ir.RateLimit{
Global: &ir.GlobalRateLimit{
Rules: make([]*ir.RateLimitRule, len(rateLimitFilter.Spec.Global.Rules)),
},
}
rules := rateLimit.Global.Rules
for i, rule := range rateLimitFilter.Spec.Global.Rules {
rules[i] = &ir.RateLimitRule{
Limit: &ir.RateLimitValue{
Requests: rule.Limit.Requests,
Unit: ir.RateLimitUnit(rule.Limit.Unit),
},
HeaderMatches: make([]*ir.StringMatch, 0),
}
for _, match := range rule.ClientSelectors {
for _, header := range match.Headers {
switch {
case header.Type == nil && header.Value != nil:
fallthrough
case *header.Type == egv1a1.HeaderMatchExact && header.Value != nil:
m := &ir.StringMatch{
Name: header.Name,
Exact: header.Value,
}
rules[i].HeaderMatches = append(rules[i].HeaderMatches, m)
case *header.Type == egv1a1.HeaderMatchRegularExpression && header.Value != nil:
m := &ir.StringMatch{
Name: header.Name,
SafeRegex: header.Value,
}
rules[i].HeaderMatches = append(rules[i].HeaderMatches, m)
case *header.Type == egv1a1.HeaderMatchRegularExpression && header.Value == nil:
m := &ir.StringMatch{
Name: header.Name,
Distinct: true,
}
rules[i].HeaderMatches = append(rules[i].HeaderMatches, m)
default:
// set negative status condition.
errMsg := fmt.Sprintf("Unable to translate RateLimitFilter: %s/%s", filterContext.HTTPRoute.Namespace,
filter.ExtensionRef.Name)
t.processUnresolvedHTTPFilter(errMsg, filterContext)
return
}
}
}

}
filterContext.HTTPFilterChains.RateLimit = rateLimit
return
}
return
}
}

// Matching AuthenticationFilter not found, so set negative status condition.
// Matching filter not found, so set negative status condition.
errMsg := fmt.Sprintf("Reference %s/%s not found for filter type: %v", filterContext.HTTPRoute.Namespace,
filter.ExtensionRef.Name, filter.Type)
filterContext.ParentRef.ResetConditions(filterContext.HTTPRoute)
filterContext.ParentRef.SetCondition(filterContext.HTTPRoute,
v1beta1.RouteConditionResolvedRefs,
metav1.ConditionFalse,
v1beta1.RouteReasonBackendNotFound,
errMsg,
)
filterContext.DirectResponse = &ir.DirectResponse{
Body: &errMsg,
StatusCode: 500,
}
t.processUnresolvedHTTPFilter(errMsg, filterContext)
}

func (t *Translator) processRequestMirrorFilter(
Expand Down Expand Up @@ -711,6 +753,25 @@ func (t *Translator) processRequestMirrorFilter(

}

func (t *Translator) processUnresolvedHTTPFilter(errMsg string, filterContext *HTTPFiltersContext) {
filterContext.ParentRef.SetCondition(filterContext.HTTPRoute,
v1beta1.RouteConditionResolvedRefs,
metav1.ConditionFalse,
v1beta1.RouteReasonBackendNotFound,
errMsg,
)
filterContext.ParentRef.SetCondition(filterContext.HTTPRoute,
v1beta1.RouteConditionAccepted,
metav1.ConditionFalse,
v1beta1.RouteReasonUnsupportedValue,
errMsg,
)
filterContext.DirectResponse = &ir.DirectResponse{
Body: &errMsg,
StatusCode: 500,
}
}

func (t *Translator) processUnsupportedHTTPFilter(filter v1beta1.HTTPRouteFilter, filterContext *HTTPFiltersContext) {
errMsg := fmt.Sprintf("Unsupported filter type: %s", filter.Type)
filterContext.ParentRef.SetCondition(filterContext.HTTPRoute,
Expand Down
5 changes: 5 additions & 0 deletions internal/gatewayapi/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,10 @@ func (t *Translator) processHTTPRouteRule(httpRoute *HTTPRouteContext, ruleIdx i
if httpFiltersContext.RequestAuthentication != nil {
irRoute.RequestAuthentication = httpFiltersContext.RequestAuthentication
}
if httpFiltersContext.RateLimit != nil {
irRoute.RateLimit = httpFiltersContext.RateLimit
}

ruleRoutes = append(ruleRoutes, irRoute)
}

Expand Down Expand Up @@ -280,6 +284,7 @@ func (t *Translator) processHTTPRouteParentRefListener(httpRoute *HTTPRouteConte
URLRewrite: routeRoute.URLRewrite,
Mirrors: routeRoute.Mirrors,
RequestAuthentication: routeRoute.RequestAuthentication,
RateLimit: routeRoute.RateLimit,
}
// Don't bother copying over the weights unless the route has invalid backends.
if routeRoute.BackendWeights.Invalid > 0 {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
gateways:
- apiVersion: gateway.networking.k8s.io/v1beta1
kind: Gateway
metadata:
namespace: envoy-gateway
name: gateway-1
spec:
gatewayClassName: envoy-gateway-class
listeners:
- name: http
protocol: HTTP
port: 80
hostname: "*.envoyproxy.io"
allowedRoutes:
namespaces:
from: All
httpRoutes:
- apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
namespace: default
name: httproute-1
spec:
hostnames:
- gateway.envoyproxy.io
parentRefs:
- namespace: envoy-gateway
name: gateway-1
sectionName: http
rules:
- matches:
- path:
value: "/"
backendRefs:
- name: service-1
port: 8080
filters:
- type: ExtensionRef
extensionRef:
group: gateway.envoyproxy.io
kind: RateLimitFilter
name: test
rateLimitFilters:
- apiVersion: gateway.envoyproxy.io/v1alpha1
kind: RateLimitFilter
metadata:
name: test
namespace: default
spec:
type: Global
global:
rules:
- clientSelectors:
- headers:
- type: Distinct
name: x-user-id
value: one
limit:
requests: 10
unit: Hour
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
gateways:
- apiVersion: gateway.networking.k8s.io/v1beta1
kind: Gateway
metadata:
namespace: envoy-gateway
name: gateway-1
spec:
gatewayClassName: envoy-gateway-class
listeners:
- name: http
protocol: HTTP
port: 80
hostname: "*.envoyproxy.io"
allowedRoutes:
namespaces:
from: All
status:
listeners:
- name: http
supportedKinds:
- group: gateway.networking.k8s.io
kind: HTTPRoute
attachedRoutes: 1
conditions:
- type: Programmed
status: "True"
reason: Programmed
message: Listener is ready
httpRoutes:
- apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
namespace: default
name: httproute-1
spec:
hostnames:
- gateway.envoyproxy.io
parentRefs:
- namespace: envoy-gateway
name: gateway-1
sectionName: http
rules:
- matches:
- path:
value: "/"
backendRefs:
- name: service-1
port: 8080
filters:
- type: ExtensionRef
extensionRef:
group: gateway.envoyproxy.io
kind: RateLimitFilter
name: test
status:
parents:
- parentRef:
namespace: envoy-gateway
name: gateway-1
sectionName: http
controllerName: gateway.envoyproxy.io/gatewayclass-controller
conditions:
- type: Accepted
status: "False"
reason: UnsupportedValue
message: "Unable to translate RateLimitFilter: default/test"
- type: ResolvedRefs
status: "False"
reason: BackendNotFound
message: "Unable to translate RateLimitFilter: default/test"
xdsIR:
envoy-gateway-gateway-1:
http:
- name: envoy-gateway-gateway-1-http
address: 0.0.0.0
port: 10080
hostnames:
- "*.envoyproxy.io"
routes:
- name: default-httproute-1-rule-0-match-0-gateway.envoyproxy.io
pathMatch:
prefix: "/"
headerMatches:
- name: ":authority"
exact: gateway.envoyproxy.io
# I believe the correct way to handle an invalid filter should be to allow the HTTPRoute to function
# normally but leave out the filter config and set the status, but this behaviour can be changed.
directResponse:
body: "Unable to translate RateLimitFilter: default/test"
statusCode: 500
infraIR:
envoy-gateway-gateway-1:
proxy:
metadata:
labels:
gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
gateway.envoyproxy.io/owning-gateway-name: gateway-1
name: envoy-gateway-gateway-1
image: envoyproxy/envoy:translator-tests
listeners:
- address: ""
ports:
- name: http
protocol: "HTTP"
containerPort: 10080
servicePort: 80
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,5 @@ httpRoutes:
extensionRef:
group: gateway.envoyproxy.io
kind: AuthenticationFilter
name: test
name: non-exist

Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ httpRoutes:
extensionRef:
group: gateway.envoyproxy.io
kind: AuthenticationFilter
name: test
name: non-exist
status:
parents:
- parentRef:
Expand All @@ -60,10 +60,14 @@ httpRoutes:
sectionName: http
controllerName: gateway.envoyproxy.io/gatewayclass-controller
conditions:
- type: Accepted
status: "False"
reason: UnsupportedValue
message: "Reference default/non-exist not found for filter type: ExtensionRef"
- type: ResolvedRefs
status: "False"
reason: BackendNotFound
message: "Reference not found for filter type: ExtensionRef"
message: "Reference default/non-exist not found for filter type: ExtensionRef"
xdsIR:
envoy-gateway-gateway-1:
http:
Expand All @@ -82,7 +86,7 @@ xdsIR:
# I believe the correct way to handle an invalid filter should be to allow the HTTPRoute to function
# normally but leave out the filter config and set the status, but this behaviour can be changed.
directResponse:
body: "Reference not found for filter type: ExtensionRef"
body: "Reference default/non-exist not found for filter type: ExtensionRef"
statusCode: 500
infraIR:
envoy-gateway-gateway-1:
Expand Down
Loading

0 comments on commit 2a03c58

Please sign in to comment.