diff --git a/charts/gateway-helm/templates/certgen.yaml b/charts/gateway-helm/templates/certgen.yaml index bea4792f242..78d5ec2a28d 100644 --- a/charts/gateway-helm/templates/certgen.yaml +++ b/charts/gateway-helm/templates/certgen.yaml @@ -6,7 +6,7 @@ metadata: labels: {{- include "eg.labels" . | nindent 4 }} annotations: - "helm.sh/hook": pre-install + "helm.sh/hook": pre-install, pre-upgrade {{- if .Values.certgen.job.annotations }} {{- toYaml .Values.certgen.job.annotations | nindent 4 -}} {{- end }} diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go index c18447c7544..352e6adf9d2 100644 --- a/internal/gatewayapi/backendtrafficpolicy.go +++ b/internal/gatewayapi/backendtrafficpolicy.go @@ -350,6 +350,8 @@ func (t *Translator) translateBackendTrafficPolicyForRoute(policy *egv1a1.Backen r.LoadBalancer = lb r.ProxyProtocol = pp r.HealthCheck = hc + // Update the Host field in HealthCheck, now that we have access to the Route Hostname. + r.HealthCheck.SetHTTPHostIfAbsent(r.Hostname) r.CircuitBreaker = cb r.FaultInjection = fi r.TCPKeepalive = ka @@ -459,7 +461,10 @@ func (t *Translator) translateBackendTrafficPolicyForGateway(policy *egv1a1.Back } if r.HealthCheck == nil { r.HealthCheck = hc + // Update the Host field in HealthCheck, now that we have access to the Route Hostname. + r.HealthCheck.SetHTTPHostIfAbsent(r.Hostname) } + if r.CircuitBreaker == nil { r.CircuitBreaker = cb } diff --git a/internal/gatewayapi/clienttrafficpolicy.go b/internal/gatewayapi/clienttrafficpolicy.go index 41f22821752..ef8869f5bc8 100644 --- a/internal/gatewayapi/clienttrafficpolicy.go +++ b/internal/gatewayapi/clienttrafficpolicy.go @@ -124,7 +124,7 @@ func (t *Translator) ProcessClientTrafficPolicies(resources *Resources, // It must exist since we've already finished processing the gateways gwXdsIR := xdsIR[irKey] if string(l.Name) == section { - err = validatePortOverlapForClientTrafficPolicy(l, gwXdsIR) + err = validatePortOverlapForClientTrafficPolicy(l, gwXdsIR, false) if err == nil { err = t.translateClientTrafficPolicyForListener(policy, l, xdsIR, infraIR, resources) } @@ -234,7 +234,7 @@ func (t *Translator) ProcessClientTrafficPolicies(resources *Resources, irKey := t.getIRKey(l.gateway) // It must exist since we've already finished processing the gateways gwXdsIR := xdsIR[irKey] - if err := validatePortOverlapForClientTrafficPolicy(l, gwXdsIR); err != nil { + if err := validatePortOverlapForClientTrafficPolicy(l, gwXdsIR, true); err != nil { errs = errors.Join(errs, err) } else if err := t.translateClientTrafficPolicyForListener(policy, l, xdsIR, infraIR, resources); err != nil { errs = errors.Join(errs, err) @@ -312,7 +312,7 @@ func resolveCTPolicyTargetRef(policy *egv1a1.ClientTrafficPolicy, gateways map[t return gateway.GatewayContext, nil } -func validatePortOverlapForClientTrafficPolicy(l *ListenerContext, xds *ir.Xds) error { +func validatePortOverlapForClientTrafficPolicy(l *ListenerContext, xds *ir.Xds, attachedToGateway bool) error { // Find Listener IR // TODO: Support TLSRoute and TCPRoute once // https://github.com/envoyproxy/gateway/issues/1635 is completed @@ -328,8 +328,29 @@ func validatePortOverlapForClientTrafficPolicy(l *ListenerContext, xds *ir.Xds) // IR must exist since we're past validation if httpIR != nil { + // Get a list of all other non-TLS listeners on this Gateway that share a port with + // the listener in question. if sameListeners := listenersWithSameHTTPPort(xds, httpIR); len(sameListeners) != 0 { - return fmt.Errorf("affects additional listeners: %s", strings.Join(sameListeners, ", ")) + if attachedToGateway { + // If this policy is attached to an entire gateway and the mergeGateways feature + // is turned on, validate that all the listeners affected by this policy originated + // from the same Gateway resource. The name of the Gateway from which this listener + // originated is part of the listener's name by construction. + gatewayName := irListenerName[0:strings.LastIndex(irListenerName, "/")] + conflictingListeners := []string{} + for _, currName := range sameListeners { + if strings.Index(currName, gatewayName) != 0 { + conflictingListeners = append(conflictingListeners, currName) + } + } + if len(conflictingListeners) != 0 { + return fmt.Errorf("ClientTrafficPolicy is being applied to multiple http (non https) listeners (%s) on the same port, which is not allowed", strings.Join(conflictingListeners, ", ")) + } + } else { + // If this policy is attached to a specific listener, any other listeners in the list + // would be affected by this policy but should not be, so this policy can't be accepted. + return fmt.Errorf("ClientTrafficPolicy is being applied to multiple http (non https) listeners (%s) on the same port, which is not allowed", strings.Join(sameListeners, ", ")) + } } } return nil diff --git a/internal/gatewayapi/listener.go b/internal/gatewayapi/listener.go index 4702d2a4e26..de94aaf27e9 100644 --- a/internal/gatewayapi/listener.go +++ b/internal/gatewayapi/listener.go @@ -24,6 +24,8 @@ type ListenersTranslator interface { } func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR XdsIRMap, infraIR InfraIRMap, resources *Resources) { + // Infra IR proxy ports must be unique. + foundPorts := make(map[string][]*protocolPort) t.validateConflictedLayer7Listeners(gateways) t.validateConflictedLayer4Listeners(gateways, gwapiv1.TCPProtocolType, gwapiv1.TLSProtocolType) t.validateConflictedLayer4Listeners(gateways, gwapiv1.UDPProtocolType) @@ -35,8 +37,6 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR XdsIRMap // and compute status for each, and add valid ones // to the Xds IR. for _, gateway := range gateways { - // Infra IR proxy ports must be unique. - var foundPorts []*protocolPort irKey := t.getIRKey(gateway.Gateway) if resources.EnvoyProxy != nil { @@ -93,7 +93,6 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR XdsIRMap if !isReady { continue } - // Add the listener to the Xds IR servicePort := &protocolPort{protocol: listener.Protocol, port: int32(listener.Port)} containerPort := servicePortToContainerPort(servicePort.port) @@ -122,42 +121,46 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR XdsIRMap // Add the listener to the Infra IR. Infra IR ports must have a unique port number per layer-4 protocol // (TCP or UDP). - if !containsPort(foundPorts, servicePort) { - foundPorts = append(foundPorts, servicePort) - var proto ir.ProtocolType - switch listener.Protocol { - case gwapiv1.HTTPProtocolType: - proto = ir.HTTPProtocolType - case gwapiv1.HTTPSProtocolType: - proto = ir.HTTPSProtocolType - case gwapiv1.TLSProtocolType: - proto = ir.TLSProtocolType - case gwapiv1.TCPProtocolType: - proto = ir.TCPProtocolType - case gwapiv1.UDPProtocolType: - proto = ir.UDPProtocolType - } + if !containsPort(foundPorts[irKey], servicePort) { + t.processInfraIRListener(listener, infraIR, irKey, servicePort) + foundPorts[irKey] = append(foundPorts[irKey], servicePort) + } + } + } +} - infraPortName := string(listener.Name) - if t.MergeGateways { - infraPortName = irHTTPListenerName(listener) - } - infraPort := ir.ListenerPort{ - Name: infraPortName, - Protocol: proto, - ServicePort: servicePort.port, - ContainerPort: containerPort, - } +func (t *Translator) processInfraIRListener(listener *ListenerContext, infraIR InfraIRMap, irKey string, servicePort *protocolPort) { + var proto ir.ProtocolType + switch listener.Protocol { + case gwapiv1.HTTPProtocolType: + proto = ir.HTTPProtocolType + case gwapiv1.HTTPSProtocolType: + proto = ir.HTTPSProtocolType + case gwapiv1.TLSProtocolType: + proto = ir.TLSProtocolType + case gwapiv1.TCPProtocolType: + proto = ir.TCPProtocolType + case gwapiv1.UDPProtocolType: + proto = ir.UDPProtocolType + } - proxyListener := &ir.ProxyListener{ - Name: irHTTPListenerName(listener), - Ports: []ir.ListenerPort{infraPort}, - } + infraPortName := string(listener.Name) + if t.MergeGateways { + infraPortName = irHTTPListenerName(listener) + } + infraPort := ir.ListenerPort{ + Name: infraPortName, + Protocol: proto, + ServicePort: servicePort.port, + ContainerPort: servicePortToContainerPort(servicePort.port), + } - infraIR[irKey].Proxy.Listeners = append(infraIR[irKey].Proxy.Listeners, proxyListener) - } - } + proxyListener := &ir.ProxyListener{ + Name: irHTTPListenerName(listener), + Ports: []ir.ListenerPort{infraPort}, } + + infraIR[irKey].Proxy.Listeners = append(infraIR[irKey].Proxy.Listeners, proxyListener) } func processAccessLog(envoyproxy *egv1a1.EnvoyProxy) *ir.AccessLog { diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go index 2951d6ff7ec..962ff54b46d 100644 --- a/internal/gatewayapi/securitypolicy.go +++ b/internal/gatewayapi/securitypolicy.go @@ -134,12 +134,7 @@ func (t *Translator) ProcessSecurityPolicies(securityPolicies []*egv1a1.Security continue } - err := validatePortOverlapForSecurityPolicyRoute(xdsIR, targetedRoute) - if err == nil { - err = t.translateSecurityPolicyForRoute(policy, targetedRoute, resources, xdsIR) - } - - if err != nil { + if err := t.translateSecurityPolicyForRoute(policy, targetedRoute, resources, xdsIR); err != nil { status.SetTranslationErrorForPolicyAncestors(&policy.Status, parentGateways, t.GatewayControllerName, @@ -191,15 +186,7 @@ func (t *Translator) ProcessSecurityPolicies(securityPolicies []*egv1a1.Security continue } - irKey := t.getIRKey(targetedGateway.Gateway) - // Should exist since we've validated this - xds := xdsIR[irKey] - err := validatePortOverlapForSecurityPolicyGateway(xds) - if err == nil { - err = t.translateSecurityPolicyForGateway(policy, targetedGateway, resources, xdsIR) - } - - if err != nil { + if err := t.translateSecurityPolicyForGateway(policy, targetedGateway, resources, xdsIR); err != nil { status.SetTranslationErrorForPolicyAncestors(&policy.Status, parentGateways, t.GatewayControllerName, @@ -407,23 +394,6 @@ func (t *Translator) translateSecurityPolicyForRoute( return errs } -func validatePortOverlapForSecurityPolicyRoute(xds XdsIRMap, route RouteContext) error { - var errs error - prefix := irRoutePrefix(route) - for _, ir := range xds { - for _, http := range ir.HTTP { - for _, r := range http.Routes { - if strings.HasPrefix(r.Name, prefix) { - if sameListeners := listenersWithSameHTTPPort(ir, http); len(sameListeners) != 0 { - errs = errors.Join(errs, fmt.Errorf("affects multiple listeners: %s", strings.Join(sameListeners, ", "))) - } - } - } - } - } - return errs -} - func (t *Translator) translateSecurityPolicyForGateway( policy *egv1a1.SecurityPolicy, gateway *GatewayContext, resources *Resources, xdsIR XdsIRMap) error { @@ -516,20 +486,6 @@ func (t *Translator) translateSecurityPolicyForGateway( return errs } -func validatePortOverlapForSecurityPolicyGateway(xds *ir.Xds) error { - affectedListeners := []string{} - for _, http := range xds.HTTP { - if sameListeners := listenersWithSameHTTPPort(xds, http); len(sameListeners) != 0 { - affectedListeners = append(affectedListeners, sameListeners...) - } - } - - if len(affectedListeners) > 0 { - return fmt.Errorf("affects multiple listeners: %s", strings.Join(affectedListeners, ", ")) - } - return nil -} - func (t *Translator) buildCORS(cors *egv1a1.CORS) *ir.CORS { var allowOrigins []*ir.StringMatch diff --git a/internal/gatewayapi/sort.go b/internal/gatewayapi/sort.go index 00a5fc6389d..75d9ebc503a 100644 --- a/internal/gatewayapi/sort.go +++ b/internal/gatewayapi/sort.go @@ -18,33 +18,33 @@ 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 path match type - // Exact > PathPrefix > RegularExpression + // Exact > RegularExpression > PathPrefix if x[i].PathMatch != nil && x[i].PathMatch.Exact != nil { if x[j].PathMatch != nil { - if x[j].PathMatch.Prefix != nil { + if x[j].PathMatch.SafeRegex != nil { return false } - if x[j].PathMatch.SafeRegex != nil { + if x[j].PathMatch.Prefix != nil { return false } } } - if x[i].PathMatch != nil && x[i].PathMatch.Prefix != nil { + if x[i].PathMatch != nil && x[i].PathMatch.SafeRegex != nil { if x[j].PathMatch != nil { if x[j].PathMatch.Exact != nil { return true } - if x[j].PathMatch.SafeRegex != nil { + if x[j].PathMatch.Prefix != nil { return false } } } - if x[i].PathMatch != nil && x[i].PathMatch.SafeRegex != nil { + if x[i].PathMatch != nil && x[i].PathMatch.Prefix != nil { if x[j].PathMatch != nil { if x[j].PathMatch.Exact != nil { return true } - if x[j].PathMatch.Prefix != nil { + if x[j].PathMatch.SafeRegex != nil { return true } } @@ -96,12 +96,12 @@ func pathMatchCount(pathMatch *ir.StringMatch) int { if pathMatch.Exact != nil { return len(*pathMatch.Exact) } - if pathMatch.Prefix != nil { - return len(*pathMatch.Prefix) - } if pathMatch.SafeRegex != nil { return len(*pathMatch.SafeRegex) } + if pathMatch.Prefix != nil { + return len(*pathMatch.Prefix) + } } return 0 } diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-healthcheck.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-healthcheck.out.yaml index 7194512bbb8..34b5a13021e 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-with-healthcheck.out.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-healthcheck.out.yaml @@ -496,6 +496,7 @@ xdsIR: expectedStatuses: - 200 - 300 + host: '*' method: GET path: /healthz interval: 3s @@ -624,6 +625,7 @@ xdsIR: expectedStatuses: - 200 - 201 + host: gateway.envoyproxy.io method: GET path: /healthz interval: 5s diff --git a/internal/gatewayapi/testdata/conflicting-policies.out.yaml b/internal/gatewayapi/testdata/conflicting-policies.out.yaml index 7e0d52a41d1..d8b882d368a 100644 --- a/internal/gatewayapi/testdata/conflicting-policies.out.yaml +++ b/internal/gatewayapi/testdata/conflicting-policies.out.yaml @@ -23,7 +23,8 @@ clientTrafficPolicies: namespace: default conditions: - lastTransitionTime: null - message: 'Affects additional listeners: default/gateway-1/http' + message: ClientTrafficPolicy is being applied to multiple http (non https) + listeners (default/gateway-1/http) on the same port, which is not allowed reason: Invalid status: "False" type: Accepted @@ -217,13 +218,6 @@ infraIR: name: default/gateway-1/http protocol: HTTP servicePort: 80 - - address: null - name: default/mfqjpuycbgjrtdww/http - ports: - - containerPort: 10080 - name: default/mfqjpuycbgjrtdww/http - protocol: HTTP - servicePort: 80 metadata: labels: gateway.envoyproxy.io/owning-gatewayclass: envoy-gateway-class @@ -261,9 +255,9 @@ securityPolicies: namespace: default conditions: - lastTransitionTime: null - message: 'Affects multiple listeners: default/mfqjpuycbgjrtdww/http, default/gateway-1/http' - reason: Invalid - status: "False" + message: Policy has been accepted. + reason: Accepted + status: "True" type: Accepted controllerName: gateway.envoyproxy.io/gatewayclass-controller xdsIR: @@ -314,6 +308,20 @@ xdsIR: - backendWeights: invalid: 0 valid: 0 + cors: + allowCredentials: true + allowMethods: + - PUT + - GET + - POST + - DELETE + - PATCH + - OPTIONS + allowOrigins: + - distinct: false + name: "" + safeRegex: http://.*\.foo\.com + maxAge: 10m0s destination: name: httproute/default/mfqjpuycbgjrtdww/rule/0 settings: diff --git a/internal/gatewayapi/testdata/grpcroute-with-service-match.out.yaml b/internal/gatewayapi/testdata/grpcroute-with-service-match.out.yaml index 9624ffcaab1..ca598b9d046 100644 --- a/internal/gatewayapi/testdata/grpcroute-with-service-match.out.yaml +++ b/internal/gatewayapi/testdata/grpcroute-with-service-match.out.yaml @@ -126,11 +126,11 @@ xdsIR: weight: 1 hostname: '*' isHTTP2: true - name: grpcroute/default/grpcroute-1/rule/0/match/0/* + name: grpcroute/default/grpcroute-1/rule/0/match/1/* pathMatch: distinct: false name: "" - prefix: /com.ExampleExact + safeRegex: /com.[A-Z]+/[A-Za-z_][A-Za-z_0-9]* - backendWeights: invalid: 0 valid: 0 @@ -145,8 +145,8 @@ xdsIR: weight: 1 hostname: '*' isHTTP2: true - name: grpcroute/default/grpcroute-1/rule/0/match/1/* + name: grpcroute/default/grpcroute-1/rule/0/match/0/* pathMatch: distinct: false name: "" - safeRegex: /com.[A-Z]+/[A-Za-z_][A-Za-z_0-9]* + prefix: /com.ExampleExact diff --git a/internal/gatewayapi/testdata/merge-valid-multiple-gateways-multiple-listeners-same-ports.in.yaml b/internal/gatewayapi/testdata/merge-valid-multiple-gateways-multiple-listeners-same-ports.in.yaml new file mode 100644 index 00000000000..81e8ae3c976 --- /dev/null +++ b/internal/gatewayapi/testdata/merge-valid-multiple-gateways-multiple-listeners-same-ports.in.yaml @@ -0,0 +1,45 @@ +envoyproxy: + apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + namespace: envoy-gateway-system + name: test + spec: + mergeGateways: true +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + port: 80 + protocol: HTTP + allowedRoutes: + namespaces: + from: Same + - name: http-2 + hostname: company.com + port: 8888 + protocol: HTTP + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + name: gateway-2 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http-3 + port: 8888 + protocol: HTTP + allowedRoutes: + namespaces: + from: Same + - name: http-4 + hostname: example.com + port: 8888 + protocol: HTTP diff --git a/internal/gatewayapi/testdata/merge-valid-multiple-gateways-multiple-listeners-same-ports.out.yaml b/internal/gatewayapi/testdata/merge-valid-multiple-gateways-multiple-listeners-same-ports.out.yaml new file mode 100755 index 00000000000..94ab094ec2e --- /dev/null +++ b/internal/gatewayapi/testdata/merge-valid-multiple-gateways-multiple-listeners-same-ports.out.yaml @@ -0,0 +1,210 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: Same + name: http + port: 80 + protocol: HTTP + - hostname: company.com + name: http-2 + port: 8888 + protocol: HTTP + status: + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http-2 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-2 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: Same + name: http-3 + port: 8888 + protocol: HTTP + - hostname: example.com + name: http-4 + port: 8888 + protocol: HTTP + status: + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http-3 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http-4 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +infraIR: + envoy-gateway-class: + proxy: + config: + apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + creationTimestamp: null + name: test + namespace: envoy-gateway-system + spec: + logging: {} + mergeGateways: true + status: {} + listeners: + - address: null + name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: envoy-gateway/gateway-1/http + protocol: HTTP + servicePort: 80 + - address: null + name: envoy-gateway/gateway-1/http-2 + ports: + - containerPort: 8888 + name: envoy-gateway/gateway-1/http-2 + protocol: HTTP + servicePort: 8888 + metadata: + labels: + gateway.envoyproxy.io/owning-gatewayclass: envoy-gateway-class + name: envoy-gateway-class +xdsIR: + envoy-gateway-class: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + - address: 0.0.0.0 + hostnames: + - company.com + isHTTP2: false + name: envoy-gateway/gateway-1/http-2 + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 8888 + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway-2/http-3 + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 8888 + - address: 0.0.0.0 + hostnames: + - example.com + isHTTP2: false + name: envoy-gateway/gateway-2/http-4 + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 8888 diff --git a/internal/gatewayapi/testdata/merge-with-isolated-policies-2.in.yaml b/internal/gatewayapi/testdata/merge-with-isolated-policies-2.in.yaml new file mode 100644 index 00000000000..888c4e4b713 --- /dev/null +++ b/internal/gatewayapi/testdata/merge-with-isolated-policies-2.in.yaml @@ -0,0 +1,225 @@ +envoyproxy: + apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + namespace: envoy-gateway-system + name: test + spec: + mergeGateways: true +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + name: gateway-1 + namespace: default + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + port: 80 + protocol: HTTP + hostname: bar.example.com + allowedRoutes: + namespaces: + from: Same + - name: http-2 + port: 80 + hostname: foo.example.com + protocol: HTTP + allowedRoutes: + namespaces: + from: Same + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + name: gateway-2 + namespace: default + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + port: 81 + protocol: HTTP + hostname: bar.example.com + allowedRoutes: + namespaces: + from: Same + - name: http-2 + port: 81 + hostname: foo.example.com + protocol: HTTP + allowedRoutes: + namespaces: + from: Same +httpRoutes: + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - bar.example.com + parentRefs: + - namespace: default + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-2 + spec: + hostnames: + - foo.example.com + parentRefs: + - namespace: default + name: gateway-1 + sectionName: http-2 + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-2 + port: 8080 + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-3 + spec: + hostnames: + - bar.example.com + parentRefs: + - namespace: default + name: gateway-2 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-4 + spec: + hostnames: + - foo.example.com + parentRefs: + - namespace: default + name: gateway-2 + sectionName: http-2 + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-2 + port: 8080 +securityPolicies: + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + namespace: default + name: policy-for-route-1 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: default + cors: + allowOrigins: + - "*" + allowMethods: + - GET + - POST + allowHeaders: + - "x-header-5" + - "x-header-6" + exposeHeaders: + - "x-header-7" + - "x-header-8" + maxAge: 2000s + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + namespace: default + name: policy-for-route-2 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-3 + namespace: default + cors: + allowOrigins: + - "*" + allowMethods: + - GET + - POST + allowHeaders: + - "x-header-5" + - "x-header-6" + exposeHeaders: + - "x-header-7" + - "x-header-8" + maxAge: 2000s +clientTrafficPolicies: + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: ClientTrafficPolicy + metadata: + namespace: default + name: target-gateway-2 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-2 + sectionName: http + namespace: default + timeout: + http: + requestReceivedTimeout: "5s" + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: ClientTrafficPolicy + metadata: + namespace: default + name: target-gateway + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: default + timeout: + http: + requestReceivedTimeout: "5s" +backendTrafficPolicies: + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: default + name: policy-for-gateway + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: default + tcpKeepalive: + probes: 3 + idleTime: 20m + interval: 60s diff --git a/internal/gatewayapi/testdata/merge-with-isolated-policies-2.out.yaml b/internal/gatewayapi/testdata/merge-with-isolated-policies-2.out.yaml new file mode 100755 index 00000000000..15e35504f91 --- /dev/null +++ b/internal/gatewayapi/testdata/merge-with-isolated-policies-2.out.yaml @@ -0,0 +1,683 @@ +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + creationTimestamp: null + name: policy-for-gateway + namespace: default + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: default + tcpKeepalive: + idleTime: 20m + interval: 60s + probes: 3 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: default + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +clientTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: ClientTrafficPolicy + metadata: + creationTimestamp: null + name: target-gateway-2 + namespace: default + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-2 + namespace: default + sectionName: http + timeout: + http: + requestReceivedTimeout: 5s + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-2 + namespace: default + sectionName: http + conditions: + - lastTransitionTime: null + message: ClientTrafficPolicy is being applied to multiple http (non https) + listeners (default/gateway-2/http-2) on the same port, which is not allowed + reason: Invalid + status: "False" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: ClientTrafficPolicy + metadata: + creationTimestamp: null + name: target-gateway + namespace: default + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: default + timeout: + http: + requestReceivedTimeout: 5s + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: default + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: default + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: Same + hostname: bar.example.com + name: http + port: 80 + protocol: HTTP + - allowedRoutes: + namespaces: + from: Same + hostname: foo.example.com + name: http-2 + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http-2 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-2 + namespace: default + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: Same + hostname: bar.example.com + name: http + port: 81 + protocol: HTTP + - allowedRoutes: + namespaces: + from: Same + hostname: foo.example.com + name: http-2 + port: 81 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http-2 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-1 + namespace: default + spec: + hostnames: + - bar.example.com + parentRefs: + - name: gateway-1 + namespace: default + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: / + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: default + sectionName: http +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-2 + namespace: default + spec: + hostnames: + - foo.example.com + parentRefs: + - name: gateway-1 + namespace: default + sectionName: http-2 + rules: + - backendRefs: + - name: service-2 + port: 8080 + matches: + - path: + value: / + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: default + sectionName: http-2 +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-3 + namespace: default + spec: + hostnames: + - bar.example.com + parentRefs: + - name: gateway-2 + namespace: default + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: / + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-2 + namespace: default + sectionName: http +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-4 + namespace: default + spec: + hostnames: + - foo.example.com + parentRefs: + - name: gateway-2 + namespace: default + sectionName: http-2 + rules: + - backendRefs: + - name: service-2 + port: 8080 + matches: + - path: + value: / + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-2 + namespace: default + sectionName: http-2 +infraIR: + envoy-gateway-class: + proxy: + config: + apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + creationTimestamp: null + name: test + namespace: envoy-gateway-system + spec: + logging: {} + mergeGateways: true + status: {} + listeners: + - address: null + name: default/gateway-1/http + ports: + - containerPort: 10080 + name: default/gateway-1/http + protocol: HTTP + servicePort: 80 + - address: null + name: default/gateway-2/http + ports: + - containerPort: 10081 + name: default/gateway-2/http + protocol: HTTP + servicePort: 81 + metadata: + labels: + gateway.envoyproxy.io/owning-gatewayclass: envoy-gateway-class + name: envoy-gateway-class +securityPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + creationTimestamp: null + name: policy-for-route-2 + namespace: default + spec: + cors: + allowHeaders: + - x-header-5 + - x-header-6 + allowMethods: + - GET + - POST + allowOrigins: + - '*' + exposeHeaders: + - x-header-7 + - x-header-8 + maxAge: 33m20s + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-3 + namespace: default + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-2 + namespace: default + sectionName: http + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + creationTimestamp: null + name: policy-for-route-1 + namespace: default + spec: + cors: + allowHeaders: + - x-header-5 + - x-header-6 + allowMethods: + - GET + - POST + allowOrigins: + - '*' + exposeHeaders: + - x-header-7 + - x-header-8 + maxAge: 33m20s + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: default + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: default + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +xdsIR: + envoy-gateway-class: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - bar.example.com + isHTTP2: false + name: default/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - backendWeights: + invalid: 0 + valid: 0 + cors: + allowHeaders: + - x-header-5 + - x-header-6 + allowMethods: + - GET + - POST + allowOrigins: + - distinct: false + name: "" + safeRegex: .* + exposeHeaders: + - x-header-7 + - x-header-8 + maxAge: 33m20s + destination: + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: bar.example.com + isHTTP2: false + name: httproute/default/httproute-1/rule/0/match/0/bar_example_com + pathMatch: + distinct: false + name: "" + prefix: / + tcpKeepalive: + idleTime: 1200 + interval: 60 + probes: 3 + timeout: + http: + requestReceivedTimeout: 5s + - address: 0.0.0.0 + hostnames: + - foo.example.com + isHTTP2: false + name: default/gateway-1/http-2 + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - backendWeights: + invalid: 0 + valid: 0 + cors: + allowHeaders: + - x-header-5 + - x-header-6 + allowMethods: + - GET + - POST + allowOrigins: + - distinct: false + name: "" + safeRegex: .* + exposeHeaders: + - x-header-7 + - x-header-8 + maxAge: 33m20s + destination: + name: httproute/default/httproute-2/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: foo.example.com + isHTTP2: false + name: httproute/default/httproute-2/rule/0/match/0/foo_example_com + pathMatch: + distinct: false + name: "" + prefix: / + tcpKeepalive: + idleTime: 1200 + interval: 60 + probes: 3 + timeout: + http: + requestReceivedTimeout: 5s + - address: 0.0.0.0 + hostnames: + - bar.example.com + isHTTP2: false + name: default/gateway-2/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10081 + routes: + - backendWeights: + invalid: 0 + valid: 0 + cors: + allowHeaders: + - x-header-5 + - x-header-6 + allowMethods: + - GET + - POST + allowOrigins: + - distinct: false + name: "" + safeRegex: .* + exposeHeaders: + - x-header-7 + - x-header-8 + maxAge: 33m20s + destination: + name: httproute/default/httproute-3/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: bar.example.com + isHTTP2: false + name: httproute/default/httproute-3/rule/0/match/0/bar_example_com + pathMatch: + distinct: false + name: "" + prefix: / + - address: 0.0.0.0 + hostnames: + - foo.example.com + isHTTP2: false + name: default/gateway-2/http-2 + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10081 + routes: + - backendWeights: + invalid: 0 + valid: 0 + destination: + name: httproute/default/httproute-4/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: foo.example.com + isHTTP2: false + name: httproute/default/httproute-4/rule/0/match/0/foo_example_com + pathMatch: + distinct: false + name: "" + prefix: / diff --git a/internal/infrastructure/runner/runner.go b/internal/infrastructure/runner/runner.go index 452e346f174..054f320b12a 100644 --- a/internal/infrastructure/runner/runner.go +++ b/internal/infrastructure/runner/runner.go @@ -66,6 +66,11 @@ func (r *Runner) subscribeToProxyInfraIR(ctx context.Context) { } } else { // Manage the proxy infra. + if len(val.Proxy.Listeners) == 0 { + r.Logger.Info("Infra IR was updated, but no listeners were found. Skipping infra creation.") + return + } + if err := r.mgr.CreateOrUpdateProxyInfra(ctx, val); err != nil { r.Logger.Error(err, "failed to create new infra") errChan <- err diff --git a/internal/ir/xds.go b/internal/ir/xds.go index 7d1af7c0602..0f5f6a3c82b 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -53,6 +53,7 @@ var ( ErrHealthCheckUnhealthyThresholdInvalid = errors.New("field HealthCheck.UnhealthyThreshold should be greater than 0") ErrHealthCheckHealthyThresholdInvalid = errors.New("field HealthCheck.HealthyThreshold should be greater than 0") ErrHealthCheckerInvalid = errors.New("health checker setting is invalid, only one health checker can be set") + ErrHCHTTPHostInvalid = errors.New("field HTTPHealthChecker.Host should be specified") ErrHCHTTPPathInvalid = errors.New("field HTTPHealthChecker.Path should be specified") ErrHCHTTPMethodInvalid = errors.New("only one of the GET, HEAD, POST, DELETE, OPTIONS, TRACE, PATCH of HTTPHealthChecker.Method could be set") ErrHCHTTPExpectedStatusesInvalid = errors.New("field HTTPHealthChecker.ExpectedStatuses should be specified") @@ -1495,6 +1496,12 @@ type ActiveHealthCheck struct { TCP *TCPHealthChecker `json:"tcp,omitempty" yaml:"tcp,omitempty"` } +func (h *HealthCheck) SetHTTPHostIfAbsent(host string) { + if h != nil && h.Active != nil && h.Active.HTTP != nil && h.Active.HTTP.Host == "" { + h.Active.HTTP.Host = host + } +} + // Validate the fields within the HealthCheck structure. func (h *HealthCheck) Validate() error { var errs error @@ -1551,6 +1558,8 @@ func (h *HealthCheck) Validate() error { // HTTPHealthChecker defines the settings of http health check. // +k8s:deepcopy-gen=true type HTTPHealthChecker struct { + // Host defines the value of the host header in the HTTP health check request. + Host string `json:"host" yaml:"host"` // Path defines the HTTP path that will be requested during health checking. Path string `json:"path" yaml:"path"` // Method defines the HTTP method used for health checking. @@ -1564,6 +1573,9 @@ type HTTPHealthChecker struct { // Validate the fields within the HTTPHealthChecker structure. func (c *HTTPHealthChecker) Validate() error { var errs error + if c.Host == "" { + errs = errors.Join(errs, ErrHCHTTPHostInvalid) + } if c.Path == "" { errs = errors.Join(errs, ErrHCHTTPPathInvalid) } diff --git a/internal/ir/xds_test.go b/internal/ir/xds_test.go index c9f2bed7411..2f4d9a46a33 100644 --- a/internal/ir/xds_test.go +++ b/internal/ir/xds_test.go @@ -1257,6 +1257,7 @@ func TestValidateHealthCheck(t *testing.T) { UnhealthyThreshold: ptr.To[uint32](3), HealthyThreshold: ptr.To[uint32](3), HTTP: &HTTPHealthChecker{ + Host: "*", Path: "/healthz", ExpectedStatuses: []HTTPStatus{200, 400}, }, @@ -1273,6 +1274,7 @@ func TestValidateHealthCheck(t *testing.T) { UnhealthyThreshold: ptr.To[uint32](3), HealthyThreshold: ptr.To[uint32](3), HTTP: &HTTPHealthChecker{ + Host: "*", Path: "/healthz", Method: ptr.To(http.MethodGet), ExpectedStatuses: []HTTPStatus{200, 400}, @@ -1290,6 +1292,7 @@ func TestValidateHealthCheck(t *testing.T) { UnhealthyThreshold: ptr.To[uint32](0), HealthyThreshold: ptr.To[uint32](3), HTTP: &HTTPHealthChecker{ + Host: "*", Path: "/healthz", Method: ptr.To(http.MethodPatch), ExpectedStatuses: []HTTPStatus{200, 400}, @@ -1307,6 +1310,7 @@ func TestValidateHealthCheck(t *testing.T) { UnhealthyThreshold: ptr.To[uint32](3), HealthyThreshold: ptr.To[uint32](0), HTTP: &HTTPHealthChecker{ + Host: "*", Path: "/healthz", Method: ptr.To(http.MethodPost), ExpectedStatuses: []HTTPStatus{200, 400}, @@ -1316,6 +1320,23 @@ func TestValidateHealthCheck(t *testing.T) { }, want: ErrHealthCheckHealthyThresholdInvalid, }, + { + name: "http-health-check: invalid host", + input: HealthCheck{&ActiveHealthCheck{ + Timeout: &metav1.Duration{Duration: time.Second}, + Interval: &metav1.Duration{Duration: time.Second}, + UnhealthyThreshold: ptr.To[uint32](3), + HealthyThreshold: ptr.To[uint32](3), + HTTP: &HTTPHealthChecker{ + Path: "/healthz", + Method: ptr.To(http.MethodPut), + ExpectedStatuses: []HTTPStatus{200, 400}, + }, + }, + &OutlierDetection{}, + }, + want: ErrHCHTTPHostInvalid, + }, { name: "http-health-check: invalid path", input: HealthCheck{&ActiveHealthCheck{ @@ -1324,6 +1345,7 @@ func TestValidateHealthCheck(t *testing.T) { UnhealthyThreshold: ptr.To[uint32](3), HealthyThreshold: ptr.To[uint32](3), HTTP: &HTTPHealthChecker{ + Host: "*", Path: "", Method: ptr.To(http.MethodPut), ExpectedStatuses: []HTTPStatus{200, 400}, @@ -1341,6 +1363,7 @@ func TestValidateHealthCheck(t *testing.T) { UnhealthyThreshold: ptr.To(uint32(3)), HealthyThreshold: ptr.To(uint32(3)), HTTP: &HTTPHealthChecker{ + Host: "*", Path: "/healthz", Method: ptr.To(http.MethodConnect), ExpectedStatuses: []HTTPStatus{200, 400}, @@ -1358,6 +1381,7 @@ func TestValidateHealthCheck(t *testing.T) { UnhealthyThreshold: ptr.To(uint32(3)), HealthyThreshold: ptr.To(uint32(3)), HTTP: &HTTPHealthChecker{ + Host: "*", Path: "/healthz", Method: ptr.To(http.MethodDelete), ExpectedStatuses: []HTTPStatus{}, @@ -1375,6 +1399,7 @@ func TestValidateHealthCheck(t *testing.T) { UnhealthyThreshold: ptr.To(uint32(3)), HealthyThreshold: ptr.To(uint32(3)), HTTP: &HTTPHealthChecker{ + Host: "*", Path: "/healthz", Method: ptr.To(http.MethodHead), ExpectedStatuses: []HTTPStatus{100, 600}, @@ -1392,6 +1417,7 @@ func TestValidateHealthCheck(t *testing.T) { UnhealthyThreshold: ptr.To(uint32(3)), HealthyThreshold: ptr.To(uint32(3)), HTTP: &HTTPHealthChecker{ + Host: "*", Path: "/healthz", Method: ptr.To(http.MethodOptions), ExpectedStatuses: []HTTPStatus{200, 300}, diff --git a/internal/xds/translator/cluster.go b/internal/xds/translator/cluster.go index 10a088c516e..14eb18f0261 100644 --- a/internal/xds/translator/cluster.go +++ b/internal/xds/translator/cluster.go @@ -206,6 +206,7 @@ func buildXdsHealthCheck(healthcheck *ir.ActiveHealthCheck) []*corev3.HealthChec } if healthcheck.HTTP != nil { httpChecker := &corev3.HealthCheck_HttpHealthCheck{ + Host: healthcheck.HTTP.Host, Path: healthcheck.HTTP.Path, } if healthcheck.HTTP.Method != nil { diff --git a/internal/xds/translator/extauth.go b/internal/xds/translator/extauth.go index 069aac77eb7..97d680e0fa4 100644 --- a/internal/xds/translator/extauth.go +++ b/internal/xds/translator/extauth.go @@ -137,9 +137,13 @@ func httpService(http *ir.HTTPExtAuthService) *extauthv3.HttpService { var ( uri string headersToBackend []*matcherv3.StringMatcher - service = new(extauthv3.HttpService) + service *extauthv3.HttpService ) + service = &extauthv3.HttpService{ + PathPrefix: http.Path, + } + u := url.URL{ // scheme should be decided by the TLS setting, but we don't have that info now. // It's safe to set it to http because the ext auth filter doesn't use the diff --git a/internal/xds/translator/httpfilters.go b/internal/xds/translator/httpfilters.go index 738e5ee9480..c77cc8097ed 100644 --- a/internal/xds/translator/httpfilters.go +++ b/internal/xds/translator/httpfilters.go @@ -49,6 +49,8 @@ func registerHTTPFilter(filter httpFilter) { // always se their own native per-route configuration. type httpFilter interface { // patchHCM patches the HttpConnectionManager with the filter. + // Note: this method may be called multiple times for the same filter, please + // make sure to avoid duplicate additions of the same filter. patchHCM(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTPListener) error // patchRoute patches the provide Route with a filter's Route level configuration. @@ -165,9 +167,18 @@ func (t *Translator) patchHCMWithFilters( // rate limit server configuration. t.patchHCMWithRateLimit(mgr, irListener) - // Add the router filter - headerSettings := ptr.Deref(irListener.Headers, ir.HeaderSettings{}) - mgr.HttpFilters = append(mgr.HttpFilters, filters.GenerateRouterFilter(headerSettings.EnableEnvoyHeaders)) + // Add the router filter if it doesn't exist. + hasRouter := false + for _, filter := range mgr.HttpFilters { + if filter.Name == wellknown.Router { + hasRouter = true + break + } + } + if !hasRouter { + headerSettings := ptr.Deref(irListener.Headers, ir.HeaderSettings{}) + mgr.HttpFilters = append(mgr.HttpFilters, filters.GenerateRouterFilter(headerSettings.EnableEnvoyHeaders)) + } // Sort the filters in the correct order. mgr.HttpFilters = sortHTTPFilters(mgr.HttpFilters) diff --git a/internal/xds/translator/ratelimit.go b/internal/xds/translator/ratelimit.go index 1a3e4ee5306..6fb3e2d86db 100644 --- a/internal/xds/translator/ratelimit.go +++ b/internal/xds/translator/ratelimit.go @@ -268,7 +268,16 @@ func GetRateLimitServiceConfigStr(pbCfg *rlsconfv3.RateLimitConfig) (string, err enc.SetIndent(2) // Translate pb config to yaml yamlRoot := config.ConfigXdsProtoToYaml(pbCfg) - err := enc.Encode(*yamlRoot) + rateLimitConfig := &struct { + Name string + Domain string + Descriptors []config.YamlDescriptor + }{ + Name: pbCfg.Name, + Domain: yamlRoot.Domain, + Descriptors: yamlRoot.Descriptors, + } + err := enc.Encode(rateLimitConfig) return buf.String(), err } @@ -306,8 +315,10 @@ func BuildRateLimitServiceConfig(irListener *ir.HTTPListener) *rlsconfv3.RateLim return nil } + domain := getRateLimitDomain(irListener) return &rlsconfv3.RateLimitConfig{ - Domain: getRateLimitDomain(irListener), + Name: domain, + Domain: domain, Descriptors: pbDescriptors, } } diff --git a/internal/xds/translator/route.go b/internal/xds/translator/route.go index 22654bc8c45..fb894d66105 100644 --- a/internal/xds/translator/route.go +++ b/internal/xds/translator/route.go @@ -52,13 +52,23 @@ func buildXdsRoute(httpRoute *ir.HTTPRoute) (*routev3.Route, error) { case httpRoute.DirectResponse != nil: router.Action = &routev3.Route_DirectResponse{DirectResponse: buildXdsDirectResponseAction(httpRoute.DirectResponse)} case httpRoute.Redirect != nil: - router.Action = &routev3.Route_Redirect{Redirect: buildXdsRedirectAction(httpRoute.Redirect)} + router.Action = &routev3.Route_Redirect{Redirect: buildXdsRedirectAction(httpRoute)} case httpRoute.URLRewrite != nil: routeAction := buildXdsURLRewriteAction(httpRoute.Destination.Name, httpRoute.URLRewrite, httpRoute.PathMatch) if httpRoute.Mirrors != nil { routeAction.RequestMirrorPolicies = buildXdsRequestMirrorPolicies(httpRoute.Mirrors) } + if !httpRoute.IsHTTP2 { + // Allow websocket upgrades for HTTP 1.1 + // Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism + routeAction.UpgradeConfigs = []*routev3.RouteAction_UpgradeConfig{ + { + UpgradeType: "websocket", + }, + } + } + router.Action = &routev3.Route_Route{Route: routeAction} default: var routeAction *routev3.RouteAction @@ -269,8 +279,11 @@ func idleTimeout(httpRoute *ir.HTTPRoute) *durationpb.Duration { return nil } -func buildXdsRedirectAction(redirection *ir.Redirect) *routev3.RedirectAction { - routeAction := &routev3.RedirectAction{} +func buildXdsRedirectAction(httpRoute *ir.HTTPRoute) *routev3.RedirectAction { + var ( + redirection = httpRoute.Redirect + routeAction = &routev3.RedirectAction{} + ) if redirection.Scheme != nil { routeAction.SchemeRewriteSpecifier = &routev3.RedirectAction_SchemeRedirect{ @@ -283,8 +296,14 @@ func buildXdsRedirectAction(redirection *ir.Redirect) *routev3.RedirectAction { PathRedirect: *redirection.Path.FullReplace, } } else if redirection.Path.PrefixMatchReplace != nil { - routeAction.PathRewriteSpecifier = &routev3.RedirectAction_PrefixRewrite{ - PrefixRewrite: *redirection.Path.PrefixMatchReplace, + if useRegexRewriteForPrefixMatchReplace(httpRoute.PathMatch, *redirection.Path.PrefixMatchReplace) { + routeAction.PathRewriteSpecifier = &routev3.RedirectAction_RegexRewrite{ + RegexRewrite: prefix2RegexRewrite(*httpRoute.PathMatch.Prefix), + } + } else { + routeAction.PathRewriteSpecifier = &routev3.RedirectAction_PrefixRewrite{ + PrefixRewrite: *redirection.Path.PrefixMatchReplace, + } } } } @@ -303,6 +322,24 @@ func buildXdsRedirectAction(redirection *ir.Redirect) *routev3.RedirectAction { return routeAction } +// useRegexRewriteForPrefixMatchReplace checks if the regex rewrite should be used for prefix match replace +// due to the issue with Envoy not handling the case of "//" when the replace string is "/". +// See: https://github.com/envoyproxy/envoy/issues/26055 +func useRegexRewriteForPrefixMatchReplace(pathMatch *ir.StringMatch, prefixMatchReplace string) bool { + return pathMatch != nil && + pathMatch.Prefix != nil && + (prefixMatchReplace == "" || prefixMatchReplace == "/") +} + +func prefix2RegexRewrite(prefix string) *matcherv3.RegexMatchAndSubstitute { + return &matcherv3.RegexMatchAndSubstitute{ + Pattern: &matcherv3.RegexMatcher{ + Regex: "^" + prefix + `\/*`, + }, + Substitution: "/", + } +} + func buildXdsURLRewriteAction(destName string, urlRewrite *ir.URLRewrite, pathMatch *ir.StringMatch) *routev3.RouteAction { routeAction := &routev3.RouteAction{ ClusterSpecifier: &routev3.RouteAction_Cluster{ @@ -323,14 +360,8 @@ func buildXdsURLRewriteAction(destName string, urlRewrite *ir.URLRewrite, pathMa // An empty replace string does not seem to solve the issue so we are using // a regex match and replace instead // Remove this workaround once https://github.com/envoyproxy/envoy/issues/26055 is fixed - if pathMatch != nil && pathMatch.Prefix != nil && - (*urlRewrite.Path.PrefixMatchReplace == "" || *urlRewrite.Path.PrefixMatchReplace == "/") { - routeAction.RegexRewrite = &matcherv3.RegexMatchAndSubstitute{ - Pattern: &matcherv3.RegexMatcher{ - Regex: "^" + *pathMatch.Prefix + `\/*`, - }, - Substitution: "/", - } + if useRegexRewriteForPrefixMatchReplace(pathMatch, *urlRewrite.Path.PrefixMatchReplace) { + routeAction.RegexRewrite = prefix2RegexRewrite(*pathMatch.Prefix) } else { routeAction.PrefixRewrite = *urlRewrite.Path.PrefixMatchReplace } diff --git a/internal/xds/translator/testdata/in/xds-ir/health-check.yaml b/internal/xds/translator/testdata/in/xds-ir/health-check.yaml index ca5f9c0a1c8..a767bdab208 100644 --- a/internal/xds/translator/testdata/in/xds-ir/health-check.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/health-check.yaml @@ -17,6 +17,7 @@ http: unhealthyThreshold: 3 healthyThreshold: 1 http: + host: "*" path: "/healthz" expectedResponse: text: "ok" @@ -46,6 +47,7 @@ http: unhealthyThreshold: 3 healthyThreshold: 3 http: + host: "*" path: "/healthz" expectedResponse: binary: "cG9uZw==" diff --git a/internal/xds/translator/testdata/in/xds-ir/http-route-redirect.yaml b/internal/xds/translator/testdata/in/xds-ir/http-route-redirect.yaml index 1aadcbb76c7..36599609deb 100644 --- a/internal/xds/translator/testdata/in/xds-ir/http-route-redirect.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/http-route-redirect.yaml @@ -9,7 +9,7 @@ http: mergeSlashes: true escapedSlashesAction: UnescapeAndRedirect routes: - - name: "redirect-route" + - name: "redirect-route-1" hostname: "*" destination: name: "redirect-route-dest" @@ -24,3 +24,29 @@ http: port: 8443 path: prefixMatchReplace: /redirected + - name: "redirect-route-2" + hostname: "*" + pathMatch: + prefix: "/redirect" + destination: + name: "redirect-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 + redirect: + path: + prefixMatchReplace: / + - name: "redirect-route-3" + hostname: "*" + pathMatch: + prefix: "/redirect/" + destination: + name: "redirect-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 + redirect: + path: + prefixMatchReplace: / diff --git a/internal/xds/translator/testdata/in/xds-ir/multiple-listeners-same-port-with-different-filters.yaml b/internal/xds/translator/testdata/in/xds-ir/multiple-listeners-same-port-with-different-filters.yaml new file mode 100644 index 00000000000..bec261bee5b --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/multiple-listeners-same-port-with-different-filters.yaml @@ -0,0 +1,119 @@ +# This is a test file for multiple Gateway HTTP listeners on the same port with different filters. +# These HTTP listeners should be merged into a single HTTP connection manager, +# and the filters should be merged into the DefaultFilterChain of the HTTP connection manager. +http: + - name: default/gateway-1/http + address: 0.0.0.0 + hostnames: + - 'www.foo.com' + isHTTP2: false + http3: + quicPort: 443 + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - name: httproute/default/httproute-1/rule/0/match/0/www_foo_com + hostname: www.foo.com + isHTTP2: false + pathMatch: + distinct: false + name: "" + prefix: /foo1 + backendWeights: + invalid: 0 + valid: 0 + destination: + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 192.168.1.1 + port: 8080 + protocol: HTTP + weight: 1 + basicAuth: + name: securitypolicy/default/policy-for-http-route-1 + users: "dXNlcjE6e1NIQX10RVNzQm1FL3lOWTNsYjZhMEw2dlZRRVpOcXc9CnVzZXIyOntTSEF9RUo5TFBGRFhzTjl5blNtYnh2anA3NUJtbHg4PQo=" + - name: httproute/default/httproute-2/rule/0/match/0/www_foo_com + hostname: www.foo.com + isHTTP2: false + pathMatch: + distinct: false + name: "" + prefix: /foo2 + backendWeights: + invalid: 0 + valid: 0 + destination: + name: httproute/default/httproute-2/rule/0 + settings: + - addressType: IP + endpoints: + - host: 192.168.1.2 + port: 8080 + protocol: HTTP + weight: 1 + extAuth: + name: securitypolicy/default/policy-for-http-route-2 + failOpen: true + http: + authority: http-backend.envoy-gateway:80 + destination: + name: securitypolicy/default/policy-for-http-route-2/envoy-gateway/http-backend + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 80 + protocol: HTTP + weight: 1 + headersToBackend: + - header1 + - header2 + path: /auth + - name: default/gateway-2/http + address: 0.0.0.0 + hostnames: + - 'www.bar.com' + isHTTP2: false + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - name: httproute/default/httproute-3/rule/0/match/0/www_bar_com + hostname: www.bar.com + isHTTP2: false + pathMatch: + distinct: false + name: "" + prefix: /bar + backendWeights: + invalid: 0 + valid: 0 + destination: + name: httproute/default/httproute-3/rule/0 + settings: + - addressType: IP + endpoints: + - host: 192.168.1.3 + port: 8080 + protocol: HTTP + weight: 1 + oidc: + name: securitypolicy/default/policy-for-gateway-2 + clientID: client.oauth.foo.com + clientSecret: Y2xpZW50MTpzZWNyZXQK + provider: + authorizationEndpoint: https://oauth.foo.com/oauth2/v2/auth + tokenEndpoint: https://oauth.foo.com/token + scopes: + - openid + - email + - profile + redirectURL: "https://www.example.com/foo/oauth2/callback" + redirectPath: "/foo/oauth2/callback" + logoutPath: "/foo/logout" + cookieSuffix: 5F93C2E4 diff --git a/internal/xds/translator/testdata/out/ratelimit-config/distinct-match.yaml b/internal/xds/translator/testdata/out/ratelimit-config/distinct-match.yaml index 0ad9f90e736..d418c71595e 100644 --- a/internal/xds/translator/testdata/out/ratelimit-config/distinct-match.yaml +++ b/internal/xds/translator/testdata/out/ratelimit-config/distinct-match.yaml @@ -1,3 +1,4 @@ +name: first-listener domain: first-listener descriptors: - key: first-route diff --git a/internal/xds/translator/testdata/out/ratelimit-config/distinct-remote-address-match.yaml b/internal/xds/translator/testdata/out/ratelimit-config/distinct-remote-address-match.yaml index c14867776b2..7f50507431f 100644 --- a/internal/xds/translator/testdata/out/ratelimit-config/distinct-remote-address-match.yaml +++ b/internal/xds/translator/testdata/out/ratelimit-config/distinct-remote-address-match.yaml @@ -1,3 +1,4 @@ +name: first-listener domain: first-listener descriptors: - key: first-route diff --git a/internal/xds/translator/testdata/out/ratelimit-config/empty-header-matches.yaml b/internal/xds/translator/testdata/out/ratelimit-config/empty-header-matches.yaml index db1ce990b00..f00ce09876f 100644 --- a/internal/xds/translator/testdata/out/ratelimit-config/empty-header-matches.yaml +++ b/internal/xds/translator/testdata/out/ratelimit-config/empty-header-matches.yaml @@ -1,3 +1,4 @@ +name: first-listener domain: first-listener descriptors: - key: first-route diff --git a/internal/xds/translator/testdata/out/ratelimit-config/masked-remote-address-match.yaml b/internal/xds/translator/testdata/out/ratelimit-config/masked-remote-address-match.yaml index e997acedcd6..d15316a3e41 100644 --- a/internal/xds/translator/testdata/out/ratelimit-config/masked-remote-address-match.yaml +++ b/internal/xds/translator/testdata/out/ratelimit-config/masked-remote-address-match.yaml @@ -1,3 +1,4 @@ +name: first-listener domain: first-listener descriptors: - key: first-route diff --git a/internal/xds/translator/testdata/out/ratelimit-config/multiple-masked-remote-address-match-with-same-cidr.yaml b/internal/xds/translator/testdata/out/ratelimit-config/multiple-masked-remote-address-match-with-same-cidr.yaml index 931e235d362..22db357e4fb 100644 --- a/internal/xds/translator/testdata/out/ratelimit-config/multiple-masked-remote-address-match-with-same-cidr.yaml +++ b/internal/xds/translator/testdata/out/ratelimit-config/multiple-masked-remote-address-match-with-same-cidr.yaml @@ -1,3 +1,4 @@ +name: first-listener domain: first-listener descriptors: - key: first-route diff --git a/internal/xds/translator/testdata/out/ratelimit-config/multiple-matches.yaml b/internal/xds/translator/testdata/out/ratelimit-config/multiple-matches.yaml index b1ba8190ced..69c29c429aa 100644 --- a/internal/xds/translator/testdata/out/ratelimit-config/multiple-matches.yaml +++ b/internal/xds/translator/testdata/out/ratelimit-config/multiple-matches.yaml @@ -1,3 +1,4 @@ +name: first-listener domain: first-listener descriptors: - key: first-route diff --git a/internal/xds/translator/testdata/out/ratelimit-config/multiple-routes.yaml b/internal/xds/translator/testdata/out/ratelimit-config/multiple-routes.yaml index 81dc4dea6ef..f2068677be6 100644 --- a/internal/xds/translator/testdata/out/ratelimit-config/multiple-routes.yaml +++ b/internal/xds/translator/testdata/out/ratelimit-config/multiple-routes.yaml @@ -1,3 +1,4 @@ +name: first-listener domain: first-listener descriptors: - key: first-route diff --git a/internal/xds/translator/testdata/out/ratelimit-config/multiple-rules.yaml b/internal/xds/translator/testdata/out/ratelimit-config/multiple-rules.yaml index ebe7d02ca8a..e3332959caa 100644 --- a/internal/xds/translator/testdata/out/ratelimit-config/multiple-rules.yaml +++ b/internal/xds/translator/testdata/out/ratelimit-config/multiple-rules.yaml @@ -1,3 +1,4 @@ +name: first-listener domain: first-listener descriptors: - key: first-route diff --git a/internal/xds/translator/testdata/out/ratelimit-config/value-match.yaml b/internal/xds/translator/testdata/out/ratelimit-config/value-match.yaml index 08078af1c67..fa71116ba75 100644 --- a/internal/xds/translator/testdata/out/ratelimit-config/value-match.yaml +++ b/internal/xds/translator/testdata/out/ratelimit-config/value-match.yaml @@ -1,3 +1,4 @@ +name: first-listener domain: first-listener descriptors: - key: first-route diff --git a/internal/xds/translator/testdata/out/xds-ir/ext-auth.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/ext-auth.listeners.yaml index 52735036294..260f772fb04 100644 --- a/internal/xds/translator/testdata/out/xds-ir/ext-auth.listeners.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/ext-auth.listeners.yaml @@ -24,6 +24,7 @@ patterns: - exact: header1 - exact: header2 + pathPrefix: /auth serverUri: cluster: securitypolicy/default/policy-for-first-route/http-backend timeout: 10s diff --git a/internal/xds/translator/testdata/out/xds-ir/health-check.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/health-check.clusters.yaml index 8c076fbdb87..b789b876c3c 100644 --- a/internal/xds/translator/testdata/out/xds-ir/health-check.clusters.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/health-check.clusters.yaml @@ -18,6 +18,7 @@ start: "200" - end: "301" start: "300" + host: '*' path: /healthz receive: - text: 6f6b @@ -53,6 +54,7 @@ expectedStatuses: - end: "202" start: "200" + host: '*' path: /healthz receive: - binary: cG9uZw== diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-redirect.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-redirect.routes.yaml index 7cac95f1009..d2c46435ee3 100644 --- a/internal/xds/translator/testdata/out/xds-ir/http-route-redirect.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-redirect.routes.yaml @@ -7,10 +7,26 @@ routes: - match: prefix: / - name: redirect-route + name: redirect-route-1 redirect: hostRedirect: redirected.com portRedirect: 8443 prefixRewrite: /redirected responseCode: FOUND schemeRedirect: https + - match: + pathSeparatedPrefix: /redirect + name: redirect-route-2 + redirect: + regexRewrite: + pattern: + regex: ^/redirect\/* + substitution: / + - match: + pathSeparatedPrefix: /redirect + name: redirect-route-3 + redirect: + regexRewrite: + pattern: + regex: ^/redirect/\/* + substitution: / diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-root-path-url-prefix.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-root-path-url-prefix.routes.yaml index 2bf01099ad2..d5a0bd98994 100644 --- a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-root-path-url-prefix.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-root-path-url-prefix.routes.yaml @@ -18,3 +18,5 @@ pattern: regex: ^/origin/\/* substitution: / + upgradeConfigs: + - upgradeType: websocket diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-fullpath.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-fullpath.routes.yaml index f1f30bbbffc..f8b81712dae 100644 --- a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-fullpath.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-fullpath.routes.yaml @@ -14,3 +14,5 @@ pattern: regex: /.+ substitution: /rewrite + upgradeConfigs: + - upgradeType: websocket diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-host.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-host.routes.yaml index 7de20ab0305..680a67404ee 100644 --- a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-host.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-host.routes.yaml @@ -17,3 +17,5 @@ cluster: rewrite-route-dest hostRewriteLiteral: 3.3.3.3 prefixRewrite: /rewrite + upgradeConfigs: + - upgradeType: websocket diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix.routes.yaml index a5f47ebab54..84bc70f04bd 100644 --- a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix.routes.yaml @@ -15,3 +15,5 @@ route: cluster: rewrite-route-dest prefixRewrite: /rewrite + upgradeConfigs: + - upgradeType: websocket diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-with-tls-system-truststore.secrets.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-with-tls-system-truststore.secrets.yaml new file mode 100755 index 00000000000..fe51488c706 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-with-tls-system-truststore.secrets.yaml @@ -0,0 +1 @@ +[] diff --git a/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port-with-different-filters.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port-with-different-filters.clusters.yaml new file mode 100755 index 00000000000..2b9b567cf39 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port-with-different-filters.clusters.yaml @@ -0,0 +1,104 @@ +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: httproute/default/httproute-1/rule/0 + lbPolicy: LEAST_REQUEST + name: httproute/default/httproute-1/rule/0 + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: httproute/default/httproute-2/rule/0 + lbPolicy: LEAST_REQUEST + name: httproute/default/httproute-2/rule/0 + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: securitypolicy/default/policy-for-http-route-2/envoy-gateway/http-backend + lbPolicy: LEAST_REQUEST + name: securitypolicy/default/policy-for-http-route-2/envoy-gateway/http-backend + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: httproute/default/httproute-3/rule/0 + lbPolicy: LEAST_REQUEST + name: httproute/default/httproute-3/rule/0 + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + dnsRefreshRate: 30s + lbPolicy: LEAST_REQUEST + loadAssignment: + clusterName: oauth_foo_com_443 + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: oauth.foo.com + portValue: 443 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: oauth_foo_com_443/backend/0 + name: oauth_foo_com_443 + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + respectDnsTtl: true + transportSocket: + name: envoy.transport_sockets.tls + typedConfig: + '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + commonTlsContext: + validationContext: + trustedCa: + filename: /etc/ssl/certs/ca-certificates.crt + sni: oauth.foo.com + type: STRICT_DNS diff --git a/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port-with-different-filters.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port-with-different-filters.endpoints.yaml new file mode 100755 index 00000000000..1f4fcb70daf --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port-with-different-filters.endpoints.yaml @@ -0,0 +1,48 @@ +- clusterName: httproute/default/httproute-1/rule/0 + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 192.168.1.1 + portValue: 8080 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: httproute/default/httproute-1/rule/0/backend/0 +- clusterName: httproute/default/httproute-2/rule/0 + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 192.168.1.2 + portValue: 8080 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: httproute/default/httproute-2/rule/0/backend/0 +- clusterName: securitypolicy/default/policy-for-http-route-2/envoy-gateway/http-backend + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 7.7.7.7 + portValue: 80 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: securitypolicy/default/policy-for-http-route-2/envoy-gateway/http-backend/backend/0 +- clusterName: httproute/default/httproute-3/rule/0 + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 192.168.1.3 + portValue: 8080 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: httproute/default/httproute-3/rule/0/backend/0 diff --git a/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port-with-different-filters.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port-with-different-filters.listeners.yaml new file mode 100755 index 00000000000..40696d89d08 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port-with-different-filters.listeners.yaml @@ -0,0 +1,158 @@ +- address: + socketAddress: + address: 0.0.0.0 + portValue: 10080 + protocol: UDP + defaultFilterChain: + filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + codecType: HTTP3 + commonHttpProtocolOptions: + headersWithUnderscoresAction: REJECT_REQUEST + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 + maxConcurrentStreams: 100 + http3ProtocolOptions: {} + httpFilters: + - disabled: true + name: envoy.filters.http.ext_authz_httproute/default/httproute-2/rule/0/match/0/www_foo_com + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz + httpService: + authorizationResponse: + allowedUpstreamHeaders: + patterns: + - exact: header1 + - exact: header2 + pathPrefix: /auth + serverUri: + cluster: securitypolicy/default/policy-for-http-route-2/envoy-gateway/http-backend + timeout: 10s + uri: http://http-backend.envoy-gateway:80/auth + transportApiVersion: V3 + - disabled: true + name: envoy.filters.http.basic_auth_httproute/default/httproute-1/rule/0/match/0/www_foo_com + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.basic_auth.v3.BasicAuth + users: + inlineBytes: dXNlcjE6e1NIQX10RVNzQm1FL3lOWTNsYjZhMEw2dlZRRVpOcXc9CnVzZXIyOntTSEF9RUo5TFBGRFhzTjl5blNtYnh2anA3NUJtbHg4PQo= + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + suppressEnvoyHeaders: true + mergeSlashes: true + normalizePath: true + pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: default/gateway-1/http + serverHeaderTransformation: PASS_THROUGH + statPrefix: http + useRemoteAddress: true + drainType: MODIFY_ONLY + name: default/gateway-1/http-quic + udpListenerConfig: + downstreamSocketConfig: {} + quicOptions: {} +- address: + socketAddress: + address: 0.0.0.0 + portValue: 10080 + defaultFilterChain: + filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + commonHttpProtocolOptions: + headersWithUnderscoresAction: REJECT_REQUEST + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 + maxConcurrentStreams: 100 + httpFilters: + - disabled: true + name: envoy.filters.http.ext_authz_httproute/default/httproute-2/rule/0/match/0/www_foo_com + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz + httpService: + authorizationResponse: + allowedUpstreamHeaders: + patterns: + - exact: header1 + - exact: header2 + pathPrefix: /auth + serverUri: + cluster: securitypolicy/default/policy-for-http-route-2/envoy-gateway/http-backend + timeout: 10s + uri: http://http-backend.envoy-gateway:80/auth + transportApiVersion: V3 + - disabled: true + name: envoy.filters.http.basic_auth_httproute/default/httproute-1/rule/0/match/0/www_foo_com + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.basic_auth.v3.BasicAuth + users: + inlineBytes: dXNlcjE6e1NIQX10RVNzQm1FL3lOWTNsYjZhMEw2dlZRRVpOcXc9CnVzZXIyOntTSEF9RUo5TFBGRFhzTjl5blNtYnh2anA3NUJtbHg4PQo= + - disabled: true + name: envoy.filters.http.oauth2_httproute/default/httproute-3/rule/0/match/0/www_bar_com + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.oauth2.v3.OAuth2 + config: + authScopes: + - openid + - email + - profile + authType: BASIC_AUTH + authorizationEndpoint: https://oauth.foo.com/oauth2/v2/auth + credentials: + clientId: client.oauth.foo.com + cookieNames: + bearerToken: BearerToken-5F93C2E4 + idToken: IdToken-5F93C2E4 + oauthExpires: OauthExpires-5F93C2E4 + oauthHmac: OauthHMAC-5F93C2E4 + refreshToken: RefreshToken-5F93C2E4 + hmacSecret: + name: httproute/default/httproute-3/rule/0/match/0/www_bar_com/oauth2/hmac_secret + sdsConfig: + ads: {} + resourceApiVersion: V3 + tokenSecret: + name: httproute/default/httproute-3/rule/0/match/0/www_bar_com/oauth2/client_secret + sdsConfig: + ads: {} + resourceApiVersion: V3 + forwardBearerToken: true + redirectPathMatcher: + path: + exact: /foo/oauth2/callback + redirectUri: https://www.example.com/foo/oauth2/callback + signoutPath: + path: + exact: /foo/logout + tokenEndpoint: + cluster: oauth_foo_com_443 + timeout: 10s + uri: https://oauth.foo.com/token + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + suppressEnvoyHeaders: true + mergeSlashes: true + normalizePath: true + pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: default/gateway-1/http + serverHeaderTransformation: PASS_THROUGH + statPrefix: http + useRemoteAddress: true + drainType: MODIFY_ONLY + name: default/gateway-1/http + perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port-with-different-filters.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port-with-different-filters.routes.yaml new file mode 100755 index 00000000000..1a4ec4068d3 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port-with-different-filters.routes.yaml @@ -0,0 +1,54 @@ +- ignorePortInHostMatching: true + name: default/gateway-1/http + virtualHosts: + - domains: + - www.foo.com + name: default/gateway-1/http/www_foo_com + routes: + - match: + pathSeparatedPrefix: /foo1 + name: httproute/default/httproute-1/rule/0/match/0/www_foo_com + responseHeadersToAdd: + - append: true + header: + key: alt-svc + value: h3=":443"; ma=86400 + route: + cluster: httproute/default/httproute-1/rule/0 + upgradeConfigs: + - upgradeType: websocket + typedPerFilterConfig: + envoy.filters.http.basic_auth_httproute/default/httproute-1/rule/0/match/0/www_foo_com: + '@type': type.googleapis.com/envoy.config.route.v3.FilterConfig + config: {} + - match: + pathSeparatedPrefix: /foo2 + name: httproute/default/httproute-2/rule/0/match/0/www_foo_com + responseHeadersToAdd: + - append: true + header: + key: alt-svc + value: h3=":443"; ma=86400 + route: + cluster: httproute/default/httproute-2/rule/0 + upgradeConfigs: + - upgradeType: websocket + typedPerFilterConfig: + envoy.filters.http.ext_authz_httproute/default/httproute-2/rule/0/match/0/www_foo_com: + '@type': type.googleapis.com/envoy.config.route.v3.FilterConfig + config: {} + - domains: + - www.bar.com + name: default/gateway-2/http/www_bar_com + routes: + - match: + pathSeparatedPrefix: /bar + name: httproute/default/httproute-3/rule/0/match/0/www_bar_com + route: + cluster: httproute/default/httproute-3/rule/0 + upgradeConfigs: + - upgradeType: websocket + typedPerFilterConfig: + envoy.filters.http.oauth2_httproute/default/httproute-3/rule/0/match/0/www_bar_com: + '@type': type.googleapis.com/envoy.config.route.v3.FilterConfig + config: {} diff --git a/internal/xds/translator/testdata/out/xds-ir/tracing.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/tracing.listeners.yaml index a69d0bb9e44..c8277f1e190 100644 --- a/internal/xds/translator/testdata/out/xds-ir/tracing.listeners.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/tracing.listeners.yaml @@ -56,6 +56,7 @@ serviceName: fake-name.fake-ns randomSampling: value: 90 + spawnUpstreamSpan: true useRemoteAddress: true drainType: MODIFY_ONLY name: first-listener diff --git a/internal/xds/translator/tracing.go b/internal/xds/translator/tracing.go index 60a66bec79b..e0ace0d49cc 100644 --- a/internal/xds/translator/tracing.go +++ b/internal/xds/translator/tracing.go @@ -15,6 +15,7 @@ import ( hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" tracingtype "github.com/envoyproxy/go-control-plane/envoy/type/tracing/v3" xdstype "github.com/envoyproxy/go-control-plane/envoy/type/v3" + "google.golang.org/protobuf/types/known/wrapperspb" "k8s.io/utils/ptr" egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" @@ -113,7 +114,8 @@ func buildHCMTracing(tracing *ir.Tracing) (*hcm.HttpConnectionManager_Tracing, e TypedConfig: ocAny, }, }, - CustomTags: tags, + CustomTags: tags, + SpawnUpstreamSpan: wrapperspb.Bool(true), }, nil } diff --git a/internal/xds/translator/translator.go b/internal/xds/translator/translator.go index d93361337eb..f00d863cf4f 100644 --- a/internal/xds/translator/translator.go +++ b/internal/xds/translator/translator.go @@ -16,15 +16,18 @@ import ( endpointv3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3" listenerv3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" tlsv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" matcherv3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" resourcev3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3" "github.com/envoyproxy/go-control-plane/pkg/wellknown" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/anypb" "google.golang.org/protobuf/types/known/wrapperspb" extensionTypes "github.com/envoyproxy/gateway/internal/extension/types" "github.com/envoyproxy/gateway/internal/ir" + "github.com/envoyproxy/gateway/internal/utils/protocov" "github.com/envoyproxy/gateway/internal/xds/types" ) @@ -170,6 +173,20 @@ func (t *Translator) processHTTPListenerXdsTranslation( return err } } + } else { + // When the DefaultFilterChain is shared by multiple Gateway HTTP + // Listeners, we need to add the HTTP filters associated with the + // HTTPListener to the HCM if they have not yet been added. + if err := t.addHTTPFiltersToHCM(xdsListener.DefaultFilterChain, httpListener); err != nil { + errs = errors.Join(errs, err) + continue + } + if enabledHTTP3 { + if err := t.addHTTPFiltersToHCM(quicXDSListener.DefaultFilterChain, httpListener); err != nil { + errs = errors.Join(errs, err) + continue + } + } } // Create a route config if we have not found one yet @@ -319,6 +336,52 @@ func (t *Translator) processHTTPListenerXdsTranslation( return errs } +func (t *Translator) addHTTPFiltersToHCM(filterChain *listenerv3.FilterChain, httpListener *ir.HTTPListener) error { + var ( + hcm *hcmv3.HttpConnectionManager + err error + ) + + if hcm, err = findHCMinFilterChain(filterChain); err != nil { + return err // should not happen + } + + // Add http filters to the HCM if they have not yet been added. + if err = t.patchHCMWithFilters(hcm, httpListener); err != nil { + return err + } + + for i, filter := range filterChain.Filters { + if filter.Name == wellknown.HTTPConnectionManager { + var mgrAny *anypb.Any + if mgrAny, err = protocov.ToAnyWithError(hcm); err != nil { + return err + } + + filterChain.Filters[i] = &listenerv3.Filter{ + Name: wellknown.HTTPConnectionManager, + ConfigType: &listenerv3.Filter_TypedConfig{ + TypedConfig: mgrAny, + }, + } + } + } + return nil +} + +func findHCMinFilterChain(filterChain *listenerv3.FilterChain) (*hcmv3.HttpConnectionManager, error) { + for _, filter := range filterChain.Filters { + if filter.Name == wellknown.HTTPConnectionManager { + hcm := &hcmv3.HttpConnectionManager{} + if err := anypb.UnmarshalTo(filter.GetTypedConfig(), hcm, proto.UnmarshalOptions{}); err != nil { + return nil, err + } + return hcm, nil + } + } + return nil, errors.New("http connection manager not found") +} + func buildHTTP3AltSvcHeader(port int) *corev3.HeaderValueOption { return &corev3.HeaderValueOption{ Append: &wrapperspb.BoolValue{Value: true}, @@ -520,12 +583,14 @@ func processTLSSocket(tlsConfig *ir.TLSUpstreamConfig, tCtx *types.ResourceVersi if tlsConfig == nil { return nil, nil } - CaSecret := buildXdsUpstreamTLSCASecret(tlsConfig) - if CaSecret != nil { + // Create a secret for the CA certificate only if it's not using the system trust store + if !tlsConfig.UseSystemTrustStore { + CaSecret := buildXdsUpstreamTLSCASecret(tlsConfig) if err := tCtx.AddXdsResource(resourcev3.SecretType, CaSecret); err != nil { return nil, err } } + // for upstreamTLS , a fixed sni can be used. use auto_sni otherwise // https://www.envoyproxy.io/docs/envoy/latest/faq/configuration/sni#faq-how-to-setup-sni:~:text=For%20clusters%2C%20a,for%20trust%20anchor. tlsSocket, err := buildXdsUpstreamTLSSocketWthCert(tlsConfig) @@ -573,9 +638,12 @@ func addXdsCluster(tCtx *types.ResourceVersionTable, args *xdsClusterArgs) error xdsEndpoints := buildXdsClusterLoadAssignment(args.name, args.settings) for _, ds := range args.settings { if ds.TLS != nil { - secret := buildXdsUpstreamTLSCASecret(ds.TLS) - if err := tCtx.AddXdsResource(resourcev3.SecretType, secret); err != nil { - return err + // Create a secret for the CA certificate only if it's not using the system trust store + if !ds.TLS.UseSystemTrustStore { + secret := buildXdsUpstreamTLSCASecret(ds.TLS) + if err := tCtx.AddXdsResource(resourcev3.SecretType, secret); err != nil { + return err + } } } } @@ -601,6 +669,7 @@ const ( func buildXdsUpstreamTLSCASecret(tlsConfig *ir.TLSUpstreamConfig) *tlsv3.Secret { // Build the tls secret + // It's just a sanity check, we shouldn't call this function if the system trust store is used if tlsConfig.UseSystemTrustStore { return nil } diff --git a/internal/xds/translator/translator_test.go b/internal/xds/translator/translator_test.go index da7fef393ad..1e20462ca46 100644 --- a/internal/xds/translator/translator_test.go +++ b/internal/xds/translator/translator_test.go @@ -91,7 +91,8 @@ func TestTranslateXds(t *testing.T) { name: "http-route-dns-cluster", }, { - name: "http-route-with-tls-system-truststore", + name: "http-route-with-tls-system-truststore", + requireSecrets: true, }, { name: "http-route-with-tlsbundle", @@ -287,6 +288,9 @@ func TestTranslateXds(t *testing.T) { { name: "retry-partial-invalid", }, + { + name: "multiple-listeners-same-port-with-different-filters", + }, } for _, tc := range testCases { diff --git a/internal/xds/types/resourceversiontable.go b/internal/xds/types/resourceversiontable.go index bf0d5a865f6..672096e6ab7 100644 --- a/internal/xds/types/resourceversiontable.go +++ b/internal/xds/types/resourceversiontable.go @@ -76,6 +76,11 @@ func (t *ResourceVersionTable) GetXdsResources() XdsResources { } func (t *ResourceVersionTable) AddXdsResource(rType resourcev3.Type, xdsResource types.Resource) error { + // It's a sanity check to make sure the xdsResource is not nil + if xdsResource == nil { + return fmt.Errorf("xds resource is nil") + } + // Perform type switch to handle different types of xdsResource switch rType { case resourcev3.ListenerType: diff --git a/test/e2e/testdata/ratelimit-multiple-listeners.yaml b/test/e2e/testdata/ratelimit-multiple-listeners.yaml new file mode 100644 index 00000000000..33d789515a9 --- /dev/null +++ b/test/e2e/testdata/ratelimit-multiple-listeners.yaml @@ -0,0 +1,54 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: eg-rate-limit + namespace: gateway-conformance-infra +spec: + gatewayClassName: "{GATEWAY_CLASS_NAME}" + listeners: + - name: http-80 + protocol: HTTP + port: 80 + - name: http-8080 + protocol: HTTP + port: 8080 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: cidr-ratelimit + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: eg-rate-limit + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: infra-backend-v1 + port: 8080 +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: BackendTrafficPolicy +metadata: + name: ratelimit-all-ips + namespace: gateway-conformance-infra +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: cidr-ratelimit + namespace: gateway-conformance-infra + rateLimit: + type: Global + global: + rules: + - clientSelectors: + - sourceCIDR: + value: 0.0.0.0/0 + type: distinct + limit: + requests: 3 + unit: Hour diff --git a/test/e2e/testdata/redirect-replaceprefixmatch-slash.yaml b/test/e2e/testdata/redirect-replaceprefixmatch-slash.yaml new file mode 100644 index 00000000000..50d4d52a3dd --- /dev/null +++ b/test/e2e/testdata/redirect-replaceprefixmatch-slash.yaml @@ -0,0 +1,29 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: redirect-replaceprefixmatch-slash + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - matches: + - path: + type: PathPrefix + value: /api/foo/ + filters: + - requestRedirect: + path: + replacePrefixMatch: / + type: ReplacePrefixMatch + type: RequestRedirect + - matches: + - path: + type: PathPrefix + value: /api/bar + filters: + - requestRedirect: + path: + replacePrefixMatch: / + type: ReplacePrefixMatch + type: RequestRedirect diff --git a/test/e2e/tests/ratelimit.go b/test/e2e/tests/ratelimit.go index 4bfed9506d8..c02802abe1f 100644 --- a/test/e2e/tests/ratelimit.go +++ b/test/e2e/tests/ratelimit.go @@ -9,8 +9,11 @@ package tests import ( + "fmt" + "net" "testing" + "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/gateway-api/conformance/utils/http" "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" @@ -22,6 +25,7 @@ func init() { ConformanceTests = append(ConformanceTests, RateLimitCIDRMatchTest) ConformanceTests = append(ConformanceTests, RateLimitHeaderMatchTest) ConformanceTests = append(ConformanceTests, RateLimitBasedJwtClaimsTest) + ConformanceTests = append(ConformanceTests, RateLimitMultipleListenersTest) } var RateLimitCIDRMatchTest = suite.ConformanceTest{ @@ -285,6 +289,65 @@ var RateLimitBasedJwtClaimsTest = suite.ConformanceTest{ }, } +var RateLimitMultipleListenersTest = suite.ConformanceTest{ + ShortName: "RateLimitMultipleListeners", + Description: "Limit requests on multiple listeners", + Manifests: []string{"testdata/ratelimit-multiple-listeners.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + t.Run("block all ips on listener 80 and 8080", func(t *testing.T) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "cidr-ratelimit", Namespace: ns} + gwNN := types.NamespacedName{Name: "eg-rate-limit", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + gwIP, _, err := net.SplitHostPort(gwAddr) + require.NoError(t, err) + + gwPorts := []string{"80", "8080"} + for _, port := range gwPorts { + gwAddr = fmt.Sprintf("%s:%s", gwIP, port) + + ratelimitHeader := make(map[string]string) + expectOkResp := http.ExpectedResponse{ + Request: http.Request{ + Path: "/", + }, + Response: http.Response{ + StatusCode: 200, + Headers: ratelimitHeader, + }, + Namespace: ns, + } + expectOkResp.Response.Headers["X-Ratelimit-Limit"] = "3, 3;w=3600" + expectOkReq := http.MakeRequest(t, &expectOkResp, gwAddr, "HTTP", "http") + + expectLimitResp := http.ExpectedResponse{ + Request: http.Request{ + Path: "/", + }, + Response: http.Response{ + StatusCode: 429, + }, + Namespace: ns, + } + expectLimitReq := http.MakeRequest(t, &expectLimitResp, gwAddr, "HTTP", "http") + + // should just send exactly 4 requests, and expect 429 + + // keep sending requests till get 200 first, that will cost one 200 + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectOkResp) + + // fire the rest of requests + if err := GotExactExpectedResponse(t, 2, suite.RoundTripper, expectOkReq, expectOkResp); err != nil { + t.Errorf("failed to get expected response for the first three requests: %v", err) + } + if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, expectLimitReq, expectLimitResp); err != nil { + t.Errorf("failed to get expected response for the last (fourth) request: %v", err) + } + } + }) + }, +} + func GotExactExpectedResponse(t *testing.T, n int, r roundtripper.RoundTripper, req roundtripper.Request, resp http.ExpectedResponse) error { for i := 0; i < n; i++ { cReq, cRes, err := r.CaptureRoundTrip(req) diff --git a/test/e2e/tests/redirect_replaceprefixmatch_slash.go b/test/e2e/tests/redirect_replaceprefixmatch_slash.go new file mode 100644 index 00000000000..3dcc4e90873 --- /dev/null +++ b/test/e2e/tests/redirect_replaceprefixmatch_slash.go @@ -0,0 +1,125 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +//go:build e2e +// +build e2e + +package tests + +import ( + "testing" + + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/gateway-api/conformance/utils/http" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/suite" +) + +func init() { + ConformanceTests = append(ConformanceTests, RedirectTrailingSlashTest) + +} + +// RedirectTrailingSlashTest tests that only one slash in the redirect URL +// See https://github.com/envoyproxy/gateway/issues/2976 +var RedirectTrailingSlashTest = suite.ConformanceTest{ + ShortName: "RedirectTrailingSlash", + Description: "Test that only one slash in the redirect URL", + Manifests: []string{"testdata/redirect-replaceprefixmatch-slash.yaml"}, + + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + testCases := []struct { + name string + path string + statusCode int + expectedLocation string + }{ + // Test cases for the HTTPRoute match /api/foo/ + { + name: "match: /api/foo/, request: /api/foo/redirect", + path: "/api/foo/redirect", + statusCode: 302, + expectedLocation: "/redirect", + }, + { + name: "match: /api/foo/, request: /api/foo/", + path: "/api/foo/", + statusCode: 302, + expectedLocation: "/", + }, + { + name: "match: /api/foo/, request: /api/foo", + path: "/api/foo", + statusCode: 302, + expectedLocation: "/", + }, + { + name: "match: /api/foo/, request: /api/foo-bar", + path: "/api/foo-bar", + statusCode: 404, + }, + + // Test cases for the HTTPRoute match /api/bar + { + name: "match: /api/bar, request: /api/bar/redirect", + path: "/api/bar/redirect", + statusCode: 302, + expectedLocation: "/redirect", + }, + { + name: "match: /api/bar, request: /api/bar/", + path: "/api/bar/", + statusCode: 302, + expectedLocation: "/", + }, + { + name: "match: /api/bar, request: /api/bar", + path: "/api/bar", + statusCode: 302, + expectedLocation: "/", + }, + { + name: "match: /api/bar, request: /api/bar-foo", + path: "/api/bar-foo", + statusCode: 404, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "redirect-replaceprefixmatch-slash", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Path: testCase.path, + UnfollowRedirect: true, + }, + Response: http.Response{ + StatusCode: testCase.statusCode, + }, + Namespace: ns, + } + if testCase.expectedLocation != "" { + expectedResponse.Response.Headers = map[string]string{ + "Location": testCase.expectedLocation, + } + } + + req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") + cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) + if err != nil { + t.Errorf("failed to get expected response: %v", err) + } + + if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil { + t.Errorf("failed to compare request and response: %v", err) + } + }) + } + }, +}