Skip to content

Commit

Permalink
WIP translate HTTPRoute to expression based routes
Browse files Browse the repository at this point in the history
  • Loading branch information
randmonkey committed May 5, 2023
1 parent ed05179 commit f360323
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 14 deletions.
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?

var (
errRouteValidationNoRules = errors.New("no rules provided")
errRouteValidationQueryParamMatchesUnsupported = errors.New("query param matches are not yet supported")
Expand Down
31 changes: 29 additions & 2 deletions internal/dataplane/parser/translate_httproute.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,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.flagEnabledRegexPathPrefix)
routes, err := generateKongRouteFromTranslation(httproute, kongRouteTranslation, p.flagEnabledRegexPathPrefix, p.featureEnabledExpressionRoutes)
if err != nil {
return err
}
Expand All @@ -108,7 +108,7 @@ func (p *Parser) ingressRulesFromHTTPRouteLegacyFallback(httproute *gatewayv1bet
// traffic, so we make separate routes and Kong services for every present rule.
for ruleNumber, rule := range httproute.Spec.Rules {
// determine the routes needed to route traffic to services for this rule
routes, err := generateKongRoutesFromHTTPRouteRule(httproute, ruleNumber, rule, p.flagEnabledRegexPathPrefix)
routes, err := generateKongRoutesFromHTTPRouteRule(httproute, ruleNumber, rule, p.flagEnabledRegexPathPrefix, p.featureEnabledExpressionRoutes)
if err != nil {
return err
}
Expand All @@ -134,6 +134,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
}

// getHTTPRouteHostnamesAsSliceOfStringPointers translates the hostnames defined
// in an HTTPRoute specification into a []*string slice, which is the type required
// by kong.Route{}.
Expand All @@ -157,6 +168,7 @@ func generateKongRoutesFromHTTPRouteRule(
ruleNumber int,
rule gatewayv1beta1.HTTPRouteRule,
addRegexPrefix bool,
expressionRoutes bool,
) ([]kongstate.Route, error) {
// gather the k8s object information and hostnames from the httproute
objectInfo := util.FromK8sObject(httproute)
Expand Down Expand Up @@ -221,11 +233,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,
objectInfo,
hostnames,
tags,
)
}

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

Expand Down
165 changes: 165 additions & 0 deletions internal/dataplane/parser/translators/httproute_atc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package translators

import (
"sort"
"strings"

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

"github.com/kong/kubernetes-ingress-controller/v2/internal/annotations"
"github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/kongstate"
"github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/parser/atc"
"github.com/kong/kubernetes-ingress-controller/v2/internal/util"
)

// GenerateKongExpressionRoutesFromHTTPRouteMatches generates Kong routes from HTTPRouteRule
// pointing to a specific backend.
func GenerateKongExpressionRoutesFromHTTPRouteMatches(
routeName string,
matches []gatewayv1beta1.HTTPRouteMatch,
filters []gatewayv1beta1.HTTPRouteFilter,
ingressObjectInfo util.K8sObjectInfo,
hostnames []string,
tags []*string,
) ([]kongstate.Route, error) {
// initialize the route with route name, preserve_host, and tags.
r := kongstate.Route{
Ingress: ingressObjectInfo,
Route: kong.Route{
Name: kong.String(routeName),
PreserveHost: kong.Bool(true),
Tags: tags,
},
ExpressionRoutes: true,
}

if len(matches) == 0 {
if len(hostnames) == 0 {
return []kongstate.Route{}, ErrRouteValidationNoMatchRulesOrHostnamesSpecified
}

hostMatcher := hostMatcherFromHosts(hostnames)
atc.ApplyExpression(&r.Route, hostMatcher, 1)
return []kongstate.Route{r}, nil
}

// TODO: generate multiple routes if required by filters or priority specifications.

routeMatcher := atc.And()
matchers := generateMatchersFromHTTPRouteMatches(matches)
routeMatcher = atc.And(atc.Or(matchers...))

// append matcher from hosts.
if len(hostnames) > 0 {
hostMatcher := hostMatcherFromHosts(hostnames)
routeMatcher = atc.And(routeMatcher, hostMatcher)
}

// override protocols and TLS SNIs from annotations.
// translate protocols.
protocols := []string{"http", "https"}
annonationProtocols := annotations.ExtractProtocolNames(ingressObjectInfo.Annotations)
if len(annonationProtocols) > 0 {
protocols = annonationProtocols
}
protocolMatcher := protocolMatcherFromProtocols(protocols)
routeMatcher.And(protocolMatcher)

// translate SNIs.
snis, exist := annotations.ExtractSNIs(ingressObjectInfo.Annotations)
if exist && len(snis) > 0 {
sniMatcher := sniMatcherFromSNIs(snis)
routeMatcher.And(sniMatcher)
}

atc.ApplyExpression(&r.Route, routeMatcher, 1)
return []kongstate.Route{r}, nil
}

func generateMatchersFromHTTPRouteMatches(matches []gatewayv1beta1.HTTPRouteMatch) []atc.Matcher {
ret := make([]atc.Matcher, 0, len(matches))
for _, match := range matches {
matcher := generateMatcherFromHTTPRouteMatch(match)
ret = append(ret, matcher)
}
return ret
}

func generateMatcherFromHTTPRouteMatch(match gatewayv1beta1.HTTPRouteMatch) atc.Matcher {
var matcher atc.Matcher

if match.Path != nil {
pathMatcher := pathMatcherFromHTTPPathMatch(match.Path)
matcher = atc.And(matcher, pathMatcher)
}

if len(match.Headers) > 0 {
headerMatcher := headerMatcherFromHTTPHeaderMatches(match.Headers)
matcher = atc.And(matcher, headerMatcher)
}

if match.Method != nil {
method := *match.Method
methodMatcher := methodMatcherFromMethods([]string{string(method)})
matcher = atc.And(matcher, methodMatcher)
}
return matcher
}

func appendSuffixSlashIfNotExist(path string) string {
if !strings.HasSuffix(path, "/") {
return path + "/"
}
return path
}

func appendRegexBeginIfNotExist(regex string) string {
if !strings.HasPrefix(regex, "^") {
return "^" + regex
}
return regex
}

func pathMatcherFromHTTPPathMatch(pathMatch *gatewayv1beta1.HTTPPathMatch) atc.Matcher {
path := ""
if pathMatch.Value != nil {
path = *pathMatch.Value
}
switch *pathMatch.Type {
case gatewayv1beta1.PathMatchExact:
return atc.NewPredicateHTTPPath(atc.OpEqual, path)
case gatewayv1beta1.PathMatchPathPrefix:
return atc.Or(
atc.NewPredicateHTTPPath(atc.OpEqual, path),
atc.NewPredicateHTTPPath(atc.OpPrefixMatch, appendSuffixSlashIfNotExist(path)),
)
case gatewayv1beta1.PathMatchRegularExpression:
return atc.NewPredicateHTTPPath(atc.OpRegexMatch, appendRegexBeginIfNotExist(path))
}

return nil // should be unreachable
}

func headerMatcherFromHTTPHeaderMatches(headerMatches []gatewayv1beta1.HTTPHeaderMatch) atc.Matcher {
// sort headerMatches by names to generate a stable output.
sort.Slice(headerMatches, func(i, j int) bool {
return string(headerMatches[i].Name) < string(headerMatches[j].Name)
})

matchers := make([]atc.Matcher, 0, len(headerMatches))
for _, headerMatch := range headerMatches {
matchType := gatewayv1beta1.HeaderMatchExact
if headerMatch.Type != nil {
matchType = *headerMatch.Type
}
headerKey := strings.ReplaceAll(strings.ToLower(string(headerMatch.Name)), "-", "_")
switch matchType {
case gatewayv1beta1.HeaderMatchExact:
matchers = append(matchers, atc.NewPredicateHTTPHeader(headerKey, atc.OpEqual, headerMatch.Value))
case gatewayv1beta1.HeaderMatchRegularExpression:
matchers = append(matchers, atc.NewPredicateHTTPHeader(headerKey, atc.OpRegexMatch, headerMatch.Value))
}
}
return atc.And(matchers...)
}
6 changes: 3 additions & 3 deletions internal/dataplane/parser/translators/ingress_atc.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func (m *ingressTranslationMeta) translateIntoKongExpressionRoutes() *kongstate.
hostAliases, _ := annotations.ExtractHostAliases(ingressAnnotations)
hosts = append(hosts, hostAliases...)
if len(hosts) > 0 {
hostMatcher := hostMatcherFromIngressHosts(hosts)
hostMatcher := hostMatcherFromHosts(hosts)
routeMatcher.And(hostMatcher)
}

Expand Down Expand Up @@ -116,8 +116,8 @@ func (m *ingressTranslationMeta) translateIntoKongExpressionRoutes() *kongstate.
return route
}

// hostMatcherFromIngressHosts translates hosts in IngressHost format to ATC matcher that matches any of them.
func hostMatcherFromIngressHosts(hosts []string) atc.Matcher {
// hostMatcherFromHosts translates hosts in IngressHost format to ATC matcher that matches any of them.
func hostMatcherFromHosts(hosts []string) atc.Matcher {
matchers := make([]atc.Matcher, 0, len(hosts))
for _, host := range hosts {
if !validHosts.MatchString(host) {
Expand Down
4 changes: 2 additions & 2 deletions internal/dataplane/parser/translators/ingress_atc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ func TestHeaderMatcherFromHeaders(t *testing.T) {
}
}

func TestHostMatcherFromIngressHosts(t *testing.T) {
func TestHostMatcherFromHosts(t *testing.T) {
testCases := []struct {
name string
hosts []string
Expand Down Expand Up @@ -352,7 +352,7 @@ func TestHostMatcherFromIngressHosts(t *testing.T) {
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
matcher := hostMatcherFromIngressHosts(tc.hosts)
matcher := hostMatcherFromHosts(tc.hosts)
require.Equal(t, tc.expression, matcher.Expression())
})
}
Expand Down
9 changes: 9 additions & 0 deletions internal/dataplane/parser/translators/translator_errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package translators

import "errors"

var (
ErrRouteValidationNoRules = errors.New("no rules provided")
ErrRouteValidationQueryParamMatchesUnsupported = errors.New("query param matches are not yet supported")
ErrRouteValidationNoMatchRulesOrHostnamesSpecified = errors.New("no match rules or hostnames specified")
)
1 change: 0 additions & 1 deletion test/integration/examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import (
const examplesDIR = "../../examples"

func TestHTTPRouteExample(t *testing.T) {
skipTestForExpressionRouter(t)
var (
httprouteExampleManifests = fmt.Sprintf("%s/gateway-httproute.yaml", examplesDIR)
ctx = context.Background()
Expand Down
5 changes: 0 additions & 5 deletions test/integration/httproute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,6 @@ import (
var emptyHeaderSet = make(map[string]string)

func TestHTTPRouteEssentials(t *testing.T) {
// TODO: implement translator for HTTPRoutes:
// https:/Kong/kubernetes-ingress-controller/issues/3751
skipTestForExpressionRouter(t)
ctx := context.Background()

ns, cleaner := helpers.Setup(ctx, t, env)
Expand Down Expand Up @@ -311,7 +308,6 @@ func TestHTTPRouteEssentials(t *testing.T) {
}

func TestHTTPRouteMultipleServices(t *testing.T) {
skipTestForExpressionRouter(t)
ctx := context.Background()

ns, cleaner := helpers.Setup(ctx, t, env)
Expand Down Expand Up @@ -499,7 +495,6 @@ func TestHTTPRouteMultipleServices(t *testing.T) {
}

func TestHTTPRouteFilterHosts(t *testing.T) {
skipTestForExpressionRouter(t)
ctx := context.Background()

ns, cleaner := helpers.Setup(ctx, t, env)
Expand Down
1 change: 0 additions & 1 deletion test/integration/httproute_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
)

func TestHTTPRouteValidationWebhook(t *testing.T) {
skipTestForExpressionRouter(t)
ctx := context.Background()

ns, cleaner := helpers.Setup(ctx, t, env)
Expand Down

0 comments on commit f360323

Please sign in to comment.