Skip to content

Commit 14ad403

Browse files
committed
AllowedRoutes support for Listeners (nginx#721)
Add support for specifying AllowedRoutes in Listeners. A user can now allow/disallow routes based on namespace. Either all namespaces, same namespace, or label selectors can be used to determine which routes are allowed.
1 parent 2972cec commit 14ad403

File tree

18 files changed

+960
-64
lines changed

18 files changed

+960
-64
lines changed

deploy/manifests/rbac.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ rules:
1212
- apiGroups:
1313
- ""
1414
resources:
15+
- namespaces
1516
- services
1617
- secrets
1718
verbs:

docs/gateway-api-compatibility.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ Fields:
6464
* `mode` - partially supported. Allowed value: `Terminate`.
6565
* `certificateRefs` - partially supported. The TLS certificate and key must be stored in a Secret resource of type `kubernetes.io/tls` in the same namespace as the Gateway resource. Only a single reference is supported. You must deploy the Secret before the Gateway resource. Secret rotation (watching for updates) is not supported.
6666
* `options` - not supported.
67-
* `allowedRoutes` - not supported.
67+
* `allowedRoutes` - supported.
6868
* `addresses` - not supported.
6969
* `status`
7070
* `addresses` - Pod IPAddress supported.
@@ -122,6 +122,7 @@ Fields:
122122
* `Accepted/True/Accepted`
123123
* `Accepted/False/NoMatchingListenerHostname`
124124
* `Accepted/False/NoMatchingParent`
125+
* `Accepted/False/NotAllowedByListeners`
125126
* `Accepted/False/UnsupportedValue`: Custom reason for when the HTTPRoute includes an invalid or unsupported value.
126127
* `Accepted/False/InvalidListener`: Custom reason for when the HTTPRoute references an invalid listener.
127128
* `Accepted/False/GatewayNotProgrammed`: Custom reason for when the Gateway is not Programmed. HTTPRoute may be valid and configured, but will maintain this status as long as the Gateway is not Programmed.

internal/events/handler.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ func (h *EventHandlerImpl) propagateUpsert(e *UpsertEvent) {
128128
h.cfg.Processor.CaptureUpsertChange(r)
129129
case *apiv1.Service:
130130
h.cfg.Processor.CaptureUpsertChange(r)
131+
case *apiv1.Namespace:
132+
h.cfg.Processor.CaptureUpsertChange(r)
131133
case *apiv1.Secret:
132134
// FIXME(kate-osborn): need to handle certificate rotation
133135
// https://github.com/nginxinc/nginx-kubernetes-gateway/issues/553
@@ -149,6 +151,8 @@ func (h *EventHandlerImpl) propagateDelete(e *DeleteEvent) {
149151
h.cfg.Processor.CaptureDeleteChange(e.Type, e.NamespacedName)
150152
case *apiv1.Service:
151153
h.cfg.Processor.CaptureDeleteChange(e.Type, e.NamespacedName)
154+
case *apiv1.Namespace:
155+
h.cfg.Processor.CaptureDeleteChange(e.Type, e.NamespacedName)
152156
case *apiv1.Secret:
153157
// FIXME(kate-osborn): make sure that affected servers are updated
154158
// https://github.com/nginxinc/nginx-kubernetes-gateway/issues/553

internal/manager/manager.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,12 @@ func Start(cfg config.Config) error {
112112
controller.WithFieldIndices(index.CreateEndpointSliceFieldIndices()),
113113
},
114114
},
115+
{
116+
objectType: &apiv1.Namespace{},
117+
options: []controller.Option{
118+
controller.WithK8sPredicate(k8spredicate.LabelChangedPredicate{}),
119+
},
120+
},
115121
}
116122

117123
ctx := ctlr.SetupSignalHandler()
@@ -195,6 +201,7 @@ func prepareFirstEventBatchPreparerArgs(
195201
objectLists := []client.ObjectList{
196202
&apiv1.ServiceList{},
197203
&apiv1.SecretList{},
204+
&apiv1.NamespaceList{},
198205
&discoveryV1.EndpointSliceList{},
199206
&gatewayv1beta1.HTTPRouteList{},
200207
}

internal/manager/manager_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ func TestPrepareFirstEventBatchPreparerArgs(t *testing.T) {
3030
expectedObjectLists: []client.ObjectList{
3131
&apiv1.ServiceList{},
3232
&apiv1.SecretList{},
33+
&apiv1.NamespaceList{},
3334
&discoveryV1.EndpointSliceList{},
3435
&gatewayv1beta1.HTTPRouteList{},
3536
&gatewayv1beta1.GatewayList{},
@@ -48,6 +49,7 @@ func TestPrepareFirstEventBatchPreparerArgs(t *testing.T) {
4849
expectedObjectLists: []client.ObjectList{
4950
&apiv1.ServiceList{},
5051
&apiv1.SecretList{},
52+
&apiv1.NamespaceList{},
5153
&discoveryV1.EndpointSliceList{},
5254
&gatewayv1beta1.HTTPRouteList{},
5355
},

internal/state/change_processor.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ type ChangeProcessorConfig struct {
6060
Logger logr.Logger
6161
// EventRecorder records events for Kubernetes resources.
6262
EventRecorder record.EventRecorder
63-
// Scheme is the a Kubernetes scheme.
63+
// Scheme is the Kubernetes scheme.
6464
Scheme *runtime.Scheme
6565
// GatewayCtlrName is the name of the Gateway controller.
6666
GatewayCtlrName string
@@ -89,6 +89,7 @@ func NewChangeProcessorImpl(cfg ChangeProcessorConfig) *ChangeProcessorImpl {
8989
Gateways: make(map[types.NamespacedName]*v1beta1.Gateway),
9090
HTTPRoutes: make(map[types.NamespacedName]*v1beta1.HTTPRoute),
9191
Services: make(map[types.NamespacedName]*apiv1.Service),
92+
Namespaces: make(map[types.NamespacedName]*apiv1.Namespace),
9293
}
9394

9495
extractGVK := func(obj client.Object) schema.GroupVersionKind {
@@ -118,6 +119,11 @@ func NewChangeProcessorImpl(cfg ChangeProcessorConfig) *ChangeProcessorImpl {
118119
store: newObjectStoreMapAdapter(clusterStore.HTTPRoutes),
119120
trackUpsertDelete: true,
120121
},
122+
{
123+
gvk: extractGVK(&apiv1.Namespace{}),
124+
store: newObjectStoreMapAdapter(clusterStore.Namespaces),
125+
trackUpsertDelete: false,
126+
},
121127
{
122128
gvk: extractGVK(&apiv1.Service{}),
123129
store: newObjectStoreMapAdapter(clusterStore.Services),

internal/state/change_processor_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -924,6 +924,54 @@ var _ = Describe("ChangeProcessor", func() {
924924
})
925925
})
926926
})
927+
Describe("namespace changes", func() {
928+
When("namespace is linked via label selectors", func() {
929+
It("triggers an update when labels are removed", func() {
930+
ns := &apiv1.Namespace{
931+
ObjectMeta: metav1.ObjectMeta{
932+
Name: "ns",
933+
Labels: map[string]string{
934+
"app": "allowed",
935+
},
936+
},
937+
}
938+
gw := &v1beta1.Gateway{
939+
ObjectMeta: metav1.ObjectMeta{
940+
Name: "gw",
941+
},
942+
Spec: v1beta1.GatewaySpec{
943+
Listeners: []v1beta1.Listener{
944+
{
945+
AllowedRoutes: &v1beta1.AllowedRoutes{
946+
Namespaces: &v1beta1.RouteNamespaces{
947+
From: helpers.GetPointer(v1beta1.NamespacesFromSelector),
948+
Selector: &metav1.LabelSelector{
949+
MatchLabels: map[string]string{
950+
"app": "allowed",
951+
},
952+
},
953+
},
954+
},
955+
},
956+
},
957+
},
958+
}
959+
960+
processor.CaptureUpsertChange(gw)
961+
processor.CaptureUpsertChange(ns)
962+
963+
changed, _ := processor.Process()
964+
Expect(changed).To(BeTrue())
965+
966+
newNS := ns.DeepCopy()
967+
newNS.Labels = nil
968+
processor.CaptureUpsertChange(newNS)
969+
970+
changed, _ = processor.Process()
971+
Expect(changed).To(BeTrue())
972+
})
973+
})
974+
})
927975
})
928976

929977
Describe("Ensuring non-changing changes don't override previously changing changes", func() {

internal/state/conditions/conditions.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,17 @@ func NewDefaultRouteConditions() []Condition {
109109
}
110110
}
111111

112+
// NewRouteNotAllowedByListeners returns a Condition that indicates that the HTTPRoute is not allowed by
113+
// any listener.
114+
func NewRouteNotAllowedByListeners() Condition {
115+
return Condition{
116+
Type: string(v1beta1.RouteConditionAccepted),
117+
Status: metav1.ConditionFalse,
118+
Reason: string(v1beta1.RouteReasonNotAllowedByListeners),
119+
Message: "HTTPRoute is not allowed by any listener",
120+
}
121+
}
122+
112123
// NewRouteNoMatchingListenerHostname returns a Condition that indicates that the hostname of the listener
113124
// does not match the hostnames of the HTTPRoute.
114125
func NewRouteNoMatchingListenerHostname() Condition {

internal/state/graph/gateway_listener.go

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package graph
33
import (
44
"fmt"
55

6+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
7+
"k8s.io/apimachinery/pkg/labels"
68
"k8s.io/apimachinery/pkg/types"
79
"k8s.io/apimachinery/pkg/util/validation/field"
810
"sigs.k8s.io/gateway-api/apis/v1beta1"
@@ -19,6 +21,8 @@ type Listener struct {
1921
// Routes holds the routes attached to the Listener.
2022
// Only valid routes are attached.
2123
Routes map[types.NamespacedName]*Route
24+
// AllowedRouteLabelSelector is the label selector for this Listener's allowed routes, if defined.
25+
AllowedRouteLabelSelector labels.Selector
2226
// SecretPath is the path to the secret on disk.
2327
SecretPath string
2428
// Conditions holds the conditions of the Listener.
@@ -78,6 +82,8 @@ func newListenerConfiguratorFactory(
7882
},
7983
http: &listenerConfigurator{
8084
validators: []listenerValidator{
85+
validateListenerAllowedRouteKind,
86+
validateListenerLabelSelector,
8187
validateListenerHostname,
8288
validateHTTPListener,
8389
},
@@ -87,6 +93,8 @@ func newListenerConfiguratorFactory(
8793
},
8894
https: &listenerConfigurator{
8995
validators: []listenerValidator{
96+
validateListenerAllowedRouteKind,
97+
validateListenerLabelSelector,
9098
validateListenerHostname,
9199
createHTTPSListenerValidator(gw.Namespace),
92100
},
@@ -135,6 +143,16 @@ func (c *listenerConfigurator) configure(listener v1beta1.Listener) *Listener {
135143
conds = append(conds, validator(listener)...)
136144
}
137145

146+
var allowedRouteSelector labels.Selector
147+
if selector := GetAllowedRouteLabelSelector(listener); selector != nil {
148+
var err error
149+
allowedRouteSelector, err = metav1.LabelSelectorAsSelector(selector)
150+
if err != nil {
151+
msg := fmt.Sprintf("invalid label selector: %s", err.Error())
152+
conds = append(conds, conditions.NewListenerUnsupportedValue(msg))
153+
}
154+
}
155+
138156
if len(conds) > 0 {
139157
return &Listener{
140158
Source: listener,
@@ -144,9 +162,10 @@ func (c *listenerConfigurator) configure(listener v1beta1.Listener) *Listener {
144162
}
145163

146164
l := &Listener{
147-
Source: listener,
148-
Routes: make(map[types.NamespacedName]*Route),
149-
Valid: true,
165+
Source: listener,
166+
AllowedRouteLabelSelector: allowedRouteSelector,
167+
Routes: make(map[types.NamespacedName]*Route),
168+
Valid: true,
150169
}
151170

152171
// resolvers might add different conditions to the listener, so we run them all.
@@ -182,6 +201,45 @@ func validateListenerHostname(listener v1beta1.Listener) []conditions.Condition
182201
return nil
183202
}
184203

204+
func validateListenerAllowedRouteKind(listener v1beta1.Listener) []conditions.Condition {
205+
validHTTPRouteKind := func(kind v1beta1.RouteGroupKind) bool {
206+
if kind.Kind != v1beta1.Kind("HTTPRoute") {
207+
return false
208+
}
209+
if kind.Group == nil || *kind.Group != v1beta1.GroupName {
210+
return false
211+
}
212+
return true
213+
}
214+
215+
switch listener.Protocol {
216+
case v1beta1.HTTPProtocolType, v1beta1.HTTPSProtocolType:
217+
if listener.AllowedRoutes != nil {
218+
for _, kind := range listener.AllowedRoutes.Kinds {
219+
if !validHTTPRouteKind(kind) {
220+
msg := fmt.Sprintf("Unsupported route kind \"%s/%s\"", *kind.Group, kind.Kind)
221+
return []conditions.Condition{conditions.NewListenerUnsupportedValue(msg)}
222+
}
223+
}
224+
}
225+
}
226+
227+
return nil
228+
}
229+
230+
func validateListenerLabelSelector(listener v1beta1.Listener) []conditions.Condition {
231+
if listener.AllowedRoutes != nil &&
232+
listener.AllowedRoutes.Namespaces != nil &&
233+
listener.AllowedRoutes.Namespaces.From != nil &&
234+
*listener.AllowedRoutes.Namespaces.From == v1beta1.NamespacesFromSelector &&
235+
listener.AllowedRoutes.Namespaces.Selector == nil {
236+
msg := "Listener's AllowedRoutes Selector must be set when From is set to type Selector"
237+
return []conditions.Condition{conditions.NewListenerUnsupportedValue(msg)}
238+
}
239+
240+
return nil
241+
}
242+
185243
func validateHTTPListener(listener v1beta1.Listener) []conditions.Condition {
186244
if listener.Port != 80 {
187245
path := field.NewPath("port")
@@ -314,3 +372,14 @@ func createExternalReferencesForTLSSecretsResolver(
314372
}
315373
}
316374
}
375+
376+
// GetAllowedRouteLabelSelector returns a listener's AllowedRoutes label selector if it exists.
377+
func GetAllowedRouteLabelSelector(l v1beta1.Listener) *metav1.LabelSelector {
378+
if l.AllowedRoutes != nil && l.AllowedRoutes.Namespaces != nil {
379+
if *l.AllowedRoutes.Namespaces.From == v1beta1.NamespacesFromSelector && l.AllowedRoutes.Namespaces.Selector != nil {
380+
return l.AllowedRoutes.Namespaces.Selector
381+
}
382+
}
383+
384+
return nil
385+
}

0 commit comments

Comments
 (0)