-
Notifications
You must be signed in to change notification settings - Fork 130
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
base: main
Are you sure you want to change the base?
Conversation
Hi @shaun-nx! Welcome to the project! 🎉 Thanks for opening this pull request! Please make sure to include the issue number in the PR description to automatically close the issue when the PR is merged. |
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #3623 +/- ##
==========================================
- Coverage 86.92% 86.90% -0.02%
==========================================
Files 127 127
Lines 15287 15287
Branches 62 62
==========================================
- Hits 13288 13285 -3
- Misses 1846 1848 +2
- Partials 153 154 +1 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this 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.
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. |
@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. |
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 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:
|
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 :) |
There was a problem hiding this 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!
@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? |
// This is used to simulate interactions with the Kubernetes API. | ||
k8sClient := fake.NewClientBuilder().WithScheme(scheme).Build() | ||
|
||
err := k8sClient.Create(context.Background(), clientSettingsPolicy) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This validation doesn't really make sense to me, when you're creating the fake how does the validation happening? I don't see how you've connected any validation here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The main validation lies here:
if err != nil {
for _, wantError := range wantErrors {
if !strings.Contains(err.Error(), wantError) {
t.Errorf("missing expected error %q in %v", wantError, err)
}
}
}
This loops over the errors that we are expecting and checks if the error we get back from the k8s API from the k8sClient.Create()
function matches the error we are expecting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah but how does the k8sClient throw any errors since its a fake
}{ | ||
{ | ||
name: "Validate gateway.networking.k8s.io TargetRef Group is allowed", | ||
wantErrors: []string{"TargetRef Group must be gateway.networking.k8s.io."}, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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
testInvalidTargetRefGroup(t) | ||
} | ||
|
||
func testValidTargetRefKind(t *testing.T) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 :)
@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. |
@@ -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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/runtime" | ||
"sigs.k8s.io/controller-runtime/pkg/client/fake" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can keep the gateway API import with the other 3rd party imports.
There was a problem hiding this comment.
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.
policySpec: ngfAPIv1alpha1.ClientSettingsPolicySpec{ | ||
TargetRef: gatewayv1alpha2.LocalPolicyTargetReference{ | ||
Kind: "Gateway", | ||
Group: "gateway.networking.k8s.io", |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great point.
}{ | ||
{ | ||
name: "Validate gateway.networking.k8s.io TargetRef Group is allowed", | ||
wantErrors: []string{"TargetRef Group must be gateway.networking.k8s.io."}, |
There was a problem hiding this comment.
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
.
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) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
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())
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 !
wantErrors: []string{"TargetRef Group must be gateway.networking.k8s.io."}, | ||
policySpec: ngfAPIv1alpha1.ClientSettingsPolicySpec{ | ||
TargetRef: gatewayv1alpha2.LocalPolicyTargetReference{ | ||
Kind: "Gateway", |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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!
Proposed changes
Add tests for CEL validation test for targetRef in ClientSettingsPolicy
Resolves
targetRef
CEL validation requirement of 3621Checklist
Before creating a PR, run through this checklist and mark each as complete.