Skip to content

Commit 2e88005

Browse files
committed
Enhance SnippetsPolicy validation and configuration generation
1 parent de442ae commit 2e88005

File tree

8 files changed

+132
-67
lines changed

8 files changed

+132
-67
lines changed

apis/v1alpha1/snippetspolicy_types.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ type SnippetsPolicySpec struct {
5656
// +kubebuilder:validation:MinItems=1
5757
// +kubebuilder:validation:MaxItems=16
5858
// +kubebuilder:validation:XValidation:message="TargetRefs Kind must be Gateway",rule="self.all(t, t.kind == 'Gateway')"
59+
// +kubebuilder:validation:XValidation:message="TargetRefs Name must be unique",rule="self.all(p1, self.exists_one(p2, (p1.name == p2.name)))"
5960
// +kubebuilder:validation:XValidation:message="TargetRefs Group must be gateway.networking.k8s.io",rule="self.all(t, t.group == 'gateway.networking.k8s.io')"
6061
//nolint:lll
6162
TargetRefs []gatewayv1.LocalPolicyTargetReference `json:"targetRefs"`
@@ -64,5 +65,7 @@ type SnippetsPolicySpec struct {
6465
// +kubebuilder:validation:MaxItems=4
6566
// +kubebuilder:validation:XValidation:message="Only one snippet allowed per context",rule="self.all(s1, self.exists_one(s2, s1.context == s2.context))"
6667
//nolint:lll
67-
Snippets []Snippet `json:"snippets"`
68+
//
69+
// +optional
70+
Snippets []Snippet `json:"snippets,omitempty"`
6871
}

config/crd/bases/gateway.nginx.org_snippetspolicies.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,11 @@ spec:
116116
x-kubernetes-validations:
117117
- message: TargetRefs Kind must be Gateway
118118
rule: self.all(t, t.kind == 'Gateway')
119+
- message: TargetRefs Name must be unique
120+
rule: self.all(p1, self.exists_one(p2, (p1.name == p2.name)))
119121
- message: TargetRefs Group must be gateway.networking.k8s.io
120122
rule: self.all(t, t.group == 'gateway.networking.k8s.io')
121123
required:
122-
- snippets
123124
- targetRefs
124125
type: object
125126
status:

internal/controller/nginx/config/policies/snippetspolicy/generator.go

Lines changed: 35 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package snippetspolicy
1818

1919
import (
2020
"fmt"
21-
"path/filepath"
2221

2322
"github.com/nginx/nginx-gateway-fabric/v2/apis/v1alpha1"
2423
"github.com/nginx/nginx-gateway-fabric/v2/internal/controller/nginx/config/http"
@@ -27,19 +26,19 @@ import (
2726

2827
const (
2928
mainTemplate = `
30-
# SnippetsPolicy %s for Gateway %s main context
29+
# SnippetsPolicy %s main context
3130
%s
3231
`
3332
httpTemplate = `
34-
# SnippetsPolicy %s for Gateway %s http context
33+
# SnippetsPolicy %s http context
3534
%s
3635
`
3736
serverTemplate = `
38-
# SnippetsPolicy %s for Gateway %s server context in listener %s
37+
# SnippetsPolicy %s server context
3938
%s
4039
`
4140
locationTemplate = `
42-
# SnippetsPolicy %s for Gateway %s location context with match key %s
41+
# SnippetsPolicy %s location context
4342
%s
4443
`
4544
)
@@ -56,28 +55,27 @@ func NewGenerator() *Generator {
5655

5756
// GenerateForMain generates policy configuration for the main block.
5857
func (g *Generator) GenerateForMain(pols []policies.Policy) policies.GenerateResultFiles {
59-
return g.generate(pols, v1alpha1.NginxContextMain, "")
58+
return g.generate(pols, v1alpha1.NginxContextMain)
6059
}
6160

6261
// GenerateForHTTP generates policy configuration for the http block.
6362
func (g *Generator) GenerateForHTTP(pols []policies.Policy) policies.GenerateResultFiles {
64-
return g.generate(pols, v1alpha1.NginxContextHTTP, "")
63+
return g.generate(pols, v1alpha1.NginxContextHTTP)
6564
}
6665

6766
// GenerateForServer generates policy configuration for the server block.
68-
func (g *Generator) GenerateForServer(pols []policies.Policy, server http.Server) policies.GenerateResultFiles {
69-
return g.generate(pols, v1alpha1.NginxContextHTTPServer, server.Listen)
67+
func (g *Generator) GenerateForServer(pols []policies.Policy, _ http.Server) policies.GenerateResultFiles {
68+
return g.generate(pols, v1alpha1.NginxContextHTTPServer)
7069
}
7170

7271
// GenerateForLocation generates policy configuration for the location block.
73-
func (g *Generator) GenerateForLocation(pols []policies.Policy, location http.Location) policies.GenerateResultFiles {
74-
return g.generate(pols, v1alpha1.NginxContextHTTPServerLocation, location.HTTPMatchKey)
72+
func (g *Generator) GenerateForLocation(pols []policies.Policy, _ http.Location) policies.GenerateResultFiles {
73+
return g.generate(pols, v1alpha1.NginxContextHTTPServerLocation)
7574
}
7675

7776
func (g *Generator) generate(
7877
pols []policies.Policy,
7978
context v1alpha1.NginxContext,
80-
id string,
8179
) policies.GenerateResultFiles {
8280
var files policies.GenerateResultFiles
8381

@@ -92,48 +90,32 @@ func (g *Generator) generate(
9290
continue
9391
}
9492

95-
for _, gwRef := range sp.Spec.TargetRefs {
96-
// TargetRef info for file path and comment
97-
gwNsName := fmt.Sprintf("%s-%s", sp.GetNamespace(), gwRef.Name) // Assuming local target ref implies same namespace
98-
99-
policyNsName := fmt.Sprintf("%s/%s", sp.GetNamespace(), sp.GetName())
100-
101-
// Build content and filename
102-
var content string
103-
var filename string
104-
105-
switch context {
106-
case v1alpha1.NginxContextMain:
107-
content = fmt.Sprintf(mainTemplate, policyNsName, gwNsName, snippet.Value)
108-
filename = filepath.Join(
109-
"includes", "policy", gwNsName,
110-
fmt.Sprintf("SnippetsPolicy_main_%s.conf", sp.GetName()),
111-
)
112-
case v1alpha1.NginxContextHTTP:
113-
content = fmt.Sprintf(httpTemplate, policyNsName, gwNsName, snippet.Value)
114-
filename = filepath.Join(
115-
"includes", "policy", gwNsName,
116-
fmt.Sprintf("SnippetsPolicy_http_%s.conf", sp.GetName()),
117-
)
118-
case v1alpha1.NginxContextHTTPServer:
119-
content = fmt.Sprintf(serverTemplate, policyNsName, gwNsName, id, snippet.Value)
120-
filename = filepath.Join(
121-
"includes", "policy", gwNsName, id,
122-
fmt.Sprintf("SnippetsPolicy_server_%s.conf", sp.GetName()),
123-
)
124-
case v1alpha1.NginxContextHTTPServerLocation:
125-
content = fmt.Sprintf(locationTemplate, policyNsName, gwNsName, id, snippet.Value)
126-
filename = filepath.Join(
127-
"includes", "policy", gwNsName, id,
128-
fmt.Sprintf("SnippetsPolicy_location_%s.conf", sp.GetName()),
129-
)
130-
}
131-
132-
files = append(files, policies.File{
133-
Name: filename,
134-
Content: []byte(content),
135-
})
93+
policyNsName := fmt.Sprintf("%s/%s", sp.GetNamespace(), sp.GetName())
94+
policyFileID := fmt.Sprintf("%s-%s", sp.GetNamespace(), sp.GetName())
95+
96+
// Build content and filename
97+
var content string
98+
var filename string
99+
100+
switch context {
101+
case v1alpha1.NginxContextMain:
102+
content = fmt.Sprintf(mainTemplate, policyNsName, snippet.Value)
103+
filename = fmt.Sprintf("SnippetsPolicy_main_%s.conf", policyFileID)
104+
case v1alpha1.NginxContextHTTP:
105+
content = fmt.Sprintf(httpTemplate, policyNsName, snippet.Value)
106+
filename = fmt.Sprintf("SnippetsPolicy_http_%s.conf", policyFileID)
107+
case v1alpha1.NginxContextHTTPServer:
108+
content = fmt.Sprintf(serverTemplate, policyNsName, snippet.Value)
109+
filename = fmt.Sprintf("SnippetsPolicy_server_%s.conf", policyFileID)
110+
case v1alpha1.NginxContextHTTPServerLocation:
111+
content = fmt.Sprintf(locationTemplate, policyNsName, snippet.Value)
112+
filename = fmt.Sprintf("SnippetsPolicy_location_%s.conf", policyFileID)
136113
}
114+
115+
files = append(files, policies.File{
116+
Name: filename,
117+
Content: []byte(content),
118+
})
137119
}
138120
}
139121
return files

internal/controller/nginx/config/policies/snippetspolicy/generator_test.go

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,15 @@ func TestGenerator(t *testing.T) {
5757
gWithT := NewWithT(t)
5858
files := g.GenerateForMain(pols)
5959
gWithT.Expect(files).To(HaveLen(1))
60-
gWithT.Expect(files[0].Name).To(Equal("includes/policy/default-gateway-1/SnippetsPolicy_main_policy-1.conf"))
60+
gWithT.Expect(files[0].Name).To(Equal("SnippetsPolicy_main_default-policy-1.conf"))
6161
gWithT.Expect(string(files[0].Content)).To(ContainSubstring("worker_processes 1;"))
6262
})
6363

6464
t.Run("GenerateForHTTP", func(t *testing.T) {
6565
gWithT := NewWithT(t)
6666
files := g.GenerateForHTTP(pols)
6767
gWithT.Expect(files).To(HaveLen(1))
68-
gWithT.Expect(files[0].Name).To(Equal("includes/policy/default-gateway-1/SnippetsPolicy_http_policy-1.conf"))
68+
gWithT.Expect(files[0].Name).To(Equal("SnippetsPolicy_http_default-policy-1.conf"))
6969
gWithT.Expect(string(files[0].Content)).To(ContainSubstring("log_format custom '...';"))
7070
})
7171

@@ -76,7 +76,7 @@ func TestGenerator(t *testing.T) {
7676
}
7777
files := g.GenerateForServer(pols, server)
7878
gWithT.Expect(files).To(HaveLen(1))
79-
gWithT.Expect(files[0].Name).To(Equal("includes/policy/default-gateway-1/80/SnippetsPolicy_server_policy-1.conf"))
79+
gWithT.Expect(files[0].Name).To(Equal("SnippetsPolicy_server_default-policy-1.conf"))
8080
gWithT.Expect(string(files[0].Content)).To(ContainSubstring("client_max_body_size 10m;"))
8181
})
8282

@@ -88,11 +88,29 @@ func TestGenerator(t *testing.T) {
8888
files := g.GenerateForLocation(pols, location)
8989
gWithT.Expect(files).To(HaveLen(1))
9090
gWithT.Expect(files[0].Name).To(Equal(
91-
"includes/policy/default-gateway-1/12345/SnippetsPolicy_location_policy-1.conf",
91+
"SnippetsPolicy_location_default-policy-1.conf",
9292
))
9393
gWithT.Expect(string(files[0].Content)).To(ContainSubstring("location_snippet;"))
9494
})
9595

96+
t.Run("GenerateForMain with empty snippets", func(t *testing.T) {
97+
gWithT := NewWithT(t)
98+
policy := &v1alpha1.SnippetsPolicy{
99+
ObjectMeta: metav1.ObjectMeta{Name: "p-empty", Namespace: "default"},
100+
Spec: v1alpha1.SnippetsPolicySpec{
101+
TargetRefs: []gatewayv1.LocalPolicyTargetReference{
102+
{
103+
Name: "gw",
104+
},
105+
},
106+
Snippets: nil,
107+
},
108+
}
109+
110+
files := g.GenerateForMain([]policies.Policy{policy})
111+
gWithT.Expect(files).To(BeEmpty())
112+
})
113+
96114
t.Run("GenerateForMain with multiple policies", func(t *testing.T) {
97115
gWithT := NewWithT(t)
98116
policy1 := &v1alpha1.SnippetsPolicy{
@@ -148,12 +166,8 @@ func TestGenerator(t *testing.T) {
148166
}
149167

150168
files := g.GenerateForMain([]policies.Policy{policy})
151-
gWithT.Expect(files).To(HaveLen(2))
152-
gWithT.Expect(files[0].Name).To(ContainSubstring("gw1"))
153-
gWithT.Expect(files[1].Name).To(ContainSubstring("gw2"))
169+
gWithT.Expect(files).To(HaveLen(1))
170+
gWithT.Expect(files[0].Name).To(Equal("SnippetsPolicy_main_default-policy-multi.conf"))
154171
gWithT.Expect(string(files[0].Content)).To(ContainSubstring("data;"))
155-
gWithT.Expect(string(files[1].Content)).To(ContainSubstring("data;"))
156-
gWithT.Expect(string(files[0].Content)).To(ContainSubstring("gw1"))
157-
gWithT.Expect(string(files[1].Content)).To(ContainSubstring("gw2"))
158172
})
159173
}

internal/controller/nginx/config/policies/snippetspolicy/validator.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ func (v *Validator) Validate(policy policies.Policy) []conditions.Condition {
4747
supportedGroups := []gatewayv1.Group{gatewayv1.GroupName}
4848

4949
// Validate TargetRef
50+
seenTargetRefs := make(map[string]struct{})
5051
for i, targetRef := range sp.Spec.TargetRefs {
5152
if err := policies.ValidateTargetRef(
5253
targetRef,
@@ -56,6 +57,12 @@ func (v *Validator) Validate(policy policies.Policy) []conditions.Condition {
5657
); err != nil {
5758
return []conditions.Condition{conditions.NewPolicyInvalid(err.Error())}
5859
}
60+
61+
if _, exists := seenTargetRefs[string(targetRef.Name)]; exists {
62+
msg := fmt.Sprintf("duplicate targetRef name %q", targetRef.Name)
63+
return []conditions.Condition{conditions.NewPolicyInvalid(msg)}
64+
}
65+
seenTargetRefs[string(targetRef.Name)] = struct{}{}
5966
}
6067

6168
// Validate Snippets

internal/controller/nginx/config/policies/snippetspolicy/validator_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,28 @@ func TestValidator_Validate(t *testing.T) {
119119
conditions.NewPolicyInvalid("duplicate context \"main\""),
120120
},
121121
},
122+
{
123+
name: "duplicate target ref name",
124+
policy: createModifiedPolicy(func(p *ngfAPI.SnippetsPolicy) *ngfAPI.SnippetsPolicy {
125+
p.Spec.TargetRefs = append(p.Spec.TargetRefs, gatewayv1.LocalPolicyTargetReference{
126+
Group: gatewayv1.GroupName,
127+
Kind: kinds.Gateway,
128+
Name: "test-gateway",
129+
})
130+
return p
131+
}),
132+
expConditions: []conditions.Condition{
133+
conditions.NewPolicyInvalid("duplicate targetRef name \"test-gateway\""),
134+
},
135+
},
136+
{
137+
name: "valid policy with empty snippets",
138+
policy: createModifiedPolicy(func(p *ngfAPI.SnippetsPolicy) *ngfAPI.SnippetsPolicy {
139+
p.Spec.Snippets = nil
140+
return p
141+
}),
142+
expConditions: nil,
143+
},
122144
}
123145

124146
v := snippetspolicy.NewValidator()

internal/controller/state/graph/policies.go

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ func (g *Graph) attachPolicies(validator validation.PolicyValidator, ctlrName st
125125
for _, ref := range policy.TargetRefs {
126126
switch ref.Kind {
127127
case kinds.Gateway:
128-
attachPolicyToGateway(policy, ref, g.Gateways, ctlrName, logger)
128+
attachPolicyToGateway(policy, ref, g.Gateways, g.Routes, ctlrName, logger)
129129
case kinds.HTTPRoute, kinds.GRPCRoute:
130130
route, exists := g.Routes[routeKeyForKind(ref.Kind, ref.Nsname)]
131131
if !exists {
@@ -292,6 +292,7 @@ func attachPolicyToGateway(
292292
policy *Policy,
293293
ref PolicyTargetRef,
294294
gateways map[types.NamespacedName]*Gateway,
295+
routes map[RouteKey]*L7Route,
295296
ctlrName string,
296297
logger logr.Logger,
297298
) {
@@ -306,6 +307,7 @@ func attachPolicyToGateway(
306307
// Ancestor already exists, but still attach policy to gateway if it's valid
307308
if exists && gw != nil && gw.Valid && gw.Source != nil {
308309
gw.Policies = append(gw.Policies, policy)
310+
propagateSnippetsPolicyToRoutes(policy, gw, routes)
309311
}
310312
return
311313
}
@@ -350,6 +352,40 @@ func attachPolicyToGateway(
350352

351353
policy.Ancestors = append(policy.Ancestors, ancestor)
352354
gw.Policies = append(gw.Policies, policy)
355+
propagateSnippetsPolicyToRoutes(policy, gw, routes)
356+
}
357+
358+
func propagateSnippetsPolicyToRoutes(
359+
policy *Policy,
360+
gw *Gateway,
361+
routes map[RouteKey]*L7Route,
362+
) {
363+
// Only SnippetsPolicy supports propagation from Gateway to Routes
364+
if getPolicyKind(policy.Source) != kinds.SnippetsPolicy {
365+
return
366+
}
367+
368+
gwNsName := client.ObjectKeyFromObject(gw.Source)
369+
370+
for _, route := range routes {
371+
for _, parentRef := range route.ParentRefs {
372+
// Check if the route is attached to this specific gateway
373+
if parentRef.Gateway != nil && parentRef.Gateway.NamespacedName == gwNsName {
374+
// Avoid duplicate attachment if logic runs multiple times (though graph build is single pass)
375+
// or if policy targets both.
376+
alreadyAttached := false
377+
for _, p := range route.Policies {
378+
if p == policy {
379+
alreadyAttached = true
380+
break
381+
}
382+
}
383+
if !alreadyAttached {
384+
route.Policies = append(route.Policies, policy)
385+
}
386+
}
387+
}
388+
}
353389
}
354390

355391
func processPolicies(

internal/controller/state/graph/policies_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -644,7 +644,7 @@ func TestAttachPolicyToGateway(t *testing.T) {
644644
t.Parallel()
645645
g := NewWithT(t)
646646

647-
attachPolicyToGateway(test.policy, test.policy.TargetRefs[0], test.gws, "nginx-gateway", logr.Discard())
647+
attachPolicyToGateway(test.policy, test.policy.TargetRefs[0], test.gws, nil, "nginx-gateway", logr.Discard())
648648

649649
if test.expAttached {
650650
for _, gw := range test.gws {

0 commit comments

Comments
 (0)