Skip to content

Commit b8fcbe1

Browse files
committed
Adds GatewayClass Watch to Gateway Controller
Signed-off-by: danehans <[email protected]>
1 parent d48a104 commit b8fcbe1

File tree

2 files changed

+310
-4
lines changed

2 files changed

+310
-4
lines changed

internal/provider/kubernetes/gateway.go

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,26 @@ func newGatewayController(mgr manager.Manager, cfg *config.Server, su status.Upd
7070
if err := c.Watch(
7171
&source.Kind{Type: &gwapiv1b1.Gateway{}},
7272
&handler.EnqueueRequestForObject{},
73-
predicate.NewPredicateFuncs(r.hasMatchingController),
73+
predicate.NewPredicateFuncs(r.gatewayHasMatchingController),
7474
); err != nil {
7575
return err
7676
}
7777
r.log.Info("watching gateway objects")
7878

79+
// Only enqueue GatewayClass objects that match this Envoy Gateway's controller name.
80+
if err := c.Watch(
81+
&source.Kind{Type: &gwapiv1b1.GatewayClass{}},
82+
&handler.EnqueueRequestForObject{},
83+
predicate.NewPredicateFuncs(r.classHasMatchingController),
84+
); err != nil {
85+
return err
86+
}
87+
r.log.Info("watching gatewayclass objects")
88+
89+
// Trigger Gateway reconciliation when a managed GatewayClass has changed.
90+
if err := c.Watch(&source.Kind{Type: &gwapiv1b1.GatewayClass{}}, r.enqueueRequestForOwningGatewayClass()); err != nil {
91+
return err
92+
}
7993
// Trigger gateway reconciliation when the Envoy Service or Deployment has changed.
8094
if err := c.Watch(&source.Kind{Type: &corev1.Service{}}, r.enqueueRequestForOwningGatewayClass()); err != nil {
8195
return err
@@ -87,9 +101,42 @@ func newGatewayController(mgr manager.Manager, cfg *config.Server, su status.Upd
87101
return nil
88102
}
89103

90-
// hasMatchingController returns true if the provided object is a Gateway
104+
// classHasMatchingController returns true if the provided object is a GatewayClass
105+
// with a Spec.Controller string matching this Envoy Gateway's controller string and
106+
// at has at least one managed Gateway, or false otherwise.
107+
func (r *gatewayReconciler) classHasMatchingController(obj client.Object) bool {
108+
log := r.log.WithName(obj.GetName())
109+
110+
gc, ok := obj.(*gwapiv1b1.GatewayClass)
111+
if !ok {
112+
log.Info("bypassing reconciliation due to unexpected object type", "type", obj)
113+
return false
114+
}
115+
116+
if gc.Spec.ControllerName == r.classController {
117+
// At least one managed gateway should exist.
118+
gateways := &gwapiv1b1.GatewayList{}
119+
if err := r.client.List(context.Background(), gateways); err != nil {
120+
r.log.Error(err, "failed to list gateways")
121+
return false
122+
}
123+
for i := range gateways.Items {
124+
if gateways.Items[i].Spec.GatewayClassName == gwapiv1b1.ObjectName(gc.Name) {
125+
log.Info("enqueueing gatewayclass")
126+
return true
127+
}
128+
}
129+
log.Info("no managed gateways; bypassing reconciliation")
130+
return false
131+
}
132+
133+
log.Info("bypassing reconciliation due to controller name", "controller", gc.Spec.ControllerName)
134+
return false
135+
}
136+
137+
// gatewayHasMatchingController returns true if the provided object is a Gateway
91138
// using a GatewayClass matching the configured gatewayclass controller name.
92-
func (r *gatewayReconciler) hasMatchingController(obj client.Object) bool {
139+
func (r *gatewayReconciler) gatewayHasMatchingController(obj client.Object) bool {
93140
gw, ok := obj.(*gwapiv1b1.Gateway)
94141
if !ok {
95142
r.log.Info("unexpected object type, bypassing reconciliation", "object", obj)
@@ -112,6 +159,41 @@ func (r *gatewayReconciler) hasMatchingController(obj client.Object) bool {
112159
return true
113160
}
114161

162+
// getGatewaysForClass uses a GatewayClass obj to fetch Gateways, iterating
163+
// through them and creating a reconciliation request for each Gateway that
164+
// references obj.
165+
func (r *gatewayReconciler) getGatewaysForClass(obj client.Object) []reconcile.Request {
166+
ctx := context.Background()
167+
168+
gc, ok := obj.(*gwapiv1b1.GatewayClass)
169+
if !ok {
170+
r.log.Info("unexpected object type, bypassing reconciliation", "object", obj)
171+
return []reconcile.Request{}
172+
}
173+
174+
gateways := &gwapiv1b1.GatewayList{}
175+
if err := r.client.List(ctx, gateways); err != nil {
176+
return []reconcile.Request{}
177+
}
178+
179+
requests := []reconcile.Request{}
180+
for i := range gateways.Items {
181+
gw := gateways.Items[i]
182+
if string(gw.Spec.GatewayClassName) == gc.Name {
183+
req := reconcile.Request{
184+
NamespacedName: types.NamespacedName{
185+
Namespace: gw.Namespace,
186+
Name: gw.Name,
187+
},
188+
}
189+
requests = append(requests, req)
190+
break
191+
}
192+
}
193+
194+
return requests
195+
}
196+
115197
// enqueueRequestForOwningGatewayClass returns an event handler that maps events with
116198
// the GatewayClass owning label to Gateway objects.
117199
func (r *gatewayReconciler) enqueueRequestForOwningGatewayClass() handler.EventHandler {

internal/provider/kubernetes/gateway_test.go

Lines changed: 225 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import (
55
"fmt"
66
"testing"
77

8+
"github.com/stretchr/testify/assert"
89
"github.com/stretchr/testify/require"
910
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1011
"k8s.io/apimachinery/pkg/types"
1112
"sigs.k8s.io/controller-runtime/pkg/client"
1213
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
14+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
1315
gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1"
1416

1517
"github.com/envoyproxy/gateway/api/config/v1alpha1"
@@ -110,12 +112,234 @@ func TestGatewayHasMatchingController(t *testing.T) {
110112
tc := tc
111113
t.Run(tc.name, func(t *testing.T) {
112114
r.client = fakeclient.NewClientBuilder().WithScheme(envoygateway.GetScheme()).WithObjects(match, nonMatch, tc.obj).Build()
113-
actual := r.hasMatchingController(tc.obj)
115+
actual := r.gatewayHasMatchingController(tc.obj)
114116
require.Equal(t, tc.expect, actual)
115117
})
116118
}
117119
}
118120

121+
func TestClassHasMatchingController(t *testing.T) {
122+
testCases := []struct {
123+
name string
124+
obj client.Object
125+
gateways []gwapiv1b1.Gateway
126+
expect bool
127+
}{
128+
{
129+
name: "matching object type, controller name, and managed gateway",
130+
obj: &gwapiv1b1.GatewayClass{
131+
ObjectMeta: metav1.ObjectMeta{
132+
Name: "gc1",
133+
Namespace: "test",
134+
},
135+
Spec: gwapiv1b1.GatewayClassSpec{
136+
ControllerName: v1alpha1.GatewayControllerName,
137+
},
138+
},
139+
gateways: []gwapiv1b1.Gateway{
140+
{
141+
TypeMeta: metav1.TypeMeta{
142+
Kind: "Gateway",
143+
APIVersion: fmt.Sprintf("%s/%s", gwapiv1b1.GroupName, gwapiv1b1.GroupVersion.Version),
144+
},
145+
ObjectMeta: metav1.ObjectMeta{
146+
Name: "test",
147+
Namespace: "test",
148+
},
149+
Spec: gwapiv1b1.GatewaySpec{
150+
GatewayClassName: gwapiv1b1.ObjectName("gc1"),
151+
},
152+
},
153+
},
154+
expect: true,
155+
},
156+
{
157+
name: "matching object type and controller name, but no managed gateways",
158+
obj: &gwapiv1b1.GatewayClass{
159+
ObjectMeta: metav1.ObjectMeta{
160+
Name: "gc1",
161+
Namespace: "test",
162+
},
163+
Spec: gwapiv1b1.GatewayClassSpec{
164+
ControllerName: v1alpha1.GatewayControllerName,
165+
},
166+
},
167+
gateways: []gwapiv1b1.Gateway{
168+
{
169+
TypeMeta: metav1.TypeMeta{
170+
Kind: "Gateway",
171+
APIVersion: fmt.Sprintf("%s/%s", gwapiv1b1.GroupName, gwapiv1b1.GroupVersion.Version),
172+
},
173+
ObjectMeta: metav1.ObjectMeta{
174+
Name: "test",
175+
Namespace: "test",
176+
},
177+
Spec: gwapiv1b1.GatewaySpec{
178+
GatewayClassName: gwapiv1b1.ObjectName("unmanaged-gc"),
179+
},
180+
},
181+
},
182+
expect: false,
183+
},
184+
{
185+
name: "matching object type and gatewayclass but not controller name",
186+
obj: &gwapiv1b1.GatewayClass{
187+
ObjectMeta: metav1.ObjectMeta{
188+
Name: "gc1",
189+
Namespace: "test",
190+
},
191+
Spec: gwapiv1b1.GatewayClassSpec{
192+
ControllerName: "not.envoy.gateway.controller",
193+
},
194+
},
195+
expect: false,
196+
},
197+
{
198+
name: "not gatewayclass object type",
199+
obj: &gwapiv1b1.Gateway{
200+
ObjectMeta: metav1.ObjectMeta{
201+
Name: "gw1",
202+
Namespace: "test",
203+
},
204+
},
205+
expect: false,
206+
},
207+
}
208+
209+
// Create the reconciler.
210+
logger, err := log.NewLogger()
211+
require.NoError(t, err)
212+
r := gatewayReconciler{
213+
classController: v1alpha1.GatewayControllerName,
214+
log: logger,
215+
}
216+
217+
for _, tc := range testCases {
218+
tc := tc
219+
t.Run(tc.name, func(t *testing.T) {
220+
var objs []client.Object
221+
for i := range tc.gateways {
222+
objs = append(objs, &tc.gateways[i])
223+
}
224+
r.client = fakeclient.NewClientBuilder().WithScheme(envoygateway.GetScheme()).WithObjects(objs...).Build()
225+
actual := r.classHasMatchingController(tc.obj)
226+
require.Equal(t, tc.expect, actual)
227+
})
228+
}
229+
}
230+
231+
func TestGetGatewaysForClass(t *testing.T) {
232+
testCases := []struct {
233+
name string
234+
obj client.Object
235+
gateways []gwapiv1b1.Gateway
236+
expect []reconcile.Request
237+
}{
238+
{
239+
name: "one gateway matches gatewayclass",
240+
obj: &gwapiv1b1.GatewayClass{
241+
ObjectMeta: metav1.ObjectMeta{
242+
Namespace: "test",
243+
Name: "gc1",
244+
},
245+
Spec: gwapiv1b1.GatewayClassSpec{
246+
ControllerName: "test.controller",
247+
},
248+
},
249+
gateways: []gwapiv1b1.Gateway{
250+
{
251+
ObjectMeta: metav1.ObjectMeta{
252+
Namespace: "test",
253+
Name: "gw1",
254+
},
255+
Spec: gwapiv1b1.GatewaySpec{
256+
GatewayClassName: gwapiv1b1.ObjectName("gc1"),
257+
},
258+
},
259+
},
260+
expect: []reconcile.Request{
261+
{
262+
NamespacedName: types.NamespacedName{
263+
Namespace: "test",
264+
Name: "gw1",
265+
},
266+
},
267+
},
268+
},
269+
{
270+
name: "one of two gateways match gatewayclass",
271+
obj: &gwapiv1b1.GatewayClass{
272+
ObjectMeta: metav1.ObjectMeta{
273+
Namespace: "test",
274+
Name: "gc1",
275+
},
276+
Spec: gwapiv1b1.GatewayClassSpec{
277+
ControllerName: "test.controller",
278+
},
279+
},
280+
gateways: []gwapiv1b1.Gateway{
281+
{
282+
ObjectMeta: metav1.ObjectMeta{
283+
Namespace: "test",
284+
Name: "gw1",
285+
},
286+
Spec: gwapiv1b1.GatewaySpec{
287+
GatewayClassName: gwapiv1b1.ObjectName("gc1"),
288+
},
289+
},
290+
{
291+
ObjectMeta: metav1.ObjectMeta{
292+
Namespace: "test",
293+
Name: "gw2",
294+
},
295+
Spec: gwapiv1b1.GatewaySpec{
296+
GatewayClassName: gwapiv1b1.ObjectName("gc2"),
297+
},
298+
},
299+
},
300+
expect: []reconcile.Request{
301+
{
302+
NamespacedName: types.NamespacedName{
303+
Namespace: "test",
304+
Name: "gw1",
305+
},
306+
},
307+
},
308+
},
309+
{
310+
name: "not a gatewayclass object",
311+
obj: &gwapiv1b1.Gateway{
312+
ObjectMeta: metav1.ObjectMeta{
313+
Namespace: "test",
314+
Name: "gw1",
315+
},
316+
},
317+
expect: []reconcile.Request{},
318+
},
319+
}
320+
321+
// Create the reconciler.
322+
logger, err := log.NewLogger()
323+
require.NoError(t, err)
324+
r := &gatewayReconciler{
325+
log: logger,
326+
classController: gwapiv1b1.GatewayController(v1alpha1.GatewayControllerName),
327+
}
328+
329+
for i := range testCases {
330+
tc := testCases[i]
331+
t.Run(tc.name, func(t *testing.T) {
332+
objs := []client.Object{tc.obj}
333+
for i := range tc.gateways {
334+
objs = append(objs, &tc.gateways[i])
335+
}
336+
r.client = fakeclient.NewClientBuilder().WithScheme(envoygateway.GetScheme()).WithObjects(objs...).Build()
337+
reqs := r.getGatewaysForClass(tc.obj)
338+
assert.Equal(t, tc.expect, reqs)
339+
})
340+
}
341+
}
342+
119343
func TestIsAccepted(t *testing.T) {
120344
testCases := []struct {
121345
name string

0 commit comments

Comments
 (0)