Skip to content

Commit f049a86

Browse files
authored
Set GatewayClass status for ignored GatewayClasses (#804)
Problem: It is expected that any GatewayClass that references our controller should have its status set properly, even if not used by our controller. Solution: Both the provisioner and controller will now add Accepted status False and ObservedGeneration to any GatewayClass that references our controller but is not configured to be used by our controller.
1 parent 104d511 commit f049a86

18 files changed

+476
-154
lines changed

cmd/gateway/commands.go

+1
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ func createProvisionerModeCommand() *cobra.Command {
193193
return provisioner.StartManager(provisioner.Config{
194194
Logger: logger,
195195
GatewayClassName: gatewayClassName.value,
196+
GatewayCtlrName: gatewayCtlrName.value,
196197
})
197198
},
198199
}

conformance/Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
NKG_TAG = edge
22
NKG_PREFIX = nginx-kubernetes-gateway
33
GATEWAY_CLASS = nginx
4-
SUPPORTED_FEATURES = HTTPRoute,HTTPRouteQueryParamMatching,HTTPRouteMethodMatching,HTTPRoutePortRedirect,HTTPRouteSchemeRedirect
4+
SUPPORTED_FEATURES = HTTPRoute,HTTPRouteQueryParamMatching,HTTPRouteMethodMatching,HTTPRoutePortRedirect,HTTPRouteSchemeRedirect,GatewayClassObservedGenerationBump
55
KIND_KUBE_CONFIG=$${HOME}/.kube/kind/config
66
TAG = latest
77
PREFIX = conformance-test-runner

docs/gateway-api-compatibility.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ Fields:
4343
* `parametersRef` - not supported.
4444
* `description` - supported.
4545
* `status`
46-
* `conditions` - partially supported.
46+
* `conditions` - supported (Condition/Status/Reason):
47+
* `Accepted/True/Accepted`
48+
* `Accepted/False/InvalidParameters`
49+
* `Accepted/False/GatewayClassConflict`: Custom reason for when the GatewayClass references this controller, but a different GatewayClass name is provided to the controller via the command-line argument.
4750

4851
### Gateway
4952

internal/manager/manager.go

+1-3
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,7 @@ func Start(cfg config.Config) error {
8080
{
8181
objectType: &gatewayv1beta1.GatewayClass{},
8282
options: []controller.Option{
83-
controller.WithNamespacedNameFilter(filter.CreateSingleResourceFilter(
84-
types.NamespacedName{Name: cfg.GatewayClassName},
85-
)),
83+
controller.WithK8sPredicate(predicate.GatewayClassPredicate{ControllerName: cfg.GatewayCtlrName}),
8684
},
8785
},
8886
{
+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package predicate
2+
3+
import (
4+
"sigs.k8s.io/controller-runtime/pkg/event"
5+
"sigs.k8s.io/controller-runtime/pkg/predicate"
6+
"sigs.k8s.io/gateway-api/apis/v1beta1"
7+
)
8+
9+
// GatewayClassPredicate implements a predicate function based on the controllerName of a GatewayClass.
10+
// This predicate will skip events for GatewayClasses that don't reference this controller.
11+
type GatewayClassPredicate struct {
12+
predicate.Funcs
13+
ControllerName string
14+
}
15+
16+
// Create implements default CreateEvent filter for validating a GatewayClass controllerName.
17+
func (gcp GatewayClassPredicate) Create(e event.CreateEvent) bool {
18+
if e.Object == nil {
19+
return false
20+
}
21+
22+
gc, ok := e.Object.(*v1beta1.GatewayClass)
23+
if !ok {
24+
return false
25+
}
26+
27+
return string(gc.Spec.ControllerName) == gcp.ControllerName
28+
}
29+
30+
// Update implements default UpdateEvent filter for validating a GatewayClass controllerName.
31+
func (gcp GatewayClassPredicate) Update(e event.UpdateEvent) bool {
32+
if e.ObjectOld != nil {
33+
gcOld, ok := e.ObjectOld.(*v1beta1.GatewayClass)
34+
if ok && string(gcOld.Spec.ControllerName) == gcp.ControllerName {
35+
return true
36+
}
37+
}
38+
39+
if e.ObjectNew != nil {
40+
gcNew, ok := e.ObjectNew.(*v1beta1.GatewayClass)
41+
if ok && string(gcNew.Spec.ControllerName) == gcp.ControllerName {
42+
return true
43+
}
44+
}
45+
46+
return false
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package predicate
2+
3+
import (
4+
"testing"
5+
6+
. "github.com/onsi/gomega"
7+
"sigs.k8s.io/controller-runtime/pkg/event"
8+
"sigs.k8s.io/gateway-api/apis/v1beta1"
9+
)
10+
11+
func TestGatewayClassPredicate(t *testing.T) {
12+
g := NewGomegaWithT(t)
13+
14+
p := GatewayClassPredicate{ControllerName: "nginx-ctlr"}
15+
16+
gc := &v1beta1.GatewayClass{
17+
Spec: v1beta1.GatewayClassSpec{
18+
ControllerName: "nginx-ctlr",
19+
},
20+
}
21+
22+
g.Expect(p.Create(event.CreateEvent{Object: gc})).To(BeTrue())
23+
g.Expect(p.Update(event.UpdateEvent{ObjectNew: gc})).To(BeTrue())
24+
25+
gc2 := &v1beta1.GatewayClass{
26+
Spec: v1beta1.GatewayClassSpec{
27+
ControllerName: "unknown",
28+
},
29+
}
30+
g.Expect(p.Create(event.CreateEvent{Object: gc2})).To(BeFalse())
31+
g.Expect(p.Update(event.UpdateEvent{ObjectOld: gc, ObjectNew: gc2})).To(BeTrue())
32+
g.Expect(p.Update(event.UpdateEvent{ObjectOld: gc2, ObjectNew: gc})).To(BeTrue())
33+
g.Expect(p.Update(event.UpdateEvent{ObjectOld: gc2, ObjectNew: gc2})).To(BeFalse())
34+
}

internal/manager/predicate/service_test.go

+14-23
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package predicate
33
import (
44
"testing"
55

6+
. "github.com/onsi/gomega"
67
v1 "k8s.io/api/core/v1"
78
"k8s.io/apimachinery/pkg/util/intstr"
89
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -224,34 +225,24 @@ func TestServicePortsChangedPredicate_Update(t *testing.T) {
224225
p := ServicePortsChangedPredicate{}
225226

226227
for _, tc := range testcases {
227-
update := p.Update(event.UpdateEvent{
228-
ObjectOld: tc.objectOld,
229-
ObjectNew: tc.objectNew,
230-
})
228+
t.Run(tc.msg, func(t *testing.T) {
229+
g := NewGomegaWithT(t)
230+
update := p.Update(event.UpdateEvent{
231+
ObjectOld: tc.objectOld,
232+
ObjectNew: tc.objectNew,
233+
})
231234

232-
if update != tc.expUpdate {
233-
t.Errorf(
234-
"ServicePortsChangedPredicate.Update() mismatch for %q; got %t, expected %t",
235-
tc.msg,
236-
update,
237-
tc.expUpdate,
238-
)
239-
}
235+
g.Expect(update).To(Equal(tc.expUpdate))
236+
})
240237
}
241238
}
242239

243240
func TestServicePortsChangedPredicate(t *testing.T) {
244-
p := ServicePortsChangedPredicate{}
245-
246-
if !p.Delete(event.DeleteEvent{Object: &v1.Service{}}) {
247-
t.Errorf("ServicePortsChangedPredicate.Delete() returned false; expected true")
248-
}
241+
g := NewGomegaWithT(t)
249242

250-
if !p.Create(event.CreateEvent{Object: &v1.Service{}}) {
251-
t.Errorf("ServicePortsChangedPredicate.Create() returned false; expected true")
252-
}
243+
p := ServicePortsChangedPredicate{}
253244

254-
if !p.Generic(event.GenericEvent{Object: &v1.Service{}}) {
255-
t.Errorf("ServicePortsChangedPredicate.Generic() returned false; expected true")
256-
}
245+
g.Expect(p.Delete(event.DeleteEvent{Object: &v1.Service{}})).To(BeTrue())
246+
g.Expect(p.Create(event.CreateEvent{Object: &v1.Service{}})).To(BeTrue())
247+
g.Expect(p.Generic(event.GenericEvent{Object: &v1.Service{}})).To(BeTrue())
257248
}

internal/provisioner/handler.go

+20-9
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,28 @@ func newEventHandler(
5353
}
5454
}
5555

56-
func (h *eventHandler) ensureGatewayClassAccepted(ctx context.Context) {
57-
gc, exist := h.store.gatewayClasses[types.NamespacedName{Name: h.gcName}]
58-
if !exist {
59-
panic(fmt.Errorf("GatewayClass %s must exist", h.gcName))
56+
func (h *eventHandler) setGatewayClassStatuses(ctx context.Context) {
57+
statuses := status.Statuses{
58+
GatewayClassStatuses: make(status.GatewayClassStatuses),
6059
}
6160

62-
statuses := status.Statuses{
63-
GatewayClassStatus: &status.GatewayClassStatus{
64-
Conditions: conditions.NewDefaultGatewayClassConditions(),
61+
var gcExists bool
62+
for nsname, gc := range h.store.gatewayClasses {
63+
var conds []conditions.Condition
64+
if gc.Name == h.gcName {
65+
gcExists = true
66+
conds = conditions.NewDefaultGatewayClassConditions()
67+
} else {
68+
conds = []conditions.Condition{conditions.NewGatewayClassConflict()}
69+
}
70+
71+
statuses.GatewayClassStatuses[nsname] = status.GatewayClassStatus{
72+
Conditions: conds,
6573
ObservedGeneration: gc.Generation,
66-
},
74+
}
75+
}
76+
if !gcExists {
77+
panic(fmt.Errorf("GatewayClass %s must exist", h.gcName))
6778
}
6879

6980
h.statusUpdater.Update(ctx, statuses)
@@ -133,7 +144,7 @@ func (h *eventHandler) ensureDeploymentsMatchGateways(ctx context.Context) {
133144

134145
func (h *eventHandler) HandleEventBatch(ctx context.Context, batch events.EventBatch) {
135146
h.store.update(batch)
136-
h.ensureGatewayClassAccepted(ctx)
147+
h.setGatewayClassStatuses(ctx)
137148
h.ensureDeploymentsMatchGateways(ctx)
138149
}
139150

internal/provisioner/handler_test.go

+53-12
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
embeddedfiles "github.com/nginxinc/nginx-kubernetes-gateway"
2020
"github.com/nginxinc/nginx-kubernetes-gateway/internal/events"
2121
"github.com/nginxinc/nginx-kubernetes-gateway/internal/helpers"
22+
"github.com/nginxinc/nginx-kubernetes-gateway/internal/state/conditions"
2223
"github.com/nginxinc/nginx-kubernetes-gateway/internal/status"
2324
"github.com/nginxinc/nginx-kubernetes-gateway/internal/status/statusfakes"
2425
)
@@ -272,6 +273,46 @@ var _ = Describe("handler", func() {
272273
Expect(deps.Items).To(HaveLen(0))
273274
})
274275
})
276+
277+
When("upserting GatewayClass that is not set in command-line argument", func() {
278+
It("should set the proper status if this controller is referenced", func() {
279+
gc := &v1beta1.GatewayClass{
280+
ObjectMeta: metav1.ObjectMeta{
281+
Name: "unknown-gc",
282+
},
283+
Spec: v1beta1.GatewayClassSpec{
284+
ControllerName: "test.example.com",
285+
},
286+
}
287+
err := k8sclient.Create(context.Background(), gc)
288+
Expect(err).ShouldNot(HaveOccurred())
289+
290+
batch := []interface{}{
291+
&events.UpsertEvent{
292+
Resource: gc,
293+
},
294+
}
295+
296+
handler.HandleEventBatch(context.Background(), batch)
297+
298+
unknownGC := &v1beta1.GatewayClass{}
299+
err = k8sclient.Get(context.Background(), client.ObjectKeyFromObject(gc), unknownGC)
300+
Expect(err).ShouldNot(HaveOccurred())
301+
302+
expectedConditions := []metav1.Condition{
303+
{
304+
Type: string(v1beta1.GatewayClassConditionStatusAccepted),
305+
Status: metav1.ConditionFalse,
306+
ObservedGeneration: 0,
307+
LastTransitionTime: fakeClockTime,
308+
Reason: string(conditions.GatewayClassReasonGatewayClassConflict),
309+
Message: string(conditions.GatewayClassMessageGatewayClassConflict),
310+
},
311+
}
312+
313+
Expect(unknownGC.Status.Conditions).To(Equal(expectedConditions))
314+
})
315+
})
275316
})
276317

277318
Describe("Edge cases", func() {
@@ -392,20 +433,20 @@ var _ = Describe("handler", func() {
392433
Expect(handle).Should(Panic())
393434
})
394435
})
395-
})
396436

397-
When("upserting Gateway with broken static Deployment YAML", func() {
398-
It("it should panic", func() {
399-
handler = newEventHandler(
400-
gcName,
401-
statusUpdater,
402-
k8sclient,
403-
zap.New(),
404-
[]byte("broken YAML"),
405-
)
437+
When("upserting Gateway with broken static Deployment YAML", func() {
438+
It("it should panic", func() {
439+
handler = newEventHandler(
440+
gcName,
441+
statusUpdater,
442+
k8sclient,
443+
zap.New(),
444+
[]byte("broken YAML"),
445+
)
406446

407-
itShouldUpsertGatewayClass()
408-
itShouldPanicWhenUpsertingGateway(types.NamespacedName{Namespace: "test-ns", Name: "test-gw"})
447+
itShouldUpsertGatewayClass()
448+
itShouldPanicWhenUpsertingGateway(types.NamespacedName{Namespace: "test-ns", Name: "test-gw"})
449+
})
409450
})
410451
})
411452
})

internal/provisioner/manager.go

+3-5
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
v1 "k8s.io/api/apps/v1"
88
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
99
"k8s.io/apimachinery/pkg/runtime"
10-
"k8s.io/apimachinery/pkg/types"
1110
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
1211
ctlr "sigs.k8s.io/controller-runtime"
1312
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -17,14 +16,15 @@ import (
1716
embeddedfiles "github.com/nginxinc/nginx-kubernetes-gateway"
1817
"github.com/nginxinc/nginx-kubernetes-gateway/internal/controller"
1918
"github.com/nginxinc/nginx-kubernetes-gateway/internal/events"
20-
"github.com/nginxinc/nginx-kubernetes-gateway/internal/manager/filter"
19+
"github.com/nginxinc/nginx-kubernetes-gateway/internal/manager/predicate"
2120
"github.com/nginxinc/nginx-kubernetes-gateway/internal/status"
2221
)
2322

2423
// Config is configuration for the provisioner mode.
2524
type Config struct {
2625
Logger logr.Logger
2726
GatewayClassName string
27+
GatewayCtlrName string
2828
}
2929

3030
// StartManager starts a Manager for the provisioner mode, which provisions
@@ -60,9 +60,7 @@ func StartManager(cfg Config) error {
6060
{
6161
objectType: &gatewayv1beta1.GatewayClass{},
6262
options: []controller.Option{
63-
controller.WithNamespacedNameFilter(
64-
filter.CreateSingleResourceFilter(types.NamespacedName{Name: cfg.GatewayClassName}),
65-
),
63+
controller.WithK8sPredicate(predicate.GatewayClassPredicate{ControllerName: cfg.GatewayCtlrName}),
6664
},
6765
},
6866
{

internal/state/conditions/conditions.go

+20
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ import (
88
)
99

1010
const (
11+
// GatewayClassReasonGatewayClassConflict indicates there are multiple GatewayClass resources
12+
// that reference this controller, and we ignored the resource in question and picked the
13+
// GatewayClass that is referenced in the command-line argument.
14+
// This reason is used with GatewayClassConditionAccepted (false).
15+
GatewayClassReasonGatewayClassConflict v1beta1.GatewayClassConditionReason = "GatewayClassConflict"
16+
17+
// GatewayClassMessageGatewayClassConflict is a message that describes GatewayClassReasonGatewayClassConflict.
18+
GatewayClassMessageGatewayClassConflict = "The resource is ignored due to a conflicting GatewayClass resource"
19+
1120
// ListenerReasonUnsupportedValue is used with the "Accepted" condition when a value of a field in a Listener
1221
// is invalid or not supported.
1322
ListenerReasonUnsupportedValue v1beta1.ListenerConditionReason = "UnsupportedValue"
@@ -427,6 +436,17 @@ func NewDefaultGatewayClassConditions() []Condition {
427436
}
428437
}
429438

439+
// NewGatewayClassConflict returns a Condition that indicates that the GatewayClass is not accepted
440+
// due to a conflict with another GatewayClass.
441+
func NewGatewayClassConflict() Condition {
442+
return Condition{
443+
Type: string(v1beta1.GatewayClassConditionStatusAccepted),
444+
Status: metav1.ConditionFalse,
445+
Reason: string(GatewayClassReasonGatewayClassConflict),
446+
Message: GatewayClassMessageGatewayClassConflict,
447+
}
448+
}
449+
430450
// NewGatewayClassInvalidParameters returns a Condition that indicates that the GatewayClass has invalid parameters.
431451
func NewGatewayClassInvalidParameters(msg string) Condition {
432452
return Condition{

0 commit comments

Comments
 (0)