Skip to content

Commit

Permalink
feat: Gateway API RequestRedirect filter translated to combination …
Browse files Browse the repository at this point in the history
…of kong plugins (#3702)

* feat: RequestRedirect filter implementation

The Gateway API filter RequestRedirect is now supported and translated
to the proper set of kong plugins.

Signed-off-by: Mattia Lavacca <lavacca.mattia@gmail.com>

* review's comments addressed

Signed-off-by: Mattia Lavacca <lavacca.mattia@gmail.com>

* chore(parser) add HTTPRoute filter tag comment

---------

Signed-off-by: Mattia Lavacca <lavacca.mattia@gmail.com>
Co-authored-by: Travis Raines <571832+rainest@users.noreply.github.com>
  • Loading branch information
mlavacca and rainest committed Mar 10, 2023
1 parent 63b9eaf commit f628552
Show file tree
Hide file tree
Showing 8 changed files with 252 additions and 103 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ Adding a new version? You'll need three changes:
of the controller. The old `all-in-one-dbless.yaml` manifest has been deprecated and
renamed to `all-in-one-dbless-legacy.yaml`. It will be removed in a future release.
[#3629](https://github.com/Kong/kubernetes-ingress-controller/pull/3629)
- The RequestRedirect Gateway API filter is now supported and translated
to the proper set of Kong plugins.
[#3702](https://github.com/Kong/kubernetes-ingress-controller/pull/3702)

### Fixed

Expand Down
1 change: 0 additions & 1 deletion internal/dataplane/parser/translate_errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import "errors"

var (
errRouteValidationNoRules = errors.New("no rules provided")
errRouteValidationMissingBackendRefs = errors.New("missing backendRef in rule")
errRouteValidationQueryParamMatchesUnsupported = errors.New("query param matches are not yet supported")
errRouteValidationNoMatchRulesOrHostnamesSpecified = errors.New("no match rules or hostnames specified")
)
216 changes: 156 additions & 60 deletions internal/dataplane/parser/translate_httproute.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package parser

import (
"fmt"
pathlib "path"
"strings"

"github.com/kong/go-kong/kong"
"github.com/samber/lo"
gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"

"github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/kongstate"
Expand Down Expand Up @@ -63,11 +65,6 @@ func validateHTTPRoute(httproute *gatewayv1beta1.HTTPRoute) error {
return errRouteValidationNoRules
}

for _, rule := range spec.Rules {
if len(rule.BackendRefs) == 0 {
return errRouteValidationMissingBackendRefs
}
}
return nil
}

Expand All @@ -88,11 +85,11 @@ func (p *Parser) ingressRulesFromHTTPRouteWithCombinedServiceRoutes(httproute *g

// generate the routes for the service and attach them to the service
for _, kongRouteTranslation := range kongServiceTranslation.KongRoutes {
route, err := generateKongRouteFromTranslation(httproute, kongRouteTranslation, p.flagEnabledRegexPathPrefix)
routes, err := generateKongRouteFromTranslation(httproute, kongRouteTranslation, p.flagEnabledRegexPathPrefix)
if err != nil {
return err
}
service.Routes = append(service.Routes, route)
service.Routes = append(service.Routes, routes...)
}

// cache the service to avoid duplicates in further loop iterations
Expand Down Expand Up @@ -172,9 +169,6 @@ func generateKongRoutesFromHTTPRouteRule(
// all matches to determine all the routes that will be needed for the services.
var routes []kongstate.Route

// generate kong plugins from rule.filters
plugins := generatePluginsFromHTTPRouteFilters(rule.Filters, tags)

if len(rule.Matches) > 0 {
for matchNumber := range rule.Matches {
// determine the name of the route, identify it as a route that belongs
Expand All @@ -187,12 +181,12 @@ func generateKongRoutesFromHTTPRouteRule(
matchNumber,
)

r, err := generateKongRouteFromHTTPRouteMatches(
r, err := generateKongRoutesFromHTTPRouteMatches(
routeName,
rule.Matches[matchNumber:matchNumber+1],
rule.Filters,
objectInfo,
hostnames,
plugins,
addRegexPrefix,
tags,
)
Expand All @@ -201,18 +195,23 @@ func generateKongRoutesFromHTTPRouteRule(
}

// add the route to the list of routes for the service(s)
routes = append(routes, r)
routes = append(routes, r...)
}
} else {
routeName := fmt.Sprintf("httproute.%s.%s.0.0", httproute.Namespace, httproute.Name)
r, err := generateKongRouteFromHTTPRouteMatches(routeName, rule.Matches, objectInfo, hostnames, plugins,
addRegexPrefix, tags)
r, err := generateKongRoutesFromHTTPRouteMatches(routeName,
rule.Matches,
rule.Filters,
objectInfo,
hostnames,
addRegexPrefix,
tags)
if err != nil {
return nil, err
}

// add the route to the list of routes for the service(s)
routes = append(routes, r)
routes = append(routes, r...)
}

return routes, nil
Expand All @@ -222,39 +221,36 @@ func generateKongRouteFromTranslation(
httproute *gatewayv1beta1.HTTPRoute,
translation translators.KongRouteTranslation,
addRegexPrefix bool,
) (kongstate.Route, error) {
) ([]kongstate.Route, error) {
// gather the k8s object information and hostnames from the httproute
objectInfo := util.FromK8sObject(httproute)
tags := util.GenerateTagsForObject(httproute)

// get the hostnames from the HTTPRoute
hostnames := getHTTPRouteHostnamesAsSliceOfStringPointers(httproute)

// generate kong plugins from rule.filters
plugins := generatePluginsFromHTTPRouteFilters(translation.Filters, tags)

return generateKongRouteFromHTTPRouteMatches(
return generateKongRoutesFromHTTPRouteMatches(
translation.Name,
translation.Matches,
translation.Filters,
objectInfo,
hostnames,
plugins,
addRegexPrefix,
tags,
)
}

// generateKongRouteFromHTTPRouteMatches converts an HTTPRouteMatches to a Kong Route object.
// generateKongRoutesFromHTTPRouteMatches converts an HTTPRouteMatches to a slice of Kong Route objects.
// This function assumes that the HTTPRouteMatches share the query params, headers and methods.
func generateKongRouteFromHTTPRouteMatches(
func generateKongRoutesFromHTTPRouteMatches(
routeName string,
matches []gatewayv1beta1.HTTPRouteMatch,
filters []gatewayv1beta1.HTTPRouteFilter,
ingressObjectInfo util.K8sObjectInfo,
hostnames []*string,
plugins []kong.Plugin,
addRegexPrefix bool,
tags []*string,
) (kongstate.Route, error) {
) ([]kongstate.Route, error) {
if len(matches) == 0 {
// it's acceptable for an HTTPRoute to have no matches in the rulesets,
// but only backends as long as there are hostnames. In this case, we
Expand All @@ -273,21 +269,18 @@ func generateKongRouteFromHTTPRouteMatches(
// however in this case there must actually be some present hostnames
// configured for the HTTPRoute or else it's not valid.
if len(hostnames) == 0 {
return kongstate.Route{}, errRouteValidationNoMatchRulesOrHostnamesSpecified
return []kongstate.Route{}, errRouteValidationNoMatchRulesOrHostnamesSpecified
}

// otherwise apply the hostnames to the route
r.Hosts = append(r.Hosts, hostnames...)

// attach the plugins to be applied to the given route
r.Plugins = append(r.Plugins, plugins...)

return r, nil
return []kongstate.Route{r}, nil
}

// TODO: implement query param matches (https://github.com/Kong/kubernetes-ingress-controller/issues/2778)
if len(matches[0].QueryParams) > 0 {
return kongstate.Route{}, errRouteValidationQueryParamMatchesUnsupported
return []kongstate.Route{}, errRouteValidationQueryParamMatchesUnsupported
}

r := generateKongstateHTTPRoute(routeName, ingressObjectInfo, hostnames)
Expand All @@ -296,7 +289,7 @@ func generateKongRouteFromHTTPRouteMatches(
// convert header matching from HTTPRoute to Route format
headers, err := convertGatewayMatchHeadersToKongRouteMatchHeaders(matches[0].Headers)
if err != nil {
return kongstate.Route{}, err
return []kongstate.Route{}, err
}
if len(headers) > 0 {
r.Route.Headers = headers
Expand All @@ -305,40 +298,90 @@ func generateKongRouteFromHTTPRouteMatches(
// stripPath needs to be disabled by default to be conformant with the Gateway API
r.StripPath = kong.Bool(false)

// attach the plugins to be applied to the given route
if len(plugins) != 0 {
if r.Plugins == nil {
r.Plugins = make([]kong.Plugin, 0, len(plugins))
}
_, hasRedirectFilter := lo.Find(filters, func(filter gatewayv1beta1.HTTPRouteFilter) bool {
return filter.Type == gatewayv1beta1.HTTPRouteFilterRequestRedirect
})

routes := getRoutesFromMatches(matches, &r, filters, tags, hasRedirectFilter, addRegexPrefix)

// if the redirect filter has not been set, we still need to set the route plugins
if !hasRedirectFilter {
plugins := generatePluginsFromHTTPRouteFilters(filters, "", tags)
r.Plugins = append(r.Plugins, plugins...)
routes = []kongstate.Route{r}
}

return routes, nil
}

// getRoutesFromMatches converts all the httpRoute matches to the proper set of kong routes.
func getRoutesFromMatches(matches []gatewayv1beta1.HTTPRouteMatch,
route *kongstate.Route,
filters []gatewayv1beta1.HTTPRouteFilter,
tags []*string,
hasRedirectFilter bool,
addRegexPrefix bool,
) []kongstate.Route {
seenMethods := make(map[string]struct{})
routes := make([]kongstate.Route, 0)

for _, match := range matches {
// configure path matching information about the route if paths matching was defined
// Kong automatically infers whether or not a path is a regular expression and uses a prefix match by
// default if it is not. For those types, we use the path value as-is and let Kong determine the type.
// For exact matches, we transform the path into a regular expression that terminates after the value
if match.Path != nil {
paths := generateKongRoutePathFromHTTPRouteMatch(match, addRegexPrefix)
for _, p := range paths {
r.Route.Paths = append(r.Route.Paths, kong.String(p))
// if the rule specifies the redirectFilter, we cannot put all the paths under the same route,
// as the kong plugin needs to know the exact path to use to perform redirection.
if hasRedirectFilter {
matchRoute := route
// configure path matching information about the route if paths matching was defined
// Kong automatically infers whether or not a path is a regular expression and uses a prefix match by
// default if it is not. For those types, we use the path value as-is and let Kong determine the type.
// For exact matches, we transform the path into a regular expression that terminates after the value
if match.Path != nil {
paths := generateKongRoutePathFromHTTPRouteMatch(match, addRegexPrefix)
for _, p := range paths {
matchRoute.Route.Paths = append(matchRoute.Route.Paths, kong.String(p))
}
}
}

// configure method matching information about the route if method
// matching was defined.
if match.Method != nil {
method := string(*match.Method)
if _, ok := seenMethods[method]; !ok {
r.Route.Methods = append(r.Route.Methods, kong.String(string(*match.Method)))
seenMethods[method] = struct{}{}
// configure method matching information about the route if method
// matching was defined.
if match.Method != nil {
method := string(*match.Method)
if _, ok := seenMethods[method]; !ok {
matchRoute.Route.Methods = append(matchRoute.Route.Methods, kong.String(string(*match.Method)))
seenMethods[method] = struct{}{}
}
}
path := ""
if match.Path.Value != nil {
path = *match.Path.Value
}

// generate kong plugins from rule.filters
plugins := generatePluginsFromHTTPRouteFilters(filters, path, tags)
matchRoute.Plugins = append(matchRoute.Plugins, plugins...)

routes = append(routes, *route)
} else {
// configure path matching information about the route if paths matching was defined
// Kong automatically infers whether or not a path is a regular expression and uses a prefix match by
// default if it is not. For those types, we use the path value as-is and let Kong determine the type.
// For exact matches, we transform the path into a regular expression that terminates after the value
if match.Path != nil {
paths := generateKongRoutePathFromHTTPRouteMatch(match, addRegexPrefix)
for _, p := range paths {
route.Route.Paths = append(route.Route.Paths, kong.String(p))
}
}

if match.Method != nil {
method := string(*match.Method)
if _, ok := seenMethods[method]; !ok {
route.Route.Methods = append(route.Route.Methods, kong.String(string(*match.Method)))
seenMethods[method] = struct{}{}
}
}
}
}

return r, nil
return routes
}

func generateKongRoutePathFromHTTPRouteMatch(match gatewayv1beta1.HTTPRouteMatch, addRegexPrefix bool) []string {
Expand Down Expand Up @@ -392,26 +435,79 @@ func generateKongstateHTTPRoute(routeName string, ingressObjectInfo util.K8sObje
return r
}

// generatePluginsFromHTTPRouteFilters converts HTTPRouteFilter into Kong filters.
func generatePluginsFromHTTPRouteFilters(filters []gatewayv1beta1.HTTPRouteFilter, tags []*string) []kong.Plugin {
// generatePluginsFromHTTPRouteFilters converts HTTPRouteFilter into Kong plugins.
// path is the parameter to be used by the redirect plugin, to perform redirection.
func generatePluginsFromHTTPRouteFilters(filters []gatewayv1beta1.HTTPRouteFilter, path string, tags []*string) []kong.Plugin {
kongPlugins := make([]kong.Plugin, 0)
if len(filters) == 0 {
return kongPlugins
}

for _, filter := range filters {
if filter.Type == gatewayv1beta1.HTTPRouteFilterRequestHeaderModifier {
switch filter.Type {
case gatewayv1beta1.HTTPRouteFilterRequestHeaderModifier:
kongPlugins = append(kongPlugins, generateRequestHeaderModifierKongPlugin(filter.RequestHeaderModifier))

case gatewayv1beta1.HTTPRouteFilterRequestRedirect:
kongPlugins = append(kongPlugins, generateRequestRedirectKongPlugin(filter.RequestRedirect, path)...)

case gatewayv1beta1.HTTPRouteFilterExtensionRef,
gatewayv1beta1.HTTPRouteFilterRequestMirror,
gatewayv1beta1.HTTPRouteFilterResponseHeaderModifier,
gatewayv1beta1.HTTPRouteFilterURLRewrite:
// not supported
}
// TODO: https://github.com/Kong/kubernetes-ingress-controller/issues/2793
}
for _, p := range kongPlugins {
// This plugin is derived from an HTTPRoute filter, not a KongPlugin, so we apply tags indicating that
// HTTPRoute as the parent Kubernetes resource for these generated plugins.
p.Tags = tags
}

return kongPlugins
}

func generateRequestRedirectKongPlugin(modifier *gatewayv1beta1.HTTPRequestRedirectFilter, path string) []kong.Plugin {
plugins := make([]kong.Plugin, 2)
plugins[0] = kong.Plugin{
Name: kong.String("request-termination"),
Config: kong.Configuration{
"status_code": modifier.StatusCode,
},
}

var locationHeader string
scheme := "http"
port := 80

if modifier.Scheme != nil {
scheme = *modifier.Scheme
}
if modifier.Port != nil {
port = int(*modifier.Port)
}
if modifier.Path != nil && modifier.Path.Type == gatewayv1beta1.FullPathHTTPPathModifier && modifier.Path.ReplaceFullPath != nil {
// only ReplaceFullPath currently supported
path = *modifier.Path.ReplaceFullPath
}
if modifier.Hostname != nil {
locationHeader = fmt.Sprintf("Location: %s://%s", scheme, pathlib.Join(fmt.Sprintf("%s:%d", *modifier.Hostname, port), path))
} else {
locationHeader = fmt.Sprintf("Location: %s", path)
}

plugins[1] = kong.Plugin{
Name: kong.String("response-transformer"),
Config: kong.Configuration{
"add": map[string][]string{
"headers": {locationHeader},
},
},
}

return plugins
}

// generateRequestHeaderModifierKongPlugin converts a gatewayv1beta1.HTTPRequestHeaderFilter into a
// kong.Plugin of type request-transformer.
func generateRequestHeaderModifierKongPlugin(modifier *gatewayv1beta1.HTTPHeaderFilter) kong.Plugin {
Expand Down

0 comments on commit f628552

Please sign in to comment.