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: translate HTTPRoute to expression based routes #3956

Merged
merged 3 commits into from
May 10, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 2 additions & 0 deletions internal/dataplane/parser/translate_errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package parser

import "errors"

// REVIEW: use error variables in translator package instead?
randmonkey marked this conversation as resolved.
Show resolved Hide resolved

var (
errRouteValidationNoRules = errors.New("no rules provided")
errRouteValidationQueryParamMatchesUnsupported = errors.New("query param matches are not yet supported")
Expand Down
152 changes: 29 additions & 123 deletions internal/dataplane/parser/translate_httproute.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package parser

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

"github.com/kong/go-kong/kong"
Expand Down Expand Up @@ -85,7 +84,7 @@ func (p *Parser) ingressRulesFromHTTPRouteWithCombinedServiceRoutes(httproute *g

// generate the routes for the service and attach them to the service
for _, kongRouteTranslation := range kongServiceTranslation.KongRoutes {
routes, err := generateKongRouteFromTranslation(httproute, kongRouteTranslation, p.featureFlags.RegexPathPrefix)
routes, err := generateKongRouteFromTranslation(httproute, kongRouteTranslation, p.featureFlags.RegexPathPrefix, p.featureFlags.ExpressionRoutes)
if err != nil {
return err
}
Expand Down Expand Up @@ -134,6 +133,17 @@ func (p *Parser) ingressRulesFromHTTPRouteLegacyFallback(httproute *gatewayv1bet
// Translate HTTPRoute - Utils
// -----------------------------------------------------------------------------

// getHTTPRouteHostnamesAsSliceOfStrings translates the hostnames defined in an
// HTTPRoute specification into a []*string slice, which is the type required by translating to matchers
// in expression based routes.
func getHTTPRouteHostnamesAsSliceOfStrings(httproute *gatewayv1beta1.HTTPRoute) []string {
hostnames := make([]string, 0, len(httproute.Spec.Hostnames))
for _, hostname := range httproute.Spec.Hostnames {
hostnames = append(hostnames, string(hostname))
}
return hostnames
randmonkey marked this conversation as resolved.
Show resolved Hide resolved
}

// getHTTPRouteHostnamesAsSliceOfStringPointers translates the hostnames defined
// in an HTTPRoute specification into a []*string slice, which is the type required
// by kong.Route{}.
Expand Down Expand Up @@ -221,11 +231,26 @@ func generateKongRouteFromTranslation(
httproute *gatewayv1beta1.HTTPRoute,
translation translators.KongRouteTranslation,
addRegexPrefix bool,
expressionRoutes bool,
) ([]kongstate.Route, error) {
// gather the k8s object information and hostnames from the httproute
objectInfo := util.FromK8sObject(httproute)
tags := util.GenerateTagsForObject(httproute)

// translate to expression based routes when expressionRoutes is enabled.
if expressionRoutes {
// get the hostnames from the HTTPRoute
hostnames := getHTTPRouteHostnamesAsSliceOfStrings(httproute)
return translators.GenerateKongExpressionRoutesFromHTTPRouteMatches(
translation.Name,
translation.Matches,
translation.Filters,
randmonkey marked this conversation as resolved.
Show resolved Hide resolved
objectInfo,
hostnames,
tags,
)
}

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

Expand Down Expand Up @@ -306,7 +331,7 @@ func generateKongRoutesFromHTTPRouteMatches(

// if the redirect filter has not been set, we still need to set the route plugins
if !hasRedirectFilter {
plugins := generatePluginsFromHTTPRouteFilters(filters, "", tags)
plugins := translators.GeneratePluginsFromHTTPRouteFilters(filters, "", tags)
r.Plugins = append(r.Plugins, plugins...)
routes = []kongstate.Route{r}
}
Expand Down Expand Up @@ -356,7 +381,7 @@ func getRoutesFromMatches(matches []gatewayv1beta1.HTTPRouteMatch,
}

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

routes = append(routes, *route)
Expand Down Expand Up @@ -435,125 +460,6 @@ func generateKongstateHTTPRoute(routeName string, ingressObjectInfo util.K8sObje
return r
}

// 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 {
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
}
}
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 {
plugin := kong.Plugin{
Name: kong.String("request-transformer"),
Config: make(kong.Configuration),
}

// modifier.Set is converted to a pair composed of "replace" and "add"
if modifier.Set != nil {
setModifiers := make([]string, 0, len(modifier.Set))
for _, s := range modifier.Set {
setModifiers = append(setModifiers, kongHeaderFormatter(s))
}
plugin.Config["replace"] = map[string][]string{
"headers": setModifiers,
}
plugin.Config["add"] = map[string][]string{
"headers": setModifiers,
}
}

// modifier.Add is converted to "append"
if modifier.Add != nil {
appendModifiers := make([]string, 0, len(modifier.Add))
for _, a := range modifier.Add {
appendModifiers = append(appendModifiers, kongHeaderFormatter(a))
}
plugin.Config["append"] = map[string][]string{
"headers": appendModifiers,
}
}

if modifier.Remove != nil {
plugin.Config["remove"] = map[string][]string{
"headers": modifier.Remove,
}
}

return plugin
}

func kongHeaderFormatter(header gatewayv1beta1.HTTPHeader) string {
return fmt.Sprintf("%s:%s", header.Name, header.Value)
}

func httpBackendRefsToBackendRefs(httpBackendRef []gatewayv1beta1.HTTPBackendRef) []gatewayv1beta1.BackendRef {
backendRefs := make([]gatewayv1beta1.BackendRef, 0, len(httpBackendRef))

Expand Down
92 changes: 0 additions & 92 deletions internal/dataplane/parser/translate_httproute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1352,98 +1352,6 @@ func TestGetHTTPRouteHostnamesAsSliceOfStringPointers(t *testing.T) {
}
}

func TestGeneratePluginsFromHTTPRouteFilters(t *testing.T) {
testCases := []struct {
name string
filters []gatewayv1beta1.HTTPRouteFilter
path string
expectedPlugins []kong.Plugin
}{
{
name: "no filters",
filters: []gatewayv1beta1.HTTPRouteFilter{},
expectedPlugins: []kong.Plugin{},
},
{
name: "request header modifier",
filters: []gatewayv1beta1.HTTPRouteFilter{
{
Type: gatewayv1beta1.HTTPRouteFilterRequestHeaderModifier,
RequestHeaderModifier: &gatewayv1beta1.HTTPHeaderFilter{
Set: []gatewayv1beta1.HTTPHeader{
{
Name: "header-to-set",
Value: "bar",
},
},
Remove: []string{"header-to-remove"},
},
},
},
expectedPlugins: []kong.Plugin{
{
Name: kong.String("request-transformer"),
Config: kong.Configuration{
"add": map[string][]string{
"headers": {
"header-to-set:bar",
},
},
"remove": map[string][]string{
"headers": {
"header-to-remove",
},
},
"replace": map[string][]string{
"headers": {
"header-to-set:bar",
},
},
},
},
},
},
{
name: "request redirect modifier",
filters: []gatewayv1beta1.HTTPRouteFilter{
{
Type: gatewayv1beta1.HTTPRouteFilterRequestRedirect,
RequestRedirect: &gatewayv1beta1.HTTPRequestRedirectFilter{
Hostname: (*gatewayv1beta1.PreciseHostname)(lo.ToPtr("example.org")),
StatusCode: lo.ToPtr(302),
},
},
},
path: "/test",
expectedPlugins: []kong.Plugin{
{
Name: kong.String("request-termination"),
Config: kong.Configuration{
"status_code": lo.ToPtr(302),
},
},
{
Name: kong.String("response-transformer"),
Config: kong.Configuration{
"add": map[string][]string{
"headers": {
"Location: http://example.org:80/test",
},
},
},
},
},
},
}

for _, tc := range testCases {
plugins := generatePluginsFromHTTPRouteFilters(tc.filters, tc.path, nil)
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expectedPlugins, plugins)
})
}
}

func TestIngressRulesFromHTTPRoutes_RegexPrefix(t *testing.T) {
fakestore, err := store.NewFakeStore(store.FakeObjects{})
require.NoError(t, err)
Expand Down
Loading