diff --git a/docs/content/reference/install-configuration/configuration-options.md b/docs/content/reference/install-configuration/configuration-options.md index 04a17d4119..ebae1fd328 100644 --- a/docs/content/reference/install-configuration/configuration-options.md +++ b/docs/content/reference/install-configuration/configuration-options.md @@ -385,6 +385,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | providers.kubernetesgateway.namespaces | Kubernetes namespaces. | | | providers.kubernetesgateway.nativelbbydefault | Defines whether to use Native Kubernetes load-balancing by default. | false | | providers.kubernetesgateway.qps | Defines the maximum QPS to the Kubernetes API server. Setting this to a negative value will disable client-side ratelimiting. | 50 | +| providers.kubernetesgateway.retrymaxrequestbodybytes | Defines the maximum request body size (in bytes) buffered for HTTPRoute retries. If negative, the limit is disabled. | 2097152 | | providers.kubernetesgateway.statusaddress.hostname | Hostname used for Kubernetes Gateway status address. | | | providers.kubernetesgateway.statusaddress.ip | IP used to set Kubernetes Gateway status address. | | | providers.kubernetesgateway.statusaddress.service | Published Kubernetes Service to copy status addresses from. | | diff --git a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-gateway.md b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-gateway.md index f9f3c59b9c..38206b7ade 100644 --- a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-gateway.md +++ b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-gateway.md @@ -89,6 +89,7 @@ providers: | `providers.kubernetesGateway.gateways[0].namespace` | Gateway namespace. | "" | No | | `providers.kubernetesGateway.throttleDuration` | Minimum amount of time to wait between two Kubernetes events before producing a new configuration.
This prevents a Kubernetes cluster that updates many times per second from continuously changing your Traefik configuration.
If empty, every event is caught. | 0s | No | | `providers.kubernetesGateway.nativeLBByDefault` | Defines whether to use Native Kubernetes load-balancing mode by default. For more information, please check out the `traefik.io/service.nativelb` service annotation documentation. | false | No | +| `providers.kubernetesGateway.retryMaxRequestBodyBytes` | Defines the maximum request body size (in bytes) buffered for HTTPRoute retries.
If negative, the limit is disabled.
A request with a larger body is forwarded without retry. | 2097152 | No | | `providers.kubernetesGateway.`
`statusAddress.hostname`
| Hostname copied to the Gateway `status.addresses`. | "" | No | | `providers.kubernetesGateway.`
`statusAddress.ip`
| IP address copied to the Gateway `status.addresses`, and currently only supports one IP value (IPv4 or IPv6). | "" | No | | `providers.kubernetesGateway.`
`statusAddress.service.namespace`
| The namespace of the Kubernetes service to copy status addresses from.
When using third parties tools like External-DNS, this option can be used to copy the service `loadbalancer.status` (containing the service's endpoints IPs) to the Gateway `status.addresses`. | "" | No | diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index 8b4538a0fc..f16507463b 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -200,6 +200,7 @@ throttleDuration = "42s" experimentalChannel = true nativeLBByDefault = true + retryMaxRequestBodyBytes = 42 [providers.kubernetesGateway.statusAddress] ip = "foobar" hostname = "foobar" diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index c3b55a3e28..c12cc2becc 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -226,6 +226,7 @@ providers: name: foobar namespace: foobar nativeLBByDefault: true + retryMaxRequestBodyBytes: 42 rest: insecure: true consulCatalog: diff --git a/integration/gateway-api-conformance-reports/v1.7.0-rc.3/experimental-v3.8-default-report.yaml b/integration/gateway-api-conformance-reports/v1.7.0-rc.3/experimental-v3.8-default-report.yaml index 195f24aa1d..9643339b2d 100644 --- a/integration/gateway-api-conformance-reports/v1.7.0-rc.3/experimental-v3.8-default-report.yaml +++ b/integration/gateway-api-conformance-reports/v1.7.0-rc.3/experimental-v3.8-default-report.yaml @@ -55,7 +55,7 @@ profiles: result: success statistics: Failed: 0 - Passed: 56 + Passed: 60 Skipped: 0 supportedFeatures: - BackendTLSPolicy @@ -89,6 +89,8 @@ profiles: - HTTPRouteQueryParamMatching - HTTPRouteRequestTimeout - HTTPRouteResponseHeaderModification + - HTTPRouteRetryCodes + - HTTPRouteRetryConnectionError - HTTPRouteSchemeRedirect - ListenerSet unsupportedFeatures: @@ -100,8 +102,6 @@ profiles: - HTTPRouteRequestMirror - HTTPRouteRequestMultipleMirrors - HTTPRouteRequestPercentageMirror - - HTTPRouteRetryCodes - - HTTPRouteRetryConnectionError name: GATEWAY-HTTP summary: Core tests succeeded. Extended tests succeeded. - core: @@ -176,3 +176,5 @@ succeededProvisionalTests: - GatewayPreserveForeignConditions - HTTPRouteBackendURLRewrite - HTTPRoutePreserveForeignStatus +- HTTPRouteRetryWithBackendTimeout +- HTTPRouteRetryWithRequestTimeout diff --git a/integration/gateway-api-conformance-reports/v1.7.0-rc.3/experimental-v3.8-merged-report.yaml b/integration/gateway-api-conformance-reports/v1.7.0-rc.3/experimental-v3.8-merged-report.yaml index f5f16fe0f6..5abf5ebf3c 100644 --- a/integration/gateway-api-conformance-reports/v1.7.0-rc.3/experimental-v3.8-merged-report.yaml +++ b/integration/gateway-api-conformance-reports/v1.7.0-rc.3/experimental-v3.8-merged-report.yaml @@ -68,7 +68,7 @@ profiles: - GatewayInvalidFrontendClientCertificateValidation statistics: Failed: 0 - Passed: 49 + Passed: 53 Skipped: 5 supportedFeatures: - BackendTLSPolicy @@ -100,6 +100,8 @@ profiles: - HTTPRouteQueryParamMatching - HTTPRouteRequestTimeout - HTTPRouteResponseHeaderModification + - HTTPRouteRetryCodes + - HTTPRouteRetryConnectionError - HTTPRouteSchemeRedirect - ListenerSet unsupportedFeatures: @@ -113,8 +115,6 @@ profiles: - HTTPRouteRequestMirror - HTTPRouteRequestMultipleMirrors - HTTPRouteRequestPercentageMirror - - HTTPRouteRetryCodes - - HTTPRouteRetryConnectionError name: GATEWAY-HTTP summary: Core tests partially succeeded with 1 test skips. Extended tests partially succeeded with 5 test skips. @@ -197,3 +197,5 @@ succeededProvisionalTests: - GatewayPreserveForeignConditions - HTTPRouteBackendURLRewrite - HTTPRoutePreserveForeignStatus +- HTTPRouteRetryWithBackendTimeout +- HTTPRouteRetryWithRequestTimeout diff --git a/pkg/provider/kubernetes/gateway/features.go b/pkg/provider/kubernetes/gateway/features.go index 8de7a69ebe..b4645ac2f7 100644 --- a/pkg/provider/kubernetes/gateway/features.go +++ b/pkg/provider/kubernetes/gateway/features.go @@ -74,6 +74,8 @@ func extendedHTTPRouteFeatures() sets.Set[features.Feature] { features.HTTPRouteRequestTimeoutFeature, features.HTTPRouteBackendTimeoutFeature, features.HTTPRouteBackendURLRewriteFeature, + features.HTTPRouteRetryCodesFeature, + features.HTTPRouteRetryConnectionErrorFeature, ) } diff --git a/pkg/provider/kubernetes/gateway/fixtures/httproute/with_retry.yml b/pkg/provider/kubernetes/gateway/fixtures/httproute/with_retry.yml new file mode 100644 index 0000000000..54e47ffd64 --- /dev/null +++ b/pkg/provider/kubernetes/gateway/fixtures/httproute/with_retry.yml @@ -0,0 +1,68 @@ +--- +kind: GatewayClass +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: my-gateway-class +spec: + controllerName: traefik.io/gateway-controller + +--- +kind: Gateway +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: my-gateway + namespace: default +spec: + gatewayClassName: my-gateway-class + listeners: # Use GatewayClass defaults for listener definition. + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + kinds: + - kind: HTTPRoute + group: gateway.networking.k8s.io + namespaces: + from: Same + +--- +kind: HTTPRoute +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: http-app-1 + namespace: default +spec: + parentRefs: + - name: my-gateway + kind: Gateway + group: gateway.networking.k8s.io + hostnames: + - "example.org" + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: whoami + port: 80 + weight: 1 + kind: Service + group: "" + filters: + - type: RequestHeaderModifier + requestHeaderModifier: + set: + - name: X-Foo + value: Bar + add: + - name: X-Bar + value: Foo + remove: + - X-Baz + retry: + codes: + - 500 + - 503 + attempts: 3 + backoff: 100ms diff --git a/pkg/provider/kubernetes/gateway/fixtures/httproute/with_retry_empty.yml b/pkg/provider/kubernetes/gateway/fixtures/httproute/with_retry_empty.yml new file mode 100644 index 0000000000..14583b83c9 --- /dev/null +++ b/pkg/provider/kubernetes/gateway/fixtures/httproute/with_retry_empty.yml @@ -0,0 +1,52 @@ +--- +kind: GatewayClass +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: my-gateway-class +spec: + controllerName: traefik.io/gateway-controller + +--- +kind: Gateway +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: my-gateway + namespace: default +spec: + gatewayClassName: my-gateway-class + listeners: # Use GatewayClass defaults for listener definition. + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + kinds: + - kind: HTTPRoute + group: gateway.networking.k8s.io + namespaces: + from: Same + +--- +kind: HTTPRoute +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: http-app-1 + namespace: default +spec: + parentRefs: + - name: my-gateway + kind: Gateway + group: gateway.networking.k8s.io + hostnames: + - "example.org" + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: whoami + port: 80 + weight: 1 + kind: Service + group: "" + retry: {} diff --git a/pkg/provider/kubernetes/gateway/httproute.go b/pkg/provider/kubernetes/gateway/httproute.go index 9398a11651..a47cd6b166 100644 --- a/pkg/provider/kubernetes/gateway/httproute.go +++ b/pkg/provider/kubernetes/gateway/httproute.go @@ -158,6 +158,11 @@ func (p *Provider) loadHTTPRouteConfiguration(ctx context.Context, gatewayName, } } + var retry *dynamic.Middleware + if routeRule.Retry != nil { + retry = createRetry(ctx, routeRule.Retry, p.RetryMaxRequestBodyBytes) + } + for _, match := range routeRule.Matches { rule, priority := buildMatchRule(hostnames, match) router := dynamic.Router{ @@ -223,6 +228,13 @@ func (p *Provider) loadHTTPRouteConfiguration(ctx context.Context, gatewayName, } } + if err == nil && retry != nil { + // The retry middleware comes last, so that only the call to the backend is retried. + retryName := routerName + "-retry" + routerConf.HTTP.Middlewares[retryName] = retry + router.Middlewares = append(router.Middlewares, retryName) + } + p.applyRouterTransform(ctx, &router, route) routerConf.HTTP.Routers[routerName] = &router @@ -1088,6 +1100,30 @@ func createCORS(filter *gatev1.HTTPCORSFilter) *dynamic.Middleware { } } +func createRetry(ctx context.Context, retry *gatev1.HTTPRouteRetry, maxRequestBodyBytes int64) *dynamic.Middleware { + // The Gateway API attempts are the number of retries, + // whereas the Traefik ones also count the initial request, hence the +1. + r := &dynamic.Retry{ + Attempts: ptr.Deref(retry.Attempts, 1) + 1, + MaxRequestBodyBytes: &maxRequestBodyBytes, + } + + for _, code := range retry.Codes { + r.Status = append(r.Status, strconv.Itoa(int(code))) + } + + if retry.Backoff != nil { + // The CRD validation guarantees a valid GEP-2257 duration string. + if backoff, err := time.ParseDuration(string(*retry.Backoff)); err != nil { + log.Ctx(ctx).Error().Err(err).Msg("Ignoring HTTPRoute retry.backoff: invalid duration") + } else { + r.InitialInterval = ptypes.Duration(backoff) + } + } + + return &dynamic.Middleware{Retry: r} +} + func getHTTPServiceProtocol(portSpec corev1.ServicePort) (string, error) { if portSpec.Protocol != corev1.ProtocolTCP { return "", errors.New("only TCP protocol is supported") diff --git a/pkg/provider/kubernetes/gateway/kubernetes.go b/pkg/provider/kubernetes/gateway/kubernetes.go index fb4135bbce..0fce10dfb0 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes.go +++ b/pkg/provider/kubernetes/gateway/kubernetes.go @@ -92,13 +92,14 @@ type Provider struct { Namespaces []string `description:"Kubernetes namespaces." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty" export:"true"` GatewayClass string `description:"Name of the GatewayClass to watch for. If empty, all GatewayClasses are processed." json:"gatewayClass,omitempty" toml:"gatewayClass,omitempty" yaml:"gatewayClass,omitempty" export:"true"` // Deprecated: please use the GatewayClass option instead. - LabelSelector string `description:"Kubernetes label selector to select specific GatewayClasses (Deprecated, please use the GatewayClass option instead)." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"` - Gateways []NamespacedName `description:"Scopes the provider to specific Gateways." json:"gateways,omitempty" toml:"gateways,omitempty" yaml:"gateways,omitempty" export:"true"` - ThrottleDuration ptypes.Duration `description:"Kubernetes refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"` - ExperimentalChannel bool `description:"Toggles Experimental Channel resources support. Requires the Experimental Channel CRDs." json:"experimentalChannel,omitempty" toml:"experimentalChannel,omitempty" yaml:"experimentalChannel,omitempty" export:"true"` - StatusAddress *StatusAddress `description:"Defines the Kubernetes Gateway status address." json:"statusAddress,omitempty" toml:"statusAddress,omitempty" yaml:"statusAddress,omitempty" export:"true"` - NativeLBByDefault bool `description:"Defines whether to use Native Kubernetes load-balancing by default." json:"nativeLBByDefault,omitempty" toml:"nativeLBByDefault,omitempty" yaml:"nativeLBByDefault,omitempty" export:"true"` - CrossProviderNamespaces []string `description:"List of namespaces from which Gateway API routes are allowed to declare TraefikService backendRef references." json:"crossProviderNamespaces,omitempty" toml:"crossProviderNamespaces,omitempty" yaml:"crossProviderNamespaces,omitempty" export:"true"` + LabelSelector string `description:"Kubernetes label selector to select specific GatewayClasses (Deprecated, please use the GatewayClass option instead)." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"` + Gateways []NamespacedName `description:"Scopes the provider to specific Gateways." json:"gateways,omitempty" toml:"gateways,omitempty" yaml:"gateways,omitempty" export:"true"` + ThrottleDuration ptypes.Duration `description:"Kubernetes refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"` + ExperimentalChannel bool `description:"Toggles Experimental Channel resources support. Requires the Experimental Channel CRDs." json:"experimentalChannel,omitempty" toml:"experimentalChannel,omitempty" yaml:"experimentalChannel,omitempty" export:"true"` + StatusAddress *StatusAddress `description:"Defines the Kubernetes Gateway status address." json:"statusAddress,omitempty" toml:"statusAddress,omitempty" yaml:"statusAddress,omitempty" export:"true"` + NativeLBByDefault bool `description:"Defines whether to use Native Kubernetes load-balancing by default." json:"nativeLBByDefault,omitempty" toml:"nativeLBByDefault,omitempty" yaml:"nativeLBByDefault,omitempty" export:"true"` + CrossProviderNamespaces []string `description:"List of namespaces from which Gateway API routes are allowed to declare TraefikService backendRef references." json:"crossProviderNamespaces,omitempty" toml:"crossProviderNamespaces,omitempty" yaml:"crossProviderNamespaces,omitempty" export:"true"` + RetryMaxRequestBodyBytes int64 `description:"Defines the maximum request body size (in bytes) buffered for HTTPRoute retries. If negative, the limit is disabled." json:"retryMaxRequestBodyBytes,omitempty" toml:"retryMaxRequestBodyBytes,omitempty" yaml:"retryMaxRequestBodyBytes,omitempty" export:"true"` EntryPoints map[string]Entrypoint `json:"-" toml:"-" yaml:"-" label:"-" file:"-"` @@ -114,6 +115,7 @@ type Provider struct { func (p *Provider) SetDefaults() { p.QPS = 50 // the default value for the QPS is 10x the default Kubernetes client QPS value. p.Burst = 100 // the default value for the Burst is 10x the default Kubernetes client Burst value. + p.RetryMaxRequestBodyBytes = dynamic.RetryDefaultMaxRequestBodyBytes } // Entrypoint defines the available entry points. diff --git a/pkg/provider/kubernetes/gateway/kubernetes_test.go b/pkg/provider/kubernetes/gateway/kubernetes_test.go index 2aa5ba08ae..f1a5a75688 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes_test.go +++ b/pkg/provider/kubernetes/gateway/kubernetes_test.go @@ -220,12 +220,13 @@ func TestGatewayScoping(t *testing.T) { func TestLoadHTTPRoutes(t *testing.T) { testCases := []struct { - desc string - ingressClass string - paths []string - expected *dynamic.Configuration - entryPoints map[string]Entrypoint - nativeLB bool + desc string + ingressClass string + paths []string + expected *dynamic.Configuration + entryPoints map[string]Entrypoint + nativeLB bool + retryMaxRequestBodyBytes int64 }{ { desc: "Empty", @@ -2880,6 +2881,255 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, }, + { + desc: "Simple HTTPRoute, retry", + paths: []string{"services.yml", "httproute/with_retry.yml"}, + retryMaxRequestBodyBytes: dynamic.RetryDefaultMaxRequestBodyBytes, + entryPoints: map[string]Entrypoint{"web": { + Address: ":80", + }}, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "listener-web-http-5b3226d4ebd42b7200f1": { + EntryPoints: []string{"web"}, + Rule: `Host("*")`, + }, + "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-c683caccb22c43e5e664": { + ParentRefs: []string{"listener-web-http-5b3226d4ebd42b7200f1"}, + Service: "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-c683caccb22c43e5e664-wrr", + Rule: `Host("example.org") && PathPrefix("/")`, + Priority: 13, + RuleSyntax: "default", + Middlewares: []string{"httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-c683caccb22c43e5e664-requestheadermodifier-0", "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-c683caccb22c43e5e664-retry"}, + }, + }, + Middlewares: map[string]*dynamic.Middleware{ + "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-c683caccb22c43e5e664-requestheadermodifier-0": { + RequestHeaderModifier: &dynamic.HeaderModifier{ + Set: map[string]string{"X-Foo": "Bar"}, + Add: map[string]string{"X-Bar": "Foo"}, + Remove: []string{"X-Baz"}, + }, + }, + "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-c683caccb22c43e5e664-retry": { + Retry: &dynamic.Retry{ + Attempts: 4, + Status: []string{"500", "503"}, + InitialInterval: ptypes.Duration(100 * time.Millisecond), + MaxRequestBodyBytes: new(dynamic.RetryDefaultMaxRequestBodyBytes), + }, + }, + }, + Services: map[string]*dynamic.Service{ + "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-c683caccb22c43e5e664-wrr": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-c683caccb22c43e5e664-svc-default-whoami-0", + Weight: new(1), + }, + }, + }, + }, + "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-c683caccb22c43e5e664-svc-default-whoami-0": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Strategy: dynamic.BalancerStrategyWRR, + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + PassHostHeader: new(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, + }, + }, + { + desc: "Simple HTTPRoute, retry with maxRequestBodyBytes", + paths: []string{"services.yml", "httproute/with_retry.yml"}, + retryMaxRequestBodyBytes: 1024, + entryPoints: map[string]Entrypoint{"web": { + Address: ":80", + }}, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "listener-web-http-5b3226d4ebd42b7200f1": { + EntryPoints: []string{"web"}, + Rule: `Host("*")`, + }, + "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-c683caccb22c43e5e664": { + ParentRefs: []string{"listener-web-http-5b3226d4ebd42b7200f1"}, + Service: "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-c683caccb22c43e5e664-wrr", + Rule: `Host("example.org") && PathPrefix("/")`, + Priority: 13, + RuleSyntax: "default", + Middlewares: []string{"httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-c683caccb22c43e5e664-requestheadermodifier-0", "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-c683caccb22c43e5e664-retry"}, + }, + }, + Middlewares: map[string]*dynamic.Middleware{ + "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-c683caccb22c43e5e664-requestheadermodifier-0": { + RequestHeaderModifier: &dynamic.HeaderModifier{ + Set: map[string]string{"X-Foo": "Bar"}, + Add: map[string]string{"X-Bar": "Foo"}, + Remove: []string{"X-Baz"}, + }, + }, + "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-c683caccb22c43e5e664-retry": { + Retry: &dynamic.Retry{ + Attempts: 4, + Status: []string{"500", "503"}, + InitialInterval: ptypes.Duration(100 * time.Millisecond), + MaxRequestBodyBytes: new(int64(1024)), + }, + }, + }, + Services: map[string]*dynamic.Service{ + "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-c683caccb22c43e5e664-wrr": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-c683caccb22c43e5e664-svc-default-whoami-0", + Weight: new(1), + }, + }, + }, + }, + "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-c683caccb22c43e5e664-svc-default-whoami-0": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Strategy: dynamic.BalancerStrategyWRR, + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + PassHostHeader: new(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, + }, + }, + { + desc: "Simple HTTPRoute, retry with defaults", + paths: []string{"services.yml", "httproute/with_retry_empty.yml"}, + retryMaxRequestBodyBytes: dynamic.RetryDefaultMaxRequestBodyBytes, + entryPoints: map[string]Entrypoint{"web": { + Address: ":80", + }}, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "listener-web-http-5b3226d4ebd42b7200f1": { + EntryPoints: []string{"web"}, + Rule: `Host("*")`, + }, + "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-c683caccb22c43e5e664": { + ParentRefs: []string{"listener-web-http-5b3226d4ebd42b7200f1"}, + Service: "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-c683caccb22c43e5e664-wrr", + Rule: `Host("example.org") && PathPrefix("/")`, + Priority: 13, + RuleSyntax: "default", + Middlewares: []string{"httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-c683caccb22c43e5e664-retry"}, + }, + }, + Middlewares: map[string]*dynamic.Middleware{ + "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-c683caccb22c43e5e664-retry": { + Retry: &dynamic.Retry{ + Attempts: 2, + MaxRequestBodyBytes: new(dynamic.RetryDefaultMaxRequestBodyBytes), + }, + }, + }, + Services: map[string]*dynamic.Service{ + "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-c683caccb22c43e5e664-wrr": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-c683caccb22c43e5e664-svc-default-whoami-0", + Weight: new(1), + }, + }, + }, + }, + "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-c683caccb22c43e5e664-svc-default-whoami-0": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Strategy: dynamic.BalancerStrategyWRR, + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + PassHostHeader: new(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, + }, + }, { desc: "Simple HTTPRoute, response header modifier", paths: []string{"services.yml", "httproute/filter_response_header_modifier.yml"}, @@ -4894,9 +5144,10 @@ func TestLoadHTTPRoutes(t *testing.T) { } p := Provider{ - EntryPoints: test.entryPoints, - NativeLBByDefault: test.nativeLB, - client: client, + EntryPoints: test.entryPoints, + NativeLBByDefault: test.nativeLB, + RetryMaxRequestBodyBytes: test.retryMaxRequestBodyBytes, + client: client, } conf, _, err := p.loadConfigurationFromGateways(t.Context())