From 28df2f809c1d8fdcfa3244d4e275415ba97edf66 Mon Sep 17 00:00:00 2001 From: Tina Usova Date: Wed, 17 Dec 2025 19:19:09 +0000 Subject: [PATCH] Add Functional tests for ProxySettingsPolicy --- .yamllint.yaml | 1 + tests/framework/crossplane.go | 4 +- tests/framework/logging.go | 22 - tests/framework/options.go | 29 + tests/framework/prometheus.go | 4 +- tests/framework/request.go | 42 +- tests/framework/resourcemanager.go | 24 +- tests/suite/graceful_recovery_test.go | 7 +- .../manifests/proxy-settings-policy/app.yaml | 154 ++++ .../coffee-proxy-settings.yaml | 19 + ...ay-and-coffee-disabled-proxy-settings.yaml | 23 + .../gateway-big-buffer-proxy-settings.yaml | 19 + ...isabled-coffee-enabled-proxy-settings.yaml | 30 + ...led-coffee-not-enabled-proxy-settings.yaml | 29 + ...nabled-coffee-disabled-proxy-settings.yaml | 27 + .../gateway-proxy-settings.yaml | 15 + .../proxy-settings-policy/gateway.yaml | 11 + .../proxy-settings-policy/httproutes.yaml | 37 + tests/suite/proxy_settings_test.go | 769 ++++++++++++++++++ 19 files changed, 1218 insertions(+), 48 deletions(-) delete mode 100644 tests/framework/logging.go create mode 100644 tests/framework/options.go create mode 100644 tests/suite/manifests/proxy-settings-policy/app.yaml create mode 100644 tests/suite/manifests/proxy-settings-policy/coffee-proxy-settings.yaml create mode 100644 tests/suite/manifests/proxy-settings-policy/gateway-and-coffee-disabled-proxy-settings.yaml create mode 100644 tests/suite/manifests/proxy-settings-policy/gateway-big-buffer-proxy-settings.yaml create mode 100644 tests/suite/manifests/proxy-settings-policy/gateway-disabled-coffee-enabled-proxy-settings.yaml create mode 100644 tests/suite/manifests/proxy-settings-policy/gateway-disabled-coffee-not-enabled-proxy-settings.yaml create mode 100644 tests/suite/manifests/proxy-settings-policy/gateway-enabled-coffee-disabled-proxy-settings.yaml create mode 100644 tests/suite/manifests/proxy-settings-policy/gateway-proxy-settings.yaml create mode 100644 tests/suite/manifests/proxy-settings-policy/gateway.yaml create mode 100644 tests/suite/manifests/proxy-settings-policy/httproutes.yaml create mode 100644 tests/suite/proxy_settings_test.go diff --git a/.yamllint.yaml b/.yamllint.yaml index e195fa7831..3b69e92dcb 100644 --- a/.yamllint.yaml +++ b/.yamllint.yaml @@ -30,6 +30,7 @@ rules: ignore: | operators/**/* examples/proxy-settings-policy/app.yaml + tests/suite/manifests/proxy-settings-policy/app.yaml key-duplicates: enable key-ordering: disable line-length: diff --git a/tests/framework/crossplane.go b/tests/framework/crossplane.go index 90b50faef4..add0101fdd 100644 --- a/tests/framework/crossplane.go +++ b/tests/framework/crossplane.go @@ -102,7 +102,7 @@ func fieldExistsInUpstream( directive Directive, opts ...Option, ) bool { - options := LogOptions(opts...) + options := TestOptions(opts...) if options.logEnabled { GinkgoWriter.Printf( "Checking upstream for directive %q with value %q\n", @@ -131,7 +131,7 @@ func getServerName(serverBlock Directives) string { } func (e ExpectedNginxField) fieldFound(directive *Directive, opts ...Option) bool { - options := LogOptions(opts...) + options := TestOptions(opts...) arg := strings.Join(directive.Args, " ") valueMatch := arg == e.Value diff --git a/tests/framework/logging.go b/tests/framework/logging.go deleted file mode 100644 index ddc02672a3..0000000000 --- a/tests/framework/logging.go +++ /dev/null @@ -1,22 +0,0 @@ -package framework - -type Option func(*Options) - -type Options struct { - logEnabled bool -} - -func WithLoggingDisabled() Option { - return func(opts *Options) { - opts.logEnabled = false - } -} - -func LogOptions(opts ...Option) *Options { - options := &Options{logEnabled: true} - for _, opt := range opts { - opt(options) - } - - return options -} diff --git a/tests/framework/options.go b/tests/framework/options.go new file mode 100644 index 0000000000..c80f592d53 --- /dev/null +++ b/tests/framework/options.go @@ -0,0 +1,29 @@ +package framework + +type Option func(*Options) + +type Options struct { + logEnabled bool + withContext bool +} + +func WithLoggingDisabled() Option { + return func(opts *Options) { + opts.logEnabled = false + } +} + +func WithContextDisabled() Option { + return func(opts *Options) { + opts.withContext = false + } +} + +func TestOptions(opts ...Option) *Options { + options := &Options{logEnabled: true, withContext: true} + for _, opt := range opts { + opt(options) + } + + return options +} diff --git a/tests/framework/prometheus.go b/tests/framework/prometheus.go index 37d72e1c49..08d7f94fea 100644 --- a/tests/framework/prometheus.go +++ b/tests/framework/prometheus.go @@ -473,7 +473,7 @@ func CreateMetricExistChecker( ) func() error { return func() error { queryWithTimestamp := fmt.Sprintf("%s @ %d", query, getTime().Unix()) - options := LogOptions(opts...) + options := TestOptions(opts...) result, err := promInstance.Query(queryWithTimestamp) if err != nil { @@ -536,7 +536,7 @@ func CreateEndTimeFinder( // CreateResponseChecker returns a function that checks if there is a successful response from a url. func CreateResponseChecker(url, address string, requestTimeout time.Duration, opts ...Option) func() error { - options := LogOptions(opts...) + options := TestOptions(opts...) if options.logEnabled { GinkgoWriter.Printf("Starting checking response for url %q and address %q\n", url, address) } diff --git a/tests/framework/request.go b/tests/framework/request.go index bd8663b8a3..43d157b0a1 100644 --- a/tests/framework/request.go +++ b/tests/framework/request.go @@ -24,7 +24,7 @@ func Get( headers, queryParams map[string]string, opts ...Option, ) (int, string, error) { - options := LogOptions(opts...) + options := TestOptions(opts...) resp, err := makeRequest(http.MethodGet, url, address, nil, timeout, headers, queryParams, opts...) if err != nil { @@ -46,12 +46,27 @@ func Get( return resp.StatusCode, "", err } if options.logEnabled { - GinkgoWriter.Printf("Successfully received response and parsed body: %s\n", body.String()) + printResponseBody(body) } return resp.StatusCode, body.String(), nil } +func printResponseBody(body *bytes.Buffer) { + maxLogBodyBytes := 2048 + bs := body.Bytes() + if len(bs) <= maxLogBodyBytes { + GinkgoWriter.Printf("Successfully received response and parsed body (%d bytes): %s\n", len(bs), string(bs)) + } else { + GinkgoWriter.Printf( + "Successfully received response and parsed body (%d bytes, showing first %d): %s...\n", + len(bs), + maxLogBodyBytes, + string(bs[:maxLogBodyBytes]), + ) + } +} + // 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( @@ -93,10 +108,7 @@ func makeRequest( return dialer.DialContext(ctx, network, fmt.Sprintf("%s:%s", address, port)) } - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - options := LogOptions(opts...) + options := TestOptions(opts...) if options.logEnabled { requestDetails := fmt.Sprintf( "Method: %s, URL: %s, Address: %s, Headers: %v, QueryParams: %v\n", @@ -106,10 +118,24 @@ func makeRequest( headers, queryParams, ) - GinkgoWriter.Printf("Sending request: %s", requestDetails) + GinkgoWriter.Printf("\nSending request: %s", requestDetails) } - req, err := http.NewRequestWithContext(ctx, method, url, body) + var ( + req *http.Request + err error + ) + if options.withContext { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + req, err = http.NewRequestWithContext(ctx, method, url, body) + } else { + // Create request without a short-lived context; rely on http.Client.Timeout + // so that the response body can be fully read by the caller without + // being canceled when this function returns. + req, err = http.NewRequest(method, url, body) //nolint:noctx + } if err != nil { return nil, err } diff --git a/tests/framework/resourcemanager.go b/tests/framework/resourcemanager.go index 2382d17102..5082963386 100644 --- a/tests/framework/resourcemanager.go +++ b/tests/framework/resourcemanager.go @@ -77,7 +77,7 @@ type ClusterInfo struct { // Apply creates or updates Kubernetes resources defined as Go objects. func (rm *ResourceManager) Apply(resources []client.Object, opts ...Option) error { - options := LogOptions(opts...) + options := TestOptions(opts...) if options.logEnabled { GinkgoWriter.Printf("Applying resources defined as Go objects\n") } @@ -141,7 +141,7 @@ func (rm *ResourceManager) Apply(resources []client.Object, opts ...Option) erro // ApplyFromFiles creates or updates Kubernetes resources defined within the provided YAML files. func (rm *ResourceManager) ApplyFromFiles(files []string, namespace string, opts ...Option) error { - options := LogOptions(opts...) + options := TestOptions(opts...) for _, file := range files { if options.logEnabled { GinkgoWriter.Printf("\nApplying resources from file: %q to namespace %q\n", file, namespace) @@ -166,7 +166,7 @@ func (rm *ResourceManager) ApplyFromBuffer(buffer *bytes.Buffer, namespace strin ctx, cancel := context.WithTimeout(context.Background(), rm.TimeoutConfig.CreateTimeout) defer cancel() - options := LogOptions(opts...) + options := TestOptions(opts...) if options.logEnabled { GinkgoWriter.Printf("Applying resources from buffer to namespace %q\n", namespace) } @@ -470,7 +470,7 @@ func (rm *ResourceManager) WaitForPodsToBeReady( namespace string, opts ...Option, ) error { - options := LogOptions(opts...) + options := TestOptions(opts...) waitingErr := wait.PollUntilContextCancel( ctx, 500*time.Millisecond, @@ -941,7 +941,7 @@ func (rm *ResourceManager) GetReadyNGFPodNames( timeout time.Duration, opts ...Option, ) ([]string, error) { - options := LogOptions(opts...) + options := TestOptions(opts...) if options.logEnabled { GinkgoWriter.Printf("Getting ready NGF Pod names in namespace %q with release name %q\n", namespace, releaseName) } @@ -1002,7 +1002,7 @@ func (rm *ResourceManager) GetReadyNginxPodNames( timeout time.Duration, opts ...Option, ) ([]string, error) { - options := LogOptions(opts...) + options := TestOptions(opts...) if options.logEnabled { GinkgoWriter.Printf("Getting ready NGINX Pod names in namespace %q\n", namespace) } @@ -1061,7 +1061,7 @@ func getReadyPodNames(podList core.PodList, opts ...Option) []string { } } } - options := LogOptions(opts...) + options := TestOptions(opts...) if options.logEnabled { GinkgoWriter.Printf("Found %d ready pod name(s): %v\n", len(names), names) } @@ -1092,7 +1092,7 @@ func (rm *ResourceManager) WaitForPodsToBeReadyWithCount( count int, opts ...Option, ) error { - options := LogOptions(opts...) + options := TestOptions(opts...) GinkgoWriter.Printf("Waiting for %d pods to be ready in namespace %q\n", count, namespace) return wait.PollUntilContextCancel( @@ -1169,7 +1169,7 @@ func (rm *ResourceManager) GetNginxConfig( opts ...Option, ) (*Payload, error) { GinkgoWriter.Printf("Getting NGINX config from pod %q in namespace %q\n", nginxPodName, namespace) - options := LogOptions(opts...) + options := TestOptions(opts...) if err := injectCrossplaneContainer( rm.ClientGoClient, @@ -1249,7 +1249,7 @@ func (rm *ResourceManager) Get( obj client.Object, opts ...Option, ) error { - options := LogOptions(opts...) + options := TestOptions(opts...) if err := rm.K8sClient.Get(ctx, key, obj); err != nil { // Don't log NotFound errors - they're often expected (e.g., when checking if resource was deleted) if options.logEnabled && !apierrors.IsNotFound(err) { @@ -1283,7 +1283,7 @@ func (rm *ResourceManager) Delete( deleteOpts []client.DeleteOption, opts ...Option, ) error { - options := LogOptions(opts...) + options := TestOptions(opts...) if err := rm.K8sClient.Delete(ctx, obj, deleteOpts...); err != nil { if options.logEnabled { GinkgoWriter.Printf("Could not delete k8s resource %q: %w\n", obj.GetName(), err) @@ -1301,7 +1301,7 @@ func (rm *ResourceManager) Update( updateOpts []client.UpdateOption, opts ...Option, ) error { - options := LogOptions(opts...) + options := TestOptions(opts...) if err := rm.K8sClient.Update(ctx, obj, updateOpts...); err != nil { updateResourceErr := fmt.Errorf("error updating k8s resource: %w", err) if options.logEnabled { diff --git a/tests/suite/graceful_recovery_test.go b/tests/suite/graceful_recovery_test.go index a5d3348958..0b5ad551e7 100644 --- a/tests/suite/graceful_recovery_test.go +++ b/tests/suite/graceful_recovery_test.go @@ -554,8 +554,11 @@ var _ = Describe("Graceful Recovery test", Ordered, FlakeAttempts(2), Label("gra }) }) -func expectRequestToSucceed(appURL, address string, responseBodyMessage string) error { - status, body, err := framework.Get(appURL, address, timeoutConfig.RequestTimeout, nil, nil) +func expectRequestToSucceed(appURL, address string, responseBodyMessage string, opts ...framework.Option) error { + timeBeforeReq := time.Now() + status, body, err := framework.Get(appURL, address, timeoutConfig.RequestTimeout, nil, nil, opts...) + timeAfterReq := time.Now() + GinkgoWriter.Printf("Request duration: %v\n", timeAfterReq.Sub(timeBeforeReq)) if status != http.StatusOK { return fmt.Errorf("http status was not 200, got %d: %w", status, err) diff --git a/tests/suite/manifests/proxy-settings-policy/app.yaml b/tests/suite/manifests/proxy-settings-policy/app.yaml new file mode 100644 index 0000000000..99a4465e2a --- /dev/null +++ b/tests/suite/manifests/proxy-settings-policy/app.yaml @@ -0,0 +1,154 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: response-generator +data: + main.go: | + package main + + import ( + "fmt" + "log" + "net/http" + "strings" + "time" + ) + + func coffeeHandler(w http.ResponseWriter, r *http.Request) { + // Returns a large response to demonstrate buffering requirements + // This generates a response larger than NGINX's default proxy_buffer_size (4k/8k) + // Without proper buffering configuration, this may cause errors + w.Header().Set("Content-Type", "text/plain") + w.Header().Set("X-Content-Type-Options", "nosniff") + + // Generate a large header that exceeds default proxy_buffer_size + // Default is typically 4k-8k depending on platform + largeHeader := strings.Repeat("X", 10000) + w.Header().Set("X-Large-Header", largeHeader) + + flusher, ok := w.(http.Flusher) + if !ok { + http.Error(w, "Streaming unsupported", http.StatusInternalServerError) + return + } + + // Generate a large response body (5MB in chunks) + // This tests both proxy_buffer_size and proxy_buffers settings + chunkSize := 1024 // 1KB chunks + totalChunks := 5120 // 5MB total + + for i := 0; i < totalChunks; i++ { + chunk := fmt.Sprintf("Coffee chunk %d: %s\n", i, strings.Repeat("x", chunkSize-20)) + fmt.Fprint(w, chunk) + if i%10 == 0 { + flusher.Flush() + } + time.Sleep(100 * time.Microsecond) // Small delay to simulate slow backend + } + } + + func healthHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, "OK") + } + + func main() { + http.HandleFunc("/coffee", coffeeHandler) + http.HandleFunc("/health", healthHandler) + + log.Println("Server starting on :8080") + if err := http.ListenAndServe(":8080", nil); err != nil { + log.Fatal(err) + } + } +--- +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: golang:1.25-alpine + command: ["/bin/sh"] + args: + - -c + - | + cd /app + go run main.go + ports: + - containerPort: 8080 + volumeMounts: + - name: app + mountPath: /app + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 + volumes: + - name: app + configMap: + name: response-generator +--- +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 diff --git a/tests/suite/manifests/proxy-settings-policy/coffee-proxy-settings.yaml b/tests/suite/manifests/proxy-settings-policy/coffee-proxy-settings.yaml new file mode 100644 index 0000000000..0349a438eb --- /dev/null +++ b/tests/suite/manifests/proxy-settings-policy/coffee-proxy-settings.yaml @@ -0,0 +1,19 @@ +apiVersion: gateway.nginx.org/v1alpha1 +kind: ProxySettingsPolicy +metadata: + name: coffee-proxy-settings +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: coffee + buffering: + # Increase buffer size to handle large response headers (>10KB) + bufferSize: "16k" + # Configure more and larger buffers to handle 5MB response body + buffers: + number: 16 + size: "64k" + # Set busy buffers size to allow more data to be sent to client + # while still receiving from upstream + busyBuffersSize: "128k" diff --git a/tests/suite/manifests/proxy-settings-policy/gateway-and-coffee-disabled-proxy-settings.yaml b/tests/suite/manifests/proxy-settings-policy/gateway-and-coffee-disabled-proxy-settings.yaml new file mode 100644 index 0000000000..9d6e8e1d71 --- /dev/null +++ b/tests/suite/manifests/proxy-settings-policy/gateway-and-coffee-disabled-proxy-settings.yaml @@ -0,0 +1,23 @@ +apiVersion: gateway.nginx.org/v1alpha1 +kind: ProxySettingsPolicy +metadata: + name: gateway-proxy-settings +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: gateway + buffering: + disabled: true +--- +apiVersion: gateway.nginx.org/v1alpha1 +kind: ProxySettingsPolicy +metadata: + name: coffee-proxy-settings +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: coffee + buffering: + disabled: true diff --git a/tests/suite/manifests/proxy-settings-policy/gateway-big-buffer-proxy-settings.yaml b/tests/suite/manifests/proxy-settings-policy/gateway-big-buffer-proxy-settings.yaml new file mode 100644 index 0000000000..b5e99b06b2 --- /dev/null +++ b/tests/suite/manifests/proxy-settings-policy/gateway-big-buffer-proxy-settings.yaml @@ -0,0 +1,19 @@ +apiVersion: gateway.nginx.org/v1alpha1 +kind: ProxySettingsPolicy +metadata: + name: gateway-proxy-settings +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: gateway + buffering: + # Increase buffer size to handle large response headers (>10KB) + bufferSize: "16k" + # Configure more and larger buffers to handle 5MB response body + buffers: + number: 16 + size: "64k" + # Set busy buffers size to allow more data to be sent to client + # while still receiving from upstream + busyBuffersSize: "128k" diff --git a/tests/suite/manifests/proxy-settings-policy/gateway-disabled-coffee-enabled-proxy-settings.yaml b/tests/suite/manifests/proxy-settings-policy/gateway-disabled-coffee-enabled-proxy-settings.yaml new file mode 100644 index 0000000000..a4febbef66 --- /dev/null +++ b/tests/suite/manifests/proxy-settings-policy/gateway-disabled-coffee-enabled-proxy-settings.yaml @@ -0,0 +1,30 @@ +# Gateway policy disables buffering, +# Route policy explicitly sets disable: false and specifies buffer sizes (should work correctly) +apiVersion: gateway.nginx.org/v1alpha1 +kind: ProxySettingsPolicy +metadata: + name: gateway-proxy-settings +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: gateway + buffering: + disabled: true +--- +apiVersion: gateway.nginx.org/v1alpha1 +kind: ProxySettingsPolicy +metadata: + name: coffee-proxy-settings +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: coffee + buffering: + disabled: false + bufferSize: "16k" + buffers: + number: 16 + size: "64k" + busyBuffersSize: "128k" diff --git a/tests/suite/manifests/proxy-settings-policy/gateway-disabled-coffee-not-enabled-proxy-settings.yaml b/tests/suite/manifests/proxy-settings-policy/gateway-disabled-coffee-not-enabled-proxy-settings.yaml new file mode 100644 index 0000000000..3caa4cd368 --- /dev/null +++ b/tests/suite/manifests/proxy-settings-policy/gateway-disabled-coffee-not-enabled-proxy-settings.yaml @@ -0,0 +1,29 @@ +# Gateway policy disables buffering, +# Route policy specifies buffer sizes without setting disable: false (should set PartiallyInvalid condition) +apiVersion: gateway.nginx.org/v1alpha1 +kind: ProxySettingsPolicy +metadata: + name: gateway-proxy-settings +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: gateway + buffering: + disabled: true +--- +apiVersion: gateway.nginx.org/v1alpha1 +kind: ProxySettingsPolicy +metadata: + name: coffee-proxy-settings +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: coffee + buffering: + bufferSize: "16k" + buffers: + number: 16 + size: "64k" + busyBuffersSize: "128k" diff --git a/tests/suite/manifests/proxy-settings-policy/gateway-enabled-coffee-disabled-proxy-settings.yaml b/tests/suite/manifests/proxy-settings-policy/gateway-enabled-coffee-disabled-proxy-settings.yaml new file mode 100644 index 0000000000..1e600839bf --- /dev/null +++ b/tests/suite/manifests/proxy-settings-policy/gateway-enabled-coffee-disabled-proxy-settings.yaml @@ -0,0 +1,27 @@ +apiVersion: gateway.nginx.org/v1alpha1 +kind: ProxySettingsPolicy +metadata: + name: gateway-proxy-settings +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: gateway + buffering: + bufferSize: "16k" + buffers: + number: 16 + size: "64k" + busyBuffersSize: "128k" +--- +apiVersion: gateway.nginx.org/v1alpha1 +kind: ProxySettingsPolicy +metadata: + name: coffee-proxy-settings +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: coffee + buffering: + disabled: true diff --git a/tests/suite/manifests/proxy-settings-policy/gateway-proxy-settings.yaml b/tests/suite/manifests/proxy-settings-policy/gateway-proxy-settings.yaml new file mode 100644 index 0000000000..150edde7b1 --- /dev/null +++ b/tests/suite/manifests/proxy-settings-policy/gateway-proxy-settings.yaml @@ -0,0 +1,15 @@ +apiVersion: gateway.nginx.org/v1alpha1 +kind: ProxySettingsPolicy +metadata: + name: gateway-proxy-settings +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: gateway + buffering: + bufferSize: "4k" + buffers: + number: 8 + size: "4k" + busyBuffersSize: "16k" diff --git a/tests/suite/manifests/proxy-settings-policy/gateway.yaml b/tests/suite/manifests/proxy-settings-policy/gateway.yaml new file mode 100644 index 0000000000..e6507f613b --- /dev/null +++ b/tests/suite/manifests/proxy-settings-policy/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/proxy-settings-policy/httproutes.yaml b/tests/suite/manifests/proxy-settings-policy/httproutes.yaml new file mode 100644 index 0000000000..67927335cb --- /dev/null +++ b/tests/suite/manifests/proxy-settings-policy/httproutes.yaml @@ -0,0 +1,37 @@ +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 diff --git a/tests/suite/proxy_settings_test.go b/tests/suite/proxy_settings_test.go new file mode 100644 index 0000000000..2ae4e06dd5 --- /dev/null +++ b/tests/suite/proxy_settings_test.go @@ -0,0 +1,769 @@ +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" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + + ngfAPI "github.com/nginx/nginx-gateway-fabric/v2/apis/v1alpha1" + "github.com/nginx/nginx-gateway-fabric/v2/tests/framework" +) + +var _ = Describe("ProxySettingsPolicy", Ordered, Label("functional", "proxy-settings"), func() { + var ( + files = []string{ + "proxy-settings-policy/app.yaml", + "proxy-settings-policy/gateway.yaml", + "proxy-settings-policy/httproutes.yaml", + } + + namespace = "proxy-settings" + + nginxPodName string + ) + + 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()) + + nginxPodNames, err := resourceManager.GetReadyNginxPodNames( + namespace, + timeoutConfig.GetStatusTimeout, + ) + Expect(err).ToNot(HaveOccurred()) + Expect(nginxPodNames).To(HaveLen(1)) + + nginxPodName = nginxPodNames[0] + setUpPortForward(nginxPodName, namespace) + }) + + AfterAll(func() { + framework.AddNginxLogsAndEventsToReport(resourceManager, namespace) + cleanUpPortForward() + + Expect(resourceManager.DeleteNamespace(namespace)).To(Succeed()) + }) + + When("valid ProxySettingsPolicies are created for both: Gateway and HTTPRoute", func() { + var ( + policies = []string{ + "proxy-settings-policy/gateway-proxy-settings.yaml", + "proxy-settings-policy/coffee-proxy-settings.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{ + "gateway-proxy-settings", + "coffee-proxy-settings", + } + + for _, name := range policyNames { + nsname := types.NamespacedName{Name: name, Namespace: namespace} + + err := waitForPSPolicyStatus(nsname) + Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("%s was not accepted", name)) + } + }) + + Context("verify working traffic", func() { + It("should return a 200 response for HTTPRoutes", func() { + baseCoffeeURL := baseURL + "/coffee" + baseTeaURL := baseURL + "/tea" + + Eventually( + func() error { + return expectRequestToSucceed(baseCoffeeURL, address, "Coffee chunk", framework.WithContextDisabled()) + }). + WithTimeout(timeoutConfig.RequestTimeout). + WithPolling(500 * time.Millisecond). + Should(Succeed()) + + Eventually( + func() error { + return expectRequestToSucceed(baseTeaURL, address, "URI: /tea") + }). + WithTimeout(timeoutConfig.RequestTimeout). + WithPolling(500 * time.Millisecond). + Should(Succeed()) + }) + }) + + Context("nginx config", func() { + var conf *framework.Payload + filePrefix := fmt.Sprintf("/etc/nginx/includes/ProxySettingsPolicy_%s", namespace) + + BeforeAll(func() { + var err error + conf, err = resourceManager.GetNginxConfig(nginxPodName, namespace, "") + Expect(err).ToNot(HaveOccurred()) + }) + + DescribeTable("is set properly for", + func(expCfgs []framework.ExpectedNginxField) { + for _, expCfg := range expCfgs { + fmt.Println("Validating nginx config:", expCfg) + Expect(framework.ValidateNginxFieldExists(conf, expCfg)).To(Succeed()) + } + }, + Entry("gateway policy", []framework.ExpectedNginxField{ + { + Directive: "include", + Value: fmt.Sprintf("%s_gateway-proxy-settings.conf", filePrefix), + File: "http.conf", + }, + { + Directive: "proxy_buffer_size", + Value: "4k", + File: fmt.Sprintf("%s_gateway-proxy-settings.conf", filePrefix), + }, + { + Directive: "proxy_buffers", + Value: "8 4k", + File: fmt.Sprintf("%s_gateway-proxy-settings.conf", filePrefix), + }, + { + Directive: "proxy_busy_buffers_size", + Value: "16k", + File: fmt.Sprintf("%s_gateway-proxy-settings.conf", filePrefix), + }, + }), + Entry("coffee route policy", []framework.ExpectedNginxField{ + { + Directive: "include", + Value: fmt.Sprintf("%s_coffee-proxy-settings.conf", filePrefix), + File: "http.conf", + Server: "cafe.example.com", + Location: "/coffee", + }, + { + Directive: "proxy_buffer_size", + Value: "16k", + File: fmt.Sprintf("%s_coffee-proxy-settings.conf", filePrefix), + }, + { + Directive: "proxy_buffers", + Value: "16 64k", + File: fmt.Sprintf("%s_coffee-proxy-settings.conf", filePrefix), + }, + { + Directive: "proxy_busy_buffers_size", + Value: "128k", + File: fmt.Sprintf("%s_coffee-proxy-settings.conf", filePrefix), + }, + }), + ) + }) + }) + + When("valid ProxySettingsPolicies are created for HTTPRoute only", func() { + var ( + policies = []string{ + "proxy-settings-policy/coffee-proxy-settings.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{ + "coffee-proxy-settings", + } + + for _, name := range policyNames { + nsname := types.NamespacedName{Name: name, Namespace: namespace} + + err := waitForPSPolicyStatus(nsname) + Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("%s was not accepted", name)) + } + }) + + Context("verify working traffic", func() { + It("should return a 200 response for HTTPRoutes", func() { + baseCoffeeURL := baseURL + "/coffee" + baseTeaURL := baseURL + "/tea" + + Eventually( + func() error { + return expectRequestToSucceed(baseCoffeeURL, address, "Coffee chunk", framework.WithContextDisabled()) + }). + WithTimeout(timeoutConfig.RequestTimeout). + WithPolling(500 * time.Millisecond). + Should(Succeed()) + + Eventually( + func() error { + return expectRequestToSucceed(baseTeaURL, address, "URI: /tea") + }). + WithTimeout(timeoutConfig.RequestTimeout). + WithPolling(500 * time.Millisecond). + Should(Succeed()) + }) + }) + + Context("nginx config", func() { + var conf *framework.Payload + filePrefix := fmt.Sprintf("/etc/nginx/includes/ProxySettingsPolicy_%s", namespace) + + BeforeAll(func() { + var err error + conf, err = resourceManager.GetNginxConfig(nginxPodName, namespace, "") + Expect(err).ToNot(HaveOccurred()) + }) + + DescribeTable("is set properly for", + func(expCfgs []framework.ExpectedNginxField) { + for _, expCfg := range expCfgs { + fmt.Println("Validating nginx config:", expCfg) + Expect(framework.ValidateNginxFieldExists(conf, expCfg)).To(Succeed()) + } + }, + Entry("coffee route policy", []framework.ExpectedNginxField{ + { + Directive: "include", + Value: fmt.Sprintf("%s_coffee-proxy-settings.conf", filePrefix), + File: "http.conf", + Server: "cafe.example.com", + Location: "/coffee", + }, + { + Directive: "proxy_buffer_size", + Value: "16k", + File: fmt.Sprintf("%s_coffee-proxy-settings.conf", filePrefix), + }, + { + Directive: "proxy_buffers", + Value: "16 64k", + File: fmt.Sprintf("%s_coffee-proxy-settings.conf", filePrefix), + }, + { + Directive: "proxy_busy_buffers_size", + Value: "128k", + File: fmt.Sprintf("%s_coffee-proxy-settings.conf", filePrefix), + }, + }), + ) + }) + }) + + When("valid ProxySettingsPolicies are created for Gateway only", func() { + var ( + policies = []string{ + "proxy-settings-policy/gateway-proxy-settings.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{ + "gateway-proxy-settings", + } + + for _, name := range policyNames { + nsname := types.NamespacedName{Name: name, Namespace: namespace} + + err := waitForPSPolicyStatus(nsname) + Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("%s was not accepted", name)) + } + }) + + Context("verify working traffic", func() { + It("should return a 200 response only for HTTPRoute tea, and fail for coffee", func() { + baseCoffeeURL := baseURL + "/coffee" + baseTeaURL := baseURL + "/tea" + + Eventually( + func() error { + return expectRequestToSucceed(baseCoffeeURL, address, "Coffee chunk", framework.WithContextDisabled()) + }). + WithTimeout(timeoutConfig.RequestTimeout). + WithPolling(500 * time.Millisecond). + ShouldNot(Succeed()) + + Eventually( + func() error { + return expectRequestToSucceed(baseTeaURL, address, "URI: /tea") + }). + WithTimeout(timeoutConfig.RequestTimeout). + WithPolling(500 * time.Millisecond). + Should(Succeed()) + }) + }) + + Context("nginx config", func() { + var conf *framework.Payload + filePrefix := fmt.Sprintf("/etc/nginx/includes/ProxySettingsPolicy_%s", namespace) + + BeforeAll(func() { + var err error + conf, err = resourceManager.GetNginxConfig(nginxPodName, namespace, "") + Expect(err).ToNot(HaveOccurred()) + }) + + DescribeTable("is set properly for", + func(expCfgs []framework.ExpectedNginxField) { + for _, expCfg := range expCfgs { + fmt.Println("Validating nginx config:", expCfg) + Expect(framework.ValidateNginxFieldExists(conf, expCfg)).To(Succeed()) + } + }, + Entry("gateway policy", []framework.ExpectedNginxField{ + { + Directive: "include", + Value: fmt.Sprintf("%s_gateway-proxy-settings.conf", filePrefix), + File: "http.conf", + }, + { + Directive: "proxy_buffer_size", + Value: "4k", + File: fmt.Sprintf("%s_gateway-proxy-settings.conf", filePrefix), + }, + { + Directive: "proxy_buffers", + Value: "8 4k", + File: fmt.Sprintf("%s_gateway-proxy-settings.conf", filePrefix), + }, + { + Directive: "proxy_busy_buffers_size", + Value: "16k", + File: fmt.Sprintf("%s_gateway-proxy-settings.conf", filePrefix), + }, + }), + ) + }) + }) + + When("valid ProxySettingsPolicies are disabled for both: Gateway and HTTPRoute", func() { + var ( + policies = []string{ + "proxy-settings-policy/gateway-and-coffee-disabled-proxy-settings.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{ + "gateway-proxy-settings", + "coffee-proxy-settings", + } + + for _, name := range policyNames { + nsname := types.NamespacedName{Name: name, Namespace: namespace} + + err := waitForPSPolicyStatus(nsname) + Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("%s was not accepted", name)) + } + }) + + Context("verify working traffic", func() { + It("should return a 200 response only for HTTPRoute tea, and fail for coffee", func() { + baseCoffeeURL := baseURL + "/coffee" + baseTeaURL := baseURL + "/tea" + + Eventually( + func() error { + return expectRequestToSucceed(baseCoffeeURL, address, "Coffee chunk", framework.WithContextDisabled()) + }). + WithTimeout(timeoutConfig.RequestTimeout). + WithPolling(500 * time.Millisecond). + ShouldNot(Succeed()) + + Eventually( + func() error { + return expectRequestToSucceed(baseTeaURL, address, "URI: /tea") + }). + WithTimeout(timeoutConfig.RequestTimeout). + WithPolling(500 * time.Millisecond). + Should(Succeed()) + }) + }) + }) + + When("valid big buffer Gateway ProxySettingsPolicies are inherited by HTTPRoute", func() { + var ( + policies = []string{ + "proxy-settings-policy/gateway-big-buffer-proxy-settings.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() { + nsnameGateway := types.NamespacedName{Name: "gateway-proxy-settings", Namespace: namespace} + nsnameCoffee := types.NamespacedName{Name: "coffee-proxy-settings", Namespace: namespace} + + errGateway := waitForPSPolicyStatus(nsnameGateway) + + Expect(errGateway).ToNot(HaveOccurred(), fmt.Sprintf("%s was not accepted", "gateway-proxy-settings")) + + errCoffee := waitForPSPolicyStatus(nsnameCoffee) + + Expect(errCoffee).To(HaveOccurred(), fmt.Sprintf("%s was accepted", "coffee-proxy-settings")) + }) + + Context("verify working traffic", func() { + It("should return a 200 response for HTTPRoutes", func() { + baseCoffeeURL := baseURL + "/coffee" + baseTeaURL := baseURL + "/tea" + + Eventually( + func() error { + return expectRequestToSucceed(baseCoffeeURL, address, "Coffee chunk", framework.WithContextDisabled()) + }). + WithTimeout(timeoutConfig.RequestTimeout). + WithPolling(500 * time.Millisecond). + Should(Succeed()) + + Eventually( + func() error { + return expectRequestToSucceed(baseTeaURL, address, "URI: /tea") + }). + WithTimeout(timeoutConfig.RequestTimeout). + WithPolling(500 * time.Millisecond). + Should(Succeed()) + }) + }) + }) + + // Gateway policy disables buffering, + // Route policy explicitly sets disable: false and specifies buffer sizes (should work correctly) + When("valid enable for HTTPRoute ProxySettingsPolicies overrides disabled Gateway settings", func() { + var ( + policies = []string{ + "proxy-settings-policy/gateway-disabled-coffee-enabled-proxy-settings.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{ + "gateway-proxy-settings", + "coffee-proxy-settings", + } + + for _, name := range policyNames { + nsname := types.NamespacedName{Name: name, Namespace: namespace} + + err := waitForPSPolicyStatus(nsname) + Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("%s was not accepted", name)) + } + }) + + Context("verify working traffic", func() { + It("should return a 200 response for HTTPRoutes", func() { + baseCoffeeURL := baseURL + "/coffee" + baseTeaURL := baseURL + "/tea" + + Eventually( + func() error { + return expectRequestToSucceed(baseCoffeeURL, address, "Coffee chunk", framework.WithContextDisabled()) + }). + WithTimeout(timeoutConfig.RequestTimeout). + WithPolling(500 * time.Millisecond). + Should(Succeed()) + + Eventually( + func() error { + return expectRequestToSucceed(baseTeaURL, address, "URI: /tea") + }). + WithTimeout(timeoutConfig.RequestTimeout). + WithPolling(500 * time.Millisecond). + Should(Succeed()) + }) + }) + }) + + // ????????????????????????????????????????????????// ???????????????????????????????? + // Route policy specifies buffer sizes without setting disable: false (should set PartiallyInvalid condition) + When("valid disable for Gateway ProxySettingsPolicies is inherited by HTTPRoute", func() { + var ( + policies = []string{ + "proxy-settings-policy/gateway-disabled-coffee-not-enabled-proxy-settings.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{ + "gateway-proxy-settings", + "coffee-proxy-settings", + } + + for _, name := range policyNames { + nsname := types.NamespacedName{Name: name, Namespace: namespace} + + err := waitForPSPolicyStatus(nsname) + Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("%s was not accepted", name)) + } + }) + + Context("verify working traffic", func() { + // ???????????????????????????????????????????????? + It("should return a 200 response only for HTTPRoute tea, and fail for coffee", func() { + baseCoffeeURL := baseURL + "/coffee" + baseTeaURL := baseURL + "/tea" + + Eventually( + func() error { + return expectRequestToSucceed(baseCoffeeURL, address, "Coffee chunk", framework.WithContextDisabled()) + }). + WithTimeout(timeoutConfig.RequestTimeout). + WithPolling(500 * time.Millisecond). + Should(Succeed()) // ???????????????????????????????????????????????? + + Eventually( + func() error { + return expectRequestToSucceed(baseTeaURL, address, "URI: /tea") + }). + WithTimeout(timeoutConfig.RequestTimeout). + WithPolling(500 * time.Millisecond). + Should(Succeed()) + }) + }) + }) + + // ????????????????????????????????????????????????// ????????????????????????????????? + When("valid disable for HTTPRoute ProxySettingsPolicies overrides enabled Gateway setting", func() { + var ( + policies = []string{ + "proxy-settings-policy/gateway-enabled-coffee-disabled-proxy-settings.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{ + "gateway-proxy-settings", + "coffee-proxy-settings", + } + + for _, name := range policyNames { + nsname := types.NamespacedName{Name: name, Namespace: namespace} + + err := waitForPSPolicyStatus(nsname) + Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("%s was not accepted", name)) + } + }) + + Context("verify working traffic", func() { + // ???????????????????????????????????????????????? + It("should return a 200 response only for HTTPRoute tea, and fail for coffee", func() { + baseCoffeeURL := baseURL + "/coffee" + baseTeaURL := baseURL + "/tea" + + Eventually( + func() error { + return expectRequestToSucceed(baseCoffeeURL, address, "Coffee chunk", framework.WithContextDisabled()) + }). + WithTimeout(timeoutConfig.RequestTimeout). + WithPolling(500 * time.Millisecond). + Should(Succeed()) // ???????????????????????????????????????????????? + + Eventually( + func() error { + return expectRequestToSucceed(baseTeaURL, address, "URI: /tea") + }). + WithTimeout(timeoutConfig.RequestTimeout). + WithPolling(500 * time.Millisecond). + Should(Succeed()) + }) + }) + }) +}) + +func waitForPSPolicyStatus( + psPolicyNsName types.NamespacedName, +) error { + ctx, cancel := context.WithTimeout(context.Background(), timeoutConfig.GetStatusTimeout*2) + defer cancel() + + var ( + condStatus metav1.ConditionStatus + condReason gatewayv1.PolicyConditionReason + ) + condStatus = "True" + condReason = "Accepted" + + GinkgoWriter.Printf( + "Waiting for ProxySettings Policy %q to have the condition %q/%q\n", + psPolicyNsName, + condStatus, + condReason, + ) + + return wait.PollUntilContextCancel( + ctx, + 2000*time.Millisecond, + true, + func(ctx context.Context) (bool, error) { + var psPolicy ngfAPI.ProxySettingsPolicy + var err error + + if err := resourceManager.Get(ctx, psPolicyNsName, &psPolicy); err != nil { + return false, err + } + + if len(psPolicy.Status.Ancestors) == 0 { + GinkgoWriter.Printf("ProxySettingsPolicy %q does not have an ancestor status yet\n", psPolicy) + + return false, nil + } + + if len(psPolicy.Status.Ancestors) != 1 { + tooManyAncestorsErr := fmt.Errorf("policy has %d ancestors, expected 1", len(psPolicy.Status.Ancestors)) + GinkgoWriter.Printf("ERROR: %v\n", tooManyAncestorsErr) + + return false, tooManyAncestorsErr + } + + ancestors := psPolicy.Status.Ancestors + + for _, ancestor := range ancestors { + err = ancestorStatusMustHaveAcceptedCondition(ancestor, condStatus, condReason) + } + return err == nil, err + }, + ) +}