From e4ad18c8c22277ce500bbeedb2a9b88a72e3ecac Mon Sep 17 00:00:00 2001 From: Saloni Date: Tue, 8 Oct 2024 13:01:03 -0700 Subject: [PATCH] add functional tests for snippetsFilter --- tests/framework/ngf.go | 2 + .../suite/manifests/snippets-filter/cafe.yaml | 98 +++++ .../manifests/snippets-filter/gateway.yaml | 11 + .../snippets-filter/grpc-backend.yaml | 39 ++ .../snippets-filter/invalid-context-sf.yaml | 37 ++ .../snippets-filter/invalid-duplicate-sf.yaml | 39 ++ .../manifests/snippets-filter/valid-sf.yaml | 77 ++++ tests/suite/snippets_filter_test.go | 347 ++++++++++++++++++ 8 files changed, 650 insertions(+) create mode 100644 tests/suite/manifests/snippets-filter/cafe.yaml create mode 100644 tests/suite/manifests/snippets-filter/gateway.yaml create mode 100644 tests/suite/manifests/snippets-filter/grpc-backend.yaml create mode 100644 tests/suite/manifests/snippets-filter/invalid-context-sf.yaml create mode 100644 tests/suite/manifests/snippets-filter/invalid-duplicate-sf.yaml create mode 100644 tests/suite/manifests/snippets-filter/valid-sf.yaml create mode 100644 tests/suite/snippets_filter_test.go diff --git a/tests/framework/ngf.go b/tests/framework/ngf.go index 67b1257464..8cf0ecd0f7 100644 --- a/tests/framework/ngf.go +++ b/tests/framework/ngf.go @@ -67,6 +67,7 @@ func InstallNGF(cfg InstallationConfig, extraArgs ...string) ([]byte, error) { "--namespace", cfg.Namespace, "--wait", "--set", "nginxGateway.productTelemetry.enable=false", + "--set", "nginxGateway.snippetsFilters.enable=true", } if cfg.ChartVersion != "" { args = append(args, "--version", cfg.ChartVersion) @@ -96,6 +97,7 @@ func UpgradeNGF(cfg InstallationConfig, extraArgs ...string) ([]byte, error) { "--wait", "--set", "nginxGateway.productTelemetry.enable=false", "--set", "nginxGateway.config.logging.level=debug", + "--set", "nginxGateway.snippetsFilter.enable=true", } if cfg.ChartVersion != "" { args = append(args, "--version", cfg.ChartVersion) diff --git a/tests/suite/manifests/snippets-filter/cafe.yaml b/tests/suite/manifests/snippets-filter/cafe.yaml new file mode 100644 index 0000000000..c6bc391de5 --- /dev/null +++ b/tests/suite/manifests/snippets-filter/cafe.yaml @@ -0,0 +1,98 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: coffee +spec: + replicas: 1 + selector: + matchLabels: + app: coffee + template: + metadata: + labels: + app: coffee + spec: + containers: + - name: coffee + image: nginxdemos/nginx-hello:plain-text + ports: + - containerPort: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: coffee +spec: + ports: + - port: 80 + targetPort: 8080 + protocol: TCP + name: http + selector: + app: coffee +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tea +spec: + replicas: 1 + selector: + matchLabels: + app: tea + template: + metadata: + labels: + app: tea + spec: + containers: + - name: tea + image: nginxdemos/nginx-hello:plain-text + ports: + - containerPort: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: tea +spec: + ports: + - port: 80 + targetPort: 8080 + protocol: TCP + name: http + selector: + app: tea +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: soda +spec: + replicas: 1 + selector: + matchLabels: + app: soda + template: + metadata: + labels: + app: soda + spec: + containers: + - name: soda + image: nginxdemos/nginx-hello:plain-text + ports: + - containerPort: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: soda +spec: + ports: + - port: 80 + targetPort: 8080 + protocol: TCP + name: http + selector: + app: soda diff --git a/tests/suite/manifests/snippets-filter/gateway.yaml b/tests/suite/manifests/snippets-filter/gateway.yaml new file mode 100644 index 0000000000..e6507f613b --- /dev/null +++ b/tests/suite/manifests/snippets-filter/gateway.yaml @@ -0,0 +1,11 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: gateway +spec: + gatewayClassName: nginx + listeners: + - name: http + port: 80 + protocol: HTTP + hostname: "*.example.com" diff --git a/tests/suite/manifests/snippets-filter/grpc-backend.yaml b/tests/suite/manifests/snippets-filter/grpc-backend.yaml new file mode 100644 index 0000000000..ca92d268c6 --- /dev/null +++ b/tests/suite/manifests/snippets-filter/grpc-backend.yaml @@ -0,0 +1,39 @@ +apiVersion: v1 +kind: Service +metadata: + name: grpc-backend +spec: + selector: + app: grpc-backend + ports: + - protocol: TCP + port: 8080 + targetPort: 50051 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: grpc-backend + labels: + app: grpc-backend +spec: + replicas: 1 + selector: + matchLabels: + app: grpc-backend + template: + metadata: + labels: + app: grpc-backend + spec: + containers: + - name: grpc-backend + image: ghcr.io/nginxinc/kic-test-grpc-server:0.2.2 + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + resources: + requests: + cpu: 10m diff --git a/tests/suite/manifests/snippets-filter/invalid-context-sf.yaml b/tests/suite/manifests/snippets-filter/invalid-context-sf.yaml new file mode 100644 index 0000000000..93438ab6d6 --- /dev/null +++ b/tests/suite/manifests/snippets-filter/invalid-context-sf.yaml @@ -0,0 +1,37 @@ +apiVersion: gateway.nginx.org/v1alpha1 +kind: SnippetsFilter +metadata: + name: invalid-context +spec: + snippets: + - context: http + value: aio on; + - context: http.server + value: worker_priority 0; # wrong context for the directive, should be main. + - context: http.server.location + value: keepalive_time 10s; +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: soda +spec: + parentRefs: + - name: gateway + sectionName: http + hostnames: + - "cafe.example.com" + rules: + - matches: + - path: + type: Exact + value: /soda + filters: + - type: ExtensionRef + extensionRef: + group: gateway.nginx.org + kind: SnippetsFilter + name: invalid-context + backendRefs: + - name: soda + port: 80 diff --git a/tests/suite/manifests/snippets-filter/invalid-duplicate-sf.yaml b/tests/suite/manifests/snippets-filter/invalid-duplicate-sf.yaml new file mode 100644 index 0000000000..ba7e5b6c55 --- /dev/null +++ b/tests/suite/manifests/snippets-filter/invalid-duplicate-sf.yaml @@ -0,0 +1,39 @@ +apiVersion: gateway.nginx.org/v1alpha1 +kind: SnippetsFilter +metadata: + name: duplicate-directive +spec: + snippets: + - context: main + value: worker_processes auto; # already present in the configuration + - context: http + value: aio on; + - context: http.server + value: auth_delay 10s; + - context: http.server.location + value: keepalive_time 10s; +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: tea +spec: + parentRefs: + - name: gateway + sectionName: http + hostnames: + - "cafe.example.com" + rules: + - matches: + - path: + type: Exact + value: /tea + filters: + - type: ExtensionRef + extensionRef: + group: gateway.nginx.org + kind: SnippetsFilter + name: duplicate-directive + backendRefs: + - name: tea + port: 80 diff --git a/tests/suite/manifests/snippets-filter/valid-sf.yaml b/tests/suite/manifests/snippets-filter/valid-sf.yaml new file mode 100644 index 0000000000..ca036a5d8f --- /dev/null +++ b/tests/suite/manifests/snippets-filter/valid-sf.yaml @@ -0,0 +1,77 @@ +apiVersion: gateway.nginx.org/v1alpha1 +kind: SnippetsFilter +metadata: + name: all-contexts +spec: + snippets: + - context: main + value: worker_priority 0; + - context: http + value: aio on; + - context: http.server + value: auth_delay 10s; + - context: http.server.location + value: keepalive_time 10s; +--- +apiVersion: gateway.nginx.org/v1alpha1 +kind: SnippetsFilter +metadata: + name: grpc-all-contexts +spec: + snippets: + - context: main + value: worker_shutdown_timeout 120s; + - context: http + value: types_hash_bucket_size 64; + - context: http.server + value: server_tokens on; + - context: http.server.location + value: tcp_nodelay on; +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: coffee +spec: + parentRefs: + - name: gateway + sectionName: http + hostnames: + - "cafe.example.com" + rules: + - matches: + - path: + type: PathPrefix + value: /coffee + filters: + - type: ExtensionRef + extensionRef: + group: gateway.nginx.org + kind: SnippetsFilter + name: all-contexts + backendRefs: + - name: coffee + port: 80 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: GRPCRoute +metadata: + name: grpc-route +spec: + parentRefs: + - name: gateway + sectionName: http + rules: + - matches: + - method: + service: helloworld.Greeter + method: SayHello + filters: + - type: ExtensionRef + extensionRef: + group: gateway.nginx.org + kind: SnippetsFilter + name: grpc-all-contexts + backendRefs: + - name: grpc-backend + port: 8080 diff --git a/tests/suite/snippets_filter_test.go b/tests/suite/snippets_filter_test.go new file mode 100644 index 0000000000..632a8199e0 --- /dev/null +++ b/tests/suite/snippets_filter_test.go @@ -0,0 +1,347 @@ +package main + +import ( + "context" + "fmt" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + core "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "sigs.k8s.io/controller-runtime/pkg/client" + v1 "sigs.k8s.io/gateway-api/apis/v1" + + ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions" + "github.com/nginxinc/nginx-gateway-fabric/tests/framework" +) + +var _ = Describe("SnippetsFilter", Ordered, Label("functional", "snippets-filter"), func() { + var ( + files = []string{ + "snippets-filter/cafe.yaml", + "snippets-filter/gateway.yaml", + "snippets-filter/grpc-backend.yaml", + } + + namespace = "snippets-filter" + ) + + BeforeAll(func() { + ns := &core.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + }, + } + + Expect(resourceManager.Apply([]client.Object{ns})).To(Succeed()) + Expect(resourceManager.ApplyFromFiles(files, namespace)).To(Succeed()) + Expect(resourceManager.WaitForAppsToBeReady(namespace)).To(Succeed()) + }) + + AfterAll(func() { + Expect(resourceManager.DeleteNamespace(namespace)).To(Succeed()) + }) + + When("SnippetsFilters are applied to the resources", func() { + snippetsFilter := []string{ + "snippets-filter/valid-sf.yaml", + } + + BeforeAll(func() { + Expect(resourceManager.ApplyFromFiles(snippetsFilter, namespace)).To(Succeed()) + }) + + AfterAll(func() { + Expect(resourceManager.DeleteFromFiles(snippetsFilter, namespace)).To(Succeed()) + }) + + Specify("snippetsFilters are accepted", func() { + snippetsFilterNames := []string{ + "all-contexts", + "grpc-all-contexts", + } + + for _, name := range snippetsFilterNames { + nsname := types.NamespacedName{Name: name, Namespace: namespace} + + err := waitForSnippetsFilterToBeAccepted(nsname) + Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("%s was not accepted", name)) + } + }) + + Context("verify working traffic", func() { + It("should return a 200 response for HTTPRoute", func() { + port := 80 + if portFwdPort != 0 { + port = portFwdPort + } + baseURL := fmt.Sprintf("http://cafe.example.com:%d%s", port, "/coffee") + + Eventually( + func() error { + return expectRequestToSucceed(baseURL, address, "URI: /coffee") + }). + WithTimeout(timeoutConfig.RequestTimeout). + WithPolling(500 * time.Millisecond). + Should(Succeed()) + }) + }) + + Context("nginx directives", func() { + var conf *framework.Payload + snippetsFilterFilePrefix := "/etc/nginx/includes/SnippetsFilter_" + + mainContext := fmt.Sprintf("%smain_", snippetsFilterFilePrefix) + httpContext := fmt.Sprintf("%shttp_", snippetsFilterFilePrefix) + httpServerContext := fmt.Sprintf("%shttp.server_", snippetsFilterFilePrefix) + httpServerLocationContext := fmt.Sprintf("%shttp.server.location_", snippetsFilterFilePrefix) + + httpRouteSuffix := fmt.Sprintf("%s_all-contexts.conf", namespace) + grpcRouteSuffix := fmt.Sprintf("%s_grpc-all-contexts.conf", namespace) + + BeforeAll(func() { + podNames, err := framework.GetReadyNGFPodNames(k8sClient, ngfNamespace, releaseName, timeoutConfig.GetTimeout) + Expect(err).ToNot(HaveOccurred()) + Expect(podNames).To(HaveLen(1)) + + ngfPodName := podNames[0] + + conf, err = resourceManager.GetNginxConfig(ngfPodName, ngfNamespace) + Expect(err).ToNot(HaveOccurred()) + }) + + DescribeTable("are set properly for", + func(expCfgs []framework.ExpectedNginxField) { + for _, expCfg := range expCfgs { + Expect(framework.ValidateNginxFieldExists(conf, expCfg)).To(Succeed()) + } + }, + Entry("HTTPRoute", []framework.ExpectedNginxField{ + { + Directive: "worker_priority", + Value: "0", + File: fmt.Sprintf("%s%s", mainContext, httpRouteSuffix), + }, + { + Directive: "include", + Value: fmt.Sprintf("%s%s", mainContext, httpRouteSuffix), + File: "main.conf", + }, + { + Directive: "aio", + Value: "on", + File: fmt.Sprintf("%s%s", httpContext, httpRouteSuffix), + }, + { + Directive: "include", + Value: fmt.Sprintf("%s%s", httpContext, httpRouteSuffix), + File: "http.conf", + }, + { + Directive: "auth_delay", + Value: "10s", + File: fmt.Sprintf("%s%s", httpServerContext, httpRouteSuffix), + }, + { + Directive: "include", + Value: fmt.Sprintf("%s%s", httpServerContext, httpRouteSuffix), + Servers: []string{"cafe.example.com"}, + File: "http.conf", + }, + { + Directive: "include", + Value: fmt.Sprintf("%s%s", httpServerLocationContext, httpRouteSuffix), + File: "http.conf", + Location: "/coffee", + Servers: []string{"cafe.example.com"}, + }, + { + Directive: "keepalive_time", + Value: "10s", + File: fmt.Sprintf("%s%s", httpServerLocationContext, httpRouteSuffix), + }, + }), + Entry("GRPCRoute", []framework.ExpectedNginxField{ + { + Directive: "worker_shutdown_timeout", + Value: "120s", + File: fmt.Sprintf("%s%s", mainContext, grpcRouteSuffix), + }, + { + Directive: "include", + Value: fmt.Sprintf("%s%s", mainContext, grpcRouteSuffix), + File: "main.conf", + }, + { + Directive: "types_hash_bucket_size", + Value: "64", + File: fmt.Sprintf("%s%s", httpContext, grpcRouteSuffix), + }, + { + Directive: "include", + Value: fmt.Sprintf("%s%s", httpContext, grpcRouteSuffix), + File: "http.conf", + }, + { + Directive: "server_tokens", + Value: "on", + File: fmt.Sprintf("%s%s", httpServerContext, grpcRouteSuffix), + }, + { + Directive: "include", + Value: fmt.Sprintf("%s%s", httpServerContext, grpcRouteSuffix), + Servers: []string{"*.example.com"}, + File: "http.conf", + }, + { + Directive: "tcp_nodelay", + Value: "on", + File: fmt.Sprintf("%s%s", httpServerLocationContext, grpcRouteSuffix), + }, + { + Directive: "include", + Value: fmt.Sprintf("%s%s", httpServerLocationContext, grpcRouteSuffix), + File: "http.conf", + Location: "/helloworld.Greeter/SayHello", + Servers: []string{"*.example.com"}, + }, + }), + ) + }) + }) + + When("SnippetsFilter is invalid", func() { + Specify("if directives already present in the config are used", func() { + files := []string{"snippets-filter/invalid-duplicate-sf.yaml"} + + Expect(resourceManager.ApplyFromFiles(files, namespace)).To(Succeed()) + + nsname := types.NamespacedName{Name: "tea", Namespace: namespace} + Expect(waitForHTTPRouteToHaveGatewayNotProgrammedCond(nsname)).To(Succeed()) + + Expect(resourceManager.DeleteFromFiles(files, namespace)).To(Succeed()) + }) + + Specify("if directives are provided in the wrong context", func() { + files := []string{"snippets-filter/invalid-context-sf.yaml"} + + Expect(resourceManager.ApplyFromFiles(files, namespace)).To(Succeed()) + + nsname := types.NamespacedName{Name: "soda", Namespace: namespace} + Expect(waitForHTTPRouteToHaveGatewayNotProgrammedCond(nsname)).To(Succeed()) + + Expect(resourceManager.DeleteFromFiles(files, namespace)).To(Succeed()) + }) + }) +}) + +func waitForHTTPRouteToHaveGatewayNotProgrammedCond(httpRouteNsName types.NamespacedName) error { + ctx, cancel := context.WithTimeout(context.Background(), timeoutConfig.GetStatusTimeout*2) + defer cancel() + + GinkgoWriter.Printf( + "Waiting for HTTPRoute %q to have the condition Accepted/True/GatewayNotProgrammed\n", + httpRouteNsName, + ) + + return wait.PollUntilContextCancel( + ctx, + 500*time.Millisecond, + true, /* poll immediately */ + func(ctx context.Context) (bool, error) { + var hr v1.HTTPRoute + var err error + + if err = k8sClient.Get(ctx, httpRouteNsName, &hr); err != nil { + return false, err + } + + if len(hr.Status.Parents) == 0 { + return false, nil + } + + if len(hr.Status.Parents) != 1 { + return false, fmt.Errorf("httproute has %d parent statuses, expected 1", len(hr.Status.Parents)) + } + + parent := hr.Status.Parents[0] + if parent.Conditions == nil { + return false, fmt.Errorf("expected parent conditions to not be nil") + } + + cond := parent.Conditions[1] + if cond.Type != string(v1.GatewayConditionAccepted) { + return false, fmt.Errorf("expected condition type to be Accepted, got %s", cond.Type) + } + + if cond.Status != metav1.ConditionFalse { + return false, fmt.Errorf("expected condition status to be False, got %s", cond.Status) + } + + if cond.Reason != string(conditions.RouteReasonGatewayNotProgrammed) { + return false, fmt.Errorf("expected condition reason to be GatewayNotProgrammed, got %s", cond.Reason) + } + return err == nil, err + }, + ) +} + +func waitForSnippetsFilterToBeAccepted(snippetsFilterNsNames types.NamespacedName) error { + ctx, cancel := context.WithTimeout(context.Background(), timeoutConfig.GetStatusTimeout) + defer cancel() + + GinkgoWriter.Printf( + "Waiting for SnippetsFilter %q to have the condition Accepted/True/Accepted\n", + snippetsFilterNsNames, + ) + + return wait.PollUntilContextCancel( + ctx, + 500*time.Millisecond, + true, /* poll immediately */ + func(ctx context.Context) (bool, error) { + var sf ngfAPI.SnippetsFilter + var err error + + if err = k8sClient.Get(ctx, snippetsFilterNsNames, &sf); err != nil { + return false, err + } + + if len(sf.Status.Controllers) == 0 { + return false, nil + } + + if len(sf.Status.Controllers) != 1 { + return false, fmt.Errorf("snippetsFilter has %d controller statuses, expected 1", len(sf.Status.Controllers)) + } + + status := sf.Status.Controllers[0] + if status.ControllerName != ngfControllerName { + return false, fmt.Errorf("expected controller name to be %s, got %s", ngfControllerName, status.ControllerName) + } + + condition := status.Conditions[0] + if condition.Type != string(ngfAPI.SnippetsFilterConditionTypeAccepted) { + return false, fmt.Errorf("expected condition type to be Accepted, got %s", condition.Type) + } + + if status.Conditions[0].Status != metav1.ConditionTrue { + return false, fmt.Errorf("expected condition status to be %s, got %s", metav1.ConditionTrue, condition.Status) + } + + if status.Conditions[0].Reason != string(ngfAPI.SnippetsFilterConditionReasonAccepted) { + return false, fmt.Errorf( + "expected condition reason to be %s, got %s", + ngfAPI.SnippetsFilterConditionReasonAccepted, + condition.Reason, + ) + } + + return err == nil, err + }, + ) +}