Skip to content

Commit d0df42b

Browse files
authored
Merge pull request #4107 from zac-nixon/znixon/gw-api-beta
[feat: gw api] Add a utility to implement Gateway API spec to generate relevant Kubernetes objects for a Gateway
2 parents e01dbf3 + aa40cf7 commit d0df42b

28 files changed

+3452
-1
lines changed

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ require (
4242
k8s.io/klog/v2 v2.130.1
4343
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
4444
sigs.k8s.io/controller-runtime v0.19.3
45+
sigs.k8s.io/gateway-api v1.2.0
4546
sigs.k8s.io/yaml v1.4.0
4647
)
4748

@@ -128,7 +129,6 @@ require (
128129
github.com/mattn/go-colorable v0.1.13 // indirect
129130
github.com/mattn/go-isatty v0.0.20 // indirect
130131
github.com/mattn/go-runewidth v0.0.9 // indirect
131-
github.com/miekg/dns v1.1.62 // indirect
132132
github.com/mitchellh/copystructure v1.2.0 // indirect
133133
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
134134
github.com/mitchellh/reflectwalk v1.0.2 // indirect

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,8 @@ oras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo=
623623
oras.land/oras-go v1.2.5/go.mod h1:PuAwRShRZCsZb7g8Ar3jKKQR/2A/qN+pkYxIOd/FAoo=
624624
sigs.k8s.io/controller-runtime v0.19.3 h1:XO2GvC9OPftRst6xWCpTgBZO04S2cbp0Qqkj8bX1sPw=
625625
sigs.k8s.io/controller-runtime v0.19.3/go.mod h1:j4j87DqtsThvwTv5/Tc5NFRyyF/RF0ip4+62tbTSIUM=
626+
sigs.k8s.io/gateway-api v1.2.0 h1:LrToiFwtqKTKZcZtoQPTuo3FxhrrhTgzQG0Te+YGSo8=
627+
sigs.k8s.io/gateway-api v1.2.0/go.mod h1:EpNfEXNjiYfUJypf0eZ0P5iXA9ekSGWaS1WgPaM42X0=
626628
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
627629
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
628630
sigs.k8s.io/kustomize/api v0.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo=

pkg/gateway/routeutils/backend.go

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package routeutils
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"github.com/pkg/errors"
7+
corev1 "k8s.io/api/core/v1"
8+
"k8s.io/apimachinery/pkg/types"
9+
"sigs.k8s.io/controller-runtime/pkg/client"
10+
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
11+
)
12+
13+
// Backend an abstraction on the Gateway Backend, meant to hide the underlying backend type from consumers (unless they really want to see it :))
14+
type Backend struct {
15+
Service *corev1.Service
16+
ServicePort *corev1.ServicePort
17+
TypeSpecificBackend interface{}
18+
Weight int
19+
// Add TG config here //
20+
}
21+
22+
// TODOs:
23+
// 1/ Add reference grant checking
24+
// 2/ Add target group configuration resolution
25+
26+
// NOTE: Currently routeKind is not used, however, we will need it to load TG specific configuration.
27+
// commonBackendLoader this function will load the services and target group configurations associated with this gateway backend.
28+
func commonBackendLoader(ctx context.Context, k8sClient client.Client, typeSpecificBackend interface{}, backendRef gwv1.BackendRef, routeIdentifier types.NamespacedName, routeKind string) (*Backend, error) {
29+
30+
// We only support references of type service.
31+
if backendRef.Kind != nil && *backendRef.Kind != "Service" {
32+
return nil, nil
33+
}
34+
35+
if backendRef.Weight != nil && *backendRef.Weight == 0 {
36+
return nil, nil
37+
}
38+
39+
if backendRef.Port == nil {
40+
return nil, errors.Errorf("Missing port in backend reference")
41+
}
42+
43+
var namespace string
44+
if backendRef.Namespace == nil {
45+
namespace = routeIdentifier.Namespace
46+
} else {
47+
namespace = string(*backendRef.Namespace)
48+
}
49+
50+
// TODO - Need to implement reference grant check here
51+
52+
svcName := types.NamespacedName{
53+
Namespace: namespace,
54+
Name: string(backendRef.Name),
55+
}
56+
svc := &corev1.Service{}
57+
err := k8sClient.Get(ctx, svcName, svc)
58+
if err != nil {
59+
return nil, errors.Wrap(err, fmt.Sprintf("Unable to fetch svc object %+v", svcName))
60+
}
61+
62+
var servicePort *corev1.ServicePort
63+
64+
for _, svcPort := range svc.Spec.Ports {
65+
if svcPort.Port == int32(*backendRef.Port) {
66+
servicePort = &svcPort
67+
break
68+
}
69+
}
70+
71+
if servicePort == nil {
72+
return nil, errors.Errorf("Unable to find service port for port %d", *backendRef.Port)
73+
}
74+
75+
// TODO - Need to TG CRD look up here
76+
77+
// Weight specifies the proportion of requests forwarded to the referenced
78+
// backend. This is computed as weight/(sum of all weights in this
79+
// BackendRefs list). For non-zero values, there may be some epsilon from
80+
// the exact proportion defined here depending on the precision an
81+
// implementation supports. Weight is not a percentage and the sum of
82+
// weights does not need to equal 100.
83+
//
84+
// If only one backend is specified, and it has a weight greater than 0, 100%
85+
// of the traffic is forwarded to that backend. If weight is set to 0, no
86+
// traffic should be forwarded for this entry. If unspecified, weight
87+
// defaults to 1.
88+
weight := 1
89+
if backendRef.Weight != nil {
90+
weight = int(*backendRef.Weight)
91+
}
92+
return &Backend{
93+
Service: svc,
94+
ServicePort: servicePort,
95+
Weight: weight,
96+
TypeSpecificBackend: typeSpecificBackend,
97+
}, nil
98+
}
+199
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
package routeutils
2+
3+
import (
4+
"context"
5+
awssdk "github.com/aws/aws-sdk-go-v2/aws"
6+
"github.com/stretchr/testify/assert"
7+
corev1 "k8s.io/api/core/v1"
8+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
9+
"k8s.io/apimachinery/pkg/types"
10+
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
11+
"testing"
12+
)
13+
14+
func TestCommonBackendLoader(t *testing.T) {
15+
16+
kind := HTTPRouteKind
17+
18+
namespaceToUse := "current-namespace"
19+
svcNameToUse := "current-svc"
20+
routeNameToUse := "my-route"
21+
22+
portConverter := func(port int) *gwv1.PortNumber {
23+
pn := gwv1.PortNumber(port)
24+
return &pn
25+
}
26+
27+
testCases := []struct {
28+
name string
29+
storedService *corev1.Service
30+
backendRef gwv1.BackendRef
31+
routeIdentifier types.NamespacedName
32+
weight int
33+
servicePort int32
34+
expectErr bool
35+
expectNoResult bool
36+
}{
37+
{
38+
name: "backend ref without namespace",
39+
routeIdentifier: types.NamespacedName{
40+
Namespace: "backend-ref-ns",
41+
Name: routeNameToUse,
42+
},
43+
backendRef: gwv1.BackendRef{
44+
BackendObjectReference: gwv1.BackendObjectReference{
45+
Name: gwv1.ObjectName(svcNameToUse),
46+
Port: portConverter(80),
47+
},
48+
},
49+
storedService: &corev1.Service{
50+
ObjectMeta: metav1.ObjectMeta{
51+
Namespace: "backend-ref-ns",
52+
Name: svcNameToUse,
53+
},
54+
Spec: corev1.ServiceSpec{
55+
Ports: []corev1.ServicePort{
56+
{
57+
Name: "port-80",
58+
Port: 80,
59+
},
60+
},
61+
},
62+
},
63+
weight: 1,
64+
servicePort: 80,
65+
},
66+
{
67+
name: "backend ref, fill in weight",
68+
routeIdentifier: types.NamespacedName{
69+
Namespace: "backend-ref-ns",
70+
Name: routeNameToUse,
71+
},
72+
backendRef: gwv1.BackendRef{
73+
BackendObjectReference: gwv1.BackendObjectReference{
74+
Name: gwv1.ObjectName(svcNameToUse),
75+
Port: portConverter(80),
76+
},
77+
Weight: awssdk.Int32(100),
78+
},
79+
storedService: &corev1.Service{
80+
ObjectMeta: metav1.ObjectMeta{
81+
Namespace: "backend-ref-ns",
82+
Name: svcNameToUse,
83+
},
84+
Spec: corev1.ServiceSpec{
85+
Ports: []corev1.ServicePort{
86+
{
87+
Name: "port-80",
88+
Port: 80,
89+
},
90+
},
91+
},
92+
},
93+
weight: 100,
94+
servicePort: 80,
95+
},
96+
{
97+
name: "backend ref with namespace",
98+
routeIdentifier: types.NamespacedName{
99+
Name: routeNameToUse,
100+
},
101+
backendRef: gwv1.BackendRef{
102+
BackendObjectReference: gwv1.BackendObjectReference{
103+
Name: gwv1.ObjectName(svcNameToUse),
104+
Namespace: (*gwv1.Namespace)(&namespaceToUse),
105+
Port: portConverter(80),
106+
},
107+
},
108+
storedService: &corev1.Service{
109+
ObjectMeta: metav1.ObjectMeta{
110+
Namespace: namespaceToUse,
111+
Name: svcNameToUse,
112+
},
113+
Spec: corev1.ServiceSpec{
114+
Ports: []corev1.ServicePort{
115+
{
116+
Name: "port-80",
117+
Port: 80,
118+
},
119+
},
120+
},
121+
},
122+
weight: 1,
123+
servicePort: 80,
124+
},
125+
{
126+
name: "0 weight backend should return nil",
127+
routeIdentifier: types.NamespacedName{
128+
Name: routeNameToUse,
129+
},
130+
backendRef: gwv1.BackendRef{
131+
BackendObjectReference: gwv1.BackendObjectReference{
132+
Name: gwv1.ObjectName(svcNameToUse),
133+
Namespace: (*gwv1.Namespace)(&namespaceToUse),
134+
Port: portConverter(80),
135+
},
136+
Weight: awssdk.Int32(0),
137+
},
138+
expectNoResult: true,
139+
},
140+
{
141+
name: "non-service based backend should return nil",
142+
routeIdentifier: types.NamespacedName{
143+
Name: routeNameToUse,
144+
},
145+
backendRef: gwv1.BackendRef{
146+
BackendObjectReference: gwv1.BackendObjectReference{
147+
Name: gwv1.ObjectName(svcNameToUse),
148+
Namespace: (*gwv1.Namespace)(&namespaceToUse),
149+
Kind: (*gwv1.Kind)(awssdk.String("cat")),
150+
Port: portConverter(80),
151+
},
152+
},
153+
expectNoResult: true,
154+
},
155+
{
156+
name: "missing port in backend ref should result in an error",
157+
routeIdentifier: types.NamespacedName{
158+
Name: routeNameToUse,
159+
},
160+
backendRef: gwv1.BackendRef{
161+
BackendObjectReference: gwv1.BackendObjectReference{
162+
Name: gwv1.ObjectName(svcNameToUse),
163+
Namespace: (*gwv1.Namespace)(&namespaceToUse),
164+
},
165+
},
166+
expectErr: true,
167+
},
168+
}
169+
170+
for _, tc := range testCases {
171+
t.Run(tc.name, func(t *testing.T) {
172+
k8sClient := generateTestClient()
173+
174+
if tc.storedService != nil {
175+
k8sClient.Create(context.Background(), tc.storedService)
176+
}
177+
178+
result, err := commonBackendLoader(context.Background(), k8sClient, tc.backendRef, tc.backendRef, tc.routeIdentifier, kind)
179+
180+
if tc.expectErr {
181+
assert.Error(t, err)
182+
return
183+
}
184+
185+
assert.NoError(t, err)
186+
187+
if tc.expectNoResult {
188+
assert.Nil(t, result)
189+
return
190+
}
191+
192+
assert.Equal(t, tc.storedService, result.Service)
193+
assert.Equal(t, tc.weight, result.Weight)
194+
assert.Equal(t, tc.servicePort, result.ServicePort.Port)
195+
assert.Equal(t, tc.backendRef, result.TypeSpecificBackend)
196+
})
197+
}
198+
199+
}

pkg/gateway/routeutils/constants.go

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package routeutils
2+
3+
import (
4+
"context"
5+
"sigs.k8s.io/controller-runtime/pkg/client"
6+
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
7+
)
8+
9+
// Route Kinds
10+
const (
11+
TCPRouteKind = "TCPRoute"
12+
UDPRouteKind = "UDPRoute"
13+
TLSRouteKind = "TLSRoute"
14+
HTTPRouteKind = "HTTPRoute"
15+
GRPCRouteKind = "GRPCRoute"
16+
)
17+
18+
// RouteKind to Route Loader. These functions will pull data directly from the kube api or local cache.
19+
var allRoutes = map[string]func(context context.Context, client client.Client) ([]preLoadRouteDescriptor, error){
20+
TCPRouteKind: ListTCPRoutes,
21+
UDPRouteKind: ListUDPRoutes,
22+
TLSRouteKind: ListTLSRoutes,
23+
HTTPRouteKind: ListHTTPRoutes,
24+
GRPCRouteKind: ListGRPCRoutes,
25+
}
26+
27+
// Default protocol map used to infer accepted route kinds when a listener doesn't specify the `allowedRoutes` field.
28+
var defaultProtocolToRouteKindMap = map[gwv1.ProtocolType]string{
29+
gwv1.TCPProtocolType: TCPRouteKind,
30+
gwv1.UDPProtocolType: UDPRouteKind,
31+
gwv1.TLSProtocolType: TLSRouteKind,
32+
gwv1.HTTPProtocolType: HTTPRouteKind,
33+
gwv1.HTTPSProtocolType: HTTPRouteKind,
34+
}

pkg/gateway/routeutils/descriptor.go

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package routeutils
2+
3+
import (
4+
"context"
5+
"k8s.io/apimachinery/pkg/types"
6+
"sigs.k8s.io/controller-runtime/pkg/client"
7+
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
8+
)
9+
10+
// routeMetadataDescriptor a common set of functions that will describe a route.
11+
// These are intentionally meant to be type agnostic;
12+
// however, consumers can use `GetRawRoute()` to inspect the actual route fields if needed.
13+
type routeMetadataDescriptor interface {
14+
GetRouteNamespacedName() types.NamespacedName
15+
GetRouteKind() string
16+
GetHostnames() []gwv1.Hostname
17+
GetParentRefs() []gwv1.ParentReference
18+
GetRawRoute() interface{}
19+
}
20+
21+
// preLoadRouteDescriptor this object is used to represent a route description that has not loaded its child data (services, tg config)
22+
// generally use this interface to represent broad data, filter that data down to the absolutely required data, and the call
23+
// loadAttachedRules() to generate a full route description.
24+
type preLoadRouteDescriptor interface {
25+
routeMetadataDescriptor
26+
loadAttachedRules(context context.Context, k8sClient client.Client) (RouteDescriptor, error)
27+
}
28+
29+
// RouteDescriptor is a type agnostic representation of a Gateway Route.
30+
// This interface holds all data necessary to construct
31+
// an ELBv2 object out of Kubernetes objects.
32+
type RouteDescriptor interface {
33+
routeMetadataDescriptor
34+
GetAttachedRules() []RouteRule
35+
}

0 commit comments

Comments
 (0)