diff --git a/tests/Makefile b/tests/Makefile index 6771828ceb..4072702f4b 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -191,6 +191,7 @@ uninstall-ngf: ## Uninstall NGF on configured kind cluster # Run CEL validation integration tests against a real cluster .PHONY: test-cel-validation test-cel-validation: + -make install-crds || true; @if [ -z "$(CEL_TEST_TARGET)" ]; then \ echo "Running all tests in ./cel"; \ go test -v ./cel; \ diff --git a/tests/cel/clientsettingspolicy_test.go b/tests/cel/clientsettingspolicy_test.go index 7e7ea5c4b1..d7de613416 100644 --- a/tests/cel/clientsettingspolicy_test.go +++ b/tests/cel/clientsettingspolicy_test.go @@ -1,34 +1,27 @@ package cel import ( - "context" "testing" - . "github.com/onsi/gomega" controllerruntime "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" ngfAPIv1alpha1 "github.com/nginx/nginx-gateway-fabric/v2/apis/v1alpha1" "github.com/nginx/nginx-gateway-fabric/v2/internal/framework/helpers" - "github.com/nginx/nginx-gateway-fabric/v2/tests/framework" ) func TestClientSettingsPoliciesTargetRefKind(t *testing.T) { t.Parallel() - g := NewWithT(t) - - k8sClient, err := getKubernetesClient(t) - g.Expect(err).ToNot(HaveOccurred()) + k8sClient := getKubernetesClient(t) tests := []struct { - policySpec ngfAPIv1alpha1.ClientSettingsPolicySpec + spec ngfAPIv1alpha1.ClientSettingsPolicySpec name string wantErrors []string }{ { name: "Validate TargetRef of kind Gateway is allowed", - policySpec: ngfAPIv1alpha1.ClientSettingsPolicySpec{ + spec: ngfAPIv1alpha1.ClientSettingsPolicySpec{ TargetRef: gatewayv1alpha2.LocalPolicyTargetReference{ Kind: gatewayKind, Group: gatewayGroup, @@ -37,7 +30,7 @@ func TestClientSettingsPoliciesTargetRefKind(t *testing.T) { }, { name: "Validate TargetRef of kind HTTPRoute is allowed", - policySpec: ngfAPIv1alpha1.ClientSettingsPolicySpec{ + spec: ngfAPIv1alpha1.ClientSettingsPolicySpec{ TargetRef: gatewayv1alpha2.LocalPolicyTargetReference{ Kind: httpRouteKind, Group: gatewayGroup, @@ -46,7 +39,7 @@ func TestClientSettingsPoliciesTargetRefKind(t *testing.T) { }, { name: "Validate TargetRef of kind GRPCRoute is allowed", - policySpec: ngfAPIv1alpha1.ClientSettingsPolicySpec{ + spec: ngfAPIv1alpha1.ClientSettingsPolicySpec{ TargetRef: gatewayv1alpha2.LocalPolicyTargetReference{ Kind: grpcRouteKind, Group: gatewayGroup, @@ -56,7 +49,7 @@ func TestClientSettingsPoliciesTargetRefKind(t *testing.T) { { name: "Validate Invalid TargetRef Kind is not allowed", wantErrors: []string{expectedTargetRefKindError}, - policySpec: ngfAPIv1alpha1.ClientSettingsPolicySpec{ + spec: ngfAPIv1alpha1.ClientSettingsPolicySpec{ TargetRef: gatewayv1alpha2.LocalPolicyTargetReference{ Kind: invalidKind, Group: gatewayGroup, @@ -66,7 +59,7 @@ func TestClientSettingsPoliciesTargetRefKind(t *testing.T) { { name: "Validate TCPRoute TargetRef Kind is not allowed", wantErrors: []string{expectedTargetRefKindError}, - policySpec: ngfAPIv1alpha1.ClientSettingsPolicySpec{ + spec: ngfAPIv1alpha1.ClientSettingsPolicySpec{ TargetRef: gatewayv1alpha2.LocalPolicyTargetReference{ Kind: tcpRouteKind, Group: gatewayGroup, @@ -78,26 +71,32 @@ func TestClientSettingsPoliciesTargetRefKind(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - validateClientSettingsPolicy(t, tt, g, k8sClient) + spec := tt.spec + spec.TargetRef.Name = gatewayv1alpha2.ObjectName(uniqueResourceName(testTargetRefName)) + clientSettingsPolicy := &ngfAPIv1alpha1.ClientSettingsPolicy{ + ObjectMeta: controllerruntime.ObjectMeta{ + Name: uniqueResourceName(testResourceName), + Namespace: defaultNamespace, + }, + Spec: spec, + } + validateCrd(t, tt.wantErrors, clientSettingsPolicy, k8sClient) }) } } func TestClientSettingsPoliciesTargetRefGroup(t *testing.T) { t.Parallel() - g := NewWithT(t) - - k8sClient, err := getKubernetesClient(t) - g.Expect(err).ToNot(HaveOccurred()) + k8sClient := getKubernetesClient(t) tests := []struct { - policySpec ngfAPIv1alpha1.ClientSettingsPolicySpec + spec ngfAPIv1alpha1.ClientSettingsPolicySpec name string wantErrors []string }{ { name: "Validate gateway.networking.k8s.io TargetRef Group is allowed", - policySpec: ngfAPIv1alpha1.ClientSettingsPolicySpec{ + spec: ngfAPIv1alpha1.ClientSettingsPolicySpec{ TargetRef: gatewayv1alpha2.LocalPolicyTargetReference{ Kind: gatewayKind, Group: gatewayGroup, @@ -107,7 +106,7 @@ func TestClientSettingsPoliciesTargetRefGroup(t *testing.T) { { name: "Validate invalid.networking.k8s.io TargetRef Group is not allowed", wantErrors: []string{expectedTargetRefGroupError}, - policySpec: ngfAPIv1alpha1.ClientSettingsPolicySpec{ + spec: ngfAPIv1alpha1.ClientSettingsPolicySpec{ TargetRef: gatewayv1alpha2.LocalPolicyTargetReference{ Kind: gatewayKind, Group: invalidGroup, @@ -117,7 +116,7 @@ func TestClientSettingsPoliciesTargetRefGroup(t *testing.T) { { name: "Validate discovery.k8s.io/v1 TargetRef Group is not allowed", wantErrors: []string{expectedTargetRefGroupError}, - policySpec: ngfAPIv1alpha1.ClientSettingsPolicySpec{ + spec: ngfAPIv1alpha1.ClientSettingsPolicySpec{ TargetRef: gatewayv1alpha2.LocalPolicyTargetReference{ Kind: gatewayKind, Group: discoveryGroup, @@ -129,26 +128,32 @@ func TestClientSettingsPoliciesTargetRefGroup(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - validateClientSettingsPolicy(t, tt, g, k8sClient) + spec := tt.spec + spec.TargetRef.Name = gatewayv1alpha2.ObjectName(uniqueResourceName(testTargetRefName)) + clientSettingsPolicy := &ngfAPIv1alpha1.ClientSettingsPolicy{ + ObjectMeta: controllerruntime.ObjectMeta{ + Name: uniqueResourceName(testResourceName), + Namespace: defaultNamespace, + }, + Spec: spec, + } + validateCrd(t, tt.wantErrors, clientSettingsPolicy, k8sClient) }) } } func TestClientSettingsPoliciesKeepAliveTimeout(t *testing.T) { t.Parallel() - g := NewWithT(t) - - k8sClient, err := getKubernetesClient(t) - g.Expect(err).ToNot(HaveOccurred()) + k8sClient := getKubernetesClient(t) tests := []struct { - policySpec ngfAPIv1alpha1.ClientSettingsPolicySpec + spec ngfAPIv1alpha1.ClientSettingsPolicySpec name string wantErrors []string }{ { name: "Validate KeepAliveTimeout is not set", - policySpec: ngfAPIv1alpha1.ClientSettingsPolicySpec{ + spec: ngfAPIv1alpha1.ClientSettingsPolicySpec{ TargetRef: gatewayv1alpha2.LocalPolicyTargetReference{ Kind: gatewayKind, Group: gatewayGroup, @@ -158,7 +163,7 @@ func TestClientSettingsPoliciesKeepAliveTimeout(t *testing.T) { }, { name: "Validate KeepAlive is set", - policySpec: ngfAPIv1alpha1.ClientSettingsPolicySpec{ + spec: ngfAPIv1alpha1.ClientSettingsPolicySpec{ TargetRef: gatewayv1alpha2.LocalPolicyTargetReference{ Kind: gatewayKind, Group: gatewayGroup, @@ -174,7 +179,7 @@ func TestClientSettingsPoliciesKeepAliveTimeout(t *testing.T) { { name: "Validate Header cannot be set without Server", wantErrors: []string{expectedHeaderWithoutServerError}, - policySpec: ngfAPIv1alpha1.ClientSettingsPolicySpec{ + spec: ngfAPIv1alpha1.ClientSettingsPolicySpec{ TargetRef: gatewayv1alpha2.LocalPolicyTargetReference{ Kind: gatewayKind, Group: gatewayGroup, @@ -191,46 +196,16 @@ func TestClientSettingsPoliciesKeepAliveTimeout(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - validateClientSettingsPolicy(t, tt, g, k8sClient) + spec := tt.spec + spec.TargetRef.Name = gatewayv1alpha2.ObjectName(uniqueResourceName(testTargetRefName)) + clientSettingsPolicy := &ngfAPIv1alpha1.ClientSettingsPolicy{ + ObjectMeta: controllerruntime.ObjectMeta{ + Name: uniqueResourceName(testResourceName), + Namespace: defaultNamespace, + }, + Spec: spec, + } + validateCrd(t, tt.wantErrors, clientSettingsPolicy, k8sClient) }) } } - -func validateClientSettingsPolicy(t *testing.T, tt struct { - policySpec ngfAPIv1alpha1.ClientSettingsPolicySpec - name string - wantErrors []string -}, g *WithT, k8sClient client.Client, -) { - t.Helper() - - policySpec := tt.policySpec - policySpec.TargetRef.Name = gatewayv1alpha2.ObjectName(uniqueResourceName(testTargetRefName)) - policyName := uniqueResourceName(testPolicyName) - - clientSettingsPolicy := &ngfAPIv1alpha1.ClientSettingsPolicy{ - ObjectMeta: controllerruntime.ObjectMeta{ - Name: policyName, - Namespace: defaultNamespace, - }, - Spec: policySpec, - } - timeoutConfig := framework.DefaultTimeoutConfig() - ctx, cancel := context.WithTimeout(context.Background(), timeoutConfig.KubernetesClientTimeout) - err := k8sClient.Create(ctx, clientSettingsPolicy) - defer cancel() - - // Clean up after test - defer func() { - _ = k8sClient.Delete(context.Background(), clientSettingsPolicy) - }() - - if len(tt.wantErrors) == 0 { - g.Expect(err).ToNot(HaveOccurred()) - } else { - g.Expect(err).To(HaveOccurred()) - for _, wantError := range tt.wantErrors { - g.Expect(err.Error()).To(ContainSubstring(wantError), "Expected error '%s' not found in: %s", wantError, err.Error()) - } - } -} diff --git a/tests/cel/common.go b/tests/cel/common.go index 2f25b1f7af..fcbb85366c 100644 --- a/tests/cel/common.go +++ b/tests/cel/common.go @@ -1,16 +1,19 @@ package cel import ( + "context" "crypto/rand" "fmt" "testing" + . "github.com/onsi/gomega" "k8s.io/apimachinery/pkg/runtime" controllerruntime "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" ngfAPIv1alpha1 "github.com/nginx/nginx-gateway-fabric/v2/apis/v1alpha1" ngfAPIv1alpha2 "github.com/nginx/nginx-gateway-fabric/v2/apis/v1alpha2" + "github.com/nginx/nginx-gateway-fabric/v2/tests/framework" ) const ( @@ -27,40 +30,46 @@ const ( discoveryGroup = "discovery.k8s.io/v1" ) +// ClientSettingsPolicy validation errors. const ( expectedTargetRefKindError = `TargetRef Kind must be one of: Gateway, HTTPRoute, or GRPCRoute` expectedTargetRefGroupError = `TargetRef Group must be gateway.networking.k8s.io.` expectedHeaderWithoutServerError = `header can only be specified if server is specified` ) +// NginxProxy validation errors. +const ( + expectedOneOfDeploymentOrDaemonSetError = `only one of deployment or daemonSet can be set` + expectedIfModeSetTrustedAddressesError = `if mode is set, trustedAddresses is a required field` + expectedMinReplicasLessThanOrEqualError = `minReplicas must be less than or equal to maxReplicas` +) + const ( defaultNamespace = "default" ) const ( - testPolicyName = "test-policy" + testResourceName = "test-resource" testTargetRefName = "test-targetRef" ) // getKubernetesClient returns a client connected to a real Kubernetes cluster. -func getKubernetesClient(t *testing.T) (k8sClient client.Client, err error) { +func getKubernetesClient(t *testing.T) (k8sClient client.Client) { t.Helper() + g := NewWithT(t) // Use controller-runtime to get cluster connection k8sConfig, err := controllerruntime.GetConfig() - if err != nil { - return nil, err - } + g.Expect(err).ToNot(HaveOccurred()) // Set up scheme with NGF types scheme := runtime.NewScheme() - if err = ngfAPIv1alpha1.AddToScheme(scheme); err != nil { - return nil, err - } - if err = ngfAPIv1alpha2.AddToScheme(scheme); err != nil { - return nil, err - } - // Create a new client with the scheme and return it - return client.New(k8sConfig, client.Options{Scheme: scheme}) + g.Expect(ngfAPIv1alpha1.AddToScheme(scheme)).To(Succeed()) + g.Expect(ngfAPIv1alpha2.AddToScheme(scheme)).To(Succeed()) + + k8sClient, err = client.New(k8sConfig, client.Options{Scheme: scheme}) + g.Expect(err).ToNot(HaveOccurred()) + + return k8sClient } // randomPrimeNumber generates a random prime number of 64 bits. @@ -77,3 +86,27 @@ func randomPrimeNumber() int64 { func uniqueResourceName(name string) string { return fmt.Sprintf("%s-%d", name, randomPrimeNumber()) } + +// validateCrd creates a k8s resource and validates it against the expected errors. +func validateCrd(t *testing.T, wantErrors []string, crd client.Object, k8sClient client.Client) { + t.Helper() + g := NewWithT(t) + + timeoutConfig := framework.DefaultTimeoutConfig() + ctx, cancel := context.WithTimeout(context.Background(), timeoutConfig.KubernetesClientTimeout) + defer cancel() + err := k8sClient.Create(ctx, crd) + + // Check for expected errors + if len(wantErrors) == 0 { + g.Expect(err).ToNot(HaveOccurred()) + // Clean up after test + // Resources only need to be deleted if they were created successfully + g.Expect(k8sClient.Delete(ctx, crd)).To(Succeed()) + } else { + g.Expect(err).To(HaveOccurred()) + for _, wantError := range wantErrors { + g.Expect(err.Error()).To(ContainSubstring(wantError), "Expected error '%s' not found in: %s", wantError, err.Error()) + } + } +} diff --git a/tests/cel/nginxproxy_test.go b/tests/cel/nginxproxy_test.go new file mode 100644 index 0000000000..022c870d63 --- /dev/null +++ b/tests/cel/nginxproxy_test.go @@ -0,0 +1,198 @@ +package cel + +import ( + "testing" + + controllerruntime "sigs.k8s.io/controller-runtime" + + ngfAPIv1alpha2 "github.com/nginx/nginx-gateway-fabric/v2/apis/v1alpha2" + "github.com/nginx/nginx-gateway-fabric/v2/internal/framework/helpers" +) + +func TestNginxProxyKubernetes(t *testing.T) { + t.Parallel() + k8sClient := getKubernetesClient(t) + + tests := []struct { + spec ngfAPIv1alpha2.NginxProxySpec + name string + wantErrors []string + }{ + { + name: "Validate NginxProxy with both Deployment and DaemonSet is invalid", + wantErrors: []string{expectedOneOfDeploymentOrDaemonSetError}, + spec: ngfAPIv1alpha2.NginxProxySpec{ + Kubernetes: &ngfAPIv1alpha2.KubernetesSpec{ + Deployment: &ngfAPIv1alpha2.DeploymentSpec{}, + DaemonSet: &ngfAPIv1alpha2.DaemonSetSpec{}, + }, + }, + }, + { + name: "Validate NginxProxy with Deployment only is valid", + spec: ngfAPIv1alpha2.NginxProxySpec{ + Kubernetes: &ngfAPIv1alpha2.KubernetesSpec{ + Deployment: &ngfAPIv1alpha2.DeploymentSpec{}, + }, + }, + }, + { + name: "Validate NginxProxy with DaemonSet only is valid", + spec: ngfAPIv1alpha2.NginxProxySpec{ + Kubernetes: &ngfAPIv1alpha2.KubernetesSpec{ + DaemonSet: &ngfAPIv1alpha2.DaemonSetSpec{}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + spec := tt.spec + resourceName := uniqueResourceName(testResourceName) + + nginxProxy := &ngfAPIv1alpha2.NginxProxy{ + ObjectMeta: controllerruntime.ObjectMeta{ + Name: resourceName, + Namespace: defaultNamespace, + }, + Spec: spec, + } + validateCrd(t, tt.wantErrors, nginxProxy, k8sClient) + }) + } +} + +func TestNginxProxyRewriteClientIP(t *testing.T) { + t.Parallel() + k8sClient := getKubernetesClient(t) + + tests := []struct { + spec ngfAPIv1alpha2.NginxProxySpec + name string + wantErrors []string + }{ + { + name: "Validate NginxProxy is invalid when trustedAddresses is not set and mode is set", + wantErrors: []string{expectedIfModeSetTrustedAddressesError}, + spec: ngfAPIv1alpha2.NginxProxySpec{ + RewriteClientIP: &ngfAPIv1alpha2.RewriteClientIP{ + Mode: helpers.GetPointer[ngfAPIv1alpha2.RewriteClientIPModeType]("XForwardedFor"), + }, + }, + }, + { + name: "Validate NginxProxy is valid when both mode and trustedAddresses are set", + spec: ngfAPIv1alpha2.NginxProxySpec{ + RewriteClientIP: &ngfAPIv1alpha2.RewriteClientIP{ + Mode: helpers.GetPointer[ngfAPIv1alpha2.RewriteClientIPModeType]("XForwardedFor"), + TrustedAddresses: []ngfAPIv1alpha2.RewriteClientIPAddress{ + { + Type: ngfAPIv1alpha2.RewriteClientIPAddressType("CIDR"), + Value: "10.0.0.0/8", + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + spec := tt.spec + resourceName := uniqueResourceName(testResourceName) + + nginxProxy := &ngfAPIv1alpha2.NginxProxy{ + ObjectMeta: controllerruntime.ObjectMeta{ + Name: resourceName, + Namespace: defaultNamespace, + }, + Spec: spec, + } + validateCrd(t, tt.wantErrors, nginxProxy, k8sClient) + }) + } +} + +func TestNginxProxyAutoscaling(t *testing.T) { + t.Parallel() + k8sClient := getKubernetesClient(t) + + tests := []struct { + spec ngfAPIv1alpha2.NginxProxySpec + name string + wantErrors []string + }{ + { + name: "Validate NginxProxy is invalid when MinReplicas not less than, or equal to MaxReplicas", + wantErrors: []string{expectedMinReplicasLessThanOrEqualError}, + spec: ngfAPIv1alpha2.NginxProxySpec{ + Kubernetes: &ngfAPIv1alpha2.KubernetesSpec{ + Deployment: &ngfAPIv1alpha2.DeploymentSpec{ + Autoscaling: &ngfAPIv1alpha2.AutoscalingSpec{ + MinReplicas: helpers.GetPointer[int32](10), + MaxReplicas: 5, + }, + }, + }, + }, + }, + { + name: "Validate NginxProxy is valid when MinReplicas is less than MaxReplicas", + spec: ngfAPIv1alpha2.NginxProxySpec{ + Kubernetes: &ngfAPIv1alpha2.KubernetesSpec{ + Deployment: &ngfAPIv1alpha2.DeploymentSpec{ + Autoscaling: &ngfAPIv1alpha2.AutoscalingSpec{ + MinReplicas: helpers.GetPointer[int32](1), + MaxReplicas: 5, + }, + }, + }, + }, + }, + { + name: "Validate NginxProxy is valid when MinReplicas is equal to MaxReplicas", + spec: ngfAPIv1alpha2.NginxProxySpec{ + Kubernetes: &ngfAPIv1alpha2.KubernetesSpec{ + Deployment: &ngfAPIv1alpha2.DeploymentSpec{ + Autoscaling: &ngfAPIv1alpha2.AutoscalingSpec{ + MinReplicas: helpers.GetPointer[int32](5), + MaxReplicas: 5, + }, + }, + }, + }, + }, + { + name: "Validate NginxProxy is valid when MinReplicas is nil", + spec: ngfAPIv1alpha2.NginxProxySpec{ + Kubernetes: &ngfAPIv1alpha2.KubernetesSpec{ + Deployment: &ngfAPIv1alpha2.DeploymentSpec{ + Autoscaling: &ngfAPIv1alpha2.AutoscalingSpec{ + MinReplicas: nil, + MaxReplicas: 5, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + spec := tt.spec + resourceName := uniqueResourceName(testResourceName) + + nginxProxy := &ngfAPIv1alpha2.NginxProxy{ + ObjectMeta: controllerruntime.ObjectMeta{ + Name: resourceName, + Namespace: defaultNamespace, + }, + Spec: spec, + } + validateCrd(t, tt.wantErrors, nginxProxy, k8sClient) + }) + } +}