diff --git a/internal/gatewayapi/clienttrafficpolicy.go b/internal/gatewayapi/clienttrafficpolicy.go index 97caa87f4cd..5bd81cabf95 100644 --- a/internal/gatewayapi/clienttrafficpolicy.go +++ b/internal/gatewayapi/clienttrafficpolicy.go @@ -324,6 +324,11 @@ func (t *Translator) translateClientTrafficPolicyForListener(policy *egv1a1.Clie // Translate Path Settings translatePathSettings(policy.Spec.Path, httpIR) + // Translate Client Timeout Settings + if err := translateClientTimeout(policy.Spec.Timeout, httpIR); err != nil { + return err + } + // Translate HTTP1 Settings if err := translateHTTP1Settings(policy.Spec.HTTP1, httpIR); err != nil { return err @@ -395,6 +400,35 @@ func translatePathSettings(pathSettings *egv1a1.PathSettings, httpIR *ir.HTTPLis } } +func translateClientTimeout(clientTimeout *egv1a1.ClientTimeout, httpIR *ir.HTTPListener) error { + if clientTimeout == nil { + return nil + } + + if clientTimeout.HTTP != nil { + if clientTimeout.HTTP.RequestReceivedTimeout != nil { + d, err := time.ParseDuration(string(*clientTimeout.HTTP.RequestReceivedTimeout)) + if err != nil { + return err + } + switch { + case httpIR.Timeout == nil: + httpIR.Timeout = &ir.ClientTimeout{} + fallthrough + + case httpIR.Timeout.HTTP == nil: + httpIR.Timeout.HTTP = &ir.HTTPClientTimeout{} + } + + httpIR.Timeout.HTTP.RequestReceivedTimeout = &metav1.Duration{ + Duration: d, + } + } + } + + return nil +} + func translateListenerProxyProtocol(enableProxyProtocol *bool, httpIR *ir.HTTPListener) { // Return early if not set if enableProxyProtocol == nil { diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-timeout-with-error.in.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-timeout-with-error.in.yaml new file mode 100644 index 00000000000..b9cad3568bc --- /dev/null +++ b/internal/gatewayapi/testdata/clienttrafficpolicy-timeout-with-error.in.yaml @@ -0,0 +1,31 @@ +clientTrafficPolicies: + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: ClientTrafficPolicy + metadata: + namespace: envoy-gateway + name: target-gateway + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway + namespace: envoy-gateway + timeout: + http: + requestReceivedTimeout: "5sec" +gateways: + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: Same + diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-timeout-with-error.out.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-timeout-with-error.out.yaml new file mode 100644 index 00000000000..7aa096ad457 --- /dev/null +++ b/internal/gatewayapi/testdata/clienttrafficpolicy-timeout-with-error.out.yaml @@ -0,0 +1,95 @@ +clientTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: ClientTrafficPolicy + metadata: + creationTimestamp: null + name: target-gateway + namespace: envoy-gateway + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway + namespace: envoy-gateway + timeout: + http: + requestReceivedTimeout: 5sec + status: + conditions: + - lastTransitionTime: null + message: 'Time: unknown unit "sec" in duration "5sec"' + reason: Invalid + status: "False" + type: Accepted +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: Same + name: http + port: 80 + 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 +infraIR: + envoy-gateway/gateway: + proxy: + listeners: + - address: null + name: envoy-gateway/gateway/http + ports: + - containerPort: 10080 + name: http + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway +xdsIR: + envoy-gateway/gateway: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-timeout.in.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-timeout.in.yaml new file mode 100644 index 00000000000..0c962e72fe8 --- /dev/null +++ b/internal/gatewayapi/testdata/clienttrafficpolicy-timeout.in.yaml @@ -0,0 +1,38 @@ +clientTrafficPolicies: + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: ClientTrafficPolicy + metadata: + namespace: envoy-gateway + name: target-gateway + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway + namespace: envoy-gateway + sectionName: http-1 + timeout: + http: + requestReceivedTimeout: "5s" +gateways: + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http-1 + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: Same + - name: http-2 + protocol: HTTP + port: 8080 + allowedRoutes: + namespaces: + from: Same + diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-timeout.out.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-timeout.out.yaml new file mode 100644 index 00000000000..32d9e109bd0 --- /dev/null +++ b/internal/gatewayapi/testdata/clienttrafficpolicy-timeout.out.yaml @@ -0,0 +1,144 @@ +clientTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: ClientTrafficPolicy + metadata: + creationTimestamp: null + name: target-gateway + namespace: envoy-gateway + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway + namespace: envoy-gateway + sectionName: http-1 + timeout: + http: + requestReceivedTimeout: 5s + status: + conditions: + - lastTransitionTime: null + message: ClientTrafficPolicy has been accepted. + reason: Accepted + status: "True" + type: Accepted +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: Same + name: http-1 + port: 80 + protocol: HTTP + - allowedRoutes: + namespaces: + from: Same + name: http-2 + port: 8080 + 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-1 + 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 +infraIR: + envoy-gateway/gateway: + proxy: + listeners: + - address: null + name: envoy-gateway/gateway/http-1 + ports: + - containerPort: 10080 + name: http-1 + protocol: HTTP + servicePort: 80 + - address: null + name: envoy-gateway/gateway/http-2 + ports: + - containerPort: 8080 + name: http-2 + protocol: HTTP + servicePort: 8080 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway +xdsIR: + envoy-gateway/gateway: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway/http-1 + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + timeout: + http: + requestReceivedTimeout: 5s + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway/http-2 + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 8080 diff --git a/internal/ir/xds.go b/internal/ir/xds.go index 39038b0f7db..2fe160ca545 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -225,6 +225,8 @@ type HTTPListener struct { // HTTP1 provides HTTP/1 configuration on the listener // +optional HTTP1 *HTTP1Settings `json:"http1,omitempty" yaml:"http1,omitempty"` + // ClientTimeout sets the timeout configuration for downstream connections + Timeout *ClientTimeout `json:"timeout,omitempty" yaml:"clientTimeout,omitempty"` } // Validate the fields within the HTTPListener structure @@ -389,6 +391,21 @@ type HeaderSettings struct { EnableEnvoyHeaders bool `json:"enableEnvoyHeaders,omitempty" yaml:"enableEnvoyHeaders,omitempty"` } +// ClientTimeout sets the timeout configuration for downstream connections +// +k8s:deepcopy-gen=true +type ClientTimeout struct { + // Timeout settings for HTTP. + HTTP *HTTPClientTimeout `json:"http,omitempty" yaml:"http,omitempty"` +} + +// HTTPClientTimeout set the configuration for client HTTP. +// +k8s:deepcopy-gen=true +type HTTPClientTimeout struct { + // The duration envoy waits for the complete request reception. This timer starts upon request + // initiation and stops when either the last byte of the request is sent upstream or when the response begins. + RequestReceivedTimeout *metav1.Duration `json:"requestReceivedTimeout,omitempty" yaml:"requestReceivedTimeout,omitempty"` +} + // HTTPRoute holds the route information associated with the HTTP Route // +k8s:deepcopy-gen=true type HTTPRoute struct { diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index 66c3e574009..1f07711beb1 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -249,6 +249,26 @@ func (in *ClientIPDetectionSettings) DeepCopy() *ClientIPDetectionSettings { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClientTimeout) DeepCopyInto(out *ClientTimeout) { + *out = *in + if in.HTTP != nil { + in, out := &in.HTTP, &out.HTTP + *out = new(HTTPClientTimeout) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientTimeout. +func (in *ClientTimeout) DeepCopy() *ClientTimeout { + if in == nil { + return nil + } + out := new(ClientTimeout) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ConsistentHash) DeepCopyInto(out *ConsistentHash) { *out = *in @@ -579,6 +599,26 @@ func (in *HTTP1Settings) DeepCopy() *HTTP1Settings { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPClientTimeout) DeepCopyInto(out *HTTPClientTimeout) { + *out = *in + if in.RequestReceivedTimeout != nil { + in, out := &in.RequestReceivedTimeout, &out.RequestReceivedTimeout + *out = new(v1.Duration) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPClientTimeout. +func (in *HTTPClientTimeout) DeepCopy() *HTTPClientTimeout { + if in == nil { + return nil + } + out := new(HTTPClientTimeout) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HTTPExtAuthService) DeepCopyInto(out *HTTPExtAuthService) { *out = *in @@ -680,6 +720,11 @@ func (in *HTTPListener) DeepCopyInto(out *HTTPListener) { *out = new(HTTP1Settings) (*in).DeepCopyInto(*out) } + if in.Timeout != nil { + in, out := &in.Timeout, &out.Timeout + *out = new(ClientTimeout) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPListener. diff --git a/internal/xds/translator/listener.go b/internal/xds/translator/listener.go index 6301eb1ca22..268cc1dd41c 100644 --- a/internal/xds/translator/listener.go +++ b/internal/xds/translator/listener.go @@ -25,6 +25,7 @@ import ( "github.com/envoyproxy/go-control-plane/pkg/wellknown" "github.com/golang/protobuf/ptypes/wrappers" "google.golang.org/protobuf/types/known/anypb" + "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/wrapperspb" "k8s.io/utils/ptr" @@ -230,6 +231,10 @@ func (t *Translator) addXdsHTTPFilterChain(xdsListener *listenerv3.Listener, irL Tracing: hcmTracing, } + if irListener.Timeout != nil && irListener.Timeout.HTTP != nil && irListener.Timeout.HTTP.RequestReceivedTimeout != nil { + mgr.RequestTimeout = durationpb.New(irListener.Timeout.HTTP.RequestReceivedTimeout.Duration) + } + // Add the proxy protocol filter if needed patchProxyProtocolFilter(xdsListener, irListener) diff --git a/internal/xds/translator/testdata/in/xds-ir/client-timeout.yaml b/internal/xds/translator/testdata/in/xds-ir/client-timeout.yaml new file mode 100644 index 00000000000..1c05b605a35 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/client-timeout.yaml @@ -0,0 +1,22 @@ +http: + - name: "first-listener" + address: "0.0.0.0" + port: 10080 + hostnames: + - "*" + path: + mergeSlashes: true + escapedSlashesAction: UnescapeAndRedirect + routes: + - name: "first-route" + hostname: "*" + destination: + name: "first-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 + timeout: + http: + requestReceivedTimeout: "5s" + diff --git a/internal/xds/translator/testdata/out/xds-ir/client-timeout.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/client-timeout.clusters.yaml new file mode 100644 index 00000000000..c8692b81602 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/client-timeout.clusters.yaml @@ -0,0 +1,14 @@ +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: first-route-dest + lbPolicy: LEAST_REQUEST + name: first-route-dest + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS diff --git a/internal/xds/translator/testdata/out/xds-ir/client-timeout.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/client-timeout.endpoints.yaml new file mode 100644 index 00000000000..3b3f2d09076 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/client-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/client-timeout.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/client-timeout.listeners.yaml new file mode 100644 index 00000000000..e6e99e720bf --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/client-timeout.listeners.yaml @@ -0,0 +1,36 @@ +- 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 + suppressEnvoyHeaders: true + mergeSlashes: true + normalizePath: true + pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: first-listener + requestTimeout: 5s + serverHeaderTransformation: PASS_THROUGH + statPrefix: http + upgradeConfigs: + - upgradeType: websocket + useRemoteAddress: true + name: first-listener + perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/client-timeout.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/client-timeout.routes.yaml new file mode 100644 index 00000000000..2734c7cc42a --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/client-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_test.go b/internal/xds/translator/translator_test.go index 2a3c34a2133..761b6345621 100644 --- a/internal/xds/translator/translator_test.go +++ b/internal/xds/translator/translator_test.go @@ -270,6 +270,9 @@ func TestTranslateXds(t *testing.T) { { name: "upstream-tcpkeepalive", }, + { + name: "client-timeout", + }, } for _, tc := range testCases {