diff --git a/tests/framework/crossplane.go b/tests/framework/crossplane.go index b6925dc8e..81f47e356 100644 --- a/tests/framework/crossplane.go +++ b/tests/framework/crossplane.go @@ -28,8 +28,10 @@ type ExpectedNginxField struct { File string // Location is the location name that the directive should exist in. Location string - // Servers are the server names that the directive should exist in. - Servers []string + // Server is the server name that the directive should exist in. + Server string + // Upstream is the upstream name that the directive should exist in. + Upstream string // ValueSubstringAllowed allows the expected value to be a substring of the real value. // This makes it easier for cases when real values are complex file names or contain things we // don't care about, and we just want to check if a substring exists. @@ -39,40 +41,66 @@ type ExpectedNginxField struct { // ValidateNginxFieldExists accepts the nginx config and the configuration for the expected field, // and returns whether or not that field exists where it should. func ValidateNginxFieldExists(conf *Payload, expFieldCfg ExpectedNginxField) error { + b, err := json.Marshal(conf) + if err != nil { + return fmt.Errorf("error marshaling nginx config: %w", err) + } + for _, config := range conf.Config { if !strings.Contains(config.File, expFieldCfg.File) { continue } for _, directive := range config.Parsed { - if len(expFieldCfg.Servers) == 0 { + if expFieldCfg.Server == "" && expFieldCfg.Upstream == "" { if expFieldCfg.fieldFound(directive) { return nil } continue } - for _, serverName := range expFieldCfg.Servers { - if directive.Directive == "server" && getServerName(directive.Block) == serverName { - for _, serverDirective := range directive.Block { - if expFieldCfg.Location == "" && expFieldCfg.fieldFound(serverDirective) { - return nil - } else if serverDirective.Directive == "location" && - fieldExistsInLocation(serverDirective, expFieldCfg) { - return nil - } - } - } + if expFieldCfg.Server != "" && fieldExistsInServer(expFieldCfg, *directive) { + return nil + } + + if expFieldCfg.Upstream != "" && fieldExistsInUpstream(expFieldCfg, *directive) { + return nil } } } - b, err := json.Marshal(conf) - if err != nil { - return fmt.Errorf("error marshaling nginx config: %w", err) + return fmt.Errorf("directive %s not found in: nginx config %s", expFieldCfg.Directive, string(b)) +} + +func fieldExistsInServer( + expFieldCfg ExpectedNginxField, + directive Directive, +) bool { + if directive.Directive == "server" && getServerName(directive.Block) == expFieldCfg.Server { + for _, serverDirective := range directive.Block { + if expFieldCfg.Location == "" && expFieldCfg.fieldFound(serverDirective) { + return true + } else if serverDirective.Directive == "location" && + fieldExistsInLocation(serverDirective, expFieldCfg) { + return true + } + } } + return false +} - return fmt.Errorf("field not found; expected: %+v\nNGINX conf: %s", expFieldCfg, string(b)) +func fieldExistsInUpstream( + expFieldCfg ExpectedNginxField, + directive Directive, +) bool { + if directive.Directive == "upstream" && directive.Args[0] == expFieldCfg.Upstream { + for _, directive := range directive.Block { + if expFieldCfg.fieldFound(directive) { + return true + } + } + } + return false } func getServerName(serverBlock Directives) string { diff --git a/tests/suite/client_settings_test.go b/tests/suite/client_settings_test.go index b5e5a8ec4..f7ab05c88 100644 --- a/tests/suite/client_settings_test.go +++ b/tests/suite/client_settings_test.go @@ -117,7 +117,13 @@ var _ = Describe("ClientSettingsPolicy", Ordered, Label("functional", "cspolicy" Directive: "include", Value: fmt.Sprintf("%s_gw-csp.conf", filePrefix), File: "http.conf", - Servers: []string{"*.example.com", "cafe.example.com"}, + Server: "*.example.com", + }, + { + Directive: "include", + Value: fmt.Sprintf("%s_gw-csp.conf", filePrefix), + File: "http.conf", + Server: "cafe.example.com", }, { Directive: "client_max_body_size", @@ -150,7 +156,7 @@ var _ = Describe("ClientSettingsPolicy", Ordered, Label("functional", "cspolicy" Directive: "include", Value: fmt.Sprintf("%s_coffee-route-csp.conf", filePrefix), File: "http.conf", - Servers: []string{"cafe.example.com"}, + Server: "cafe.example.com", Location: "/coffee", }, { @@ -164,7 +170,7 @@ var _ = Describe("ClientSettingsPolicy", Ordered, Label("functional", "cspolicy" Directive: "include", Value: fmt.Sprintf("%s_tea-route-csp.conf", filePrefix), File: "http.conf", - Servers: []string{"cafe.example.com"}, + Server: "cafe.example.com", Location: "/tea", }, { @@ -178,7 +184,7 @@ var _ = Describe("ClientSettingsPolicy", Ordered, Label("functional", "cspolicy" Directive: "include", Value: fmt.Sprintf("%s_soda-route-csp.conf", filePrefix), File: "http.conf", - Servers: []string{"cafe.example.com"}, + Server: "cafe.example.com", Location: "/soda", }, { @@ -192,7 +198,7 @@ var _ = Describe("ClientSettingsPolicy", Ordered, Label("functional", "cspolicy" Directive: "include", Value: fmt.Sprintf("%s_grpc-route-csp.conf", filePrefix), File: "http.conf", - Servers: []string{"*.example.com"}, + Server: "*.example.com", Location: "/helloworld.Greeter/SayHello", }, { diff --git a/tests/suite/manifests/upstream-settings-policy/cafe.yaml b/tests/suite/manifests/upstream-settings-policy/cafe.yaml new file mode 100644 index 000000000..c0466158f --- /dev/null +++ b/tests/suite/manifests/upstream-settings-policy/cafe.yaml @@ -0,0 +1,66 @@ +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 +--- diff --git a/tests/suite/manifests/upstream-settings-policy/gateway.yaml b/tests/suite/manifests/upstream-settings-policy/gateway.yaml new file mode 100644 index 000000000..e6507f613 --- /dev/null +++ b/tests/suite/manifests/upstream-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/upstream-settings-policy/grpc-backend.yaml b/tests/suite/manifests/upstream-settings-policy/grpc-backend.yaml new file mode 100644 index 000000000..3ae352f57 --- /dev/null +++ b/tests/suite/manifests/upstream-settings-policy/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.3 + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + resources: + requests: + cpu: 10m diff --git a/tests/suite/manifests/upstream-settings-policy/invalid-svc-usps.yaml b/tests/suite/manifests/upstream-settings-policy/invalid-svc-usps.yaml new file mode 100644 index 000000000..cc1ed6c55 --- /dev/null +++ b/tests/suite/manifests/upstream-settings-policy/invalid-svc-usps.yaml @@ -0,0 +1,15 @@ +apiVersion: gateway.nginx.org/v1alpha1 +kind: UpstreamSettingsPolicy +metadata: + name: usps-target-not-found +spec: + zoneSize: 512k + targetRefs: + - group: core + kind: Service + name: targeted-svc-dne + keepAlive: + connections: 10 + requests: 3 + time: 10s + timeout: 50s diff --git a/tests/suite/manifests/upstream-settings-policy/invalid-target-usps.yaml b/tests/suite/manifests/upstream-settings-policy/invalid-target-usps.yaml new file mode 100644 index 000000000..8bbd25366 --- /dev/null +++ b/tests/suite/manifests/upstream-settings-policy/invalid-target-usps.yaml @@ -0,0 +1,81 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: gateway-not-valid +spec: + gatewayClassName: nginx + addresses: + - value: "10.0.0.1" + listeners: + - name: http + port: 80 + protocol: HTTP + hostname: "soda.example.com" +--- +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 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: soda +spec: + parentRefs: + - name: gateway-not-valid + sectionName: http + hostnames: + - "soda.example.com" + rules: + - matches: + - path: + type: Exact + value: /soda + backendRefs: + - name: soda + port: 80 +--- +apiVersion: gateway.nginx.org/v1alpha1 +kind: UpstreamSettingsPolicy +metadata: + name: soda-svc-usp +spec: + zoneSize: 512k + targetRefs: + - group: core + kind: Service + name: soda + keepAlive: + connections: 10 + requests: 3 + time: 10s + timeout: 50s diff --git a/tests/suite/manifests/upstream-settings-policy/routes.yaml b/tests/suite/manifests/upstream-settings-policy/routes.yaml new file mode 100644 index 000000000..f26e705a4 --- /dev/null +++ b/tests/suite/manifests/upstream-settings-policy/routes.yaml @@ -0,0 +1,54 @@ +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: 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/upstream-settings-policy/valid-merge-usps.yaml b/tests/suite/manifests/upstream-settings-policy/valid-merge-usps.yaml new file mode 100644 index 000000000..7c713b475 --- /dev/null +++ b/tests/suite/manifests/upstream-settings-policy/valid-merge-usps.yaml @@ -0,0 +1,60 @@ +apiVersion: gateway.nginx.org/v1alpha1 +kind: UpstreamSettingsPolicy +metadata: + name: merge-usp-1 +spec: + targetRefs: + - group: core + kind: Service + name: coffee + keepAlive: + time: 1m + timeout: 5h +--- +apiVersion: gateway.nginx.org/v1alpha1 +kind: UpstreamSettingsPolicy +metadata: + name: merge-usp-2 +spec: + targetRefs: + - group: core + kind: Service + name: coffee + keepAlive: + connections: 100 + requests: 55 +--- +apiVersion: gateway.nginx.org/v1alpha1 +kind: UpstreamSettingsPolicy +metadata: + name: z-merge-usp-3 +spec: + targetRefs: + - group: core + kind: Service + name: coffee + keepAlive: + connections: 11 + requests: 15 +--- +apiVersion: gateway.nginx.org/v1alpha1 +kind: UpstreamSettingsPolicy +metadata: + name: z-usp +spec: + zoneSize: 64k + targetRefs: + - group: core + kind: Service + name: tea +--- +apiVersion: gateway.nginx.org/v1alpha1 +kind: UpstreamSettingsPolicy +metadata: + name: a-usp-wins +spec: + zoneSize: 128k + targetRefs: + - group: core + kind: Service + name: tea diff --git a/tests/suite/manifests/upstream-settings-policy/valid-usps.yaml b/tests/suite/manifests/upstream-settings-policy/valid-usps.yaml new file mode 100644 index 000000000..cb4e4bf05 --- /dev/null +++ b/tests/suite/manifests/upstream-settings-policy/valid-usps.yaml @@ -0,0 +1,33 @@ +apiVersion: gateway.nginx.org/v1alpha1 +kind: UpstreamSettingsPolicy +metadata: + name: multiple-http-svc-usp +spec: + targetRefs: + - group: core + kind: Service + name: coffee + - group: core + kind: Service + name: tea + keepAlive: + connections: 10 + requests: 3 + time: 10s + timeout: 50s +--- +apiVersion: gateway.nginx.org/v1alpha1 +kind: UpstreamSettingsPolicy +metadata: + name: grpc-svc-usp +spec: + targetRefs: + - group: core + kind: Service + name: grpc-backend + zoneSize: 64k + keepAlive: + connections: 100 + requests: 45 + time: 1m + timeout: 5h diff --git a/tests/suite/snippets_filter_test.go b/tests/suite/snippets_filter_test.go index 632a8199e..e92e44d0a 100644 --- a/tests/suite/snippets_filter_test.go +++ b/tests/suite/snippets_filter_test.go @@ -149,7 +149,7 @@ var _ = Describe("SnippetsFilter", Ordered, Label("functional", "snippets-filter { Directive: "include", Value: fmt.Sprintf("%s%s", httpServerContext, httpRouteSuffix), - Servers: []string{"cafe.example.com"}, + Server: "cafe.example.com", File: "http.conf", }, { @@ -157,7 +157,7 @@ var _ = Describe("SnippetsFilter", Ordered, Label("functional", "snippets-filter Value: fmt.Sprintf("%s%s", httpServerLocationContext, httpRouteSuffix), File: "http.conf", Location: "/coffee", - Servers: []string{"cafe.example.com"}, + Server: "cafe.example.com", }, { Directive: "keepalive_time", @@ -194,7 +194,7 @@ var _ = Describe("SnippetsFilter", Ordered, Label("functional", "snippets-filter { Directive: "include", Value: fmt.Sprintf("%s%s", httpServerContext, grpcRouteSuffix), - Servers: []string{"*.example.com"}, + Server: "*.example.com", File: "http.conf", }, { @@ -207,7 +207,7 @@ var _ = Describe("SnippetsFilter", Ordered, Label("functional", "snippets-filter Value: fmt.Sprintf("%s%s", httpServerLocationContext, grpcRouteSuffix), File: "http.conf", Location: "/helloworld.Greeter/SayHello", - Servers: []string{"*.example.com"}, + Server: "*.example.com", }, }), ) diff --git a/tests/suite/upstream_settings_test.go b/tests/suite/upstream_settings_test.go new file mode 100644 index 000000000..39159e440 --- /dev/null +++ b/tests/suite/upstream_settings_test.go @@ -0,0 +1,523 @@ +package main + +import ( + "context" + "errors" + "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" + "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("UpstreamSettingsPolicy", Ordered, Label("functional", "uspolicy"), func() { + var ( + files = []string{ + "upstream-settings-policy/cafe.yaml", + "upstream-settings-policy/gateway.yaml", + "upstream-settings-policy/grpc-backend.yaml", + "upstream-settings-policy/routes.yaml", + } + + namespace = "uspolicy" + gatewayName = "gateway" + ) + + 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("UpstreamSettingsPolicies target distinct Services", func() { + usps := []string{ + "upstream-settings-policy/valid-usps.yaml", + } + + BeforeAll(func() { + Expect(resourceManager.ApplyFromFiles(usps, namespace)).To(Succeed()) + }) + + AfterAll(func() { + Expect(resourceManager.DeleteFromFiles(usps, namespace)).To(Succeed()) + }) + + Specify("they are accepted", func() { + usPolicies := []string{ + "multiple-http-svc-usp", + "grpc-svc-usp", + } + + for _, name := range usPolicies { + uspolicyNsName := types.NamespacedName{Name: name, Namespace: namespace} + + err := waitForUSPolicyStatus( + uspolicyNsName, + gatewayName, + metav1.ConditionTrue, + v1alpha2.PolicyReasonAccepted, + ) + 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() { + port := 80 + if portFwdPort != 0 { + port = portFwdPort + } + baseCoffeeURL := fmt.Sprintf("http://cafe.example.com:%d%s", port, "/coffee") + baseTeaURL := fmt.Sprintf("http://cafe.example.com:%d%s", port, "/tea") + + Eventually( + func() error { + return expectRequestToSucceed(baseCoffeeURL, address, "URI: /coffee") + }). + 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 directives", func() { + var conf *framework.Payload + + 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("HTTP upstreams", []framework.ExpectedNginxField{ + { + Directive: "upstream", + Value: "uspolicy_coffee_80", + File: "http.conf", + }, + { + Directive: "upstream", + Value: "uspolicy_tea_80", + File: "http.conf", + }, + { + Directive: "zone", + Value: "uspolicy_coffee_80 512k", + Upstream: "uspolicy_coffee_80", + File: "http.conf", + }, + { + Directive: "zone", + Value: "uspolicy_tea_80 512k", + Upstream: "uspolicy_tea_80", + File: "http.conf", + }, + { + Directive: "keepalive", + Value: "10", + Upstream: "uspolicy_coffee_80", + File: "http.conf", + }, + { + Directive: "keepalive", + Value: "10", + Upstream: "uspolicy_tea_80", + File: "http.conf", + }, + { + Directive: "keepalive_requests", + Value: "3", + Upstream: "uspolicy_coffee_80", + File: "http.conf", + }, + { + Directive: "keepalive_requests", + Value: "3", + Upstream: "uspolicy_tea_80", + File: "http.conf", + }, + { + Directive: "keepalive_time", + Value: "10s", + Upstream: "uspolicy_coffee_80", + File: "http.conf", + }, + { + Directive: "keepalive_time", + Value: "10s", + Upstream: "uspolicy_tea_80", + File: "http.conf", + }, + { + Directive: "keepalive_timeout", + Value: "50s", + Upstream: "uspolicy_coffee_80", + File: "http.conf", + }, + { + Directive: "keepalive_timeout", + Value: "50s", + Upstream: "uspolicy_tea_80", + File: "http.conf", + }, + }), + Entry("GRPC upstreams", []framework.ExpectedNginxField{ + { + Directive: "upstream", + Value: "uspolicy_grpc-backend_8080", + File: "http.conf", + }, + { + Directive: "zone", + Value: "uspolicy_grpc-backend_8080 64k", + Upstream: "uspolicy_grpc-backend_8080", + File: "http.conf", + }, + { + Directive: "keepalive", + Value: "100", + Upstream: "uspolicy_grpc-backend_8080", + File: "http.conf", + }, + { + Directive: "keepalive_requests", + Value: "45", + Upstream: "uspolicy_grpc-backend_8080", + File: "http.conf", + }, + { + Directive: "keepalive_time", + Value: "1m", + Upstream: "uspolicy_grpc-backend_8080", + File: "http.conf", + }, + { + Directive: "keepalive_timeout", + Value: "5h", + Upstream: "uspolicy_grpc-backend_8080", + File: "http.conf", + }, + }), + ) + }) + }) + + When("multiple UpstreamSettingsPolicies with overlapping settings target the same Service", func() { + usps := []string{ + "upstream-settings-policy/valid-merge-usps.yaml", + } + + BeforeAll(func() { + Expect(resourceManager.ApplyFromFiles(usps, namespace)).To(Succeed()) + }) + + AfterAll(func() { + Expect(resourceManager.DeleteFromFiles(usps, namespace)).To(Succeed()) + }) + + DescribeTable("upstreamSettingsPolicy status is set as expected", + func(name string, status metav1.ConditionStatus, condReason v1alpha2.PolicyConditionReason) { + uspolicyNsName := types.NamespacedName{Name: name, Namespace: namespace} + Expect(waitForUSPolicyStatus(uspolicyNsName, gatewayName, status, condReason)).To(Succeed()) + }, + Entry("uspolicy merge-usp-1", "merge-usp-1", metav1.ConditionTrue, v1alpha2.PolicyReasonAccepted), + Entry("uspolicy merge-usp-2", "merge-usp-2", metav1.ConditionTrue, v1alpha2.PolicyReasonAccepted), + Entry("uspolicy merge-usp-3", "z-merge-usp-3", metav1.ConditionFalse, v1alpha2.PolicyReasonConflicted), + Entry("uspolicy a-usp-wins", "a-usp-wins", metav1.ConditionTrue, v1alpha2.PolicyReasonAccepted), + Entry("uspolicy z-usp", "z-usp", metav1.ConditionFalse, v1alpha2.PolicyReasonConflicted), + ) + + Context("verify working traffic", func() { + It("should return a 200 response for HTTPRoutes", func() { + port := 80 + if portFwdPort != 0 { + port = portFwdPort + } + baseCoffeeURL := fmt.Sprintf("http://cafe.example.com:%d%s", port, "/coffee") + baseTeaURL := fmt.Sprintf("http://cafe.example.com:%d%s", port, "/tea") + + Eventually( + func() error { + return expectRequestToSucceed(baseCoffeeURL, address, "URI: /coffee") + }). + WithTimeout(timeoutConfig.RequestTimeout). + WithPolling(1000 * time.Millisecond). + Should(Succeed()) + + Eventually( + func() error { + return expectRequestToSucceed(baseTeaURL, address, "URI: /tea") + }). + WithTimeout(timeoutConfig.RequestTimeout). + WithPolling(1000 * time.Millisecond). + Should(Succeed()) + }) + }) + + Context("nginx directives", func() { + var conf *framework.Payload + + 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("Coffee upstream", []framework.ExpectedNginxField{ + { + Directive: "upstream", + Value: "uspolicy_coffee_80", + File: "http.conf", + }, + { + Directive: "zone", + Value: "uspolicy_coffee_80 512k", + Upstream: "uspolicy_coffee_80", + File: "http.conf", + }, + { + Directive: "keepalive", + Value: "100", + Upstream: "uspolicy_coffee_80", + File: "http.conf", + }, + { + Directive: "keepalive_requests", + Value: "55", + Upstream: "uspolicy_coffee_80", + File: "http.conf", + }, + { + Directive: "keepalive_time", + Value: "1m", + Upstream: "uspolicy_coffee_80", + File: "http.conf", + }, + { + Directive: "keepalive_timeout", + Value: "5h", + Upstream: "uspolicy_coffee_80", + File: "http.conf", + }, + }), + Entry("Tea upstream", []framework.ExpectedNginxField{ + { + Directive: "zone", + Value: "uspolicy_tea_80 128k", + Upstream: "uspolicy_tea_80", + File: "http.conf", + }, + { + Directive: "upstream", + Value: "uspolicy_tea_80", + File: "http.conf", + }, + }), + ) + }) + }) + + When("UpstreamSettingsPolicy targets a Service that does not exist", func() { + Specify("upstreamSettingsPolicy sets no condition", func() { + files := []string{"upstream-settings-policy/invalid-svc-usps.yaml"} + + Expect(resourceManager.ApplyFromFiles(files, namespace)).To(Succeed()) + + uspolicyNsName := types.NamespacedName{Name: "usps-target-not-found", Namespace: namespace} + + Consistently( + func() bool { + return usPolicyHasNoAncestors(uspolicyNsName) + }).WithTimeout(timeoutConfig.GetTimeout). + WithPolling(500 * time.Millisecond). + Should(BeTrue()) + + Expect(resourceManager.DeleteFromFiles(files, namespace)).To(Succeed()) + }) + }) + + When("UpstreamSettingsPolicy targets a Service that is owned by an invalid Gateway", func() { + Specify("upstreamSettingsPolicy is not Accepted with the reason TargetNotFound", func() { + // delete existing gateway + gatewayFileName := "upstream-settings-policy/gateway.yaml" + Expect(resourceManager.DeleteFromFiles([]string{gatewayFileName}, namespace)).To(Succeed()) + + files := []string{"upstream-settings-policy/invalid-target-usps.yaml"} + Expect(resourceManager.ApplyFromFiles(files, namespace)).To(Succeed()) + + uspolicyNsName := types.NamespacedName{Name: "soda-svc-usp", Namespace: namespace} + gatewayName = "gateway-not-valid" + Expect(waitForUSPolicyStatus( + uspolicyNsName, + gatewayName, + metav1.ConditionFalse, + v1alpha2.PolicyReasonTargetNotFound, + )).To(Succeed()) + + Expect(resourceManager.DeleteFromFiles(files, namespace)).To(Succeed()) + }) + }) +}) + +func usPolicyHasNoAncestors(usPolicyNsName types.NamespacedName) bool { + GinkgoWriter.Printf("Checking that UpstreamSettingsPolicy %q has no ancestors in status", usPolicyNsName) + + ctx, cancel := context.WithTimeout(context.Background(), timeoutConfig.GetStatusTimeout) + defer cancel() + + var usPolicy ngfAPI.UpstreamSettingsPolicy + if err := k8sClient.Get(ctx, usPolicyNsName, &usPolicy); err != nil { + GinkgoWriter.Printf("Failed to get UpstreamSettingsPolicy %q: %s", usPolicyNsName, err.Error()) + return false + } + + return len(usPolicy.Status.Ancestors) == 0 +} + +func waitForUSPolicyStatus( + usPolicyNsName types.NamespacedName, + gatewayName string, + condStatus metav1.ConditionStatus, + condReason v1alpha2.PolicyConditionReason, +) error { + ctx, cancel := context.WithTimeout(context.Background(), timeoutConfig.GetStatusTimeout*2) + defer cancel() + + GinkgoWriter.Printf( + "Waiting for UpstreamSettings Policy %q to have the condition %q/%q\n", + usPolicyNsName, + condStatus, + condReason, + ) + + return wait.PollUntilContextCancel( + ctx, + 2000*time.Millisecond, + true, /* poll immediately */ + func(ctx context.Context) (bool, error) { + var usPolicy ngfAPI.UpstreamSettingsPolicy + var err error + + if err := k8sClient.Get(ctx, usPolicyNsName, &usPolicy); err != nil { + return false, err + } + + if len(usPolicy.Status.Ancestors) == 0 { + GinkgoWriter.Printf("UpstreamSettingsPolicy %q does not have an ancestor status yet\n", usPolicy) + + return false, nil + } + + if len(usPolicy.Status.Ancestors) != 1 { + return false, fmt.Errorf("policy has %d ancestors, expected 1", len(usPolicy.Status.Ancestors)) + } + + ancestors := usPolicy.Status.Ancestors + + for _, ancestor := range ancestors { + if err := ancestorMustEqualGatewayRef(ancestor, gatewayName, usPolicy.Namespace); err != nil { + return false, err + } + + err = ancestorStatusMustHaveAcceptedCondition(ancestor, condStatus, condReason) + } + return err == nil, err + }, + ) +} + +func ancestorMustEqualGatewayRef( + ancestor v1alpha2.PolicyAncestorStatus, + gatewayName string, + 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 string(ancestorRef.Name) != gatewayName { + return fmt.Errorf("expected ancestorRef to have name %s, got %s", gatewayName, ancestorRef.Name) + } + + if ancestorRef.Kind == nil { + return errors.New("expected ancestorRef to have kind Gateway, got nil") + } + + if *ancestorRef.Kind != gatewayv1.Kind("Gateway") { + return fmt.Errorf( + "expected ancestorRef to have kind %s, got %s", + "Gateway", + string(*ancestorRef.Kind), + ) + } + + return nil +}