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 95244b4
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 6 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
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.GenerateKongExpressionRoutesFromGRPCRoutes(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
153 changes: 153 additions & 0 deletions internal/dataplane/parser/translators/grpcroute_atc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package translators

import (
"fmt"
"sort"
"strings"

"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/dataplane/kongstate"
"github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/parser/atc"
"github.com/kong/kubernetes-ingress-controller/v2/internal/util"
)

func GenerateKongExpressionRoutesFromGRPCRoutes(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)
// TODO: override protocols/snis from annotations
atc.ApplyExpression(&r.Route, matcher, 1)
routes = append(routes, r)
}

return routes
}

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

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

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

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

return routeMatcher
}

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.NewPredicateHTTPMethod(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 headerMatcherFromGRPCHeaderMatch(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 {
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...)
}

func getGRPCRouteHostnamesAsSliceOfStrings(grpcroute *gatewayv1alpha2.GRPCRoute) []string {
return lo.Map(grpcroute.Spec.Hostnames, func(h gatewayv1alpha2.Hostname, _ int) string {
return string(h)
})
}
1 change: 0 additions & 1 deletion test/integration/examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,6 @@ func TestTLSRouteExample(t *testing.T) {
}

func TestGRPCRouteExample(t *testing.T) {
skipTestForExpressionRouter(t)
var (
grpcrouteExampleManifests = fmt.Sprintf("%s/gateway-grpcroute.yaml", examplesDIR)
ctx = context.Background()
Expand Down
3 changes: 0 additions & 3 deletions test/integration/grpcroute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,6 @@ func grpcEchoResponds(ctx context.Context, url, hostname, input string) error {
}

func TestGRPCRouteEssentials(t *testing.T) {
// TODO: implement translator for grpc routes:
// https://github.com/Kong/kubernetes-ingress-controller/issues/3752
skipTestForExpressionRouter(t)
ctx := context.Background()

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

0 comments on commit 95244b4

Please sign in to comment.