diff --git a/.github/workflows/functional.yml b/.github/workflows/functional.yml index 0163227394..4cec276d40 100644 --- a/.github/workflows/functional.yml +++ b/.github/workflows/functional.yml @@ -119,3 +119,10 @@ jobs: ngf_tag=${{ steps.ngf-meta.outputs.version }} make test${{ inputs.image == 'plus' && '-with-plus' || ''}} PREFIX=${ngf_prefix} TAG=${ngf_tag} GINKGO_LABEL=telemetry working-directory: ./tests + + - name: Run functional tests + run: | + ngf_prefix=ghcr.io/nginxinc/nginx-gateway-fabric + ngf_tag=${{ steps.ngf-meta.outputs.version }} + make test${{ inputs.image == 'plus' && '-with-plus' || ''}} PREFIX=${ngf_prefix} TAG=${ngf_tag} + working-directory: ./tests diff --git a/tests/README.md b/tests/README.md index 541b898914..c53a66edfc 100644 --- a/tests/README.md +++ b/tests/README.md @@ -255,6 +255,10 @@ Directory structure is as follows: > Note: Existing NFR tests will be migrated into this testing `suite` and results stored in the `results` directory. +### Logging in tests + +To log in the tests, use the `GinkgoWriter` interface described here: https://onsi.github.io/ginkgo/#logging-output. + ### Step 1 - Run the tests #### 1a - Run the functional tests locally diff --git a/tests/conformance/Dockerfile b/tests/conformance/Dockerfile index b1d2c6f921..f899e12a39 100644 --- a/tests/conformance/Dockerfile +++ b/tests/conformance/Dockerfile @@ -3,6 +3,7 @@ FROM golang:1.22 WORKDIR /go/src/github.com/nginxinc/nginx-gateway-fabric/tests/conformance +COPY ../. /go/src/github.com/nginxinc/nginx-gateway-fabric/ COPY --link go.mod /go/src/github.com/nginxinc/nginx-gateway-fabric/tests/ COPY --link go.sum /go/src/github.com/nginxinc/nginx-gateway-fabric/tests/ RUN go mod download diff --git a/tests/framework/ngf.go b/tests/framework/ngf.go index 9f6c13c4f8..5c6695ee8c 100644 --- a/tests/framework/ngf.go +++ b/tests/framework/ngf.go @@ -32,11 +32,7 @@ type InstallationConfig struct { } // InstallGatewayAPI installs the specified version of the Gateway API resources. -func InstallGatewayAPI( - k8sClient client.Client, - apiVersion, - k8sVersion string, -) ([]byte, error) { +func InstallGatewayAPI(apiVersion string) ([]byte, error) { apiPath := fmt.Sprintf("%s/v%s/standard-install.yaml", gwInstallBasePath, apiVersion) if output, err := exec.Command("kubectl", "apply", "-f", apiPath).CombinedOutput(); err != nil { @@ -163,7 +159,9 @@ func setImageArgs(cfg InstallationConfig) []string { if cfg.ServiceType != "" { args = append(args, formatValueSet("service.type", cfg.ServiceType)...) if cfg.ServiceType == "LoadBalancer" && cfg.IsGKEInternalLB { - args = append(args, formatValueSet(`service.annotations.networking\.gke\.io\/load-balancer-type`, "Internal")...) + args = append( + args, + formatValueSet(`service.annotations.networking\.gke\.io\/load-balancer-type`, "Internal")...) } } diff --git a/tests/framework/request.go b/tests/framework/request.go index f5a03c32a6..48705321a1 100644 --- a/tests/framework/request.go +++ b/tests/framework/request.go @@ -5,6 +5,7 @@ import ( "context" "crypto/tls" "fmt" + "io" "net" "net/http" "strings" @@ -15,6 +16,29 @@ import ( // It resolves to the specified address instead of using DNS. // The status and body of the response is returned, or an error. func Get(url, address string, timeout time.Duration) (int, string, error) { + resp, err := makeRequest(http.MethodGet, url, address, nil, timeout) + if err != nil { + return 0, "", err + } + + defer resp.Body.Close() + + body := new(bytes.Buffer) + _, err = body.ReadFrom(resp.Body) + if err != nil { + return resp.StatusCode, "", err + } + + return resp.StatusCode, body.String(), nil +} + +// Post sends a POST request to the specified url with the body as the payload. +// It resolves to the specified address instead of using DNS. +func Post(url, address string, body io.Reader, timeout time.Duration) (*http.Response, error) { + return makeRequest(http.MethodPost, url, address, body, timeout) +} + +func makeRequest(method, url, address string, body io.Reader, timeout time.Duration) (*http.Response, error) { dialer := &net.Dialer{} http.DefaultTransport.(*http.Transport).DialContext = func( @@ -30,9 +54,9 @@ func Get(url, address string, timeout time.Duration) (int, string, error) { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + req, err := http.NewRequestWithContext(ctx, method, url, body) if err != nil { - return 0, "", err + return nil, err } var resp *http.Response @@ -48,15 +72,8 @@ func Get(url, address string, timeout time.Duration) (int, string, error) { } if err != nil { - return 0, "", err - } - defer resp.Body.Close() - - body := new(bytes.Buffer) - _, err = body.ReadFrom(resp.Body) - if err != nil { - return resp.StatusCode, "", err + return nil, err } - return resp.StatusCode, body.String(), nil + return resp, nil } diff --git a/tests/framework/resourcemanager.go b/tests/framework/resourcemanager.go index 3cc73d08a8..193556f9b3 100644 --- a/tests/framework/resourcemanager.go +++ b/tests/framework/resourcemanager.go @@ -313,7 +313,11 @@ func (rm *ResourceManager) WaitForAppsToBeReadyWithCtx(ctx context.Context, name return err } - if err := rm.waitForRoutesToBeReady(ctx, namespace); err != nil { + if err := rm.waitForHTTPRoutesToBeReady(ctx, namespace); err != nil { + return err + } + + if err := rm.waitForGRPCRoutesToBeReady(ctx, namespace); err != nil { return err } @@ -371,7 +375,7 @@ func (rm *ResourceManager) waitForGatewaysToBeReady(ctx context.Context, namespa ) } -func (rm *ResourceManager) waitForRoutesToBeReady(ctx context.Context, namespace string) error { +func (rm *ResourceManager) waitForHTTPRoutesToBeReady(ctx context.Context, namespace string) error { return wait.PollUntilContextCancel( ctx, 500*time.Millisecond, @@ -385,13 +389,29 @@ func (rm *ResourceManager) waitForRoutesToBeReady(ctx context.Context, namespace var numParents, readyCount int for _, route := range routeList.Items { numParents += len(route.Spec.ParentRefs) - for _, parent := range route.Status.Parents { - for _, cond := range parent.Conditions { - if cond.Type == string(v1.RouteConditionAccepted) && cond.Status == metav1.ConditionTrue { - readyCount++ - } - } - } + readyCount += countNumberOfReadyParents(route.Status.Parents) + } + + return numParents == readyCount, nil + }, + ) +} + +func (rm *ResourceManager) waitForGRPCRoutesToBeReady(ctx context.Context, namespace string) error { + return wait.PollUntilContextCancel( + ctx, + 500*time.Millisecond, + true, /* poll immediately */ + func(ctx context.Context) (bool, error) { + var routeList v1.GRPCRouteList + if err := rm.K8sClient.List(ctx, &routeList, client.InNamespace(namespace)); err != nil { + return false, err + } + + var numParents, readyCount int + for _, route := range routeList.Items { + numParents += len(route.Spec.ParentRefs) + readyCount += countNumberOfReadyParents(route.Status.Parents) } return numParents == readyCount, nil @@ -649,3 +669,17 @@ func GetReadyNGFPodNames( return nil, errors.New("unable to find NGF Pod(s)") } + +func countNumberOfReadyParents(parents []v1.RouteParentStatus) int { + readyCount := 0 + + for _, parent := range parents { + for _, cond := range parent.Conditions { + if cond.Type == string(v1.RouteConditionAccepted) && cond.Status == metav1.ConditionTrue { + readyCount++ + } + } + } + + return readyCount +} diff --git a/tests/framework/timeout.go b/tests/framework/timeout.go index 2ed69457db..69830237b1 100644 --- a/tests/framework/timeout.go +++ b/tests/framework/timeout.go @@ -29,6 +29,9 @@ type TimeoutConfig struct { // GetLeaderLeaseTimeout represents the maximum time for NGF to retrieve the leader lease. GetLeaderLeaseTimeout time.Duration + + // GetStatusTimeout represents the maximum time for NGF to update the status of a resource. + GetStatusTimeout time.Duration } // DefaultTimeoutConfig populates a TimeoutConfig with the default values. @@ -43,5 +46,6 @@ func DefaultTimeoutConfig() TimeoutConfig { RequestTimeout: 10 * time.Second, ContainerRestartTimeout: 10 * time.Second, GetLeaderLeaseTimeout: 60 * time.Second, + GetStatusTimeout: 60 * time.Second, } } diff --git a/tests/go.mod b/tests/go.mod index 21029ba4a2..09e3f2e906 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -3,6 +3,7 @@ module github.com/nginxinc/nginx-gateway-fabric/tests go 1.22.2 require ( + github.com/nginxinc/nginx-gateway-fabric v0.0.0 github.com/onsi/ginkgo/v2 v2.19.0 github.com/onsi/gomega v1.33.1 github.com/prometheus/client_golang v1.19.1 @@ -24,7 +25,7 @@ require ( github.com/emicklei/go-restful/v3 v3.12.0 // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect @@ -59,7 +60,7 @@ require ( golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/net v0.25.0 // indirect - golang.org/x/oauth2 v0.19.0 // indirect + golang.org/x/oauth2 v0.20.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.20.0 // indirect golang.org/x/term v0.20.0 // indirect @@ -67,9 +68,9 @@ require ( golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.21.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect - google.golang.org/grpc v1.63.2 // indirect - google.golang.org/protobuf v1.33.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect + google.golang.org/grpc v1.64.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -79,3 +80,5 @@ require ( sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect ) + +replace github.com/nginxinc/nginx-gateway-fabric => ../ diff --git a/tests/go.sum b/tests/go.sum index 3fca028ee2..5c388c891e 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -19,8 +19,8 @@ github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0 github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= @@ -122,8 +122,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= -go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -140,8 +140,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= -golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= +golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= +golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -176,12 +176,12 @@ gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuB gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca h1:PupagGYwj8+I4ubCxcmcBRk3VlUWtTg5huQpZR9flmE= gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/netlib v0.0.0-20181029234149-ec6d1f5cefe6/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= -google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= -google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 h1:AgADTJarZTBqgjiUzRgfaBchgYB3/WFTC80GPwsMcRI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= +google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/tests/suite/client_settings_test.go b/tests/suite/client_settings_test.go new file mode 100644 index 0000000000..2619fc2a84 --- /dev/null +++ b/tests/suite/client_settings_test.go @@ -0,0 +1,347 @@ +package suite + +import ( + "bytes" + "context" + "crypto/rand" + "fmt" + "net/http" + "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" + "sigs.k8s.io/gateway-api/apis/v1alpha2" + + ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1" + "github.com/nginxinc/nginx-gateway-fabric/tests/framework" +) + +var _ = Describe("ClientSettingsPolicy", Ordered, Label("functional", "cspolicy"), func() { + var ( + files = []string{ + "clientsettings/cafe.yaml", + "clientsettings/gateway.yaml", + "clientsettings/cafe-routes.yaml", + "clientsettings/grpc-route.yaml", + "clientsettings/grpc-backend.yaml", + } + + namespace = "clientsettings" + ) + + 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("valid ClientSettingsPolicies are created", func() { + var ( + policies = []string{ + "clientsettings/valid-csps.yaml", + } + + baseURL string + ) + + BeforeAll(func() { + Expect(resourceManager.ApplyFromFiles(policies, namespace)).To(Succeed()) + + port := 80 + if portFwdPort != 0 { + port = portFwdPort + } + + baseURL = fmt.Sprintf("http://cafe.example.com:%d", port) + }) + + AfterAll(func() { + Expect(resourceManager.DeleteFromFiles(policies, namespace)).To(Succeed()) + }) + + Specify("they are accepted by the target resource", func() { + policyNames := []string{ + "gw-csp", + "coffee-route-csp", + "tea-route-csp", + "soda-route-csp", + "grpc-route-csp", + } + + for _, name := range policyNames { + nsname := types.NamespacedName{Name: name, Namespace: namespace} + + err := waitForCSPolicyToBeAccepted(nsname) + Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("%s was not accepted", name)) + } + }) + + // We only test that the client_max_body_size directive in this test is propagated correctly. + // This is because we can easily verify this directive by sending requests with different sized payloads. + DescribeTable("the settings are propagated to the nginx config", + func(uri string, byteLengthOfRequestBody, expStatus int) { + url := baseURL + uri + + payload := make([]byte, byteLengthOfRequestBody) + _, err := rand.Read(payload) + Expect(err).ToNot(HaveOccurred()) + + resp, err := framework.Post(url, address, bytes.NewReader(payload), timeoutConfig.RequestTimeout) + Expect(err).ToNot(HaveOccurred()) + Expect(resp).To(HaveHTTPStatus(expStatus)) + + if expStatus == http.StatusOK { + Expect(resp).To(HaveHTTPBody(ContainSubstring(fmt.Sprintf("URI: %s", uri)))) + } + }, + func(uri string, byteLengthOfRequestBody, expStatus int) string { + return fmt.Sprintf( + "request body of %d should return %d for %s", + byteLengthOfRequestBody, + expStatus, + uri, + ) + }, + Entry(nil, "/tea", 900, http.StatusOK), + Entry(nil, "/tea", 1200, http.StatusRequestEntityTooLarge), + Entry(nil, "/coffee", 1200, http.StatusOK), + Entry(nil, "/coffee", 2500, http.StatusRequestEntityTooLarge), + Entry(nil, "/soda", 2500, http.StatusOK), + Entry(nil, "/soda", 3300, http.StatusRequestEntityTooLarge), + ) + }) + + When("a ClientSettingsPolicy targets an invalid resources", func() { + Specify("their accepted condition is set to TargetNotFound", func() { + files := []string{ + "clientsettings/ignored-gateway.yaml", + "clientsettings/invalid-csp.yaml", + } + + Expect(resourceManager.ApplyFromFiles(files, namespace)).To(Succeed()) + + nsname := types.NamespacedName{Name: "invalid-csp", Namespace: namespace} + Expect(waitForCSPolicyToHaveTargetNotFoundAcceptedCond(nsname)).To(Succeed()) + + Expect(resourceManager.DeleteFromFiles(files, namespace)).To(Succeed()) + }) + }) + + Context("Merging behavior", func() { + When("multiple policies target the same resource", func() { + Specify("policies that cannot be merged are marked as conflicted", func() { + policies := []string{ + "clientsettings/merging-csps.yaml", + } + + mergeablePolicyNames := []string{ + "hr-merge-1", + "hr-merge-2", + "hr-merge-3", + "grpc-merge-1", + "grpc-merge-2", + } + + conflictedPolicyNames := []string{ + "z-hr-conflict-1", + "z-hr-conflict-2", + "z-grpc-conflict", + } + + Expect(resourceManager.ApplyFromFiles(policies, namespace)).To(Succeed()) + + for _, name := range conflictedPolicyNames { + nsname := types.NamespacedName{Name: name, Namespace: namespace} + + err := waitForCSPolicyToBeConflicted(nsname) + Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("%s was not marked as conflicted", name)) + } + + for _, name := range mergeablePolicyNames { + nsname := types.NamespacedName{Name: name, Namespace: namespace} + + err := waitForCSPolicyToBeAccepted(nsname) + Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("%s was not accepted", name)) + } + + Expect(resourceManager.DeleteFromFiles(policies, namespace)).To(Succeed()) + }) + }) + }) +}) + +func waitForCSPolicyToBeAccepted(policyNsname types.NamespacedName) error { + ctx, cancel := context.WithTimeout(context.Background(), timeoutConfig.GetStatusTimeout) + defer cancel() + + GinkgoWriter.Printf( + "Waiting for ClientSettingsPolicy %q to have the condition Accepted/True/Accepted\n", + policyNsname, + ) + + return waitForClientSettingsAncestorStatus(ctx, policyNsname, metav1.ConditionTrue, v1alpha2.PolicyReasonAccepted) +} + +func waitForCSPolicyToBeConflicted(policyNsname types.NamespacedName) error { + ctx, cancel := context.WithTimeout(context.Background(), timeoutConfig.GetStatusTimeout) + defer cancel() + + GinkgoWriter.Printf( + "Waiting for ClientSettingsPolicy %q to have the condition Accepted/False/Conflicted\n", + policyNsname, + ) + + return waitForClientSettingsAncestorStatus( + ctx, + policyNsname, + metav1.ConditionFalse, + v1alpha2.PolicyReasonConflicted, + ) +} + +func waitForCSPolicyToHaveTargetNotFoundAcceptedCond(policyNsname types.NamespacedName) error { + ctx, cancel := context.WithTimeout(context.Background(), timeoutConfig.GetStatusTimeout) + defer cancel() + + GinkgoWriter.Printf( + "Waiting for ClientSettingsPolicy %q to have the condition Accepted/False/TargetNotFound\n", + policyNsname, + ) + + return waitForClientSettingsAncestorStatus( + ctx, + policyNsname, + metav1.ConditionFalse, + v1alpha2.PolicyReasonTargetNotFound, + ) +} + +func waitForClientSettingsAncestorStatus( + ctx context.Context, + policyNsname types.NamespacedName, + condStatus metav1.ConditionStatus, + condReason v1alpha2.PolicyConditionReason, +) error { + return wait.PollUntilContextCancel( + ctx, + 500*time.Millisecond, + true, /* poll immediately */ + func(ctx context.Context) (bool, error) { + + var pol ngfAPI.ClientSettingsPolicy + + if err := k8sClient.Get(ctx, policyNsname, &pol); err != nil { + return false, err + } + + if len(pol.Status.Ancestors) == 0 { + GinkgoWriter.Printf("ClientSettingsPolicy %q does not have an ancestor status yet\n", policyNsname) + + return false, nil + } + + if len(pol.Status.Ancestors) != 1 { + return false, fmt.Errorf("policy has %d ancestors, expected 1", len(pol.Status.Ancestors)) + } + + ancestor := pol.Status.Ancestors[0] + + if err := ancestorMustEqualTargetRef(ancestor, pol.GetTargetRef(), policyNsname.Namespace); err != nil { + return false, err + } + + err := ancestorStatusMustHaveAcceptedCondition(ancestor, condStatus, condReason) + + return err == nil, err + }, + ) +} + +func ancestorStatusMustHaveAcceptedCondition( + status v1alpha2.PolicyAncestorStatus, + condStatus metav1.ConditionStatus, + condReason v1alpha2.PolicyConditionReason, +) error { + if len(status.Conditions) != 1 { + return fmt.Errorf("expected 1 condition in status, got %d", len(status.Conditions)) + } + + if status.Conditions[0].Type != string(v1alpha2.RouteConditionAccepted) { + return fmt.Errorf("expected condition type to be Accepted, got %s", status.Conditions[0].Type) + } + + if status.Conditions[0].Status != condStatus { + return fmt.Errorf("expected condition status to be %s, got %s", condStatus, status.Conditions[0].Status) + } + + if status.Conditions[0].Reason != string(condReason) { + return fmt.Errorf("expected condition reason to be %s, got %s", condReason, status.Conditions[0].Reason) + } + + return nil +} + +func ancestorMustEqualTargetRef( + ancestor v1alpha2.PolicyAncestorStatus, + targetRef v1alpha2.LocalPolicyTargetReference, + namespace string, +) error { + if ancestor.ControllerName != ngfControllerName { + return fmt.Errorf( + "expected ancestor controller name to be %s, got %s", + ngfControllerName, + ancestor.ControllerName, + ) + } + + if ancestor.AncestorRef.Namespace == nil { + return fmt.Errorf("expected ancestor namespace to be %s, got nil", namespace) + } + + if string(*ancestor.AncestorRef.Namespace) != namespace { + return fmt.Errorf( + "expected ancestor namespace to be %s, got %s", + namespace, + string(*ancestor.AncestorRef.Namespace), + ) + } + + ancestorRef := ancestor.AncestorRef + + if ancestorRef.Name != targetRef.Name { + return fmt.Errorf("expected ancestorRef to have name %s, got %s", targetRef.Name, ancestorRef.Name) + } + + if ancestorRef.Group == nil { + return fmt.Errorf("expected ancestorRef to have group %s, got nil", targetRef.Group) + } + + if *ancestorRef.Group != targetRef.Group { + return fmt.Errorf("expected ancestorRef to have group %s, got %s", targetRef.Group, string(*ancestorRef.Group)) + } + + if ancestorRef.Kind == nil { + return fmt.Errorf("expected ancestorRef to have kind %s, got nil", targetRef.Kind) + } + + if *ancestorRef.Kind != targetRef.Kind { + return fmt.Errorf("expected ancestorRef to have kind %s, got %s", targetRef.Kind, string(*ancestorRef.Kind)) + } + + return nil +} diff --git a/tests/suite/manifests/clientsettings/cafe-routes.yaml b/tests/suite/manifests/clientsettings/cafe-routes.yaml new file mode 100644 index 0000000000..2a7cac241d --- /dev/null +++ b/tests/suite/manifests/clientsettings/cafe-routes.yaml @@ -0,0 +1,56 @@ +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 + backendRefs: + - name: coffee + port: 80 +--- +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 + backendRefs: + - name: tea + port: 80 +--- +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 + backendRefs: + - name: soda + port: 80 diff --git a/tests/suite/manifests/clientsettings/cafe.yaml b/tests/suite/manifests/clientsettings/cafe.yaml new file mode 100644 index 0000000000..c6bc391de5 --- /dev/null +++ b/tests/suite/manifests/clientsettings/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/clientsettings/gateway.yaml b/tests/suite/manifests/clientsettings/gateway.yaml new file mode 100644 index 0000000000..e6507f613b --- /dev/null +++ b/tests/suite/manifests/clientsettings/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/clientsettings/grpc-backend.yaml b/tests/suite/manifests/clientsettings/grpc-backend.yaml new file mode 100644 index 0000000000..ad2eca60c2 --- /dev/null +++ b/tests/suite/manifests/clientsettings/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.1 + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + resources: + requests: + cpu: 10m diff --git a/tests/suite/manifests/clientsettings/grpc-route.yaml b/tests/suite/manifests/clientsettings/grpc-route.yaml new file mode 100644 index 0000000000..dcd5282d77 --- /dev/null +++ b/tests/suite/manifests/clientsettings/grpc-route.yaml @@ -0,0 +1,16 @@ +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 + backendRefs: + - name: grpc-backend + port: 8080 diff --git a/tests/suite/manifests/clientsettings/ignored-gateway.yaml b/tests/suite/manifests/clientsettings/ignored-gateway.yaml new file mode 100644 index 0000000000..74d8317b01 --- /dev/null +++ b/tests/suite/manifests/clientsettings/ignored-gateway.yaml @@ -0,0 +1,11 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: ignored-gateway +spec: + gatewayClassName: nginx + listeners: + - name: http + port: 80 + protocol: HTTP + hostname: "*.example.com" diff --git a/tests/suite/manifests/clientsettings/invalid-csp.yaml b/tests/suite/manifests/clientsettings/invalid-csp.yaml new file mode 100644 index 0000000000..cedfb52e46 --- /dev/null +++ b/tests/suite/manifests/clientsettings/invalid-csp.yaml @@ -0,0 +1,18 @@ +apiVersion: gateway.nginx.org/v1alpha1 +kind: ClientSettingsPolicy +metadata: + name: invalid-csp +spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: ignored-gateway + body: + maxSize: 10m + timeout: 30s + keepAlive: + requests: 100 + time: 5s + timeout: + server: 2s + header: 1s diff --git a/tests/suite/manifests/clientsettings/merging-csps.yaml b/tests/suite/manifests/clientsettings/merging-csps.yaml new file mode 100644 index 0000000000..4b8634f1ae --- /dev/null +++ b/tests/suite/manifests/clientsettings/merging-csps.yaml @@ -0,0 +1,106 @@ +apiVersion: gateway.nginx.org/v1alpha1 +kind: ClientSettingsPolicy +metadata: + name: hr-merge-1 +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: coffee + body: + maxSize: 9m +--- +apiVersion: gateway.nginx.org/v1alpha1 +kind: ClientSettingsPolicy +metadata: + name: hr-merge-2 +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: coffee + keepAlive: + requests: 1000 +--- +apiVersion: gateway.nginx.org/v1alpha1 +kind: ClientSettingsPolicy +metadata: + name: hr-merge-3 +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: coffee + body: + timeout: 30s + keepAlive: + time: 5s + timeout: + server: 2s + header: 1s +--- +apiVersion: gateway.nginx.org/v1alpha1 +kind: ClientSettingsPolicy +metadata: + name: grpc-merge-1 +spec: + targetRef: + group: gateway.networking.k8s.io + kind: GRPCRoute + name: grpc-route + keepAlive: + requests: 800 +--- +apiVersion: gateway.nginx.org/v1alpha1 +kind: ClientSettingsPolicy +metadata: + name: grpc-merge-2 +spec: + targetRef: + group: gateway.networking.k8s.io + kind: GRPCRoute + name: grpc-route + body: + timeout: 50s + keepAlive: + time: 3s + timeout: + server: 30s + header: 60s +--- +apiVersion: gateway.nginx.org/v1alpha1 +kind: ClientSettingsPolicy +metadata: + name: z-hr-conflict-1 +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: coffee + body: + maxSize: 12m +--- +apiVersion: gateway.nginx.org/v1alpha1 +kind: ClientSettingsPolicy +metadata: + name: z-hr-conflict-2 +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: coffee + keepAlive: + timeout: + server: 2s +--- +apiVersion: gateway.nginx.org/v1alpha1 +kind: ClientSettingsPolicy +metadata: + name: z-grpc-conflict +spec: + targetRef: + group: gateway.networking.k8s.io + kind: GRPCRoute + name: grpc-route + keepAlive: + time: 15s diff --git a/tests/suite/manifests/clientsettings/valid-csps.yaml b/tests/suite/manifests/clientsettings/valid-csps.yaml new file mode 100644 index 0000000000..e64dd7873b --- /dev/null +++ b/tests/suite/manifests/clientsettings/valid-csps.yaml @@ -0,0 +1,66 @@ +apiVersion: gateway.nginx.org/v1alpha1 +kind: ClientSettingsPolicy +metadata: + name: tea-route-csp +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: tea + keepAlive: + requests: 200 +--- +apiVersion: gateway.nginx.org/v1alpha1 +kind: ClientSettingsPolicy +metadata: + name: soda-route-csp +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: soda + body: + maxSize: "3000" +--- +apiVersion: gateway.nginx.org/v1alpha1 +kind: ClientSettingsPolicy +metadata: + name: coffee-route-csp +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: coffee + body: + maxSize: "2000" +--- +apiVersion: gateway.nginx.org/v1alpha1 +kind: ClientSettingsPolicy +metadata: + name: gw-csp +spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway + body: + maxSize: "1000" + timeout: 30s + keepAlive: + requests: 100 + time: 5s + timeout: + server: 2s + header: 1s +--- +apiVersion: gateway.nginx.org/v1alpha1 +kind: ClientSettingsPolicy +metadata: + name: grpc-route-csp +spec: + targetRef: + group: gateway.networking.k8s.io + kind: GRPCRoute + name: grpc-route + body: + maxSize: "0" diff --git a/tests/suite/system_suite_test.go b/tests/suite/system_suite_test.go index 0dd0b3f4bb..e8a7ea9876 100644 --- a/tests/suite/system_suite_test.go +++ b/tests/suite/system_suite_test.go @@ -29,6 +29,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" v1 "sigs.k8s.io/gateway-api/apis/v1" + ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1" "github.com/nginxinc/nginx-gateway-fabric/tests/framework" ) @@ -81,6 +82,7 @@ const ( ngfNamespace = "nginx-gateway" ngfHTTPForwardedPort = 10080 ngfHTTPSForwardedPort = 10443 + ngfControllerName = "gateway.nginx.org/nginx-gateway-controller" ) type setupConfig struct { @@ -100,8 +102,9 @@ func setup(cfg setupConfig, extraInstallArgs ...string) { Expect(apps.AddToScheme(scheme)).To(Succeed()) Expect(apiext.AddToScheme(scheme)).To(Succeed()) Expect(coordination.AddToScheme(scheme)).To(Succeed()) - Expect(v1.AddToScheme(scheme)).To(Succeed()) + Expect(v1.Install(scheme)).To(Succeed()) Expect(batchv1.AddToScheme(scheme)).To(Succeed()) + Expect(ngfAPI.AddToScheme(scheme)).To(Succeed()) options := client.Options{ Scheme: scheme, @@ -167,7 +170,7 @@ func setup(cfg setupConfig, extraInstallArgs ...string) { installCfg.ImagePullPolicy = *imagePullPolicy } - output, err := framework.InstallGatewayAPI(k8sClient, cfg.gwAPIVersion, *k8sVersion) + output, err := framework.InstallGatewayAPI(cfg.gwAPIVersion) Expect(err).ToNot(HaveOccurred(), string(output)) output, err = framework.InstallNGF(installCfg, extraInstallArgs...) diff --git a/tests/suite/upgrade_test.go b/tests/suite/upgrade_test.go index 21e3d9dc48..37226ac71b 100644 --- a/tests/suite/upgrade_test.go +++ b/tests/suite/upgrade_test.go @@ -185,7 +185,7 @@ var _ = Describe("Upgrade testing", Label("nfr", "upgrade"), func() { time.Sleep(2 * time.Second) // update Gateway API and NGF - output, err := framework.InstallGatewayAPI(k8sClient, *gatewayAPIVersion, *k8sVersion) + output, err := framework.InstallGatewayAPI(*gatewayAPIVersion) Expect(err).ToNot(HaveOccurred(), string(output)) output, err = framework.UpgradeNGF(cfg, "--values", valuesFile)