Skip to content

Commit ad133d8

Browse files
authored
Only include relevant snippets (#3586)
Problem: SnippetsFilters are being included in NGINX configuration for all gateways regardless of whether routes attached to those specific gateways actually referenced the filters. Solution: Implemented gateway-specific scoping by adding GetReferencedSnippetsFilters() method that returns only SnippetsFilters referenced by routes attached to the current gateway.
1 parent 4ccd74f commit ad133d8

File tree

4 files changed

+285
-24
lines changed

4 files changed

+285
-24
lines changed

internal/controller/state/dataplane/configuration.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ func BuildConfiguration(
4444
return config
4545
}
4646

47-
baseHTTPConfig := buildBaseHTTPConfig(g, gateway)
47+
// Get SnippetsFilters that are specifically referenced by routes attached to this gateway
48+
gatewaySnippetsFilters := gateway.GetReferencedSnippetsFilters(g.Routes, g.SnippetsFilters)
49+
50+
baseHTTPConfig := buildBaseHTTPConfig(gateway, gatewaySnippetsFilters)
4851

4952
httpServers, sslServers := buildServers(gateway)
5053
backendGroups := buildBackendGroups(append(httpServers, sslServers...))
@@ -77,7 +80,7 @@ func BuildConfiguration(
7780
BaseHTTPConfig: baseHTTPConfig,
7881
Logging: buildLogging(gateway),
7982
NginxPlus: nginxPlus,
80-
MainSnippets: buildSnippetsForContext(g.SnippetsFilters, ngfAPIv1alpha1.NginxContextMain),
83+
MainSnippets: buildSnippetsForContext(gatewaySnippetsFilters, ngfAPIv1alpha1.NginxContextMain),
8184
AuxiliarySecrets: buildAuxiliarySecrets(g.PlusSecrets),
8285
}
8386

@@ -963,12 +966,15 @@ func CreateRatioVarName(ratio int32) string {
963966
}
964967

965968
// buildBaseHTTPConfig generates the base http context config that should be applied to all servers.
966-
func buildBaseHTTPConfig(g *graph.Graph, gateway *graph.Gateway) BaseHTTPConfig {
969+
func buildBaseHTTPConfig(
970+
gateway *graph.Gateway,
971+
gatewaySnippetsFilters map[types.NamespacedName]*graph.SnippetsFilter,
972+
) BaseHTTPConfig {
967973
baseConfig := BaseHTTPConfig{
968974
// HTTP2 should be enabled by default
969975
HTTP2: true,
970976
IPFamily: Dual,
971-
Snippets: buildSnippetsForContext(g.SnippetsFilters, ngfAPIv1alpha1.NginxContextHTTP),
977+
Snippets: buildSnippetsForContext(gatewaySnippetsFilters, ngfAPIv1alpha1.NginxContextHTTP),
972978
}
973979

974980
// safe to access EffectiveNginxProxy since we only call this function when the Gateway is not nil.

internal/controller/state/dataplane/configuration_test.go

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2462,31 +2462,17 @@ func TestBuildConfiguration(t *testing.T) {
24622462
return g
24632463
}),
24642464
expConf: getModifiedExpectedConfiguration(func(conf Configuration) Configuration {
2465-
conf.MainSnippets = []Snippet{
2466-
{
2467-
Name: createSnippetName(
2468-
ngfAPIv1alpha1.NginxContextMain,
2469-
client.ObjectKeyFromObject(sf1.Source),
2470-
),
2471-
Contents: "main snippet",
2472-
},
2473-
}
2474-
conf.BaseHTTPConfig.Snippets = []Snippet{
2475-
{
2476-
Name: createSnippetName(
2477-
ngfAPIv1alpha1.NginxContextHTTP,
2478-
client.ObjectKeyFromObject(sf1.Source),
2479-
),
2480-
Contents: "http snippet",
2481-
},
2482-
}
2465+
// With proper scoping, no snippets should be included since no routes
2466+
// attached to this gateway reference the SnippetsFilters
2467+
conf.MainSnippets = nil // nil - no snippets should be included
2468+
conf.BaseHTTPConfig.Snippets = nil // nil - no snippets should be included
24832469
conf.HTTPServers = []VirtualServer{}
24842470
conf.SSLServers = []VirtualServer{}
24852471
conf.SSLKeyPairs = map[SSLKeyPairID]SSLKeyPair{}
24862472

24872473
return conf
24882474
}),
2489-
msg: "SnippetsFilters with main and http snippet",
2475+
msg: "SnippetsFilters scoped per gateway - no routes reference SnippetsFilters",
24902476
},
24912477
{
24922478
graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph {
@@ -4488,7 +4474,10 @@ func TestBuildRewriteIPSettings(t *testing.T) {
44884474
t.Run(tc.msg, func(t *testing.T) {
44894475
t.Parallel()
44904476
g := NewWithT(t)
4491-
baseConfig := buildBaseHTTPConfig(tc.g, tc.g.Gateways[types.NamespacedName{}])
4477+
baseConfig := buildBaseHTTPConfig(
4478+
tc.g.Gateways[types.NamespacedName{}],
4479+
make(map[types.NamespacedName]*graph.SnippetsFilter),
4480+
)
44924481
g.Expect(baseConfig.RewriteClientIPSettings).To(Equal(tc.expRewriteIPSettings))
44934482
})
44944483
}

internal/controller/state/graph/gateway.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package graph
33
import (
44
"k8s.io/apimachinery/pkg/types"
55
"k8s.io/apimachinery/pkg/util/validation/field"
6+
"sigs.k8s.io/controller-runtime/pkg/client"
67
v1 "sigs.k8s.io/gateway-api/apis/v1"
78

89
"github.com/nginx/nginx-gateway-fabric/internal/controller/config"
@@ -196,3 +197,70 @@ func validateGateway(gw *v1.Gateway, gc *GatewayClass, npCfg *NginxProxy) ([]con
196197

197198
return conds, valid
198199
}
200+
201+
// GetReferencedSnippetsFilters returns all SnippetsFilters that are referenced by routes attached to this Gateway.
202+
func (g *Gateway) GetReferencedSnippetsFilters(
203+
routes map[RouteKey]*L7Route,
204+
allSnippetsFilters map[types.NamespacedName]*SnippetsFilter,
205+
) map[types.NamespacedName]*SnippetsFilter {
206+
if len(routes) == 0 || len(allSnippetsFilters) == 0 {
207+
return nil
208+
}
209+
210+
gatewayNsName := client.ObjectKeyFromObject(g.Source)
211+
referencedSnippetsFilters := make(map[types.NamespacedName]*SnippetsFilter)
212+
213+
for _, route := range routes {
214+
if !route.Valid || !g.isRouteAttachedToGateway(route, gatewayNsName) {
215+
continue
216+
}
217+
218+
g.collectSnippetsFiltersFromRoute(route, allSnippetsFilters, referencedSnippetsFilters)
219+
}
220+
221+
if len(referencedSnippetsFilters) == 0 {
222+
return nil
223+
}
224+
225+
return referencedSnippetsFilters
226+
}
227+
228+
// isRouteAttachedToGateway checks if the given route is attached to this gateway.
229+
func (g *Gateway) isRouteAttachedToGateway(route *L7Route, gatewayNsName types.NamespacedName) bool {
230+
for _, parentRef := range route.ParentRefs {
231+
if parentRef.Gateway != nil && parentRef.Gateway.NamespacedName == gatewayNsName {
232+
return true
233+
}
234+
}
235+
return false
236+
}
237+
238+
// collectSnippetsFiltersFromRoute extracts SnippetsFilters from a single route's rules.
239+
func (g *Gateway) collectSnippetsFiltersFromRoute(
240+
route *L7Route,
241+
allSnippetsFilters map[types.NamespacedName]*SnippetsFilter,
242+
referencedFilters map[types.NamespacedName]*SnippetsFilter,
243+
) {
244+
for _, rule := range route.Spec.Rules {
245+
if !rule.Filters.Valid {
246+
continue
247+
}
248+
249+
for _, filter := range rule.Filters.Filters {
250+
if filter.FilterType != FilterExtensionRef ||
251+
filter.ResolvedExtensionRef == nil ||
252+
filter.ResolvedExtensionRef.SnippetsFilter == nil {
253+
continue
254+
}
255+
256+
sf := filter.ResolvedExtensionRef.SnippetsFilter
257+
nsName := client.ObjectKeyFromObject(sf.Source)
258+
259+
// Only include if it exists in the cluster-wide map and is valid
260+
// Using the cluster-wide version ensures consistency and avoids duplicates
261+
if clusterSF, exists := allSnippetsFilters[nsName]; exists && clusterSF.Valid {
262+
referencedFilters[nsName] = clusterSF
263+
}
264+
}
265+
}
266+
}

internal/controller/state/graph/gateway_test.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
v1 "sigs.k8s.io/gateway-api/apis/v1"
1414
"sigs.k8s.io/gateway-api/apis/v1beta1"
1515

16+
ngfAPIv1alpha1 "github.com/nginx/nginx-gateway-fabric/apis/v1alpha1"
1617
ngfAPIv1alpha2 "github.com/nginx/nginx-gateway-fabric/apis/v1alpha2"
1718
"github.com/nginx/nginx-gateway-fabric/internal/controller/state/conditions"
1819
"github.com/nginx/nginx-gateway-fabric/internal/framework/controller"
@@ -1582,3 +1583,200 @@ func TestValidateGatewayParametersRef(t *testing.T) {
15821583
})
15831584
}
15841585
}
1586+
1587+
func TestGetReferencedSnippetsFilters(t *testing.T) {
1588+
t.Parallel()
1589+
1590+
gw := &Gateway{
1591+
Source: &v1.Gateway{
1592+
ObjectMeta: metav1.ObjectMeta{
1593+
Namespace: "gateway-ns",
1594+
Name: "test-gateway",
1595+
},
1596+
},
1597+
}
1598+
1599+
sf1 := &SnippetsFilter{
1600+
Source: &ngfAPIv1alpha1.SnippetsFilter{
1601+
ObjectMeta: metav1.ObjectMeta{
1602+
Namespace: "app1",
1603+
Name: "app1-logging",
1604+
},
1605+
},
1606+
Valid: true,
1607+
}
1608+
1609+
sf2 := &SnippetsFilter{
1610+
Source: &ngfAPIv1alpha1.SnippetsFilter{
1611+
ObjectMeta: metav1.ObjectMeta{
1612+
Namespace: "app2",
1613+
Name: "app2-logging",
1614+
},
1615+
},
1616+
Valid: true,
1617+
}
1618+
1619+
sf3Invalid := &SnippetsFilter{
1620+
Source: &ngfAPIv1alpha1.SnippetsFilter{
1621+
ObjectMeta: metav1.ObjectMeta{
1622+
Namespace: "app3",
1623+
Name: "invalid-filter",
1624+
},
1625+
},
1626+
Valid: false,
1627+
}
1628+
1629+
routeAttachedToGateway := &L7Route{
1630+
Source: &v1.HTTPRoute{
1631+
ObjectMeta: metav1.ObjectMeta{
1632+
Namespace: "app1",
1633+
Name: "attached-route",
1634+
},
1635+
},
1636+
Valid: true,
1637+
ParentRefs: []ParentRef{
1638+
{
1639+
Gateway: &ParentRefGateway{
1640+
NamespacedName: types.NamespacedName{
1641+
Namespace: "gateway-ns",
1642+
Name: "test-gateway",
1643+
},
1644+
},
1645+
},
1646+
},
1647+
Spec: L7RouteSpec{
1648+
Rules: []RouteRule{
1649+
{
1650+
Filters: RouteRuleFilters{
1651+
Valid: true,
1652+
Filters: []Filter{
1653+
{
1654+
FilterType: FilterExtensionRef,
1655+
ResolvedExtensionRef: &ExtensionRefFilter{
1656+
SnippetsFilter: sf1,
1657+
Valid: true,
1658+
},
1659+
},
1660+
},
1661+
},
1662+
},
1663+
},
1664+
},
1665+
}
1666+
1667+
routeNotAttachedToGateway := &L7Route{
1668+
Source: &v1.HTTPRoute{
1669+
ObjectMeta: metav1.ObjectMeta{
1670+
Namespace: "app2",
1671+
Name: "not-attached-route",
1672+
},
1673+
},
1674+
Valid: true,
1675+
ParentRefs: []ParentRef{
1676+
{
1677+
Gateway: &ParentRefGateway{
1678+
NamespacedName: types.NamespacedName{
1679+
Namespace: "other-gateway-ns",
1680+
Name: "other-gateway",
1681+
},
1682+
},
1683+
},
1684+
},
1685+
Spec: L7RouteSpec{
1686+
Rules: []RouteRule{
1687+
{
1688+
Filters: RouteRuleFilters{
1689+
Valid: true,
1690+
Filters: []Filter{
1691+
{
1692+
FilterType: FilterExtensionRef,
1693+
ResolvedExtensionRef: &ExtensionRefFilter{
1694+
SnippetsFilter: sf2,
1695+
Valid: true,
1696+
},
1697+
},
1698+
},
1699+
},
1700+
},
1701+
},
1702+
},
1703+
}
1704+
1705+
routeWithInvalidFilter := &L7Route{
1706+
Source: &v1.HTTPRoute{
1707+
ObjectMeta: metav1.ObjectMeta{
1708+
Namespace: "app3",
1709+
Name: "route-with-invalid-filter",
1710+
},
1711+
},
1712+
Valid: true,
1713+
ParentRefs: []ParentRef{
1714+
{
1715+
Gateway: &ParentRefGateway{
1716+
NamespacedName: types.NamespacedName{
1717+
Namespace: "gateway-ns",
1718+
Name: "test-gateway",
1719+
},
1720+
},
1721+
},
1722+
},
1723+
Spec: L7RouteSpec{
1724+
Rules: []RouteRule{
1725+
{
1726+
Filters: RouteRuleFilters{
1727+
Valid: true,
1728+
Filters: []Filter{
1729+
{
1730+
FilterType: FilterExtensionRef,
1731+
ResolvedExtensionRef: &ExtensionRefFilter{
1732+
SnippetsFilter: sf3Invalid,
1733+
Valid: false,
1734+
},
1735+
},
1736+
},
1737+
},
1738+
},
1739+
},
1740+
},
1741+
}
1742+
1743+
routes := map[RouteKey]*L7Route{
1744+
{
1745+
NamespacedName: types.NamespacedName{Namespace: "app1", Name: "attached-route"},
1746+
RouteType: RouteTypeHTTP,
1747+
}: routeAttachedToGateway,
1748+
{
1749+
NamespacedName: types.NamespacedName{Namespace: "app2", Name: "not-attached-route"},
1750+
RouteType: RouteTypeHTTP,
1751+
}: routeNotAttachedToGateway,
1752+
{
1753+
NamespacedName: types.NamespacedName{Namespace: "app3", Name: "route-with-invalid-filter"},
1754+
RouteType: RouteTypeHTTP,
1755+
}: routeWithInvalidFilter,
1756+
}
1757+
1758+
allSnippetsFilters := map[types.NamespacedName]*SnippetsFilter{
1759+
{Namespace: "app1", Name: "app1-logging"}: sf1,
1760+
{Namespace: "app2", Name: "app2-logging"}: sf2,
1761+
{Namespace: "app3", Name: "invalid-filter"}: sf3Invalid,
1762+
}
1763+
1764+
g := NewWithT(t)
1765+
1766+
result := gw.GetReferencedSnippetsFilters(routes, allSnippetsFilters)
1767+
1768+
// Should only include sf1 (valid filter from route attached to this gateway)
1769+
expectedResult := map[types.NamespacedName]*SnippetsFilter{
1770+
{Namespace: "app1", Name: "app1-logging"}: sf1,
1771+
}
1772+
1773+
g.Expect(result).To(Equal(expectedResult))
1774+
1775+
// Test with no routes
1776+
emptyResult := gw.GetReferencedSnippetsFilters(map[RouteKey]*L7Route{}, allSnippetsFilters)
1777+
g.Expect(emptyResult).To(BeEmpty())
1778+
1779+
// Test with routes but no snippets filters
1780+
emptyFilterResult := gw.GetReferencedSnippetsFilters(routes, map[types.NamespacedName]*SnippetsFilter{})
1781+
g.Expect(emptyFilterResult).To(BeEmpty())
1782+
}

0 commit comments

Comments
 (0)