Skip to content

Add CEL validation test for targetRef in ClientSettingsPolicy #3623

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 22 commits into
base: main
Choose a base branch
from

Conversation

shaun-nx
Copy link

@shaun-nx shaun-nx commented Jul 14, 2025

Proposed changes

Add tests for CEL validation test for targetRef in ClientSettingsPolicy

Resolves targetRef CEL validation requirement of 3621

Checklist

Before creating a PR, run through this checklist and mark each as complete.

  • I have read the CONTRIBUTING doc
  • I have added tests that prove my fix is effective or that my feature works
  • I have checked that all unit tests pass after adding my changes
  • I have updated necessary documentation
  • I have rebased my branch onto main
  • I will ensure my PR is targeting the main branch and pulling from my branch from my own fork

Copy link

nginx-bot bot commented Jul 14, 2025

Hi @shaun-nx! Welcome to the project! 🎉

Thanks for opening this pull request!
Be sure to check out our Contributing Guidelines while you wait for someone on the team to review this.

Please make sure to include the issue number in the PR description to automatically close the issue when the PR is merged.
See Linking a pull request to an issue and our Pull Request Guidelines for more information.

@nginx-bot nginx-bot bot added the community label Jul 14, 2025
Copy link

codecov bot commented Jul 14, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 87.05%. Comparing base (19fb47b) to head (1adf168).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3623      +/-   ##
==========================================
+ Coverage   87.02%   87.05%   +0.02%     
==========================================
  Files         127      127              
  Lines       15579    15579              
  Branches       62       62              
==========================================
+ Hits        13558    13562       +4     
+ Misses       1861     1858       -3     
+ Partials      160      159       -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@shaun-nx shaun-nx added the tests Pull requests that update tests label Jul 14, 2025
Copy link
Contributor

@ciarams87 ciarams87 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think what we want is to make sure our CEL validation is correct - this test file is testing Go logic rather than our actual validation (that is, we need to test the CEL expressions themselves, not the logic we hope they are enforcing).

There is a package we could probably use for cel validation k8s.io/apiserver/pkg/cel which might be useful.

@github-project-automation github-project-automation bot moved this from 🆕 New to 🏗 In Progress in NGINX Gateway Fabric Jul 14, 2025
@ciarams87
Copy link
Contributor

Or, now that I've read the parent issue, we could follow the pattern set in the Gateway API and simply run some functional tests where we actually create the resources and test the validation is working that way.

@bjee19
Copy link
Contributor

bjee19 commented Jul 14, 2025

@ciarams87, though looking at the CEL tests in the Gateway API, https://github.com/kubernetes-sigs/gateway-api/blob/main/pkg/test/cel/httproute_test.go, doesn't this PR follow the same standard set by them?

EDIT: oh actually nvm i see where in the gateway api they create the resource.

@bjee19
Copy link
Contributor

bjee19 commented Jul 15, 2025

Also regards to file structure, i would support just doing a single test file per CRD similar to how the gateway api repo does it. Thus the tests would just be like tests/cel/clientsettingspolicy_test.go and so on.

And i would kinda agree with @ciarams87, though not too sure if a functional test is needed, but would instead follow whats done in the gateway api and create the actual crd object through k8sclient and validate the errors like this:

func validateHTTPRoute(t *testing.T, route *gatewayv1.HTTPRoute, wantErrors []string) {
	t.Helper()

	ctx := context.Background()
	err := k8sClient.Create(ctx, route)

	if (len(wantErrors) != 0) != (err != nil) {
		t.Fatalf("Unexpected response while creating HTTPRoute %q; got err=\n%v\n;want error=%v", fmt.Sprintf("%v/%v", route.Namespace, route.Name), err, wantErrors)
	}

	var missingErrorStrings []string
	for _, wantError := range wantErrors {
		if !celErrorStringMatches(err.Error(), wantError) {
			missingErrorStrings = append(missingErrorStrings, wantError)
		}
	}
	if len(missingErrorStrings) != 0 {
		t.Errorf("Unexpected response while creating HTTPRoute %q; got err=\n%v\n;missing strings within error=%q", fmt.Sprintf("%v/%v", route.Namespace, route.Name), err, missingErrorStrings)
	}
}

@shaun-nx
Copy link
Author

Thanks @bjee19 and @ciarams87 for the inputs! I didn't notice originally that the cel tests in the in the gateway api created resources in the test. I'll update my tests now to better follow that pattern.

I'll also adjust the filename as per your suggestion @bjee19 :)

@github-actions github-actions bot removed the tests Pull requests that update tests label Jul 15, 2025
@shaun-nx shaun-nx requested review from ciarams87 and bjee19 July 15, 2025 10:34
Copy link
Contributor

@ciarams87 ciarams87 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I probably should have called it an integration test maybe, but yes that's what I meant @bjee19 - create the actual crd object through k8sclient. That way, we are testing the actual CEL expressions in the CRD. @bjee19 gave you an example from the Gateway API already, but here is one specific to ClientSettingsPolicy for clarity:

func TestClientSettingsPolicyValidation(t *testing.T) {
    ctx := context.Background()
    
    testCases := []struct {
        desc       string
        policy     *ngfAPIv1alpha1.ClientSettingsPolicy
        wantErrors []string
    }{
        {
            desc: "valid Gateway targetRef",
            policy: &ngfAPIv1alpha1.ClientSettingsPolicy{
                ObjectMeta: metav1.ObjectMeta{
                    Name:      "test-policy",
                    Namespace: "default",
                },
                Spec: ngfAPIv1alpha1.ClientSettingsPolicySpec{
                    TargetRef: gatewayv1alpha2.LocalPolicyTargetReference{
                        Kind:  "Gateway",
                        Group: "gateway.networking.k8s.io",
                        Name:  "test-gateway",
                    },
                },
            },
            // No wantErrors = should succeed
        },
        {
            desc: "invalid targetRef kind",
            policy: &ngfAPIv1alpha1.ClientSettingsPolicy{
                ObjectMeta: metav1.ObjectMeta{
                    Name:      "test-policy-invalid",
                    Namespace: "default",
                },
                Spec: ngfAPIv1alpha1.ClientSettingsPolicySpec{
                    TargetRef: gatewayv1alpha2.LocalPolicyTargetReference{
                        Kind:  "Service", // Invalid
                        Group: "gateway.networking.k8s.io",
                        Name:  "test-service",
                    },
                },
            },
            wantErrors: []string{"TargetRef Kind must be one of: Gateway, HTTPRoute, or GRPCRoute"},
        },
    }

    for _, tc := range testCases {
        t.Run(tc.desc, func(t *testing.T) {
            err := k8sClient.Create(ctx, tc.policy)
            
            if (len(tc.wantErrors) != 0) != (err != nil) {
                t.Fatalf("got err=%v; want error=%v", err, tc.wantErrors != nil)
            }
            
            if err != nil {
                for _, wantError := range tc.wantErrors {
                    if !strings.Contains(err.Error(), wantError) {
                        t.Errorf("missing expected error %q in %v", wantError, err)
                    }
                }
            }
        })
    }
}

Notice we are calling err := k8sClient.Create(ctx, tc.policy) to create the actual Policy object, and then asserting on that creation - if we are creating an invalid policy, we expect an error, and we assert the error is one that we expect. Otherwise, we are testing Go logic in the test file instead of the actual CEL validation rules that Kubernetes will enforce.

Your current approach creates a ClientSettingsPolicy struct in memory and then checks it against a Go map, which doesn't invoke the actual CEL validation. When users apply the CRD to a Kubernetes cluster, it's the CEL expressions defined in the CRD spec that validate the resource.

By using k8sClient.Create(), we're testing the exact same validation path that real users will experience: the Kubernetes API server will evaluate your CRD's CEL rules against the incoming resource and either accept or reject it with the actual error messages that CEL produces.

Hopefully this makes sense!

@shaun-nx shaun-nx requested a review from ciarams87 July 17, 2025 09:53
@tataruty
Copy link
Contributor

@sjberman is this about those policies with limit of 16? 🤪 Sorry, I guess i will ask till i understand what are those 🥹 Anyway, if they are then maybe there should be a test about limit as well or do we have it in some other test cases?

@tataruty
Copy link
Contributor

@sjberman is this about those policies with limit of 16? 🤪 Sorry, I guess i will ask till i understand what are those 🥹 Anyway, if they are then maybe there should be a test about limit as well or do we have it in some other test cases?

or @bjee19 maybe you know the answer to my question?

}{
{
name: "Validate gateway.networking.k8s.io TargetRef Group is allowed",
wantErrors: []string{"TargetRef Group must be gateway.networking.k8s.io."},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would there be errors in a validTargetRefGroup

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great point, I didn't notice I had that there. I'll update that

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How did this pass in the pipeline? is it actually running? I'm assuming not since our unit tests only run on the internal and cmd directories. Our make unit-test target will need to be updated to also run in tests/cel.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, nice catch @sjberman !
Something must be off in my logic if that's the case. I'll re-review that today

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@salonichf5
It looks like the make unit-test target doesn't cover anything in the tests/ directory from what I can see

.PHONY: unit-test
unit-test: ## Run unit tests for the go code
	go test ./cmd/... ./internal/... -buildvcs -race -shuffle=on -coverprofile=coverage.out -covermode=atomic
	go tool cover -html=coverage.out -o cover.html

I know we also have a Makefile in tests/ as well. Does something need to be updated in there?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The make unit-test target should be updated to include tests/cel/.... The Makefile in the tests/ directory right now controls functional/ngf/conformance tests.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sjberman this should be resolved now.

There is a new Makefile target for the cel test. I do still need to create a new workflow for these test so that they run as part of the pipeline for other PRs

testInvalidTargetRefGroup(t)
}

func testValidTargetRefKind(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you combine testValid…, testInvalid…, etc. into one test block? It seems redundant to have multiple functions that are doing the same thing basically, the valid and invalid can be determined by the name of the the test right?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like to do this to avoid having a single test function with a massive set of table tests. I find it makes the tests easier to read a digest.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps, a helper function that runs the tests would be useful for readability

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where each of the functions like testValidTargetRefKind just defines the tests and calls the helper

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also agree with @sarthyparty on combining the invalid/valid tests mainly to maintain consistency with the existing tests in our repo and because it follows standards set in the gateway api, example of their test structure: https://github.com/kubernetes-sigs/gateway-api/blob/main/pkg/test/cel/httproute_test.go

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bjee19 that's a fair point. I'll make that update today as well then. Thanks :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bjee19 @sarthyparty this should be updated now.

@sjberman
Copy link
Collaborator

@tataruty The max items is handled by the OpenAPI Schema validation in the CRD definition, which we don't need to test. This is for validation that can't be done with basic json schema, and is instead done with CEL in the CRD definition (which are comments that have additional validation for certain fields). Good observation though.

tests/go.mod Outdated
@@ -5,7 +5,7 @@ go 1.24.2
replace github.com/nginx/nginx-gateway-fabric => ../

require (
github.com/nginx/nginx-gateway-fabric v0.0.0
github.com/nginx/nginx-gateway-fabric v1.6.2
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should stay as-is.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @sjberman
I'm not sure why that updated originally. Just so I know, what is the impact of this version being v1.6.2 vs v0.0.0?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functionally it may not matter since we perform the replace above, but it's a little confusing IMO versus just pointing to an empty version to show that we're just importing whatever the same version is that we have checked out.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sjberman This should be fixed now

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

can keep the gateway API import with the other 3rd party imports.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I remember, this affected the linter. I will double check that though.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed 👍

policySpec: ngfAPIv1alpha1.ClientSettingsPolicySpec{
TargetRef: gatewayv1alpha2.LocalPolicyTargetReference{
Kind: "Gateway",
Group: "gateway.networking.k8s.io",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could make this group value a constant

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great point.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sjberman This change has been made in the latest push

}{
{
name: "Validate gateway.networking.k8s.io TargetRef Group is allowed",
wantErrors: []string{"TargetRef Group must be gateway.networking.k8s.io."},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How did this pass in the pipeline? is it actually running? I'm assuming not since our unit tests only run on the internal and cmd directories. Our make unit-test target will need to be updated to also run in tests/cel.

Comment on lines 237 to 248
if err != nil {
t.Logf("Error creating ClientSettingsPolicy %q: %v",
fmt.Sprintf("%v/%v", clientSettingsPolicy.Namespace, clientSettingsPolicy.Name), err)
}

if err != nil {
for _, wantError := range wantErrors {
if !strings.Contains(err.Error(), wantError) {
t.Errorf("missing expected error %q in %v", wantError, err)
}
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We typically use the gomega library to do assertion matching. See our other unit tests for examples, but you can basically do:

g := NewWithT(T)
g.Expect(err).ToNot(HaveOccurred())

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also use t.Parallel() at the beginning of each unit test and in each tt.Run loop iteration to parrallelize the tests.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to know, thanks @sjberman !

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sjberman I still need to make this update to the test 📝

wantErrors: []string{"TargetRef Group must be gateway.networking.k8s.io."},
policySpec: ngfAPIv1alpha1.ClientSettingsPolicySpec{
TargetRef: gatewayv1alpha2.LocalPolicyTargetReference{
Kind: "Gateway",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can make kind Gateway constant too, i see multiple uses.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we do have the various resource Kinds as constants in a kinds package.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: on that note, we can use a different kind for one of these tests, just to try all combinations for invalid targetRef group

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@salonichf5 @sjberman great suggestions here. I'll be sure to make those updates!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed 👍

@github-actions github-actions bot added the tests Pull requests that update tests label Jul 21, 2025
@shaun-nx shaun-nx linked an issue Jul 22, 2025 that may be closed by this pull request
2 tasks
.PHONY: setup-and-test-cel-validation
setup-and-test-cel-validation:
kind create cluster --name $(CLUSTER_NAME) || true
kubectl kustomize ../config/crd | kubectl apply -f -
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I ran this target, I got this error: The CustomResourceDefinition "nginxproxies.gateway.nginx.org" is invalid: metadata.annotations: Too long: may not be more than 262144 bytes. I was able to fix it by adding changing the apply command to kubectl apply --server-side -f -. Can you check this out?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if its just my machine

# Set up a kind cluster, install NGF CRDs, and run CEL validation tests
.PHONY: setup-and-test-cel-validation
setup-and-test-cel-validation:
kind create cluster --name $(CLUSTER_NAME) || true
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have a Makefile target to create kind cluster, so shouldn't need this.

Comment on lines +191 to +200
# Run CEL validation integration tests against a real cluster
.PHONY: test-cel-validation
test-cel-validation:
@if [ -z "$(CEL_TEST_TARGET)" ]; then \
echo "Running all tests in ./cel"; \
go test -v ./cel; \
else \
echo "Running test: $(CEL_TEST_TARGET) in ./cel"; \
go test -v ./cel -run "$(CEL_TEST_TARGET)"; \
fi
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how important this is to have a separate section for running a specific test. We don't do it for our other tests, and it's pretty straightforward for the one-off cases to just specify the test directly in the command line.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're also going to need a job in our CI pipeline to run these.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
community tests Pull requests that update tests
Projects
Status: 🏗 In Progress
Development

Successfully merging this pull request may close these issues.

Add CEL test for ClientSettingsPolicy Policy
7 participants