Skip to content
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 go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ require (
k8s.io/klog/v2 v2.130.1
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
sigs.k8s.io/controller-runtime v0.19.3
sigs.k8s.io/gateway-api v1.2.0
sigs.k8s.io/yaml v1.4.0
)

Expand Down Expand Up @@ -128,7 +129,6 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/miekg/dns v1.1.62 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,8 @@ oras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo=
oras.land/oras-go v1.2.5/go.mod h1:PuAwRShRZCsZb7g8Ar3jKKQR/2A/qN+pkYxIOd/FAoo=
sigs.k8s.io/controller-runtime v0.19.3 h1:XO2GvC9OPftRst6xWCpTgBZO04S2cbp0Qqkj8bX1sPw=
sigs.k8s.io/controller-runtime v0.19.3/go.mod h1:j4j87DqtsThvwTv5/Tc5NFRyyF/RF0ip4+62tbTSIUM=
sigs.k8s.io/gateway-api v1.2.0 h1:LrToiFwtqKTKZcZtoQPTuo3FxhrrhTgzQG0Te+YGSo8=
sigs.k8s.io/gateway-api v1.2.0/go.mod h1:EpNfEXNjiYfUJypf0eZ0P5iXA9ekSGWaS1WgPaM42X0=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
sigs.k8s.io/kustomize/api v0.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo=
Expand Down
98 changes: 98 additions & 0 deletions pkg/gateway/routeutils/backend.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package routeutils

import (
"context"
"fmt"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
)

// Backend an abstraction on the Gateway Backend, meant to hide the underlying backend type from consumers (unless they really want to see it :))
type Backend struct {
Service *corev1.Service
ServicePort *corev1.ServicePort
TypeSpecificBackend interface{}
Weight int
// Add TG config here //
}

// TODOs:
// 1/ Add reference grant checking
// 2/ Add target group configuration resolution

// NOTE: Currently routeKind is not used, however, we will need it to load TG specific configuration.
// commonBackendLoader this function will load the services and target group configurations associated with this gateway backend.
func commonBackendLoader(ctx context.Context, k8sClient client.Client, typeSpecificBackend interface{}, backendRef gwv1.BackendRef, routeIdentifier types.NamespacedName, routeKind string) (*Backend, error) {

// We only support references of type service.
if backendRef.Kind != nil && *backendRef.Kind != "Service" {
return nil, nil
}

if backendRef.Weight != nil && *backendRef.Weight == 0 {
return nil, nil
}

if backendRef.Port == nil {
return nil, errors.Errorf("Missing port in backend reference")
}

var namespace string
if backendRef.Namespace == nil {
namespace = routeIdentifier.Namespace
} else {
namespace = string(*backendRef.Namespace)
}

// TODO - Need to implement reference grant check here

svcName := types.NamespacedName{
Namespace: namespace,
Name: string(backendRef.Name),
}
svc := &corev1.Service{}
err := k8sClient.Get(ctx, svcName, svc)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("Unable to fetch svc object %+v", svcName))
}

var servicePort *corev1.ServicePort

for _, svcPort := range svc.Spec.Ports {
if svcPort.Port == int32(*backendRef.Port) {
servicePort = &svcPort
break
}
}

if servicePort == nil {
return nil, errors.Errorf("Unable to find service port for port %d", *backendRef.Port)
}

// TODO - Need to TG CRD look up here

// Weight specifies the proportion of requests forwarded to the referenced
// backend. This is computed as weight/(sum of all weights in this
// BackendRefs list). For non-zero values, there may be some epsilon from
// the exact proportion defined here depending on the precision an
// implementation supports. Weight is not a percentage and the sum of
// weights does not need to equal 100.
//
// If only one backend is specified, and it has a weight greater than 0, 100%
// of the traffic is forwarded to that backend. If weight is set to 0, no
// traffic should be forwarded for this entry. If unspecified, weight
// defaults to 1.
weight := 1
if backendRef.Weight != nil {
weight = int(*backendRef.Weight)
}
return &Backend{
Service: svc,
ServicePort: servicePort,
Weight: weight,
TypeSpecificBackend: typeSpecificBackend,
}, nil
}
199 changes: 199 additions & 0 deletions pkg/gateway/routeutils/backend_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package routeutils

import (
"context"
awssdk "github.com/aws/aws-sdk-go-v2/aws"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
"testing"
)

func TestCommonBackendLoader(t *testing.T) {

kind := HTTPRouteKind

namespaceToUse := "current-namespace"
svcNameToUse := "current-svc"
routeNameToUse := "my-route"

portConverter := func(port int) *gwv1.PortNumber {
pn := gwv1.PortNumber(port)
return &pn
}

testCases := []struct {
name string
storedService *corev1.Service
backendRef gwv1.BackendRef
routeIdentifier types.NamespacedName
weight int
servicePort int32
expectErr bool
expectNoResult bool
}{
{
name: "backend ref without namespace",
routeIdentifier: types.NamespacedName{
Namespace: "backend-ref-ns",
Name: routeNameToUse,
},
backendRef: gwv1.BackendRef{
BackendObjectReference: gwv1.BackendObjectReference{
Name: gwv1.ObjectName(svcNameToUse),
Port: portConverter(80),
},
},
storedService: &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Namespace: "backend-ref-ns",
Name: svcNameToUse,
},
Spec: corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Name: "port-80",
Port: 80,
},
},
},
},
weight: 1,
servicePort: 80,
},
{
name: "backend ref, fill in weight",
routeIdentifier: types.NamespacedName{
Namespace: "backend-ref-ns",
Name: routeNameToUse,
},
backendRef: gwv1.BackendRef{
BackendObjectReference: gwv1.BackendObjectReference{
Name: gwv1.ObjectName(svcNameToUse),
Port: portConverter(80),
},
Weight: awssdk.Int32(100),
},
storedService: &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Namespace: "backend-ref-ns",
Name: svcNameToUse,
},
Spec: corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Name: "port-80",
Port: 80,
},
},
},
},
weight: 100,
servicePort: 80,
},
{
name: "backend ref with namespace",
routeIdentifier: types.NamespacedName{
Name: routeNameToUse,
},
backendRef: gwv1.BackendRef{
BackendObjectReference: gwv1.BackendObjectReference{
Name: gwv1.ObjectName(svcNameToUse),
Namespace: (*gwv1.Namespace)(&namespaceToUse),
Port: portConverter(80),
},
},
storedService: &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespaceToUse,
Name: svcNameToUse,
},
Spec: corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Name: "port-80",
Port: 80,
},
},
},
},
weight: 1,
servicePort: 80,
},
{
name: "0 weight backend should return nil",
routeIdentifier: types.NamespacedName{
Name: routeNameToUse,
},
backendRef: gwv1.BackendRef{
BackendObjectReference: gwv1.BackendObjectReference{
Name: gwv1.ObjectName(svcNameToUse),
Namespace: (*gwv1.Namespace)(&namespaceToUse),
Port: portConverter(80),
},
Weight: awssdk.Int32(0),
},
expectNoResult: true,
},
{
name: "non-service based backend should return nil",
routeIdentifier: types.NamespacedName{
Name: routeNameToUse,
},
backendRef: gwv1.BackendRef{
BackendObjectReference: gwv1.BackendObjectReference{
Name: gwv1.ObjectName(svcNameToUse),
Namespace: (*gwv1.Namespace)(&namespaceToUse),
Kind: (*gwv1.Kind)(awssdk.String("cat")),
Port: portConverter(80),
},
},
expectNoResult: true,
},
{
name: "missing port in backend ref should result in an error",
routeIdentifier: types.NamespacedName{
Name: routeNameToUse,
},
backendRef: gwv1.BackendRef{
BackendObjectReference: gwv1.BackendObjectReference{
Name: gwv1.ObjectName(svcNameToUse),
Namespace: (*gwv1.Namespace)(&namespaceToUse),
},
},
expectErr: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
k8sClient := generateTestClient()

if tc.storedService != nil {
k8sClient.Create(context.Background(), tc.storedService)
}

result, err := commonBackendLoader(context.Background(), k8sClient, tc.backendRef, tc.backendRef, tc.routeIdentifier, kind)

if tc.expectErr {
assert.Error(t, err)
return
}

assert.NoError(t, err)

if tc.expectNoResult {
assert.Nil(t, result)
return
}

assert.Equal(t, tc.storedService, result.Service)
assert.Equal(t, tc.weight, result.Weight)
assert.Equal(t, tc.servicePort, result.ServicePort.Port)
assert.Equal(t, tc.backendRef, result.TypeSpecificBackend)
})
}

}
34 changes: 34 additions & 0 deletions pkg/gateway/routeutils/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package routeutils

import (
"context"
"sigs.k8s.io/controller-runtime/pkg/client"
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
)

// Route Kinds
const (
TCPRouteKind = "TCPRoute"
UDPRouteKind = "UDPRoute"
TLSRouteKind = "TLSRoute"
HTTPRouteKind = "HTTPRoute"
GRPCRouteKind = "GRPCRoute"
)

// RouteKind to Route Loader. These functions will pull data directly from the kube api or local cache.
var allRoutes = map[string]func(context context.Context, client client.Client) ([]preLoadRouteDescriptor, error){
TCPRouteKind: ListTCPRoutes,
UDPRouteKind: ListUDPRoutes,
TLSRouteKind: ListTLSRoutes,
HTTPRouteKind: ListHTTPRoutes,
GRPCRouteKind: ListGRPCRoutes,
}

// Default protocol map used to infer accepted route kinds when a listener doesn't specify the `allowedRoutes` field.
var defaultProtocolToRouteKindMap = map[gwv1.ProtocolType]string{
gwv1.TCPProtocolType: TCPRouteKind,
gwv1.UDPProtocolType: UDPRouteKind,
gwv1.TLSProtocolType: TLSRouteKind,
gwv1.HTTPProtocolType: HTTPRouteKind,
gwv1.HTTPSProtocolType: HTTPRouteKind,
}
35 changes: 35 additions & 0 deletions pkg/gateway/routeutils/descriptor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package routeutils

import (
"context"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
)

// routeMetadataDescriptor a common set of functions that will describe a route.
// These are intentionally meant to be type agnostic;
// however, consumers can use `GetRawRoute()` to inspect the actual route fields if needed.
type routeMetadataDescriptor interface {
GetRouteNamespacedName() types.NamespacedName
GetRouteKind() string
GetHostnames() []gwv1.Hostname
GetParentRefs() []gwv1.ParentReference
GetRawRoute() interface{}
}

// preLoadRouteDescriptor this object is used to represent a route description that has not loaded its child data (services, tg config)
// generally use this interface to represent broad data, filter that data down to the absolutely required data, and the call
// loadAttachedRules() to generate a full route description.
type preLoadRouteDescriptor interface {
routeMetadataDescriptor
loadAttachedRules(context context.Context, k8sClient client.Client) (RouteDescriptor, error)
}

// RouteDescriptor is a type agnostic representation of a Gateway Route.
// This interface holds all data necessary to construct
// an ELBv2 object out of Kubernetes objects.
type RouteDescriptor interface {
routeMetadataDescriptor
GetAttachedRules() []RouteRule
}
Loading
Loading