Skip to content

Commit ae48c2b

Browse files
authored
Namespace resources are tracked by graph (#1320)
Refactor code such that Namespace resources are tracked by the graph, thus letting us remove dependency on using the Capturer to do so. Problem: The Capturer is currently tracking relevant Namespaces and we would like to instead use the Graph. Solution: Refactor codebase so Graph tracks referenced namespaces and remove dependency on the Capturer.
1 parent 40e234b commit ae48c2b

File tree

8 files changed

+596
-263
lines changed

8 files changed

+596
-263
lines changed

internal/mode/static/state/change_processor.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ func NewChangeProcessorImpl(cfg ChangeProcessorConfig) *ChangeProcessorImpl {
146146
{
147147
gvk: extractGVK(&apiv1.Namespace{}),
148148
store: newObjectStoreMapAdapter(clusterStore.Namespaces),
149-
predicate: nil,
149+
predicate: funcPredicate{stateChanged: isReferenced},
150150
},
151151
{
152152
gvk: extractGVK(&apiv1.Service{}),

internal/mode/static/state/change_processor_test.go

Lines changed: 141 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1293,53 +1293,166 @@ var _ = Describe("ChangeProcessor", func() {
12931293
})
12941294
})
12951295
})
1296-
Describe("namespace changes", func() {
1297-
When("namespace is linked via label selectors", func() {
1298-
It("triggers an update when labels are removed", func() {
1299-
ns := &apiv1.Namespace{
1300-
ObjectMeta: metav1.ObjectMeta{
1301-
Name: "ns",
1302-
Labels: map[string]string{
1303-
"app": "allowed",
1304-
},
1296+
Describe("namespace changes", Ordered, func() {
1297+
var (
1298+
ns, nsDifferentLabels, nsNoLabels *apiv1.Namespace
1299+
gw *v1.Gateway
1300+
)
1301+
1302+
BeforeAll(func() {
1303+
ns = &apiv1.Namespace{
1304+
ObjectMeta: metav1.ObjectMeta{
1305+
Name: "ns",
1306+
Labels: map[string]string{
1307+
"app": "allowed",
13051308
},
1306-
}
1307-
gw := &v1.Gateway{
1308-
ObjectMeta: metav1.ObjectMeta{
1309-
Name: "gw",
1309+
},
1310+
}
1311+
nsDifferentLabels = &apiv1.Namespace{
1312+
ObjectMeta: metav1.ObjectMeta{
1313+
Name: "ns-different-labels",
1314+
Labels: map[string]string{
1315+
"oranges": "bananas",
13101316
},
1311-
Spec: v1.GatewaySpec{
1312-
Listeners: []v1.Listener{
1313-
{
1314-
AllowedRoutes: &v1.AllowedRoutes{
1315-
Namespaces: &v1.RouteNamespaces{
1316-
From: helpers.GetPointer(v1.NamespacesFromSelector),
1317-
Selector: &metav1.LabelSelector{
1318-
MatchLabels: map[string]string{
1319-
"app": "allowed",
1320-
},
1317+
},
1318+
}
1319+
nsNoLabels = &apiv1.Namespace{
1320+
ObjectMeta: metav1.ObjectMeta{
1321+
Name: "no-labels",
1322+
},
1323+
}
1324+
gw = &v1.Gateway{
1325+
ObjectMeta: metav1.ObjectMeta{
1326+
Name: "gw",
1327+
},
1328+
Spec: v1.GatewaySpec{
1329+
GatewayClassName: gcName,
1330+
Listeners: []v1.Listener{
1331+
{
1332+
Port: 80,
1333+
Protocol: v1.HTTPProtocolType,
1334+
AllowedRoutes: &v1.AllowedRoutes{
1335+
Namespaces: &v1.RouteNamespaces{
1336+
From: helpers.GetPointer(v1.NamespacesFromSelector),
1337+
Selector: &metav1.LabelSelector{
1338+
MatchLabels: map[string]string{
1339+
"app": "allowed",
13211340
},
13221341
},
13231342
},
13241343
},
13251344
},
13261345
},
1327-
}
1346+
},
1347+
}
1348+
processor = state.NewChangeProcessorImpl(state.ChangeProcessorConfig{
1349+
GatewayCtlrName: controllerName,
1350+
GatewayClassName: gcName,
1351+
RelationshipCapturer: relationship.NewCapturerImpl(),
1352+
Logger: zap.New(),
1353+
Validators: createAlwaysValidValidators(),
1354+
Scheme: createScheme(),
1355+
})
1356+
processor.CaptureUpsertChange(gc)
1357+
processor.CaptureUpsertChange(gw)
1358+
processor.Process()
1359+
})
13281360

1329-
processor.CaptureUpsertChange(gw)
1361+
When("a namespace is created that is not linked to a listener", func() {
1362+
It("does not trigger an update", func() {
1363+
processor.CaptureUpsertChange(nsNoLabels)
1364+
changed, _ := processor.Process()
1365+
Expect(changed).To(BeFalse())
1366+
})
1367+
})
1368+
When("a namespace is created that is linked to a listener", func() {
1369+
It("triggers an update", func() {
13301370
processor.CaptureUpsertChange(ns)
1371+
changed, _ := processor.Process()
1372+
Expect(changed).To(BeTrue())
1373+
})
1374+
})
1375+
When("a namespace is deleted that is not linked to a listener", func() {
1376+
It("does not trigger an update", func() {
1377+
processor.CaptureDeleteChange(nsNoLabels, types.NamespacedName{Name: "no-labels"})
1378+
changed, _ := processor.Process()
1379+
Expect(changed).To(BeFalse())
1380+
})
1381+
})
1382+
When("a namespace is deleted that is linked to a listener", func() {
1383+
It("triggers an update", func() {
1384+
processor.CaptureDeleteChange(ns, types.NamespacedName{Name: "ns"})
1385+
changed, _ := processor.Process()
1386+
Expect(changed).To(BeTrue())
1387+
})
1388+
})
1389+
When("a namespace that is not linked to a listener has its labels changed to match a listener", func() {
1390+
It("triggers an update", func() {
1391+
processor.CaptureUpsertChange(nsDifferentLabels)
1392+
changed, _ := processor.Process()
1393+
Expect(changed).To(BeFalse())
13311394

1395+
nsDifferentLabels.Labels = map[string]string{
1396+
"app": "allowed",
1397+
}
1398+
processor.CaptureUpsertChange(nsDifferentLabels)
1399+
changed, _ = processor.Process()
1400+
Expect(changed).To(BeTrue())
1401+
})
1402+
})
1403+
When("a namespace that is linked to a listener has its labels changed to no longer match a listener", func() {
1404+
It("triggers an update", func() {
1405+
nsDifferentLabels.Labels = map[string]string{
1406+
"oranges": "bananas",
1407+
}
1408+
processor.CaptureUpsertChange(nsDifferentLabels)
1409+
changed, _ := processor.Process()
1410+
Expect(changed).To(BeTrue())
1411+
})
1412+
})
1413+
When("a gateway changes its listener's labels", func() {
1414+
It("triggers an update when a namespace that matches the new labels is created", func() {
1415+
gwChangedLabel := gw.DeepCopy()
1416+
gwChangedLabel.Spec.Listeners[0].AllowedRoutes.Namespaces.Selector.MatchLabels = map[string]string{
1417+
"oranges": "bananas",
1418+
}
1419+
gwChangedLabel.Generation++
1420+
processor.CaptureUpsertChange(gwChangedLabel)
13321421
changed, _ := processor.Process()
13331422
Expect(changed).To(BeTrue())
13341423

1335-
newNS := ns.DeepCopy()
1336-
newNS.Labels = nil
1337-
processor.CaptureUpsertChange(newNS)
1424+
// After changing the gateway's labels and generation, the processor should be marked to update
1425+
// the nginx configuration and build a new graph. When processor.Process() gets called,
1426+
// the nginx configuration gets updated and a new graph is built with an updated
1427+
// referencedNamespaces. Thus, when the namespace "ns" is upserted with labels that no longer match
1428+
// the new labels on the gateway, it would not trigger a change as the namespace would no longer
1429+
// be in the updated referencedNamespaces and the labels no longer match the new labels on the
1430+
// gateway.
1431+
processor.CaptureUpsertChange(ns)
1432+
changed, _ = processor.Process()
1433+
Expect(changed).To(BeFalse())
13381434

1435+
processor.CaptureUpsertChange(nsDifferentLabels)
13391436
changed, _ = processor.Process()
13401437
Expect(changed).To(BeTrue())
13411438
})
13421439
})
1440+
When("a namespace that is not linked to a listener has its labels removed", func() {
1441+
It("does not trigger an update", func() {
1442+
ns.Labels = nil
1443+
processor.CaptureUpsertChange(ns)
1444+
changed, _ := processor.Process()
1445+
Expect(changed).To(BeFalse())
1446+
})
1447+
})
1448+
When("a namespace that is linked to a listener has its labels removed", func() {
1449+
It("triggers an update when labels are removed", func() {
1450+
nsDifferentLabels.Labels = nil
1451+
processor.CaptureUpsertChange(nsDifferentLabels)
1452+
changed, _ := processor.Process()
1453+
Expect(changed).To(BeTrue())
1454+
})
1455+
})
13431456
})
13441457
})
13451458

internal/mode/static/state/graph/graph.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,22 +44,40 @@ type Graph struct {
4444
// in the cluster. We need such entries so that we can query the Graph to determine if a Secret is referenced
4545
// by the Gateway, including the case when the Secret is newly created.
4646
ReferencedSecrets map[types.NamespacedName]*Secret
47+
// ReferencedNamespaces includes Namespaces with labels that match the Gateway Listener's label selector.
48+
ReferencedNamespaces map[types.NamespacedName]*v1.Namespace
4749
}
4850

4951
// ProtectedPorts are the ports that may not be configured by a listener with a descriptive name of each port.
5052
type ProtectedPorts map[int32]string
5153

5254
// IsReferenced returns true if the Graph references the resource.
5355
func (g *Graph) IsReferenced(resourceType client.Object, nsname types.NamespacedName) bool {
54-
// FIMXE(pleshakov): For now, only works with Secrets.
55-
// Support EndpointSlices and Namespaces so that we can remove relationship.Capturer and use the Graph
56+
// FIMXE(bjee19): For now, only works with Secrets and Namespaces.
57+
// Support EndpointSlices so that we can remove relationship.Capturer and use the Graph
5658
// as source to determine the relationships.
5759
// See https://github.com/nginxinc/nginx-gateway-fabric/issues/824
5860

59-
switch resourceType.(type) {
61+
switch obj := resourceType.(type) {
6062
case *v1.Secret:
6163
_, exists := g.ReferencedSecrets[nsname]
6264
return exists
65+
case *v1.Namespace:
66+
// `existed` is needed as it checks the graph's ReferencedNamespaces which stores all the namespaces that
67+
// match the Gateway listener's label selector when the graph was created. This covers the case when
68+
// a Namespace changes its label so it no longer matches a Gateway listener's label selector, but because
69+
// it was in the graph's ReferencedNamespaces we know that the Graph did reference the Namespace.
70+
//
71+
// However, if there is a Namespace which changes its label (previously it did not match) to match a Gateway
72+
// listener's label selector, it will not be in the current graph's ReferencedNamespaces until it is rebuilt
73+
// and thus not be caught in `existed`. Therefore, we need `exists` to check the graph's Gateway and see if the
74+
// new Namespace actually matches any of the Gateway listener's label selector.
75+
//
76+
// `exists` does not cover the case highlighted above by `existed` and vice versa so both are needed.
77+
78+
_, existed := g.ReferencedNamespaces[nsname]
79+
exists := isNamespaceReferenced(obj, g.Gateway)
80+
return existed || exists
6381
default:
6482
return false
6583
}
@@ -92,13 +110,16 @@ func BuildGraph(
92110
bindRoutesToListeners(routes, gw, state.Namespaces)
93111
addBackendRefsToRouteRules(routes, refGrantResolver, state.Services)
94112

113+
referencedNamespaces := buildReferencedNamespaces(state.Namespaces, gw)
114+
95115
g := &Graph{
96116
GatewayClass: gc,
97117
Gateway: gw,
98118
Routes: routes,
99119
IgnoredGatewayClasses: processedGwClasses.Ignored,
100120
IgnoredGateways: processedGws.Ignored,
101121
ReferencedSecrets: secretResolver.getResolvedSecrets(),
122+
ReferencedNamespaces: referencedNamespaces,
102123
}
103124

104125
return g

0 commit comments

Comments
 (0)