Skip to content

Commit

Permalink
feat: translate grpcroute to expression based routes (#3988)
Browse files Browse the repository at this point in the history
* feat: translate grpcroute to expression based routes

* address comments and move GenerateKongRoutesFromGRPCRouteRule to translators
  • Loading branch information
randmonkey authored May 15, 2023
1 parent e402bfa commit 420f02a
Show file tree
Hide file tree
Showing 14 changed files with 847 additions and 174 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
108 changes: 6 additions & 102 deletions internal/dataplane/parser/translate_grpcroute.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,11 @@ package parser
import (
"fmt"

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

// -----------------------------------------------------------------------------
Expand Down Expand Up @@ -61,7 +58,12 @@ 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)
} else {
routes = translators.GenerateKongRoutesFromGRPCRouteRule(grpcroute, ruleNumber)
}

// create a service and attach the routes to it
service, err := generateKongServiceFromBackendRefWithRuleNumber(p.logger, p.storer, result, grpcroute, ruleNumber, "grpcs", grpcBackendRefsToBackendRefs(rule.BackendRefs)...)
Expand All @@ -77,91 +79,6 @@ func (p *Parser) ingressRulesFromGRPCRoute(result *ingressRules, grpcroute *gate
return nil
}

func getGRPCMatchDefaults() (
map[gatewayv1alpha2.GRPCMethodMatchType]string,
map[gatewayv1alpha2.GRPCMethodMatchType]string,
) {
// Kong routes derived from a GRPCRoute use a path composed of the match's gRPC service and method
// If either the service or method is omitted, there is a default regex determined by the match type
// https://gateway-api.sigs.k8s.io/geps/gep-1016/#matcher-types describes the defaults

// default path components for the GRPC service
return map[gatewayv1alpha2.GRPCMethodMatchType]string{
gatewayv1alpha2.GRPCMethodMatchType(""): ".+",
gatewayv1alpha2.GRPCMethodMatchExact: ".+",
gatewayv1alpha2.GRPCMethodMatchRegularExpression: ".+",
},
// default path components for the GRPC method
map[gatewayv1alpha2.GRPCMethodMatchType]string{
gatewayv1alpha2.GRPCMethodMatchType(""): "",
gatewayv1alpha2.GRPCMethodMatchExact: "",
gatewayv1alpha2.GRPCMethodMatchRegularExpression: ".+",
}
}

func generateKongRoutesFromGRPCRouteRule(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),
Protocols: kong.StringSlice("grpc", "grpcs"),
},
}

if match.Method != nil {
serviceMap, methodMap := getGRPCMatchDefaults()
var method, service string
matchMethod := match.Method.Method
matchService := match.Method.Service
var matchType gatewayv1alpha2.GRPCMethodMatchType
if match.Method.Type == nil {
matchType = gatewayv1alpha2.GRPCMethodMatchExact
} else {
matchType = *match.Method.Type
}
if matchMethod == nil {
method = methodMap[matchType]
} else {
method = *matchMethod
}
if matchService == nil {
service = serviceMap[matchType]
} else {
service = *matchService
}
r.Paths = append(r.Paths, kong.String(fmt.Sprintf("~/%s/%s", service, method)))
}

if len(grpcroute.Spec.Hostnames) > 0 {
r.Hosts = getGRPCRouteHostnamesAsSliceOfStringPointers(grpcroute)
}

r.Headers = map[string][]string{}
for _, hmatch := range match.Headers {
name := string(hmatch.Name)
r.Headers[name] = append(r.Headers[name], hmatch.Value)
}

routes = append(routes, r)
}

return routes
}

func grpcBackendRefsToBackendRefs(grpcBackendRef []gatewayv1alpha2.GRPCBackendRef) []gatewayv1beta1.BackendRef {
backendRefs := make([]gatewayv1beta1.BackendRef, 0, len(grpcBackendRef))

Expand All @@ -170,16 +87,3 @@ func grpcBackendRefsToBackendRefs(grpcBackendRef []gatewayv1alpha2.GRPCBackendRe
}
return backendRefs
}

// -----------------------------------------------------------------------------
// Translate GRPCRoute - Utils
// -----------------------------------------------------------------------------

// getGRPCRouteHostnamesAsSliceOfStringPointers translates the hostnames defined
// in an GRPCRoute specification into a []*string slice, which is the type required
// by kong.Route{}.
func getGRPCRouteHostnamesAsSliceOfStringPointers(grpcroute *gatewayv1alpha2.GRPCRoute) []*string {
return lo.Map(grpcroute.Spec.Hostnames, func(h gatewayv1beta1.Hostname, _ int) *string {
return lo.ToPtr(string(h))
})
}
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())
})
}
}
115 changes: 115 additions & 0 deletions internal/dataplane/parser/translators/grpcroute.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package translators

import (
"fmt"

"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/util"
)

func getGRPCMatchDefaults() (
map[gatewayv1alpha2.GRPCMethodMatchType]string,
map[gatewayv1alpha2.GRPCMethodMatchType]string,
) {
// Kong routes derived from a GRPCRoute use a path composed of the match's gRPC service and method
// If either the service or method is omitted, there is a default regex determined by the match type
// https://gateway-api.sigs.k8s.io/geps/gep-1016/#matcher-types describes the defaults

// default path components for the GRPC service
return map[gatewayv1alpha2.GRPCMethodMatchType]string{
gatewayv1alpha2.GRPCMethodMatchType(""): ".+",
gatewayv1alpha2.GRPCMethodMatchExact: ".+",
gatewayv1alpha2.GRPCMethodMatchRegularExpression: ".+",
},
// default path components for the GRPC method
map[gatewayv1alpha2.GRPCMethodMatchType]string{
gatewayv1alpha2.GRPCMethodMatchType(""): "",
gatewayv1alpha2.GRPCMethodMatchExact: "",
gatewayv1alpha2.GRPCMethodMatchRegularExpression: ".+",
}
}

func GenerateKongRoutesFromGRPCRouteRule(grpcroute *gatewayv1alpha2.GRPCRoute, ruleNumber int) []kongstate.Route {
if ruleNumber >= len(grpcroute.Spec.Rules) {
return nil
}
rule := grpcroute.Spec.Rules[ruleNumber]

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),
Protocols: kong.StringSlice("grpc", "grpcs"),
},
}

if match.Method != nil {
serviceMap, methodMap := getGRPCMatchDefaults()
var method, service string
matchMethod := match.Method.Method
matchService := match.Method.Service
var matchType gatewayv1alpha2.GRPCMethodMatchType
if match.Method.Type == nil {
matchType = gatewayv1alpha2.GRPCMethodMatchExact
} else {
matchType = *match.Method.Type
}
if matchMethod == nil {
method = methodMap[matchType]
} else {
method = *matchMethod
}
if matchService == nil {
service = serviceMap[matchType]
} else {
service = *matchService
}
r.Paths = append(r.Paths, kong.String(fmt.Sprintf("~/%s/%s", service, method)))
}

if len(grpcroute.Spec.Hostnames) > 0 {
r.Hosts = getGRPCRouteHostnamesAsSliceOfStringPointers(grpcroute)
}

r.Headers = map[string][]string{}
for _, hmatch := range match.Headers {
name := string(hmatch.Name)
r.Headers[name] = append(r.Headers[name], hmatch.Value)
}

routes = append(routes, r)
}

return routes
}

// -----------------------------------------------------------------------------
// Translate GRPCRoute - Utils
// -----------------------------------------------------------------------------

// getGRPCRouteHostnamesAsSliceOfStringPointers translates the hostnames defined
// in an GRPCRoute specification into a []*string slice, which is the type required
// by kong.Route{}.
func getGRPCRouteHostnamesAsSliceOfStringPointers(grpcroute *gatewayv1alpha2.GRPCRoute) []*string {
return lo.Map(grpcroute.Spec.Hostnames, func(h gatewayv1beta1.Hostname, _ int) *string {
return lo.ToPtr(string(h))
})
}
Loading

0 comments on commit 420f02a

Please sign in to comment.