diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go index e7975c308ec5..4eb1c4d6ef9b 100644 --- a/internal/gatewayapi/backendtrafficpolicy.go +++ b/internal/gatewayapi/backendtrafficpolicy.go @@ -12,6 +12,7 @@ import ( "net" "sort" "strings" + "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -248,6 +249,7 @@ func (t *Translator) translateBackendTrafficPolicyForRoute(policy *egv1a1.Backen hc *ir.HealthCheck cb *ir.CircuitBreaker fi *ir.FaultInjection + to *ir.Timeout ) // Build IR @@ -266,6 +268,9 @@ func (t *Translator) translateBackendTrafficPolicyForRoute(policy *egv1a1.Backen if policy.Spec.CircuitBreaker != nil { cb = t.buildCircuitBreaker(policy) } + if policy.Spec.Timeout != nil { + to = t.buildTimeout(policy) + } if policy.Spec.FaultInjection != nil { fi = t.buildFaultInjection(policy) @@ -283,6 +288,7 @@ func (t *Translator) translateBackendTrafficPolicyForRoute(policy *egv1a1.Backen r.HealthCheck = hc r.CircuitBreaker = cb r.FaultInjection = fi + r.BackendTimeout = to } } } @@ -298,6 +304,7 @@ func (t *Translator) translateBackendTrafficPolicyForGateway(policy *egv1a1.Back hc *ir.HealthCheck cb *ir.CircuitBreaker fi *ir.FaultInjection + ct *ir.Timeout ) // Build IR @@ -319,6 +326,9 @@ func (t *Translator) translateBackendTrafficPolicyForGateway(policy *egv1a1.Back if policy.Spec.FaultInjection != nil { fi = t.buildFaultInjection(policy) } + if policy.Spec.Timeout != nil { + ct = t.buildTimeout(policy) + } // Apply IR to all the routes within the specific Gateway // If the feature is already set, then skip it, since it must be have @@ -348,6 +358,9 @@ func (t *Translator) translateBackendTrafficPolicyForGateway(policy *egv1a1.Back if r.FaultInjection == nil { r.FaultInjection = fi } + if r.BackendTimeout == nil { + r.BackendTimeout = ct + } } } @@ -776,7 +789,7 @@ func (t *Translator) buildCircuitBreaker(policy *egv1a1.BackendTrafficPolicy) *i if ui32, ok := int64ToUint32(*pcb.MaxConnections); ok { cb.MaxConnections = &ui32 } else { - setCircuitBreakerPolicyErrorCondition(policy, fmt.Sprintf("invalid MaxConnections value %d", *pcb.MaxConnections)) + SetBackendTrafficPolicyTranslationErrorCondition(policy, "Circuit Breaker", fmt.Sprintf("invalid MaxConnections value %d", *pcb.MaxConnections)) return nil } } @@ -785,7 +798,7 @@ func (t *Translator) buildCircuitBreaker(policy *egv1a1.BackendTrafficPolicy) *i if ui32, ok := int64ToUint32(*pcb.MaxParallelRequests); ok { cb.MaxParallelRequests = &ui32 } else { - setCircuitBreakerPolicyErrorCondition(policy, fmt.Sprintf("invalid MaxParallelRequests value %d", *pcb.MaxParallelRequests)) + SetBackendTrafficPolicyTranslationErrorCondition(policy, "Circuit Breaker", fmt.Sprintf("invalid MaxParallelRequests value %d", *pcb.MaxParallelRequests)) return nil } } @@ -794,7 +807,7 @@ func (t *Translator) buildCircuitBreaker(policy *egv1a1.BackendTrafficPolicy) *i if ui32, ok := int64ToUint32(*pcb.MaxPendingRequests); ok { cb.MaxPendingRequests = &ui32 } else { - setCircuitBreakerPolicyErrorCondition(policy, fmt.Sprintf("invalid MaxPendingRequests value %d", *pcb.MaxPendingRequests)) + SetBackendTrafficPolicyTranslationErrorCondition(policy, "Circuit Breaker", fmt.Sprintf("invalid MaxPendingRequests value %d", *pcb.MaxPendingRequests)) return nil } } @@ -803,8 +816,61 @@ func (t *Translator) buildCircuitBreaker(policy *egv1a1.BackendTrafficPolicy) *i return cb } -func setCircuitBreakerPolicyErrorCondition(policy *egv1a1.BackendTrafficPolicy, errMsg string) { - message := fmt.Sprintf("Unable to translate Circuit Breaker: %s", errMsg) +func (t *Translator) buildTimeout(policy *egv1a1.BackendTrafficPolicy) *ir.Timeout { + var tto *ir.TCPTimeout + var hto *ir.HTTPTimeout + + pto := policy.Spec.Timeout + + if pto.TCP != nil && pto.TCP.ConnectTimeout != nil { + d, err := time.ParseDuration(string(*pto.TCP.ConnectTimeout)) + if err != nil { + SetBackendTrafficPolicyTranslationErrorCondition(policy, "TCP Timeout", fmt.Sprintf("invalid ConnectTimeout value %s", *pto.TCP.ConnectTimeout)) + return nil + } + tto = &ir.TCPTimeout{ + ConnectTimeout: ptr.To(metav1.Duration{Duration: d}), + } + } + + if pto.HTTP != nil { + var cit *metav1.Duration + var mcd *metav1.Duration + + if pto.HTTP.ConnectionIdleTimeout != nil { + d, err := time.ParseDuration(string(*pto.HTTP.ConnectionIdleTimeout)) + if err != nil { + SetBackendTrafficPolicyTranslationErrorCondition(policy, "HTTP Timeout", fmt.Sprintf("invalid ConnectionIdleTimeout value %s", *pto.HTTP.ConnectionIdleTimeout)) + return nil + } + cit = ptr.To(metav1.Duration{Duration: d}) + } + + if pto.HTTP.MaxConnectionDuration != nil { + d, err := time.ParseDuration(string(*pto.HTTP.MaxConnectionDuration)) + if err != nil { + SetBackendTrafficPolicyTranslationErrorCondition(policy, "HTTP Timeout", fmt.Sprintf("invalid MaxConnectionDuration value %s", *pto.HTTP.MaxConnectionDuration)) + return nil + } + mcd = ptr.To(metav1.Duration{Duration: d}) + } + + hto = &ir.HTTPTimeout{ + ConnectionIdleTimeout: cit, + MaxConnectionDuration: mcd, + } + + } + + return &ir.Timeout{ + TCP: tto, + HTTP: hto, + } + +} + +func SetBackendTrafficPolicyTranslationErrorCondition(policy *egv1a1.BackendTrafficPolicy, field, errMsg string) { + message := fmt.Sprintf("Unable to translate %s: %s", field, errMsg) status.SetBackendTrafficPolicyCondition(policy, gwv1a2.PolicyConditionAccepted, metav1.ConditionFalse, diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-timeout-error.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-timeout-error.in.yaml new file mode 100644 index 000000000000..1214e7ddd94f --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-timeout-error.in.yaml @@ -0,0 +1,45 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +grpcRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: GRPCRoute + metadata: + namespace: default + name: grpcroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: envoy-gateway + name: policy-for-gateway + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + timeout: + tcp: + connectTimeout: 20kib diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-timeout-error.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-timeout-error.out.yaml new file mode 100755 index 000000000000..6daabb916563 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-timeout-error.out.yaml @@ -0,0 +1,144 @@ +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + creationTimestamp: null + name: policy-for-gateway + namespace: envoy-gateway + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + timeout: + tcp: + connectTimeout: 20kib + status: + conditions: + - lastTransitionTime: null + message: 'Unable to translate TCP Timeout: invalid ConnectTimeout value 20kib' + reason: Invalid + status: "False" + type: Accepted +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + 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 +grpcRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: GRPCRoute + metadata: + creationTimestamp: null + name: grpcroute-1 + namespace: default + spec: + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + 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: envoy-gateway + sectionName: http +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - address: null + name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-1 +xdsIR: + envoy-gateway/gateway-1: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: true + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - backendWeights: + invalid: 0 + valid: 0 + destination: + name: grpcroute/default/grpcroute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: GRPC + weight: 1 + hostname: '*' + name: grpcroute/default/grpcroute-1/rule/0/match/-1/* diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-timeout.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-timeout.in.yaml new file mode 100644 index 000000000000..c73a4504d4c2 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-timeout.in.yaml @@ -0,0 +1,99 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-2 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +grpcRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: GRPCRoute + metadata: + namespace: default + name: grpcroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-2 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: envoy-gateway + name: policy-for-gateway + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + timeout: + tcp: + connectTimeout: 15s + http: + connectionIdleTimeout: 16s + maxConnectionDuration: 17s +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: default + name: policy-for-route + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + namespace: default + timeout: + tcp: + connectTimeout: 20s + http: + connectionIdleTimeout: 21s + maxConnectionDuration: 22s diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-timeout.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-timeout.out.yaml new file mode 100755 index 000000000000..bb922a7527f9 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-timeout.out.yaml @@ -0,0 +1,311 @@ +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + creationTimestamp: null + name: policy-for-route + namespace: default + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + namespace: default + timeout: + http: + connectionIdleTimeout: 21s + maxConnectionDuration: 22s + tcp: + connectTimeout: 20s + status: + conditions: + - lastTransitionTime: null + message: BackendTrafficPolicy has been accepted. + reason: Accepted + status: "True" + type: Accepted +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + creationTimestamp: null + name: policy-for-gateway + namespace: envoy-gateway + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + timeout: + http: + connectionIdleTimeout: 16s + maxConnectionDuration: 17s + tcp: + connectTimeout: 15s + status: + conditions: + - lastTransitionTime: null + message: BackendTrafficPolicy has been accepted. + reason: Accepted + status: "True" + type: Accepted +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + 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 +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-2 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + 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 +grpcRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: GRPCRoute + metadata: + creationTimestamp: null + name: grpcroute-1 + namespace: default + spec: + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + 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: envoy-gateway + sectionName: http +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-1 + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-2 + namespace: envoy-gateway + 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: envoy-gateway + sectionName: http +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - address: null + name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-1 + envoy-gateway/gateway-2: + proxy: + listeners: + - address: null + name: envoy-gateway/gateway-2/http + ports: + - containerPort: 10080 + name: http + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-2 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-2 +xdsIR: + envoy-gateway/gateway-1: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: true + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - backendTimeout: + http: + connectionIdleTimeout: 16s + maxConnectionDuration: 17s + tcp: + connectTimeout: 15s + backendWeights: + invalid: 0 + valid: 0 + destination: + name: grpcroute/default/grpcroute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: GRPC + weight: 1 + hostname: '*' + name: grpcroute/default/grpcroute-1/rule/0/match/-1/* + envoy-gateway/gateway-2: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway-2/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - backendTimeout: + http: + connectionIdleTimeout: 21s + maxConnectionDuration: 22s + tcp: + connectTimeout: 20s + backendWeights: + invalid: 0 + valid: 0 + destination: + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: gateway.envoyproxy.io + name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: / diff --git a/internal/ir/xds.go b/internal/ir/xds.go index b2400cf8823d..5f9c9ae83d76 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -391,6 +391,8 @@ type HTTPRoute struct { ExtensionRefs []*UnstructuredRef `json:"extensionRefs,omitempty" yaml:"extensionRefs,omitempty"` // Circuit Breaker Settings CircuitBreaker *CircuitBreaker `json:"circuitBreaker,omitempty" yaml:"circuitBreaker,omitempty"` + // Connection timeout Settings + BackendTimeout *Timeout `json:"backendTimeout,omitempty" yaml:"backendTimeout,omitempty"` } // UnstructuredRef holds unstructured data for an arbitrary k8s resource introduced by an extension @@ -1456,3 +1458,28 @@ func (p *HealthCheckPayload) Validate() error { } return errs } + +// Backend connection timeout settings +// +k8s:deepcopy-gen=true +type Timeout struct { + // Timeout settings for TCP. + TCP *TCPTimeout `json:"tcp,omitempty" yaml:"tcp,omitempty"` + + // Timeout settings for HTTP. + HTTP *HTTPTimeout `json:"http,omitempty" yaml:"tcp,omitempty"` +} + +// +k8s:deepcopy-gen=true +type TCPTimeout struct { + // The timeout for network connection establishment, including TCP and TLS handshakes. + ConnectTimeout *metav1.Duration `json:"connectTimeout,omitempty" yaml:"connectTimeout,omitempty"` +} + +// +k8s:deepcopy-gen=true +type HTTPTimeout struct { + // The idle timeout for an HTTP connection. Idle time is defined as a period in which there are no active requests in the connection. + ConnectionIdleTimeout *metav1.Duration `json:"connectionIdleTimeout,omitempty" yaml:"connectionIdleTimeout,omitempty"` + + // The maximum duration of an HTTP connection. + MaxConnectionDuration *metav1.Duration `json:"maxConnectionDuration,omitempty" yaml:"maxConnectionDuration,omitempty"` +} diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index e905756dbc2c..619ec9ebc985 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -667,6 +667,11 @@ func (in *HTTPRoute) DeepCopyInto(out *HTTPRoute) { *out = new(CircuitBreaker) (*in).DeepCopyInto(*out) } + if in.BackendTimeout != nil { + in, out := &in.BackendTimeout, &out.BackendTimeout + *out = new(Timeout) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPRoute. @@ -679,6 +684,31 @@ func (in *HTTPRoute) DeepCopy() *HTTPRoute { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPTimeout) DeepCopyInto(out *HTTPTimeout) { + *out = *in + if in.ConnectionIdleTimeout != nil { + in, out := &in.ConnectionIdleTimeout, &out.ConnectionIdleTimeout + *out = new(v1.Duration) + **out = **in + } + if in.MaxConnectionDuration != nil { + in, out := &in.MaxConnectionDuration, &out.MaxConnectionDuration + *out = new(v1.Duration) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPTimeout. +func (in *HTTPTimeout) DeepCopy() *HTTPTimeout { + if in == nil { + return nil + } + out := new(HTTPTimeout) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HealthCheck) DeepCopyInto(out *HealthCheck) { *out = *in @@ -1460,6 +1490,26 @@ func (in *TCPListener) DeepCopy() *TCPListener { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TCPTimeout) DeepCopyInto(out *TCPTimeout) { + *out = *in + if in.ConnectTimeout != nil { + in, out := &in.ConnectTimeout, &out.ConnectTimeout + *out = new(v1.Duration) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TCPTimeout. +func (in *TCPTimeout) DeepCopy() *TCPTimeout { + if in == nil { + return nil + } + out := new(TCPTimeout) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLS) DeepCopyInto(out *TLS) { *out = *in @@ -1602,6 +1652,31 @@ func (in *TextAccessLog) DeepCopy() *TextAccessLog { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Timeout) DeepCopyInto(out *Timeout) { + *out = *in + if in.TCP != nil { + in, out := &in.TCP, &out.TCP + *out = new(TCPTimeout) + (*in).DeepCopyInto(*out) + } + if in.HTTP != nil { + in, out := &in.HTTP, &out.HTTP + *out = new(HTTPTimeout) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Timeout. +func (in *Timeout) DeepCopy() *Timeout { + if in == nil { + return nil + } + out := new(Timeout) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Tracing) DeepCopyInto(out *Tracing) { *out = *in diff --git a/internal/xds/translator/cluster.go b/internal/xds/translator/cluster.go index 58e6641c6675..8408c5d83f99 100644 --- a/internal/xds/translator/cluster.go +++ b/internal/xds/translator/cluster.go @@ -31,6 +31,7 @@ const ( extensionOptionsKey = "envoy.extensions.upstreams.http.v3.HttpProtocolOptions" // https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto#envoy-v3-api-field-config-cluster-v3-cluster-per-connection-buffer-limit-bytes tcpClusterPerConnectionBufferLimitBytes = 32768 + tcpClusterPerConnectTimeout = 10 * time.Second ) type xdsClusterArgs struct { @@ -42,6 +43,7 @@ type xdsClusterArgs struct { proxyProtocol *ir.ProxyProtocol circuitBreaker *ir.CircuitBreaker healthCheck *ir.HealthCheck + timeout *ir.Timeout } type EndpointType int @@ -54,7 +56,6 @@ const ( func buildXdsCluster(args *xdsClusterArgs) *clusterv3.Cluster { cluster := &clusterv3.Cluster{ Name: args.name, - ConnectTimeout: durationpb.New(10 * time.Second), DnsLookupFamily: clusterv3.Cluster_V4_ONLY, CommonLbConfig: &clusterv3.Cluster_CommonLbConfig{ LocalityConfigSpecifier: &clusterv3.Cluster_CommonLbConfig_LocalityWeightedLbConfig_{ @@ -63,6 +64,8 @@ func buildXdsCluster(args *xdsClusterArgs) *clusterv3.Cluster { PerConnectionBufferLimitBytes: wrapperspb.UInt32(tcpClusterPerConnectionBufferLimitBytes), } + cluster.ConnectTimeout = buildConnectTimeout(args.timeout) + // Set Proxy Protocol if args.proxyProtocol != nil { cluster.TransportSocket = buildProxyProtocolSocket(args.proxyProtocol, args.tSocket) @@ -87,16 +90,10 @@ func buildXdsCluster(args *xdsClusterArgs) *clusterv3.Cluster { cluster.RespectDnsTtl = true } - isHTTP2 := false - for _, ds := range args.settings { - if ds.Protocol == ir.GRPC || - ds.Protocol == ir.HTTP2 { - isHTTP2 = true - break - } - } - if isHTTP2 { - cluster.TypedExtensionProtocolOptions = buildTypedExtensionProtocolOptions() + // build common, HTTP/1 and HTTP/2 protocol options for cluster + epo := buildTypedExtensionProtocolOptions(args) + if epo != nil { + cluster.TypedExtensionProtocolOptions = epo } // Set Load Balancer policy @@ -315,13 +312,38 @@ func buildXdsClusterLoadAssignment(clusterName string, destSettings []*ir.Destin return &endpointv3.ClusterLoadAssignment{ClusterName: clusterName, Endpoints: localities} } -func buildTypedExtensionProtocolOptions() map[string]*anypb.Any { - protocolOptions := httpv3.HttpProtocolOptions{ - UpstreamProtocolOptions: &httpv3.HttpProtocolOptions_ExplicitHttpConfig_{ +func buildTypedExtensionProtocolOptions(args *xdsClusterArgs) map[string]*anypb.Any { + isHTTP2 := false + for _, ds := range args.settings { + if ds.Protocol == ir.GRPC || + ds.Protocol == ir.HTTP2 { + isHTTP2 = true + break + } + } + + hasCommonHTTPOptions := args.timeout != nil && args.timeout.HTTP != nil && + (args.timeout.HTTP.MaxConnectionDuration != nil || args.timeout.HTTP.ConnectionIdleTimeout != nil) + + if !hasCommonHTTPOptions && !isHTTP2 { + return nil + } + + protocolOptions := httpv3.HttpProtocolOptions{} + + if hasCommonHTTPOptions { + protocolOptions.CommonHttpProtocolOptions = &corev3.HttpProtocolOptions{ + IdleTimeout: durationpb.New(args.timeout.HTTP.ConnectionIdleTimeout.Duration), + MaxConnectionDuration: durationpb.New(args.timeout.HTTP.MaxConnectionDuration.Duration), + } + } + + if isHTTP2 { + protocolOptions.UpstreamProtocolOptions = &httpv3.HttpProtocolOptions_ExplicitHttpConfig_{ ExplicitHttpConfig: &httpv3.HttpProtocolOptions_ExplicitHttpConfig{ ProtocolConfig: &httpv3.HttpProtocolOptions_ExplicitHttpConfig_Http2ProtocolOptions{}, }, - }, + } } anyProtocolOptions, _ := anypb.New(&protocolOptions) @@ -389,3 +411,10 @@ func buildProxyProtocolSocket(proxyProtocol *ir.ProxyProtocol, tSocket *corev3.T }, } } + +func buildConnectTimeout(to *ir.Timeout) *durationpb.Duration { + if to != nil && to.TCP != nil && to.TCP.ConnectTimeout != nil { + return durationpb.New(to.TCP.ConnectTimeout.Duration) + } + return durationpb.New(tcpClusterPerConnectTimeout) +} diff --git a/internal/xds/translator/testdata/in/xds-ir/timeout.yaml b/internal/xds/translator/testdata/in/xds-ir/timeout.yaml new file mode 100644 index 000000000000..78c0e230e5d9 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/timeout.yaml @@ -0,0 +1,24 @@ +http: +- name: "first-listener" + address: "0.0.0.0" + port: 10080 + hostnames: + - "*" + path: + mergeSlashes: true + escapedSlashesAction: UnescapeAndRedirect + routes: + - name: "first-route" + hostname: "*" + backendTimeout: + tcp: + connectTimeout: "31s" + http: + connectionIdleTimeout: "32s" + maxConnectionDuration: "33s" + destination: + name: "first-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 diff --git a/internal/xds/translator/testdata/out/xds-ir/timeout.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/timeout.clusters.yaml new file mode 100644 index 000000000000..793aac85c7e4 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/timeout.clusters.yaml @@ -0,0 +1,20 @@ +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 31s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: first-route-dest + lbPolicy: LEAST_REQUEST + name: first-route-dest + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS + typedExtensionProtocolOptions: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + '@type': type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + commonHttpProtocolOptions: + idleTimeout: 32s + maxConnectionDuration: 33s diff --git a/internal/xds/translator/testdata/out/xds-ir/timeout.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/timeout.endpoints.yaml new file mode 100644 index 000000000000..3b3f2d09076e --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/timeout.endpoints.yaml @@ -0,0 +1,12 @@ +- clusterName: first-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: first-route-dest/backend/0 diff --git a/internal/xds/translator/testdata/out/xds-ir/timeout.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/timeout.listeners.yaml new file mode 100644 index 000000000000..73ee1b42ef6f --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/timeout.listeners.yaml @@ -0,0 +1,33 @@ +- 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: + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + mergeSlashes: true + normalizePath: true + pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: first-listener + statPrefix: http + upgradeConfigs: + - upgradeType: websocket + useRemoteAddress: true + name: first-listener + perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/timeout.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/timeout.routes.yaml new file mode 100644 index 000000000000..2734c7cc42a0 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/timeout.routes.yaml @@ -0,0 +1,12 @@ +- ignorePortInHostMatching: true + name: first-listener + virtualHosts: + - domains: + - '*' + name: first-listener/* + routes: + - match: + prefix: / + name: first-route + route: + cluster: first-route-dest diff --git a/internal/xds/translator/translator.go b/internal/xds/translator/translator.go index bbb350565db7..fcd646d5ff97 100644 --- a/internal/xds/translator/translator.go +++ b/internal/xds/translator/translator.go @@ -506,6 +506,7 @@ func processXdsCluster(tCtx *types.ResourceVersionTable, httpRoute *ir.HTTPRoute proxyProtocol: httpRoute.ProxyProtocol, circuitBreaker: httpRoute.CircuitBreaker, healthCheck: httpRoute.HealthCheck, + timeout: httpRoute.BackendTimeout, }); err != nil && !errors.Is(err, ErrXdsClusterExists) { return err } diff --git a/internal/xds/translator/translator_test.go b/internal/xds/translator/translator_test.go index e319f876e8a5..6bb234ab2622 100644 --- a/internal/xds/translator/translator_test.go +++ b/internal/xds/translator/translator_test.go @@ -236,6 +236,9 @@ func TestTranslateXds(t *testing.T) { { name: "path-settings", }, + { + name: "timeout", + }, } for _, tc := range testCases {