Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Gateway API RequestRedirect filter translated to combination of kong plugins #3702

Merged
merged 3 commits into from
Mar 10, 2023
Merged
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 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 {
mlavacca marked this conversation as resolved.
Show resolved Hide resolved
// 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
mlavacca marked this conversation as resolved.
Show resolved Hide resolved
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
Loading