Skip to content

Commit 966d874

Browse files
authored
Add CEL validation test for targetRef in ClientSettingsPolicy (#3623)
1 parent 0651c28 commit 966d874

File tree

6 files changed

+318
-0
lines changed

6 files changed

+318
-0
lines changed

.github/workflows/ci.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,3 +311,32 @@ jobs:
311311
- name: Push to GitHub Container Registry
312312
run: |
313313
helm push ${{ steps.package.outputs.path }} oci://ghcr.io/nginx/charts
314+
315+
cel-tests:
316+
name: CEL Tests
317+
runs-on: ubuntu-24.04
318+
needs: vars
319+
steps:
320+
- name: Checkout Repository
321+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
322+
323+
- name: Setup Golang Environment
324+
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
325+
with:
326+
go-version: stable
327+
cache-dependency-path: |
328+
go.sum
329+
.github/.cache/buster-for-unit-tests
330+
331+
- name: Deploy Kubernetes
332+
id: k8s
333+
run: |
334+
kind create cluster --name ${{ github.run_id }} --image=kindest/node:${{ needs.vars.outputs.k8s_latest }}
335+
336+
- name: Apply CustomResourceDefinition
337+
run: |
338+
kubectl kustomize config/crd | kubectl apply --server-side -f -
339+
340+
- name: Run Tests
341+
run: make test-cel-validation
342+
working-directory: ./tests

tests/Makefile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ STANDARD_CONFORMANCE_PROFILES = GATEWAY-HTTP,GATEWAY-GRPC
1717
EXPERIMENTAL_CONFORMANCE_PROFILES = GATEWAY-TLS
1818
CONFORMANCE_PROFILES = $(STANDARD_CONFORMANCE_PROFILES) # by default we use the standard conformance profiles. If experimental is enabled we override this and add the experimental profiles.
1919
SKIP_TESTS =
20+
CEL_TEST_TARGET =
2021

2122
# Check if ENABLE_EXPERIMENTAL is true
2223
ifeq ($(ENABLE_EXPERIMENTAL),true)
@@ -186,3 +187,14 @@ uninstall-ngf: ## Uninstall NGF on configured kind cluster
186187
-make uninstall-gateway-crds
187188
-kubectl delete namespace nginx-gateway
188189
-kubectl kustomize ../config/crd | kubectl delete -f -
190+
191+
# Run CEL validation integration tests against a real cluster
192+
.PHONY: test-cel-validation
193+
test-cel-validation:
194+
@if [ -z "$(CEL_TEST_TARGET)" ]; then \
195+
echo "Running all tests in ./cel"; \
196+
go test -v ./cel; \
197+
else \
198+
echo "Running test: $(CEL_TEST_TARGET) in ./cel"; \
199+
go test -v ./cel -run "$(CEL_TEST_TARGET)"; \
200+
fi
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package cel
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
. "github.com/onsi/gomega"
8+
controllerruntime "sigs.k8s.io/controller-runtime"
9+
"sigs.k8s.io/controller-runtime/pkg/client"
10+
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
11+
12+
ngfAPIv1alpha1 "github.com/nginx/nginx-gateway-fabric/v2/apis/v1alpha1"
13+
"github.com/nginx/nginx-gateway-fabric/v2/tests/framework"
14+
)
15+
16+
func TestClientSettingsPoliciesTargetRefKind(t *testing.T) {
17+
t.Parallel()
18+
g := NewWithT(t)
19+
k8sClient, err := getKubernetesClient(t)
20+
g.Expect(err).ToNot(HaveOccurred())
21+
tests := []struct {
22+
policySpec ngfAPIv1alpha1.ClientSettingsPolicySpec
23+
name string
24+
wantErrors []string
25+
}{
26+
{
27+
name: "Validate TargetRef of kind Gateway is allowed",
28+
policySpec: ngfAPIv1alpha1.ClientSettingsPolicySpec{
29+
TargetRef: gatewayv1alpha2.LocalPolicyTargetReference{
30+
Kind: gatewayKind,
31+
Group: gatewayGroup,
32+
},
33+
},
34+
},
35+
{
36+
name: "Validate TargetRef of kind HTTPRoute is allowed",
37+
policySpec: ngfAPIv1alpha1.ClientSettingsPolicySpec{
38+
TargetRef: gatewayv1alpha2.LocalPolicyTargetReference{
39+
Kind: httpRouteKind,
40+
Group: gatewayGroup,
41+
},
42+
},
43+
},
44+
{
45+
name: "Validate TargetRef of kind GRPCRoute is allowed",
46+
policySpec: ngfAPIv1alpha1.ClientSettingsPolicySpec{
47+
TargetRef: gatewayv1alpha2.LocalPolicyTargetReference{
48+
Kind: grpcRouteKind,
49+
Group: gatewayGroup,
50+
},
51+
},
52+
},
53+
{
54+
name: "Validate Invalid TargetRef Kind is not allowed",
55+
wantErrors: []string{expectedTargetRefKindError},
56+
policySpec: ngfAPIv1alpha1.ClientSettingsPolicySpec{
57+
TargetRef: gatewayv1alpha2.LocalPolicyTargetReference{
58+
Kind: invalidKind,
59+
Group: gatewayGroup,
60+
},
61+
},
62+
},
63+
{
64+
name: "Validate TCPRoute TargetRef Kind is not allowed",
65+
wantErrors: []string{expectedTargetRefKindError},
66+
policySpec: ngfAPIv1alpha1.ClientSettingsPolicySpec{
67+
TargetRef: gatewayv1alpha2.LocalPolicyTargetReference{
68+
Kind: tcpRouteKind,
69+
Group: gatewayGroup,
70+
},
71+
},
72+
},
73+
}
74+
75+
for _, tt := range tests {
76+
t.Run(tt.name, func(t *testing.T) {
77+
t.Parallel()
78+
validateClientSettingsPolicy(t, tt, g, k8sClient)
79+
})
80+
}
81+
}
82+
83+
func TestClientSettingsPoliciesTargetRefGroup(t *testing.T) {
84+
t.Parallel()
85+
g := NewWithT(t)
86+
k8sClient, err := getKubernetesClient(t)
87+
g.Expect(err).ToNot(HaveOccurred())
88+
tests := []struct {
89+
policySpec ngfAPIv1alpha1.ClientSettingsPolicySpec
90+
name string
91+
wantErrors []string
92+
}{
93+
{
94+
name: "Validate gateway.networking.k8s.io TargetRef Group is allowed",
95+
policySpec: ngfAPIv1alpha1.ClientSettingsPolicySpec{
96+
TargetRef: gatewayv1alpha2.LocalPolicyTargetReference{
97+
Kind: gatewayKind,
98+
Group: gatewayGroup,
99+
},
100+
},
101+
},
102+
{
103+
name: "Validate invalid.networking.k8s.io TargetRef Group is not allowed",
104+
wantErrors: []string{expectedTargetRefGroupError},
105+
policySpec: ngfAPIv1alpha1.ClientSettingsPolicySpec{
106+
TargetRef: gatewayv1alpha2.LocalPolicyTargetReference{
107+
Kind: gatewayKind,
108+
Group: invalidGroup,
109+
},
110+
},
111+
},
112+
{
113+
name: "Validate discovery.k8s.io/v1 TargetRef Group is not allowed",
114+
wantErrors: []string{expectedTargetRefGroupError},
115+
policySpec: ngfAPIv1alpha1.ClientSettingsPolicySpec{
116+
TargetRef: gatewayv1alpha2.LocalPolicyTargetReference{
117+
Kind: gatewayKind,
118+
Group: discoveryGroup,
119+
},
120+
},
121+
},
122+
}
123+
124+
for _, tt := range tests {
125+
t.Run(tt.name, func(t *testing.T) {
126+
t.Parallel()
127+
validateClientSettingsPolicy(t, tt, g, k8sClient)
128+
})
129+
}
130+
}
131+
132+
func validateClientSettingsPolicy(t *testing.T, tt struct {
133+
policySpec ngfAPIv1alpha1.ClientSettingsPolicySpec
134+
name string
135+
wantErrors []string
136+
}, g *WithT, k8sClient client.Client,
137+
) {
138+
t.Helper()
139+
140+
policySpec := tt.policySpec
141+
policySpec.TargetRef.Name = gatewayv1alpha2.ObjectName(uniqueResourceName(testTargetRefName))
142+
policyName := uniqueResourceName(testPolicyName)
143+
144+
clientSettingsPolicy := &ngfAPIv1alpha1.ClientSettingsPolicy{
145+
ObjectMeta: controllerruntime.ObjectMeta{
146+
Name: policyName,
147+
Namespace: defaultNamespace,
148+
},
149+
Spec: policySpec,
150+
}
151+
timeoutConfig := framework.DefaultTimeoutConfig()
152+
ctx, cancel := context.WithTimeout(context.Background(), timeoutConfig.KubernetesClientTimeout)
153+
err := k8sClient.Create(ctx, clientSettingsPolicy)
154+
defer cancel()
155+
156+
// Clean up after test
157+
defer func() {
158+
_ = k8sClient.Delete(context.Background(), clientSettingsPolicy)
159+
}()
160+
161+
if len(tt.wantErrors) == 0 {
162+
g.Expect(err).ToNot(HaveOccurred())
163+
} else {
164+
g.Expect(err).To(HaveOccurred())
165+
for _, wantError := range tt.wantErrors {
166+
g.Expect(err.Error()).To(ContainSubstring(wantError), "Expected error '%s' not found in: %s", wantError, err.Error())
167+
}
168+
}
169+
}

tests/cel/common.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package cel
2+
3+
import (
4+
"crypto/rand"
5+
"fmt"
6+
"testing"
7+
8+
"k8s.io/apimachinery/pkg/runtime"
9+
controllerruntime "sigs.k8s.io/controller-runtime"
10+
"sigs.k8s.io/controller-runtime/pkg/client"
11+
12+
ngfAPIv1alpha1 "github.com/nginx/nginx-gateway-fabric/v2/apis/v1alpha1"
13+
ngfAPIv1alpha2 "github.com/nginx/nginx-gateway-fabric/v2/apis/v1alpha2"
14+
)
15+
16+
const (
17+
gatewayKind = "Gateway"
18+
httpRouteKind = "HTTPRoute"
19+
grpcRouteKind = "GRPCRoute"
20+
tcpRouteKind = "TCPRoute"
21+
invalidKind = "InvalidKind"
22+
)
23+
24+
const (
25+
gatewayGroup = "gateway.networking.k8s.io"
26+
invalidGroup = "invalid.networking.k8s.io"
27+
discoveryGroup = "discovery.k8s.io/v1"
28+
)
29+
30+
const (
31+
expectedTargetRefKindError = `TargetRef Kind must be one of: Gateway, HTTPRoute, or GRPCRoute`
32+
expectedTargetRefGroupError = `TargetRef Group must be gateway.networking.k8s.io.`
33+
)
34+
35+
const (
36+
defaultNamespace = "default"
37+
)
38+
39+
const (
40+
testPolicyName = "test-policy"
41+
testTargetRefName = "test-targetRef"
42+
)
43+
44+
// getKubernetesClient returns a client connected to a real Kubernetes cluster.
45+
func getKubernetesClient(t *testing.T) (k8sClient client.Client, err error) {
46+
t.Helper()
47+
// Use controller-runtime to get cluster connection
48+
k8sConfig, err := controllerruntime.GetConfig()
49+
if err != nil {
50+
return nil, err
51+
}
52+
53+
// Set up scheme with NGF types
54+
scheme := runtime.NewScheme()
55+
if err = ngfAPIv1alpha1.AddToScheme(scheme); err != nil {
56+
return nil, err
57+
}
58+
if err = ngfAPIv1alpha2.AddToScheme(scheme); err != nil {
59+
return nil, err
60+
}
61+
// Create a new client with the scheme and return it
62+
return client.New(k8sConfig, client.Options{Scheme: scheme})
63+
}
64+
65+
// randomPrimeNumber generates a random prime number of 64 bits.
66+
// It panics if it fails to generate a random prime number.
67+
func randomPrimeNumber() int64 {
68+
primeNum, err := rand.Prime(rand.Reader, 64)
69+
if err != nil {
70+
panic(fmt.Errorf("failed to generate random prime number: %w", err))
71+
}
72+
return primeNum.Int64()
73+
}
74+
75+
// uniqueResourceName generates a unique resource name by appending a random prime number to the given name.
76+
func uniqueResourceName(name string) string {
77+
return fmt.Sprintf("%s-%d", name, randomPrimeNumber())
78+
}

tests/cel/common_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package cel
2+
3+
import (
4+
"testing"
5+
6+
. "github.com/onsi/gomega"
7+
)
8+
9+
func TestMustGenerateRandomPrimeNumber(t *testing.T) {
10+
t.Parallel()
11+
g := NewWithT(t)
12+
g.Expect(func() {
13+
_ = randomPrimeNumber()
14+
}).ToNot(Panic())
15+
}
16+
17+
func TestMustReturnUniqueResourceName(t *testing.T) {
18+
t.Parallel()
19+
g := NewWithT(t)
20+
21+
name := "test-resource"
22+
uniqueName := uniqueResourceName(name)
23+
24+
g.Expect(uniqueName).To(HavePrefix(name))
25+
g.Expect(len(uniqueName)).To(BeNumerically(">", len(name)))
26+
}

tests/framework/timeout.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ type TimeoutConfig struct {
3535

3636
// TestForTrafficTimeout represents the maximum time for NGF to test for passing or failing traffic.
3737
TestForTrafficTimeout time.Duration
38+
39+
// KubernetesClientTimeout represents the maximum time for Kubernetes client operations.
40+
KubernetesClientTimeout time.Duration
3841
}
3942

4043
// DefaultTimeoutConfig populates a TimeoutConfig with the default values.
@@ -51,5 +54,6 @@ func DefaultTimeoutConfig() TimeoutConfig {
5154
GetLeaderLeaseTimeout: 60 * time.Second,
5255
GetStatusTimeout: 60 * time.Second,
5356
TestForTrafficTimeout: 60 * time.Second,
57+
KubernetesClientTimeout: 10 * time.Second,
5458
}
5559
}

0 commit comments

Comments
 (0)