Skip to content

Commit

Permalink
feat: translate grpcroute to expression based routes
Browse files Browse the repository at this point in the history
  • Loading branch information
randmonkey committed May 11, 2023
1 parent ecd562c commit 871493d
Show file tree
Hide file tree
Showing 12 changed files with 553 additions and 73 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/_integration_tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
router-flavor: 'traditional_compatible'
- name: dbless-expression-router
test: dbless
feature_gates: "ExpressionRoutes=true"
feature_gates: "ExpressionRoutes=true,GatewayAlpha=true"
router-flavor: "expressions"
# TODO: remove this once CombinedServices is enabled by default.
# https://github.com/Kong/kubernetes-ingress-controller/issues/3979
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ Adding a new version? You'll need three changes:
It will become the default behavior in the next minor release with the possibility
to opt-out.
[#3963](https://github.com/Kong/kubernetes-ingress-controller/pull/3963)
- Added translator to translate `HTTPRoute` and `GRPCRoute` in gateway APIs to
expression based kong routes. Similar to ingresses, this translator is only
enabled when feature gate `ExpressionRoutes` is turned on and the managed
Kong gateway runs in router flavor `expressions`.
[#3956](https://github.com/Kong/kubernetes-ingress-controller/pull/3956)
[#3988](https://github.com/Kong/kubernetes-ingress-controller/pull/3988)

### Fixed

Expand Down
8 changes: 7 additions & 1 deletion internal/dataplane/parser/translate_grpcroute.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,13 @@ func (p *Parser) ingressRulesFromGRPCRoute(result *ingressRules, grpcroute *gate
// traffic, so we make separate routes and Kong services for every present rule.
for ruleNumber, rule := range spec.Rules {
// determine the routes needed to route traffic to services for this rule
routes := generateKongRoutesFromGRPCRouteRule(grpcroute, ruleNumber, rule)
var routes []kongstate.Route
if p.featureFlags.ExpressionRoutes {
routes = translators.GenerateKongExpressionRoutesFromGRPCRouteRule(grpcroute, ruleNumber, rule)
} else {
// REVIEW: move generateKongRoutesFromGRPCRouteRule to package translators?
routes = generateKongRoutesFromGRPCRouteRule(grpcroute, ruleNumber, rule)
}

// create a service and attach the routes to it
service, err := generateKongServiceFromBackendRefWithRuleNumber(p.logger, p.storer, result, grpcroute, ruleNumber, "grpcs", grpcBackendRefsToBackendRefs(rule.BackendRefs)...)
Expand Down
34 changes: 34 additions & 0 deletions internal/dataplane/parser/translators/atc_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package translators

import (
"strings"

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

// -----------------------------------------------------------------------------
// Translator - common functions in translating expression(ATC) routes from multiple kinds of k8s objects.
// -----------------------------------------------------------------------------

// hostMatcherFromHosts translates hosts to ATC matcher that matches any of them.
// used in translating hostname matches in ingresses, HTTPRoutes, GRPCRoutes.
// the hostname format includes:
// - wildcard hosts, starting with exactly one *
// - precise hosts, otherwise.
func hostMatcherFromHosts(hosts []string) atc.Matcher {
matchers := make([]atc.Matcher, 0, len(hosts))
for _, host := range hosts {
if !validHosts.MatchString(host) {
continue
}

if strings.HasPrefix(host, "*") {
// wildcard match on hosts (like *.foo.com), genreate a suffix match.
matchers = append(matchers, atc.NewPrediacteHTTPHost(atc.OpSuffixMatch, strings.TrimPrefix(host, "*")))
} else {
// exact match on hosts, generate an exact match.
matchers = append(matchers, atc.NewPrediacteHTTPHost(atc.OpEqual, host))
}
}
return atc.Or(matchers...)
}
44 changes: 44 additions & 0 deletions internal/dataplane/parser/translators/atc_utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package translators

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestHostMatcherFromHosts(t *testing.T) {
testCases := []struct {
name string
hosts []string
expression string
}{
{
name: "simple exact host",
hosts: []string{"a.example.com"},
expression: `http.host == "a.example.com"`,
},
{
name: "single wildcard host",
hosts: []string{"*.example.com"},
expression: `http.host =^ ".example.com"`,
},
{
name: "multiple hosts with mixture of exact and wildcard",
hosts: []string{"foo.com", "*.bar.com"},
expression: `(http.host == "foo.com") || (http.host =^ ".bar.com")`,
},
{
name: "multiple hosts including invalid host",
hosts: []string{"foo.com", "a..bar.com"},
expression: `http.host == "foo.com"`,
},
}

for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
matcher := hostMatcherFromHosts(tc.hosts)
require.Equal(t, tc.expression, matcher.Expression())
})
}
}
162 changes: 162 additions & 0 deletions internal/dataplane/parser/translators/grpcroute_atc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package translators

import (
"fmt"
"sort"

"github.com/kong/go-kong/kong"
"github.com/samber/lo"
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
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"
)

func GenerateKongExpressionRoutesFromGRPCRouteRule(grpcroute *gatewayv1alpha2.GRPCRoute, ruleNumber int, rule gatewayv1alpha2.GRPCRouteRule) []kongstate.Route {
routes := make([]kongstate.Route, 0, len(rule.Matches))

// gather the k8s object information and hostnames from the grpcroute
ingressObjectInfo := util.FromK8sObject(grpcroute)

for matchNumber, match := range rule.Matches {
routeName := fmt.Sprintf(
"grpcroute.%s.%s.%d.%d",
grpcroute.Namespace,
grpcroute.Name,
ruleNumber,
matchNumber,
)

r := kongstate.Route{
Ingress: ingressObjectInfo,
Route: kong.Route{
Name: kong.String(routeName),
},
ExpressionRoutes: true,
}

hostnames := getGRPCRouteHostnamesAsSliceOfStrings(grpcroute)
matcher := generateMathcherFromGRPCMatch(match, hostnames, ingressObjectInfo.Annotations)

atc.ApplyExpression(&r.Route, matcher, 1)
routes = append(routes, r)
}

return routes
}

func generateMathcherFromGRPCMatch(match gatewayv1alpha2.GRPCRouteMatch, hostnames []string, metaAnnotations map[string]string) atc.Matcher {
routeMatcher := atc.And()

if match.Method != nil {
methodMatcher := methodMatcherFromGRPCMethodMatch(match.Method)
routeMatcher.And(methodMatcher)
}

if len(match.Headers) > 0 {
headerMatcher := headerMatcherFromGRPCHeaderMatches(match.Headers)
routeMatcher.And(headerMatcher)
}

if len(hostnames) > 0 {
hostMatcher := hostMatcherFromHosts(hostnames)
routeMatcher.And(hostMatcher)
}

protocols := annotations.ExtractProtocolNames(metaAnnotations)
if len(protocols) > 0 {
protocolMatcher := protocolMatcherFromProtocols(protocols)
routeMatcher.And(protocolMatcher)
}

snis, exist := annotations.ExtractSNIs(metaAnnotations)
if exist && len(snis) > 0 {
sniMatcher := sniMatcherFromSNIs(snis)
routeMatcher.And(sniMatcher)
}

return routeMatcher
}

// methodMatcherFromGRPCMethodMatch translates ONE GRPC method match in GRPCRoute to ATC matcher.
// REVIEW(naming): this function actually generates matcher to match HTTP path but not HTTP method. rename to pathMatcher...?
func methodMatcherFromGRPCMethodMatch(methodMatch *gatewayv1alpha2.GRPCMethodMatch) atc.Matcher {
matchType := gatewayv1alpha2.GRPCMethodMatchExact
if methodMatch.Type != nil {
matchType = *methodMatch.Type
}

switch matchType {
case gatewayv1alpha2.GRPCMethodMatchExact:
return methodMatcherFromGRPCExactMethodMatch(methodMatch.Service, methodMatch.Method)
case gatewayv1alpha2.GRPCMethodMatchRegularExpression:
return methodMatcherFromGRPCRegexMethodMatch(methodMatch.Service, methodMatch.Method)
}

return nil // should be unreachable
}

// methodMatcherFromGRPCExactMethodMatch translates exact GRPC method match to ATC matcher.
// reference: https://gateway-api.sigs.k8s.io/geps/gep-1016/?h=#method-matchers
func methodMatcherFromGRPCExactMethodMatch(service *string, method *string) atc.Matcher {
if service == nil && method == nil {
// should not happen, but we gernerate a catch-all matcher here.
return atc.NewPredicateHTTPPath(atc.OpPrefixMatch, "/")
}
if service != nil && method == nil {
// Prefix /${SERVICE}/
return atc.NewPredicateHTTPPath(atc.OpPrefixMatch, fmt.Sprintf("/%s/", *service))
}
if service == nil && method != nil {
// Suffix /${METHOD}
return atc.NewPredicateHTTPPath(atc.OpSuffixMatch, fmt.Sprintf("/%s", *method))
}
// service and method are both specified
return atc.NewPredicateHTTPPath(atc.OpEqual, fmt.Sprintf("/%s/%s", *service, *method))
}

// methodMatcherFromGRPCRegexMethodMatch translates regular expression GRPC method match to ATC matcher.
// reference: https://gateway-api.sigs.k8s.io/geps/gep-1016/?h=#type-regularexpression
func methodMatcherFromGRPCRegexMethodMatch(service *string, method *string) atc.Matcher {
if service == nil && method == nil {
return atc.NewPredicateHTTPPath(atc.OpPrefixMatch, "/")
}
// the regex to match service part and method part match any non-empty string if they are not specified.
serviceRegex := ".+"
methodRegex := ".+"

if service != nil {
serviceRegex = *service
}
if method != nil {
methodRegex = *method
}
return atc.NewPredicateHTTPPath(atc.OpRegexMatch, fmt.Sprintf("^/%s/%s", serviceRegex, methodRegex))
}

func headerMatcherFromGRPCHeaderMatches(headerMatches []gatewayv1alpha2.GRPCHeaderMatch) 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 {
httpHeaderMatch := gatewayv1beta1.HTTPHeaderMatch{
Type: headerMatch.Type,
Name: gatewayv1beta1.HTTPHeaderName(headerMatch.Name),
Value: headerMatch.Value,
}
matchers = append(matchers, headerMatcherFromHTTPHeaderMatch(httpHeaderMatch))
}
return atc.And(matchers...)
}

func getGRPCRouteHostnamesAsSliceOfStrings(grpcroute *gatewayv1alpha2.GRPCRoute) []string {
return lo.Map(grpcroute.Spec.Hostnames, func(h gatewayv1alpha2.Hostname, _ int) string {
return string(h)
})
}
Loading

0 comments on commit 871493d

Please sign in to comment.