Skip to content

Commit feb4d16

Browse files
committed
Support NoMatchingListenerHostname reason in HTTPRoute status (nginx#290)
If an HTTPRoute references a listener with the hostname that does not match any hostnames of the HTTPRoute, NGINX Kubernetes Gateway will set the condition "Accepted" with the status "False" and the reason "NoMatchingListenerHostname" in the status of the HTTPRoute. The commit introduces a new type RouteCondition. It is used to capture status-related conditions when building the graph. It will be used to capture other conditions too, including when building configuration. Note: the implementation required implementing a few additional conditions. In one case, to wait until the corresponding reasons (v1beta1.RouteConditionReason) are added to the Gateway API and in the other case, to not increase the scope, those conditions are left to be implemented (FIXMEs).
1 parent c020898 commit feb4d16

12 files changed

+437
-107
lines changed

docs/gateway-api-compatibility.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,9 @@ Fields:
9696
* `parents`
9797
* `parentRef` - supported.
9898
* `controllerName` - supported.
99-
* `conditions` - partially supported.
99+
* `conditions` - partially supported. Supported (Condition/Status/Reason):
100+
* `Accepted/True/Accepted`
101+
* `Accepted/False/NoMatchingListenerHostname`
100102

101103
### TLSRoute
102104

internal/state/change_processor_test.go

Lines changed: 98 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package state_test
22

33
import (
44
"context"
5+
"sort"
56

67
. "github.com/onsi/ginkgo/v2"
78
. "github.com/onsi/gomega"
@@ -17,6 +18,7 @@ import (
1718
"github.com/nginxinc/nginx-kubernetes-gateway/internal/helpers"
1819
"github.com/nginxinc/nginx-kubernetes-gateway/internal/manager/index"
1920
"github.com/nginxinc/nginx-kubernetes-gateway/internal/state"
21+
"github.com/nginxinc/nginx-kubernetes-gateway/internal/state/conditions"
2022
"github.com/nginxinc/nginx-kubernetes-gateway/internal/state/relationship"
2123
"github.com/nginxinc/nginx-kubernetes-gateway/internal/state/relationship/relationshipfakes"
2224
"github.com/nginxinc/nginx-kubernetes-gateway/internal/state/statefakes"
@@ -228,6 +230,24 @@ var _ = Describe("ChangeProcessor", func() {
228230

229231
gw2 = createGatewayWithTLSListener("gateway-2")
230232
})
233+
234+
assertStatuses := func(expected, result state.Statuses) {
235+
sortConditions := func(statuses state.HTTPRouteStatuses) {
236+
for _, status := range statuses {
237+
for _, ps := range status.ParentStatuses {
238+
sort.Slice(ps.Conditions, func(i, j int) bool {
239+
return ps.Conditions[i].Type < ps.Conditions[j].Type
240+
})
241+
}
242+
}
243+
}
244+
245+
sortConditions(expected.HTTPRouteStatuses)
246+
sortConditions(result.HTTPRouteStatuses)
247+
248+
ExpectWithOffset(1, helpers.Diff(expected, result)).To(BeEmpty())
249+
}
250+
231251
When("no upsert has occurred", func() {
232252
It("returns empty configuration and statuses", func() {
233253
changed, conf, statuses := processor.Process(context.TODO())
@@ -279,8 +299,18 @@ var _ = Describe("ChangeProcessor", func() {
279299
{Namespace: "test", Name: "hr-1"}: {
280300
ObservedGeneration: hr1.Generation,
281301
ParentStatuses: map[string]state.ParentStatus{
282-
"listener-80-1": {Attached: false},
283-
"listener-443-1": {Attached: false},
302+
"listener-80-1": {
303+
Conditions: append(
304+
conditions.NewDefaultRouteConditions(),
305+
conditions.NewRouteTODO("GatewayClass is invalid or doesn't exist"),
306+
),
307+
},
308+
"listener-443-1": {
309+
Conditions: append(
310+
conditions.NewDefaultRouteConditions(),
311+
conditions.NewRouteTODO("GatewayClass is invalid or doesn't exist"),
312+
),
313+
},
284314
},
285315
},
286316
},
@@ -289,7 +319,7 @@ var _ = Describe("ChangeProcessor", func() {
289319
changed, conf, statuses := processor.Process(context.TODO())
290320
Expect(changed).To(BeTrue())
291321
Expect(helpers.Diff(expectedConf, conf)).To(BeEmpty())
292-
Expect(helpers.Diff(expectedStatuses, statuses)).To(BeEmpty())
322+
assertStatuses(expectedStatuses, statuses)
293323
})
294324
})
295325
})
@@ -367,8 +397,12 @@ var _ = Describe("ChangeProcessor", func() {
367397
{Namespace: "test", Name: "hr-1"}: {
368398
ObservedGeneration: hr1.Generation,
369399
ParentStatuses: map[string]state.ParentStatus{
370-
"listener-80-1": {Attached: true},
371-
"listener-443-1": {Attached: true},
400+
"listener-80-1": {
401+
Conditions: conditions.NewDefaultRouteConditions(),
402+
},
403+
"listener-443-1": {
404+
Conditions: conditions.NewDefaultRouteConditions(),
405+
},
372406
},
373407
},
374408
},
@@ -377,7 +411,7 @@ var _ = Describe("ChangeProcessor", func() {
377411
changed, conf, statuses := processor.Process(context.TODO())
378412
Expect(changed).To(BeTrue())
379413
Expect(helpers.Diff(expectedConf, conf)).To(BeEmpty())
380-
Expect(helpers.Diff(expectedStatuses, statuses)).To(BeEmpty())
414+
assertStatuses(expectedStatuses, statuses)
381415
})
382416
})
383417
When("the first HTTPRoute without a generation changed is processed", func() {
@@ -465,8 +499,12 @@ var _ = Describe("ChangeProcessor", func() {
465499
{Namespace: "test", Name: "hr-1"}: {
466500
ObservedGeneration: hr1Updated.Generation,
467501
ParentStatuses: map[string]state.ParentStatus{
468-
"listener-80-1": {Attached: true},
469-
"listener-443-1": {Attached: true},
502+
"listener-80-1": {
503+
Conditions: conditions.NewDefaultRouteConditions(),
504+
},
505+
"listener-443-1": {
506+
Conditions: conditions.NewDefaultRouteConditions(),
507+
},
470508
},
471509
},
472510
},
@@ -475,7 +513,7 @@ var _ = Describe("ChangeProcessor", func() {
475513
changed, conf, statuses := processor.Process(context.TODO())
476514
Expect(changed).To(BeTrue())
477515
Expect(helpers.Diff(expectedConf, conf)).To(BeEmpty())
478-
Expect(helpers.Diff(expectedStatuses, statuses)).To(BeEmpty())
516+
assertStatuses(expectedStatuses, statuses)
479517
},
480518
)
481519
})
@@ -564,8 +602,12 @@ var _ = Describe("ChangeProcessor", func() {
564602
{Namespace: "test", Name: "hr-1"}: {
565603
ObservedGeneration: hr1Updated.Generation,
566604
ParentStatuses: map[string]state.ParentStatus{
567-
"listener-80-1": {Attached: true},
568-
"listener-443-1": {Attached: true},
605+
"listener-80-1": {
606+
Conditions: conditions.NewDefaultRouteConditions(),
607+
},
608+
"listener-443-1": {
609+
Conditions: conditions.NewDefaultRouteConditions(),
610+
},
569611
},
570612
},
571613
},
@@ -574,7 +616,7 @@ var _ = Describe("ChangeProcessor", func() {
574616
changed, conf, statuses := processor.Process(context.TODO())
575617
Expect(changed).To(BeTrue())
576618
Expect(helpers.Diff(expectedConf, conf)).To(BeEmpty())
577-
Expect(helpers.Diff(expectedStatuses, statuses)).To(BeEmpty())
619+
assertStatuses(expectedStatuses, statuses)
578620
})
579621
})
580622
When("the GatewayClass update without generation change is processed", func() {
@@ -662,8 +704,12 @@ var _ = Describe("ChangeProcessor", func() {
662704
{Namespace: "test", Name: "hr-1"}: {
663705
ObservedGeneration: hr1Updated.Generation,
664706
ParentStatuses: map[string]state.ParentStatus{
665-
"listener-80-1": {Attached: true},
666-
"listener-443-1": {Attached: true},
707+
"listener-80-1": {
708+
Conditions: conditions.NewDefaultRouteConditions(),
709+
},
710+
"listener-443-1": {
711+
Conditions: conditions.NewDefaultRouteConditions(),
712+
},
667713
},
668714
},
669715
},
@@ -672,7 +718,7 @@ var _ = Describe("ChangeProcessor", func() {
672718
changed, conf, statuses := processor.Process(context.TODO())
673719
Expect(changed).To(BeTrue())
674720
Expect(helpers.Diff(expectedConf, conf)).To(BeEmpty())
675-
Expect(helpers.Diff(expectedStatuses, statuses)).To(BeEmpty())
721+
assertStatuses(expectedStatuses, statuses)
676722
})
677723
})
678724
When("no changes are captured", func() {
@@ -763,8 +809,12 @@ var _ = Describe("ChangeProcessor", func() {
763809
{Namespace: "test", Name: "hr-1"}: {
764810
ObservedGeneration: hr1Updated.Generation,
765811
ParentStatuses: map[string]state.ParentStatus{
766-
"listener-80-1": {Attached: true},
767-
"listener-443-1": {Attached: true},
812+
"listener-80-1": {
813+
Conditions: conditions.NewDefaultRouteConditions(),
814+
},
815+
"listener-443-1": {
816+
Conditions: conditions.NewDefaultRouteConditions(),
817+
},
768818
},
769819
},
770820
},
@@ -773,7 +823,7 @@ var _ = Describe("ChangeProcessor", func() {
773823
changed, conf, statuses := processor.Process(context.TODO())
774824
Expect(changed).To(BeTrue())
775825
Expect(helpers.Diff(expectedConf, conf)).To(BeEmpty())
776-
Expect(helpers.Diff(expectedStatuses, statuses)).To(BeEmpty())
826+
assertStatuses(expectedStatuses, statuses)
777827
})
778828
})
779829
When("the second HTTPRoute is upserted", func() {
@@ -853,15 +903,29 @@ var _ = Describe("ChangeProcessor", func() {
853903
{Namespace: "test", Name: "hr-1"}: {
854904
ObservedGeneration: hr1Updated.Generation,
855905
ParentStatuses: map[string]state.ParentStatus{
856-
"listener-80-1": {Attached: true},
857-
"listener-443-1": {Attached: true},
906+
"listener-80-1": {
907+
Conditions: conditions.NewDefaultRouteConditions(),
908+
},
909+
"listener-443-1": {
910+
Conditions: conditions.NewDefaultRouteConditions(),
911+
},
858912
},
859913
},
860914
{Namespace: "test", Name: "hr-2"}: {
861915
ObservedGeneration: hr2.Generation,
862916
ParentStatuses: map[string]state.ParentStatus{
863-
"listener-80-1": {Attached: false},
864-
"listener-443-1": {Attached: false},
917+
"listener-80-1": {
918+
Conditions: append(
919+
conditions.NewDefaultRouteConditions(),
920+
conditions.NewRouteTODO("Gateway is ignored"),
921+
),
922+
},
923+
"listener-443-1": {
924+
Conditions: append(
925+
conditions.NewDefaultRouteConditions(),
926+
conditions.NewRouteTODO("Gateway is ignored"),
927+
),
928+
},
865929
},
866930
},
867931
},
@@ -870,7 +934,7 @@ var _ = Describe("ChangeProcessor", func() {
870934
changed, conf, statuses := processor.Process(context.TODO())
871935
Expect(changed).To(BeTrue())
872936
Expect(helpers.Diff(expectedConf, conf)).To(BeEmpty())
873-
Expect(helpers.Diff(expectedStatuses, statuses)).To(BeEmpty())
937+
assertStatuses(expectedStatuses, statuses)
874938
})
875939
})
876940
When("the first Gateway is deleted", func() {
@@ -949,8 +1013,12 @@ var _ = Describe("ChangeProcessor", func() {
9491013
{Namespace: "test", Name: "hr-2"}: {
9501014
ObservedGeneration: hr2.Generation,
9511015
ParentStatuses: map[string]state.ParentStatus{
952-
"listener-80-1": {Attached: true},
953-
"listener-443-1": {Attached: true},
1016+
"listener-80-1": {
1017+
Conditions: conditions.NewDefaultRouteConditions(),
1018+
},
1019+
"listener-443-1": {
1020+
Conditions: conditions.NewDefaultRouteConditions(),
1021+
},
9541022
},
9551023
},
9561024
},
@@ -959,7 +1027,7 @@ var _ = Describe("ChangeProcessor", func() {
9591027
changed, conf, statuses := processor.Process(context.TODO())
9601028
Expect(changed).To(BeTrue())
9611029
Expect(helpers.Diff(expectedConf, conf)).To(BeEmpty())
962-
Expect(helpers.Diff(expectedStatuses, statuses)).To(BeEmpty())
1030+
assertStatuses(expectedStatuses, statuses)
9631031
})
9641032
})
9651033
When("the second HTTPRoute is deleted", func() {
@@ -1003,7 +1071,7 @@ var _ = Describe("ChangeProcessor", func() {
10031071
changed, conf, statuses := processor.Process(context.TODO())
10041072
Expect(changed).To(BeTrue())
10051073
Expect(helpers.Diff(expectedConf, conf)).To(BeEmpty())
1006-
Expect(helpers.Diff(expectedStatuses, statuses)).To(BeEmpty())
1074+
assertStatuses(expectedStatuses, statuses)
10071075
})
10081076
})
10091077
When("the GatewayClass is deleted", func() {
@@ -1035,7 +1103,7 @@ var _ = Describe("ChangeProcessor", func() {
10351103
changed, conf, statuses := processor.Process(context.TODO())
10361104
Expect(changed).To(BeTrue())
10371105
Expect(helpers.Diff(expectedConf, conf)).To(BeEmpty())
1038-
Expect(helpers.Diff(expectedStatuses, statuses)).To(BeEmpty())
1106+
assertStatuses(expectedStatuses, statuses)
10391107
})
10401108
})
10411109
When("the second Gateway is deleted", func() {
@@ -1054,7 +1122,7 @@ var _ = Describe("ChangeProcessor", func() {
10541122
changed, conf, statuses := processor.Process(context.TODO())
10551123
Expect(changed).To(BeTrue())
10561124
Expect(helpers.Diff(expectedConf, conf)).To(BeEmpty())
1057-
Expect(helpers.Diff(expectedStatuses, statuses)).To(BeEmpty())
1125+
assertStatuses(expectedStatuses, statuses)
10581126
})
10591127
})
10601128
When("the first HTTPRoute is deleted", func() {
@@ -1073,7 +1141,7 @@ var _ = Describe("ChangeProcessor", func() {
10731141
changed, conf, statuses := processor.Process(context.TODO())
10741142
Expect(changed).To(BeTrue())
10751143
Expect(helpers.Diff(expectedConf, conf)).To(BeEmpty())
1076-
Expect(helpers.Diff(expectedStatuses, statuses)).To(BeEmpty())
1144+
assertStatuses(expectedStatuses, statuses)
10771145
})
10781146
})
10791147
})
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package conditions
2+
3+
import (
4+
"fmt"
5+
6+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
7+
"sigs.k8s.io/gateway-api/apis/v1beta1"
8+
)
9+
10+
// RouteCondition defines a condition to be reported in the status of an HTTPRoute.
11+
type RouteCondition struct {
12+
Type v1beta1.RouteConditionType
13+
Status metav1.ConditionStatus
14+
Reason v1beta1.RouteConditionReason
15+
Message string
16+
}
17+
18+
// DeduplicateRouteConditions removes duplicate conditions based on the condition type.
19+
// The last condition wins.
20+
func DeduplicateRouteConditions(conds []RouteCondition) []RouteCondition {
21+
uniqueConds := make(map[v1beta1.RouteConditionType]RouteCondition)
22+
23+
for _, cond := range conds {
24+
uniqueConds[cond.Type] = cond
25+
}
26+
27+
result := make([]RouteCondition, 0, len(uniqueConds))
28+
29+
for _, cond := range uniqueConds {
30+
result = append(result, cond)
31+
}
32+
33+
return result
34+
}
35+
36+
// NewDefaultRouteConditions returns the default conditions that must be present in the status of an HTTPRoute.
37+
func NewDefaultRouteConditions() []RouteCondition {
38+
return []RouteCondition{
39+
NewRouteAccepted(),
40+
}
41+
}
42+
43+
// NewRouteNoMatchingListenerHostname returns a RouteCondition that indicates that the hostname of the listener
44+
// does not match the hostnames of the HTTPRoute.
45+
func NewRouteNoMatchingListenerHostname() RouteCondition {
46+
return RouteCondition{
47+
Type: v1beta1.RouteConditionAccepted,
48+
Status: metav1.ConditionFalse,
49+
Reason: v1beta1.RouteReasonNoMatchingListenerHostname,
50+
Message: "Listener hostname does not match the HTTPRoute hostnames",
51+
}
52+
}
53+
54+
// NewRouteAccepted returns a RouteCondition that indicates that the HTTPRoute is accepted.
55+
func NewRouteAccepted() RouteCondition {
56+
return RouteCondition{
57+
Type: v1beta1.RouteConditionAccepted,
58+
Status: metav1.ConditionTrue,
59+
Reason: v1beta1.RouteReasonAccepted,
60+
Message: "The route is accepted",
61+
}
62+
}
63+
64+
// NewRouteTODO returns a RouteCondition that can be used as a placeholder for a condition that is not yet implemented.
65+
func NewRouteTODO(msg string) RouteCondition {
66+
return RouteCondition{
67+
Type: "TODO",
68+
Status: metav1.ConditionTrue,
69+
Reason: "TODO",
70+
Message: fmt.Sprintf("The condition for this has not been implemented yet: %s", msg),
71+
}
72+
}

0 commit comments

Comments
 (0)