diff --git a/internal/xds/translator/httpfilters.go b/internal/xds/translator/httpfilters.go new file mode 100644 index 000000000000..2f9257e78c3a --- /dev/null +++ b/internal/xds/translator/httpfilters.go @@ -0,0 +1,101 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package translator + +import ( + "sort" + + hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + "github.com/envoyproxy/go-control-plane/pkg/wellknown" + + "github.com/envoyproxy/gateway/internal/ir" + xdsfilters "github.com/envoyproxy/gateway/internal/xds/filters" +) + +type OrderedHTTPFilter struct { + filter *hcmv3.HttpFilter + order int +} + +type OrderedHTTPFilters []*OrderedHTTPFilter + +func newOrderedHTTPFilter(filter *hcmv3.HttpFilter) *OrderedHTTPFilter { + order := 50 + + // Set a rational order for all the filters. + switch filter.Name { + case wellknown.CORS: + order = 1 + case jwtAuthenFilter: + order = 2 + case wellknown.HTTPRateLimit: + order = 3 + case wellknown.Router: + order = 100 + } + + return &OrderedHTTPFilter{ + filter: filter, + order: order, + } +} + +// sort.Interface implementation. +func (o OrderedHTTPFilters) Len() int { + return len(o) +} + +func (o OrderedHTTPFilters) Less(i, j int) bool { + return o[i].order < o[j].order +} + +func (o OrderedHTTPFilters) Swap(i, j int) { + o[i], o[j] = o[j], o[i] +} + +// sortHTTPFilters sorts the HTTP filters in the correct order. +// This is needed because the order of the filters is important. +// For example, the cors filter must be before the router filter. +// The router filter must be the last one. +func sortHTTPFilters(filters []*hcmv3.HttpFilter) []*hcmv3.HttpFilter { + orderedFilters := make(OrderedHTTPFilters, len(filters)) + for i := 0; i < len(filters); i++ { + orderedFilters[i] = newOrderedHTTPFilter(filters[i]) + } + sort.Sort(orderedFilters) + + for i := 0; i < len(filters); i++ { + filters[i] = orderedFilters[i].filter + } + return filters +} + +// patchHCMWithFilters builds and appends HTTP Filters to the HTTP connection +// manager. +func (t *Translator) patchHCMWithFilters(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTPListener) error { + // The order of filter patching is not relevant here. + // All the filters will be sorted in correct order after the patching is done. + // TODO: Make this a generic interface for all API Gateway features. + // https://github.com/envoyproxy/gateway/issues/882 + t.patchHCMWithRateLimit(mgr, irListener) + + // Add the jwt authn filter, if needed. + if err := patchHCMWithJwtAuthnFilter(mgr, irListener); err != nil { + return err + } + + // Add the cors filter, if needed + if err := patchHCMWithCorsFilter(mgr, irListener); err != nil { + return err + } + + // Add the router filter + mgr.HttpFilters = append(mgr.HttpFilters, xdsfilters.HTTPRouter) + + // Sort the filters in the correct order. + mgr.HttpFilters = sortHTTPFilters(mgr.HttpFilters) + return nil +} diff --git a/internal/xds/translator/httpfilters_test.go b/internal/xds/translator/httpfilters_test.go new file mode 100644 index 000000000000..928afb183c45 --- /dev/null +++ b/internal/xds/translator/httpfilters_test.go @@ -0,0 +1,49 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package translator + +import ( + "testing" + + hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + "github.com/envoyproxy/go-control-plane/pkg/wellknown" + "github.com/stretchr/testify/assert" +) + +func Test_sortHTTPFilters(t *testing.T) { + tests := []struct { + name string + filters []*hcmv3.HttpFilter + want []*hcmv3.HttpFilter + }{ + { + name: "sort filters", + filters: []*hcmv3.HttpFilter{ + httpFilterForTest(wellknown.Router), + httpFilterForTest(wellknown.CORS), + httpFilterForTest(jwtAuthenFilter), + httpFilterForTest(wellknown.HTTPRateLimit), + }, + want: []*hcmv3.HttpFilter{ + httpFilterForTest(wellknown.CORS), + httpFilterForTest(jwtAuthenFilter), + httpFilterForTest(wellknown.HTTPRateLimit), + httpFilterForTest(wellknown.Router), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, sortHTTPFilters(tt.filters), "sortHTTPFilters(%v)", tt.filters) + }) + } +} + +func httpFilterForTest(name string) *hcmv3.HttpFilter { + return &hcmv3.HttpFilter{ + Name: name, + } +} diff --git a/internal/xds/translator/listener.go b/internal/xds/translator/listener.go index 72fabee2253b..3a52aa0589b4 100644 --- a/internal/xds/translator/listener.go +++ b/internal/xds/translator/listener.go @@ -134,22 +134,12 @@ func (t *Translator) addXdsHTTPFilterChain(xdsListener *listenerv3.Listener, irL } } - // TODO: Make this a generic interface for all API Gateway features. - // https://github.com/envoyproxy/gateway/issues/882 - t.patchHCMWithRateLimit(mgr, irListener) - - // Add the jwt authn filter, if needed. - if err := patchHCMWithJwtAuthnFilter(mgr, irListener); err != nil { - return err - } - - // Add the cors filter, if needed - if err := patchHCMWithCorsFilter(mgr, irListener); err != nil { + // Add the HTTP filters to the HCM, the filters have been sorted in the + // correct order. + if err := t.patchHCMWithFilters(mgr, irListener); err != nil { return err } - // Make sure the router filter is the last one. - mgr.HttpFilters = append(mgr.HttpFilters, xdsfilters.HTTPRouter) mgrAny, err := protocov.ToAnyWithError(mgr) if err != nil { return err