diff --git a/internal/gatewayapi/sort.go b/internal/gatewayapi/sort.go new file mode 100644 index 0000000000..035d2f6093 --- /dev/null +++ b/internal/gatewayapi/sort.go @@ -0,0 +1,69 @@ +package gatewayapi + +import ( + "sort" + + "github.com/envoyproxy/gateway/internal/ir" +) + +type XdsIRRoutes []*ir.HTTPRoute + +func (x XdsIRRoutes) Len() int { return len(x) } +func (x XdsIRRoutes) Swap(i, j int) { x[i], x[j] = x[j], x[i] } +func (x XdsIRRoutes) Less(i, j int) bool { + // 1. Sort based on characters in a matching path. + pCountI := pathMatchCount(x[i].PathMatch) + pCountJ := pathMatchCount(x[j].PathMatch) + if pCountI < pCountJ { + return true + } + if pCountI > pCountJ { + return false + } + // Equal case + + // 2. Sort based on the number of Header matches. + hCountI := len(x[i].HeaderMatches) + hCountJ := len(x[j].HeaderMatches) + if hCountI < hCountJ { + return true + } + if hCountI > hCountJ { + return false + } + // Equal case + + // 3. Sort based on the number of Query param matches. + qCountI := len(x[i].QueryParamMatches) + qCountJ := len(x[j].QueryParamMatches) + return qCountI < qCountJ +} + +// sortXdsIR sorts the xdsIR based on the match precedence +// defined in the Gateway API spec. +// https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteRule +func sortXdsIRMap(xdsIR XdsIRMap) { + for _, ir := range xdsIR { + ir := ir + sort.SliceStable(ir.HTTP, func(i, j int) bool { return ir.HTTP[i].Name < ir.HTTP[j].Name }) + for _, http := range ir.HTTP { + // descending order + sort.Sort(sort.Reverse(XdsIRRoutes(http.Routes))) + } + } +} + +func pathMatchCount(pathMatch *ir.StringMatch) int { + if pathMatch != nil { + if pathMatch.Exact != nil { + return len(*pathMatch.Exact) + } + if pathMatch.Prefix != nil { + return len(*pathMatch.Prefix) + } + if pathMatch.SafeRegex != nil { + return len(*pathMatch.SafeRegex) + } + } + return 0 +} diff --git a/internal/gatewayapi/testdata/httproutes-with-multiple-matches.in.yaml b/internal/gatewayapi/testdata/httproutes-with-multiple-matches.in.yaml new file mode 100644 index 0000000000..8edfe68340 --- /dev/null +++ b/internal/gatewayapi/testdata/httproutes-with-multiple-matches.in.yaml @@ -0,0 +1,129 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: Same +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: envoy-gateway + name: httproute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: envoy-gateway + name: httproute-2 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + hostnames: + - example.com + rules: + - matches: + - path: + value: "/v1/example" + queryParams: + - name: "debug" + value: "yes" + backendRefs: + - name: service-1 + port: 8080 +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: envoy-gateway + name: httproute-3 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + hostnames: + - example.com + rules: + - matches: + - path: + value: "/v1/example" + backendRefs: + - name: service-2 + port: 8080 +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: envoy-gateway + name: httproute-4 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + hostnames: + - example.net + rules: + - matches: + - path: + value: "/v1/status" + headers: + - name: "version" + value: "one" + backendRefs: + - name: service-1 + port: 8080 +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: envoy-gateway + name: httproute-5 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + hostnames: + - example.net + rules: + - matches: + - path: + value: "/v1/status" + backendRefs: + - name: service-2 + port: 8080 +services: +- apiVersion: v1 + kind: Service + metadata: + namespace: envoy-gateway + name: service-1 + spec: + clusterIP: 7.7.7.7 + ports: + - port: 8080 +- apiVersion: v1 + kind: Service + metadata: + namespace: envoy-gateway + name: service-2 + spec: + clusterIP: 8.8.8.8 + ports: + - port: 8080 diff --git a/internal/gatewayapi/testdata/httproutes-with-multiple-matches.out.yaml b/internal/gatewayapi/testdata/httproutes-with-multiple-matches.out.yaml new file mode 100644 index 0000000000..3911654a2f --- /dev/null +++ b/internal/gatewayapi/testdata/httproutes-with-multiple-matches.out.yaml @@ -0,0 +1,255 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: Same + status: + listeners: + - name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 5 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: envoy-gateway + name: httproute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "True" + reason: Accepted + message: Route is accepted +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: envoy-gateway + name: httproute-2 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + hostnames: + - example.com + rules: + - matches: + - path: + value: "/v1/example" + queryParams: + - name: "debug" + value: "yes" + backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "True" + reason: Accepted + message: Route is accepted +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: envoy-gateway + name: httproute-3 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + hostnames: + - example.com + rules: + - matches: + - path: + value: "/v1/example" + backendRefs: + - name: service-2 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "True" + reason: Accepted + message: Route is accepted +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: envoy-gateway + name: httproute-4 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + hostnames: + - example.net + rules: + - matches: + - path: + value: "/v1/status" + headers: + - name: "version" + value: "one" + backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "True" + reason: Accepted + message: Route is accepted +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: envoy-gateway + name: httproute-5 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + hostnames: + - example.net + rules: + - matches: + - path: + value: "/v1/status" + backendRefs: + - name: service-2 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "True" + reason: Accepted + message: Route is accepted +xdsIR: + envoy-gateway-gateway-1: + http: + - name: envoy-gateway-gateway-1-http + address: 0.0.0.0 + port: 10080 + hostnames: + - "*" + routes: + - name: envoy-gateway-httproute-2-rule-0-match-0-example.com + pathMatch: + prefix: "/v1/example" + # Remove comments once https://github.com/envoyproxy/gateway/issues/512 is fixed + # queryParamMatches: + # - name: "debug" + # exact: "yes" + headerMatches: + - name: :authority + exact: example.com + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 + - name: envoy-gateway-httproute-3-rule-0-match-0-example.com + pathMatch: + prefix: "/v1/example" + headerMatches: + - name: :authority + exact: example.com + destinations: + - host: 8.8.8.8 + port: 8080 + weight: 1 + - name: envoy-gateway-httproute-4-rule-0-match-0-example.net + pathMatch: + prefix: "/v1/status" + headerMatches: + - name: :authority + exact: example.net + - name: "version" + exact: "one" + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 + - name: envoy-gateway-httproute-5-rule-0-match-0-example.net + pathMatch: + prefix: "/v1/status" + headerMatches: + - name: :authority + exact: example.net + destinations: + - host: 8.8.8.8 + port: 8080 + weight: 1 + - name: envoy-gateway-httproute-1-rule-0-match-0-* + pathMatch: + prefix: "/" + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:v1.23-latest + listeners: + - address: "" + ports: + - name: envoy-gateway-gateway-1 + protocol: "HTTP" + servicePort: 80 + containerPort: 10080 diff --git a/internal/gatewayapi/translator.go b/internal/gatewayapi/translator.go index 52ec4e7e80..2acb7ba7f5 100644 --- a/internal/gatewayapi/translator.go +++ b/internal/gatewayapi/translator.go @@ -3,7 +3,6 @@ package gatewayapi import ( "fmt" "net/netip" - "sort" "strings" "golang.org/x/exp/slices" @@ -124,6 +123,9 @@ func (t *Translator) Translate(resources *Resources) *TranslateResult { // Process all relevant HTTPRoutes. httpRoutes := t.ProcessHTTPRoutes(resources.HTTPRoutes, gateways, resources, xdsIR) + // Sort xdsIR based on the Gateway API spec + sortXdsIRMap(xdsIR) + return newTranslateResult(gateways, httpRoutes, xdsIR, infraIR) } @@ -491,8 +493,6 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR XdsIRMap gwInfraIR.Proxy.Listeners[0].Ports = append(gwInfraIR.Proxy.Listeners[0].Ports, infraPort) } } - // sort result to ensure translation does not change across reboots. - sort.Slice(xdsIR[irKey].HTTP, func(i, j int) bool { return xdsIR[irKey].HTTP[i].Name < xdsIR[irKey].HTTP[j].Name }) } } diff --git a/test/conformance/conformance_test.go b/test/conformance/conformance_test.go index 3c0dd07c52..e69f5586b7 100644 --- a/test/conformance/conformance_test.go +++ b/test/conformance/conformance_test.go @@ -45,6 +45,7 @@ func TestGatewayAPIConformance(t *testing.T) { tests.HTTPRouteInvalidCrossNamespaceParentRef, tests.HTTPExactPathMatching, tests.HTTPRouteCrossNamespace, + tests.HTTPRouteHeaderMatching, tests.HTTPRouteMatchingAcrossRoutes, tests.HTTPRouteInvalidNonExistentBackendRef, tests.HTTPRouteInvalidBackendRefUnknownKind,