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 grpcroute to expression based routes #3988

Merged
merged 2 commits into from
May 15, 2023
Merged
Show file tree
Hide file tree
Changes from all 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: 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