Skip to content

Commit

Permalink
fix(parser) add HTTPRoute combined services routes
Browse files Browse the repository at this point in the history
Add support for combined service routes when parsing gw api HTTPRoutes
  • Loading branch information
jrsmroz committed Sep 30, 2022
1 parent 963623d commit 277538c
Show file tree
Hide file tree
Showing 3 changed files with 229 additions and 44 deletions.
63 changes: 54 additions & 9 deletions internal/dataplane/parser/translate_httproute.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,28 +59,65 @@ func (p *Parser) ingressRulesFromHTTPRoute(result *ingressRules, httproute *gate
return fmt.Errorf("no rules provided")
}

// each rule may represent a different set of backend services that will be accepting
// traffic, so we make separate routes and Kong services for every present rule.
for ruleNumber, rule := range spec.Rules {
for _, rule := range spec.Rules {
// TODO: add this to a generic HTTPRoute validation, and then we should probably
// simply be calling validation on each httproute object at the begininning
// of the topmost list.
if len(rule.BackendRefs) == 0 {
return fmt.Errorf("missing backendRef in rule")
}
}

if p.featureEnabledCombinedServiceRoutes {
return p.ingressRulesFromHTTPRouteWithCombinedServiceRoutes(httproute, result)
}

return p.ingressRulesFromHTTPRouteLegacyFallback(httproute, result)
}

func (p *Parser) ingressRulesFromHTTPRouteWithCombinedServiceRoutes(httproute *gatewayv1beta1.HTTPRoute, result *ingressRules) error {
for i, translationMeta := range translators.TranslateHTTPRoute(httproute) {
// HTTPRoute uses a wrapper HTTPBackendRef to add optional filters to its BackendRefs
backendRefs := httpBackendRefsToBackendRefs(translationMeta.BackendRefs)

// create a service and attach the routes to it
service, err := generateKongServiceFromBackendRef(p.logger, p.storer, result, httproute, i, "http", backendRefs...)
if err != nil {
return err
}

// generate the routes for the service and attach them to the service
for ruleNumber, rule := range translationMeta.Rules {
routes, err := generateKongRoutesFromHTTPRouteRule(httproute, ruleNumber, rule, p.flagEnabledRegexPathPrefix)
if err != nil {
return err
}
service.Routes = append(service.Routes, routes...)
}

// cache the service to avoid duplicates in further loop iterations
result.ServiceNameToServices[*service.Service.Name] = service
}

return nil
}

// ingressRulesFromHTTPRouteLegacyFallback is to be depracated in favor of the combined service routes.
func (p *Parser) ingressRulesFromHTTPRouteLegacyFallback(httproute *gatewayv1beta1.HTTPRoute, result *ingressRules) error {
// each rule may represent a different set of backend services that will be accepting
// 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)
if err != nil {
return err
}

// create a service and attach the routes to it
var backendRefs []gatewayv1beta1.BackendRef
// HTTPRoute uses a wrapper HTTPBackendRef to add optional filters to its BackendRefs
for _, hRef := range rule.BackendRefs {
backendRefs = append(backendRefs, hRef.BackendRef)
}
backendRefs := httpBackendRefsToBackendRefs(rule.BackendRefs)

// create a service and attach the routes to it
service, err := generateKongServiceFromBackendRef(p.logger, p.storer, result, httproute, ruleNumber, "http", backendRefs...)
if err != nil {
return err
Expand All @@ -90,7 +127,6 @@ func (p *Parser) ingressRulesFromHTTPRoute(result *ingressRules, httproute *gate
// cache the service to avoid duplicates in further loop iterations
result.ServiceNameToServices[*service.Service.Name] = service
}

return nil
}

Expand Down Expand Up @@ -317,3 +353,12 @@ func generateRequestHeaderModifierKongPlugin(modifier *gatewayv1beta1.HTTPReques
func kongHeaderFormatter(header gatewayv1beta1.HTTPHeader) string {
return fmt.Sprintf("%s:%s", header.Name, header.Value)
}

func httpBackendRefsToBackendRefs(httpBackendRef []gatewayv1beta1.HTTPBackendRef) []gatewayv1beta1.BackendRef {
var backendRefs []gatewayv1beta1.BackendRef

for _, hRef := range httpBackendRef {
backendRefs = append(backendRefs, hRef.BackendRef)
}
return backendRefs
}
86 changes: 51 additions & 35 deletions internal/dataplane/parser/translate_httproute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ var (
func TestIngressRulesFromHTTPRoutes(t *testing.T) {
fakestore, err := store.NewFakeStore(store.FakeObjects{})
require.NoError(t, err)
p := NewParser(logrus.New(), fakestore)
parser := NewParser(logrus.New(), fakestore)
parserWithCombinedServiceRoutes := NewParser(logrus.New(), fakestore)
parserWithCombinedServiceRoutes.EnableCombinedServiceRoutes()

httpPort := gatewayv1beta1.PortNumber(80)

for _, tt := range []struct {
Expand Down Expand Up @@ -640,27 +643,32 @@ func TestIngressRulesFromHTTPRoutes(t *testing.T) {
},
},
} {
t.Run(tt.msg, func(t *testing.T) {
ingressRules := newIngressRules()
withParser := func(p *Parser) func(t *testing.T) {
return func(t *testing.T) {
ingressRules := newIngressRules()

var errs []error
for _, httproute := range tt.routes {
// initialize the HTTPRoute object
httproute.SetGroupVersionKind(httprouteGVK)
var errs []error
for _, httproute := range tt.routes {
// initialize the HTTPRoute object
httproute.SetGroupVersionKind(httprouteGVK)

// generate the ingress rules
err := p.ingressRulesFromHTTPRoute(&ingressRules, httproute)
if err != nil {
errs = append(errs, err)
// generate the ingress rules
err := p.ingressRulesFromHTTPRoute(&ingressRules, httproute)
if err != nil {
errs = append(errs, err)
}
}
}

// verify that we receive the expected values
assert.Equal(t, tt.expected, ingressRules)
// verify that we receive the expected values
assert.Equal(t, tt.expected, ingressRules)

// verify that we receive any and all expected errors
assert.Equal(t, tt.errs, errs)
})
// verify that we receive any and all expected errors
assert.Equal(t, tt.errs, errs)
}
}

t.Run(tt.msg+" using lagacy parser", withParser(parser))
t.Run(tt.msg+" using combined service routes parser", withParser(parserWithCombinedServiceRoutes))
}
}

Expand Down Expand Up @@ -715,8 +723,11 @@ func TestGetHTTPRouteHostnamesAsSliceOfStringPointers(t *testing.T) {
func TestIngressRulesFromHTTPRoutes_RegexPrefix(t *testing.T) {
fakestore, err := store.NewFakeStore(store.FakeObjects{})
require.NoError(t, err)
p := NewParser(logrus.New(), fakestore)
p.EnableRegexPathPrefix()
parser := NewParser(logrus.New(), fakestore)
parser.EnableRegexPathPrefix()
parserWithCombinedServiceRoutes := NewParser(logrus.New(), fakestore)
parserWithCombinedServiceRoutes.EnableRegexPathPrefix()
parserWithCombinedServiceRoutes.EnableCombinedServiceRoutes()
httpPort := gatewayv1beta1.PortNumber(80)

for _, tt := range []struct {
Expand Down Expand Up @@ -849,26 +860,31 @@ func TestIngressRulesFromHTTPRoutes_RegexPrefix(t *testing.T) {
},
},
} {
t.Run(tt.msg, func(t *testing.T) {
ingressRules := newIngressRules()
withParser := func(p *Parser) func(t *testing.T) {
return func(t *testing.T) {
ingressRules := newIngressRules()

var errs []error
for _, httproute := range tt.routes {
// initialize the HTTPRoute object
httproute.SetGroupVersionKind(httprouteGVK)
var errs []error
for _, httproute := range tt.routes {
// initialize the HTTPRoute object
httproute.SetGroupVersionKind(httprouteGVK)

// generate the ingress rules
err := p.ingressRulesFromHTTPRoute(&ingressRules, httproute)
if err != nil {
errs = append(errs, err)
// generate the ingress rules
err := p.ingressRulesFromHTTPRoute(&ingressRules, httproute)
if err != nil {
errs = append(errs, err)
}
}
}

// verify that we receive the expected values
assert.Equal(t, tt.expected, ingressRules)
// verify that we receive the expected values
assert.Equal(t, tt.expected, ingressRules)

// verify that we receive any and all expected errors
assert.Equal(t, tt.errs, errs)
})
// verify that we receive any and all expected errors
assert.Equal(t, tt.errs, errs)
}
}

t.Run(tt.msg+" using lagacy parser", withParser(parser))
t.Run(tt.msg+" using combined service routes parser", withParser(parserWithCombinedServiceRoutes))
}
}
124 changes: 124 additions & 0 deletions internal/dataplane/parser/translators/httproute.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package translators

import (
"sort"
"strconv"
"strings"

gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
)

// HTTPRouteTranslationMeta is a translation of a single HTTPRoute into metadata
// that can be used to instantiate Kong routes and services.
// Rules from this object should route traffic to the BAckendRefs from this object.
type HTTPRouteTranslationMeta struct {
BackendRefs []gatewayv1beta1.HTTPBackendRef
Rules []gatewayv1beta1.HTTPRouteRule
}

// TranslateHTTPRoutes translates a list of HTTPRoutes into a list of HTTPRouteTranslationMeta
// objects that can be used to instantiate Kong routes and services.
// The translation is done by grouping the HTTPRoutes by their backendRefs.
// This means that all the rules of a single HTTPRoute will be grouped together
// if they share the same backendRefs.
func TranslateHTTPRoute(route *gatewayv1beta1.HTTPRoute) []*HTTPRouteTranslationMeta {
index := newHTTPRouteTranslationIndex()
index.addRoute(route)
return index.translate()
}

// -----------------------------------------------------------------------------
// HTTPRoute Translation - Private - Index
// -----------------------------------------------------------------------------

// httpRouteTranslationIndex aggregates all rules routing to the same backends group.
type httpRouteTranslationIndex struct {
backendsRules map[httpBackendRefsKey][]gatewayv1beta1.HTTPRouteRule
}

// newHTTPRouteTranslationIndex creates a new httpRouteTranslationIndex.
func newHTTPRouteTranslationIndex() *httpRouteTranslationIndex {
return &httpRouteTranslationIndex{
backendsRules: make(map[httpBackendRefsKey][]gatewayv1beta1.HTTPRouteRule),
}
}

// addRoute an HTTPRoute to the index, grouping the rules by their backendRefs.
func (i *httpRouteTranslationIndex) addRoute(route *gatewayv1beta1.HTTPRoute) {
for _, rule := range route.Spec.Rules {
backendRefsKey := getHTTPBackendRefsKey(rule.BackendRefs...)
i.backendsRules[backendRefsKey] = append(i.backendsRules[backendRefsKey], rule)
}
}

// translate the index into a list of HTTPRouteTranslationMeta objects.
func (i *httpRouteTranslationIndex) translate() []*HTTPRouteTranslationMeta {
translations := make([]*HTTPRouteTranslationMeta, 0)
for _, rules := range i.backendsRules {
// get the backendRefs from any rule, as they are all the same
backendRefs := rules[0].BackendRefs

translations = append(translations, &HTTPRouteTranslationMeta{
BackendRefs: backendRefs,
Rules: rules,
})
}

return translations
}

// -----------------------------------------------------------------------------
// HttpRoute Translation - Private - Metadata
// -----------------------------------------------------------------------------

// httpBackendRefsKey is a key computed from a list of backendRefs.
type httpBackendRefsKey string

// getHTTPBackendRefsKey computes a key from a list of backendRefs.
func getHTTPBackendRefsKey(backendRefs ...gatewayv1beta1.HTTPBackendRef) httpBackendRefsKey {
backendKeys := make([]string, 0, len(backendRefs))

// create a list of backend keys
for _, backendRef := range backendRefs {
var backendKey strings.Builder

if backendRef.Group != nil {
backendKey.WriteString(string(*backendRef.Group))
}
backendKey.WriteString(".")

if backendRef.Kind != nil {
backendKey.WriteString(string(*backendRef.Kind))
}
backendKey.WriteString(".")

if backendRef.Namespace != nil {
backendKey.WriteString(string(*backendRef.Namespace))
}
backendKey.WriteString(".")

backendKey.WriteString(string(backendRef.Name))
backendKey.WriteString(".")

if backendRef.Port != nil {
backendKey.WriteString(strconv.Itoa(int(*backendRef.Port)))
}
backendKey.WriteString(".")

if backendRef.Weight != nil {
backendKey.WriteString(strconv.Itoa(int(*backendRef.Weight)))
}

backendKeys = append(backendKeys, backendKey.String())
}
sort.Strings(backendKeys)

// create a string representation of the backend keys
var keyBuilder strings.Builder
for _, backendKey := range backendKeys {
keyBuilder.WriteString(backendKey)
keyBuilder.WriteString(";")
}

return httpBackendRefsKey(keyBuilder.String())
}

0 comments on commit 277538c

Please sign in to comment.