diff --git a/api/v1alpha1/healthcheck_types.go b/api/v1alpha1/healthcheck_types.go index 05f70937b3c..c2dd6a4b002 100644 --- a/api/v1alpha1/healthcheck_types.go +++ b/api/v1alpha1/healthcheck_types.go @@ -13,6 +13,60 @@ type HealthCheck struct { // Active health check configuration // +optional Active *ActiveHealthCheck `json:"active,omitempty"` + + // Passive passive check configuration + // +optional + Passive *PassiveHealthCheck `json:"passive,omitempty"` +} + +// PassiveHealthCheck defines the configuration for passive health checks in the context of Envoy's Outlier Detection, +// see https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/outlier +type PassiveHealthCheck struct { + + // SplitExternalLocalOriginErrors enables splitting of errors between external and local origin. + // + // +kubebuilder:default=false + // +optional + SplitExternalLocalOriginErrors *bool `json:"splitExternalLocalOriginErrors,omitempty"` + + // Interval defines the time between passive health checks. + // + // +kubebuilder:validation:Format=duration + // +kubebuilder:default="3s" + // +optional + Interval *metav1.Duration `json:"interval,omitempty"` + + // ConsecutiveLocalOriginFailures sets the number of consecutive local origin failures triggering ejection. + // Parameter takes effect only when split_external_local_origin_errors is set to true. + // + // +kubebuilder:default=5 + // +optional + ConsecutiveLocalOriginFailures *uint32 `json:"consecutiveLocalOriginFailures,omitempty"` + + // ConsecutiveGatewayErrors sets the number of consecutive gateway errors triggering ejection. + // + // +kubebuilder:default=0 + // +optional + ConsecutiveGatewayErrors *uint32 `json:"consecutiveGatewayErrors,omitempty"` + + // Consecutive5xxErrors sets the number of consecutive 5xx errors triggering ejection. + // + // +kubebuilder:default=5 + // +optional + Consecutive5xxErrors *uint32 `json:"consecutive5XxErrors,omitempty"` + + // BaseEjectionTime defines the base duration for which a host will be ejected on consecutive failures. + // + // +kubebuilder:validation:Format=duration + // +kubebuilder:default="30s" + // +optional + BaseEjectionTime *metav1.Duration `json:"baseEjectionTime,omitempty"` + + // MaxEjectionPercent sets the maximum percentage of hosts in a cluster that can be ejected. + // + // +kubebuilder:default=10 + // +optional + MaxEjectionPercent *int32 `json:"maxEjectionPercent,omitempty"` } // ActiveHealthCheck defines the active health check configuration. @@ -29,7 +83,7 @@ type ActiveHealthCheck struct { // +optional Timeout *metav1.Duration `json:"timeout"` - // Interval defines the time between health checks. + // Interval defines the time between active health checks. // // +kubebuilder:validation:Format=duration // +kubebuilder:default="3s" diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index adf3e8dfe3f..abd1571ac13 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1794,6 +1794,11 @@ func (in *HealthCheck) DeepCopyInto(out *HealthCheck) { *out = new(ActiveHealthCheck) (*in).DeepCopyInto(*out) } + if in.Passive != nil { + in, out := &in.Passive, &out.Passive + *out = new(PassiveHealthCheck) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HealthCheck. @@ -2359,6 +2364,56 @@ func (in *OpenTelemetryEnvoyProxyAccessLog) DeepCopy() *OpenTelemetryEnvoyProxyA return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PassiveHealthCheck) DeepCopyInto(out *PassiveHealthCheck) { + *out = *in + if in.SplitExternalLocalOriginErrors != nil { + in, out := &in.SplitExternalLocalOriginErrors, &out.SplitExternalLocalOriginErrors + *out = new(bool) + **out = **in + } + if in.Interval != nil { + in, out := &in.Interval, &out.Interval + *out = new(v1.Duration) + **out = **in + } + if in.ConsecutiveLocalOriginFailures != nil { + in, out := &in.ConsecutiveLocalOriginFailures, &out.ConsecutiveLocalOriginFailures + *out = new(uint32) + **out = **in + } + if in.ConsecutiveGatewayErrors != nil { + in, out := &in.ConsecutiveGatewayErrors, &out.ConsecutiveGatewayErrors + *out = new(uint32) + **out = **in + } + if in.Consecutive5xxErrors != nil { + in, out := &in.Consecutive5xxErrors, &out.Consecutive5xxErrors + *out = new(uint32) + **out = **in + } + if in.BaseEjectionTime != nil { + in, out := &in.BaseEjectionTime, &out.BaseEjectionTime + *out = new(v1.Duration) + **out = **in + } + if in.MaxEjectionPercent != nil { + in, out := &in.MaxEjectionPercent, &out.MaxEjectionPercent + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PassiveHealthCheck. +func (in *PassiveHealthCheck) DeepCopy() *PassiveHealthCheck { + if in == nil { + return nil + } + out := new(PassiveHealthCheck) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PathSettings) DeepCopyInto(out *PathSettings) { *out = *in diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml index d36b1fe290b..84a144de160 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml @@ -234,7 +234,8 @@ spec: type: object interval: default: 3s - description: Interval defines the time between health checks. + description: Interval defines the time between active health + checks. format: duration type: string tcp: @@ -337,6 +338,53 @@ spec: - message: If Health Checker type is TCP, tcp field needs to be set. rule: 'self.type == ''TCP'' ? has(self.tcp) : !has(self.tcp)' + passive: + description: Passive passive check configuration + properties: + baseEjectionTime: + default: 30s + description: BaseEjectionTime defines the base duration for + which a host will be ejected on consecutive failures. + format: duration + type: string + consecutive5XxErrors: + default: 5 + description: Consecutive5xxErrors sets the number of consecutive + 5xx errors triggering ejection. + format: int32 + type: integer + consecutiveGatewayErrors: + default: 0 + description: ConsecutiveGatewayErrors sets the number of consecutive + gateway errors triggering ejection. + format: int32 + type: integer + consecutiveLocalOriginFailures: + default: 5 + description: ConsecutiveLocalOriginFailures sets the number + of consecutive local origin failures triggering ejection. + Parameter takes effect only when split_external_local_origin_errors + is set to true. + format: int32 + type: integer + interval: + default: 3s + description: Interval defines the time between passive health + checks. + format: duration + type: string + maxEjectionPercent: + default: 10 + description: MaxEjectionPercent sets the maximum percentage + of hosts in a cluster that can be ejected. + format: int32 + type: integer + splitExternalLocalOriginErrors: + default: false + description: SplitExternalLocalOriginErrors enables splitting + of errors between external and local origin. + type: boolean + type: object type: object loadBalancer: description: LoadBalancer policy to apply when routing traffic from diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go index 34cd5de4044..d5d71b1fd38 100644 --- a/internal/gatewayapi/backendtrafficpolicy.go +++ b/internal/gatewayapi/backendtrafficpolicy.go @@ -699,12 +699,44 @@ func (t *Translator) buildProxyProtocol(policy *egv1a1.BackendTrafficPolicy) *ir } func (t *Translator) buildHealthCheck(policy *egv1a1.BackendTrafficPolicy) *ir.HealthCheck { + if policy.Spec.HealthCheck == nil { + return nil + } + irhc := &ir.HealthCheck{} + if policy.Spec.HealthCheck.Passive != nil { + irhc.Passive = t.buildPassiveHealthCheck(policy) + } + if policy.Spec.HealthCheck.Active != nil { + irhc.Active = t.buildActiveHealthCheck(policy) + } + return irhc +} + +func (t *Translator) buildPassiveHealthCheck(policy *egv1a1.BackendTrafficPolicy) *ir.OutlierDetection { + if policy.Spec.HealthCheck == nil || policy.Spec.HealthCheck.Passive == nil { + return nil + } + + hc := policy.Spec.HealthCheck.Passive + irOD := &ir.OutlierDetection{ + Interval: hc.Interval, + SplitExternalLocalOriginErrors: hc.SplitExternalLocalOriginErrors, + ConsecutiveLocalOriginFailures: hc.ConsecutiveLocalOriginFailures, + ConsecutiveGatewayErrors: hc.ConsecutiveGatewayErrors, + Consecutive5xxErrors: hc.Consecutive5xxErrors, + BaseEjectionTime: hc.BaseEjectionTime, + MaxEjectionPercent: hc.MaxEjectionPercent, + } + return irOD +} + +func (t *Translator) buildActiveHealthCheck(policy *egv1a1.BackendTrafficPolicy) *ir.ActiveHealthCheck { if policy.Spec.HealthCheck == nil || policy.Spec.HealthCheck.Active == nil { return nil } hc := policy.Spec.HealthCheck.Active - irHC := &ir.HealthCheck{ + irHC := &ir.ActiveHealthCheck{ Timeout: hc.Timeout, Interval: hc.Interval, UnhealthyThreshold: hc.UnhealthyThreshold, diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-healthcheck.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-healthcheck.in.yaml index 486f9c5abcb..e0b71ac1328 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-with-healthcheck.in.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-healthcheck.in.yaml @@ -128,6 +128,10 @@ backendTrafficPolicies: expectedResponse: type: Binary binary: RXZlcnl0aGluZyBPSw== + passive: + baseEjectionTime: 160s + interval: 2s + maxEjectionPercent: 100 - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: BackendTrafficPolicy metadata: @@ -155,6 +159,10 @@ backendTrafficPolicies: expectedResponse: type: Text text: pong + passive: + baseEjectionTime: 150s + interval: 1s + maxEjectionPercent: 100 - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: BackendTrafficPolicy metadata: @@ -180,6 +188,10 @@ backendTrafficPolicies: receive: type: Text text: pong + passive: + baseEjectionTime: 180s + interval: 1s + maxEjectionPercent: 100 - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: BackendTrafficPolicy metadata: @@ -205,3 +217,7 @@ backendTrafficPolicies: receive: type: Binary binary: RXZlcnl0aGluZyBPSw== + passive: + baseEjectionTime: 160s + interval: 8ms + maxEjectionPercent: 11 diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-healthcheck.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-healthcheck.out.yaml index 4a48cda66a5..c17d82f6c58 100755 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-with-healthcheck.out.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-healthcheck.out.yaml @@ -22,6 +22,10 @@ backendTrafficPolicies: timeout: 1s type: HTTP unhealthyThreshold: 3 + passive: + baseEjectionTime: 2m30s + interval: 1s + maxEjectionPercent: 100 targetRef: group: gateway.networking.k8s.io kind: HTTPRoute @@ -55,6 +59,10 @@ backendTrafficPolicies: timeout: 1s type: TCP unhealthyThreshold: 3 + passive: + baseEjectionTime: 3m0s + interval: 1s + maxEjectionPercent: 100 targetRef: group: gateway.networking.k8s.io kind: HTTPRoute @@ -88,6 +96,10 @@ backendTrafficPolicies: timeout: 1s type: TCP unhealthyThreshold: 3 + passive: + baseEjectionTime: 2m40s + interval: 8ms + maxEjectionPercent: 11 targetRef: group: gateway.networking.k8s.io kind: HTTPRoute @@ -123,6 +135,10 @@ backendTrafficPolicies: timeout: 500ms type: HTTP unhealthyThreshold: 3 + passive: + baseEjectionTime: 2m40s + interval: 2s + maxEjectionPercent: 100 targetRef: group: gateway.networking.k8s.io kind: Gateway @@ -425,18 +441,23 @@ xdsIR: protocol: GRPC weight: 1 healthCheck: - healthyThreshold: 1 - http: - expectedResponse: - binary: RXZlcnl0aGluZyBPSw== - expectedStatuses: - - 200 - - 300 - method: GET - path: /healthz - interval: 3s - timeout: 500ms - unhealthyThreshold: 3 + active: + healthyThreshold: 1 + http: + expectedResponse: + binary: RXZlcnl0aGluZyBPSw== + expectedStatuses: + - 200 + - 300 + method: GET + path: /healthz + interval: 3s + timeout: 500ms + unhealthyThreshold: 3 + passive: + baseEjectionTime: 2m40s + interval: 2s + maxEjectionPercent: 100 hostname: '*' name: grpcroute/default/grpcroute-1/rule/0/match/-1/* envoy-gateway/gateway-2: @@ -467,15 +488,20 @@ xdsIR: protocol: HTTP weight: 1 healthCheck: - healthyThreshold: 3 - interval: 5s - tcp: - receive: - text: pong - send: - text: ping - timeout: 1s - unhealthyThreshold: 3 + active: + healthyThreshold: 3 + interval: 5s + tcp: + receive: + text: pong + send: + text: ping + timeout: 1s + unhealthyThreshold: 3 + passive: + baseEjectionTime: 3m0s + interval: 1s + maxEjectionPercent: 100 hostname: gateway.envoyproxy.io name: httproute/default/httproute-2/rule/0/match/0/gateway_envoyproxy_io pathMatch: @@ -495,15 +521,20 @@ xdsIR: protocol: HTTP weight: 1 healthCheck: - healthyThreshold: 1 - interval: 3s - tcp: - receive: - binary: RXZlcnl0aGluZyBPSw== - send: - binary: cGluZw== - timeout: 1s - unhealthyThreshold: 3 + active: + healthyThreshold: 1 + interval: 3s + tcp: + receive: + binary: RXZlcnl0aGluZyBPSw== + send: + binary: cGluZw== + timeout: 1s + unhealthyThreshold: 3 + passive: + baseEjectionTime: 2m40s + interval: 8ms + maxEjectionPercent: 11 hostname: gateway.envoyproxy.io name: httproute/default/httproute-3/rule/0/match/0/gateway_envoyproxy_io pathMatch: @@ -523,18 +554,23 @@ xdsIR: protocol: HTTP weight: 1 healthCheck: - healthyThreshold: 3 - http: - expectedResponse: - text: pong - expectedStatuses: - - 200 - - 201 - method: GET - path: /healthz - interval: 5s - timeout: 1s - unhealthyThreshold: 3 + active: + healthyThreshold: 3 + http: + expectedResponse: + text: pong + expectedStatuses: + - 200 + - 201 + method: GET + path: /healthz + interval: 5s + timeout: 1s + unhealthyThreshold: 3 + passive: + baseEjectionTime: 2m30s + interval: 1s + maxEjectionPercent: 100 hostname: gateway.envoyproxy.io name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io pathMatch: diff --git a/internal/ir/xds.go b/internal/ir/xds.go index 04941dfe86a..1d1f3efa5ab 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -24,39 +24,41 @@ import ( ) var ( - ErrListenerNameEmpty = errors.New("field Name must be specified") - ErrListenerAddressInvalid = errors.New("field Address must be a valid IP address") - ErrListenerPortInvalid = errors.New("field Port specified is invalid") - ErrHTTPListenerHostnamesEmpty = errors.New("field Hostnames must be specified with at least a single hostname entry") - ErrTCPListenerSNIsEmpty = errors.New("field SNIs must be specified with at least a single server name entry") - ErrTLSServerCertEmpty = errors.New("field ServerCertificate must be specified") - ErrTLSPrivateKey = errors.New("field PrivateKey must be specified") - ErrHTTPRouteNameEmpty = errors.New("field Name must be specified") - ErrHTTPRouteHostnameEmpty = errors.New("field Hostname must be specified") - ErrDestinationNameEmpty = errors.New("field Name must be specified") - ErrDestEndpointHostInvalid = errors.New("field Address must be a valid IP or FQDN address") - ErrDestEndpointPortInvalid = errors.New("field Port specified is invalid") - ErrStringMatchConditionInvalid = errors.New("only one of the Exact, Prefix, SafeRegex or Distinct fields must be set") - ErrStringMatchNameIsEmpty = errors.New("field Name must be specified") - ErrDirectResponseStatusInvalid = errors.New("only HTTP status codes 100 - 599 are supported for DirectResponse") - ErrRedirectUnsupportedStatus = errors.New("only HTTP status codes 301 and 302 are supported for redirect filters") - ErrRedirectUnsupportedScheme = errors.New("only http and https are supported for the scheme in redirect filters") - ErrHTTPPathModifierDoubleReplace = errors.New("redirect filter cannot have a path modifier that supplies both fullPathReplace and prefixMatchReplace") - ErrHTTPPathModifierNoReplace = errors.New("redirect filter cannot have a path modifier that does not supply either fullPathReplace or prefixMatchReplace") - ErrAddHeaderEmptyName = errors.New("header modifier filter cannot configure a header without a name to be added") - ErrAddHeaderDuplicate = errors.New("header modifier filter attempts to add the same header more than once (case insensitive)") - ErrRemoveHeaderDuplicate = errors.New("header modifier filter attempts to remove the same header more than once (case insensitive)") - ErrLoadBalancerInvalid = errors.New("loadBalancer setting is invalid, only one setting can be set") - ErrHealthCheckTimeoutInvalid = errors.New("field HealthCheck.Timeout must be specified") - ErrHealthCheckIntervalInvalid = errors.New("field HealthCheck.Interval must be specified") - 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") - 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") - ErrHealthCheckPayloadInvalid = errors.New("one of Text, Binary fields must be set in payload") - ErrHTTPStatusInvalid = errors.New("HTTPStatus should be in [200,600)") + ErrListenerNameEmpty = errors.New("field Name must be specified") + ErrListenerAddressInvalid = errors.New("field Address must be a valid IP address") + ErrListenerPortInvalid = errors.New("field Port specified is invalid") + ErrHTTPListenerHostnamesEmpty = errors.New("field Hostnames must be specified with at least a single hostname entry") + ErrTCPListenerSNIsEmpty = errors.New("field SNIs must be specified with at least a single server name entry") + ErrTLSServerCertEmpty = errors.New("field ServerCertificate must be specified") + ErrTLSPrivateKey = errors.New("field PrivateKey must be specified") + ErrHTTPRouteNameEmpty = errors.New("field Name must be specified") + ErrHTTPRouteHostnameEmpty = errors.New("field Hostname must be specified") + ErrDestinationNameEmpty = errors.New("field Name must be specified") + ErrDestEndpointHostInvalid = errors.New("field Address must be a valid IP or FQDN address") + ErrDestEndpointPortInvalid = errors.New("field Port specified is invalid") + ErrStringMatchConditionInvalid = errors.New("only one of the Exact, Prefix, SafeRegex or Distinct fields must be set") + ErrStringMatchNameIsEmpty = errors.New("field Name must be specified") + ErrDirectResponseStatusInvalid = errors.New("only HTTP status codes 100 - 599 are supported for DirectResponse") + ErrRedirectUnsupportedStatus = errors.New("only HTTP status codes 301 and 302 are supported for redirect filters") + ErrRedirectUnsupportedScheme = errors.New("only http and https are supported for the scheme in redirect filters") + ErrHTTPPathModifierDoubleReplace = errors.New("redirect filter cannot have a path modifier that supplies both fullPathReplace and prefixMatchReplace") + ErrHTTPPathModifierNoReplace = errors.New("redirect filter cannot have a path modifier that does not supply either fullPathReplace or prefixMatchReplace") + ErrAddHeaderEmptyName = errors.New("header modifier filter cannot configure a header without a name to be added") + ErrAddHeaderDuplicate = errors.New("header modifier filter attempts to add the same header more than once (case insensitive)") + ErrRemoveHeaderDuplicate = errors.New("header modifier filter attempts to remove the same header more than once (case insensitive)") + ErrLoadBalancerInvalid = errors.New("loadBalancer setting is invalid, only one setting can be set") + ErrHealthCheckTimeoutInvalid = errors.New("field HealthCheck.Timeout must be specified") + ErrHealthCheckIntervalInvalid = errors.New("field HealthCheck.Interval must be specified") + 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") + 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") + ErrHealthCheckPayloadInvalid = errors.New("one of Text, Binary fields must be set in payload") + ErrHTTPStatusInvalid = errors.New("HTTPStatus should be in [200,600)") + ErrOutlierDetectionBaseEjectionTimeInvalid = errors.New("field OutlierDetection.BaseEjectionTime must be specified") + ErrOutlierDetectionIntervalInvalid = errors.New("field OutlierDetection.Interval must be specified") redacted = []byte("[redacted]") ) @@ -418,7 +420,7 @@ type HTTPRoute struct { BasicAuth *BasicAuth `json:"basicAuth,omitempty" yaml:"basicAuth,omitempty"` // ExtAuth defines the schema for the external authorization. ExtAuth *ExtAuth `json:"extAuth,omitempty" yaml:"extAuth,omitempty"` - // HealthCheck defines the configuration for active health checking on the upstream. + // HealthCheck defines the configuration for health checking on the upstream. HealthCheck *HealthCheck `json:"healthCheck,omitempty" yaml:"healthCheck,omitempty"` // FaultInjection defines the schema for injecting faults into HTTP requests. FaultInjection *FaultInjection `json:"faultInjection,omitempty" yaml:"faultInjection,omitempty"` @@ -1401,9 +1403,36 @@ type CircuitBreaker struct { // HealthCheck defines health check settings // +k8s:deepcopy-gen=true type HealthCheck struct { + Active *ActiveHealthCheck `json:"active,omitempty" yaml:"active,omitempty"` + + Passive *OutlierDetection `json:"passive,omitempty" yaml:"passive,omitempty"` +} + +// OutlierDetection defines passive health check settings +// +k8s:deepcopy-gen=true +type OutlierDetection struct { + // Interval defines the time between passive health checks. + Interval *metav1.Duration `json:"interval,omitempty"` + // SplitExternalLocalOriginErrors enables splitting of errors between external and local origin. + SplitExternalLocalOriginErrors *bool `json:"splitExternalLocalOriginErrors,omitempty" yaml:"splitExternalLocalOriginErrors,omitempty"` + // ConsecutiveLocalOriginFailures sets the number of consecutive local origin failures triggering ejection. + ConsecutiveLocalOriginFailures *uint32 `json:"consecutiveLocalOriginFailures,omitempty" yaml:"consecutiveLocalOriginFailures,omitempty"` + // ConsecutiveGatewayErrors sets the number of consecutive gateway errors triggering ejection. + ConsecutiveGatewayErrors *uint32 `json:"consecutiveGatewayErrors,omitempty" yaml:"consecutiveGatewayErrors,omitempty"` + // Consecutive5xxErrors sets the number of consecutive 5xx errors triggering ejection. + Consecutive5xxErrors *uint32 `json:"consecutive5XxErrors,omitempty" yaml:"consecutive5XxErrors,omitempty"` + // BaseEjectionTime defines the base duration for which a host will be ejected on consecutive failures. + BaseEjectionTime *metav1.Duration `json:"baseEjectionTime,omitempty" yaml:"baseEjectionTime,omitempty"` + // MaxEjectionPercent sets the maximum percentage of hosts in a cluster that can be ejected. + MaxEjectionPercent *int32 `json:"maxEjectionPercent,omitempty" yaml:"maxEjectionPercent,omitempty"` +} + +// ActiveHealthCheck defines active health check settings +// +k8s:deepcopy-gen=true +type ActiveHealthCheck struct { // Timeout defines the time to wait for a health check response. Timeout *metav1.Duration `json:"timeout"` - // Interval defines the time between health checks. + // Interval defines the time between active health checks. Interval *metav1.Duration `json:"interval"` // UnhealthyThreshold defines the number of unhealthy health checks required before a backend host is marked unhealthy. UnhealthyThreshold *uint32 `json:"unhealthyThreshold"` @@ -1418,39 +1447,50 @@ type HealthCheck struct { // Validate the fields within the HealthCheck structure. func (h *HealthCheck) Validate() error { var errs error + if h.Active != nil { + if h.Active.Timeout != nil && h.Active.Timeout.Duration == 0 { + errs = errors.Join(errs, ErrHealthCheckTimeoutInvalid) + } + if h.Active.Interval != nil && h.Active.Interval.Duration == 0 { + errs = errors.Join(errs, ErrHealthCheckIntervalInvalid) + } + if h.Active.UnhealthyThreshold != nil && *h.Active.UnhealthyThreshold == 0 { + errs = errors.Join(errs, ErrHealthCheckUnhealthyThresholdInvalid) + } + if h.Active.HealthyThreshold != nil && *h.Active.HealthyThreshold == 0 { + errs = errors.Join(errs, ErrHealthCheckHealthyThresholdInvalid) + } - if h.Timeout != nil && h.Timeout.Duration == 0 { - errs = errors.Join(errs, ErrHealthCheckTimeoutInvalid) - } - if h.Interval != nil && h.Interval.Duration == 0 { - errs = errors.Join(errs, ErrHealthCheckIntervalInvalid) - } - if h.UnhealthyThreshold != nil && *h.UnhealthyThreshold == 0 { - errs = errors.Join(errs, ErrHealthCheckUnhealthyThresholdInvalid) - } - if h.HealthyThreshold != nil && *h.HealthyThreshold == 0 { - errs = errors.Join(errs, ErrHealthCheckHealthyThresholdInvalid) - } + matchCount := 0 + if h.Active.HTTP != nil { + matchCount++ + } + if h.Active.TCP != nil { + matchCount++ + } + if matchCount > 1 { + errs = errors.Join(errs, ErrHealthCheckerInvalid) + } - matchCount := 0 - if h.HTTP != nil { - matchCount++ - } - if h.TCP != nil { - matchCount++ - } - if matchCount != 1 { - errs = errors.Join(errs, ErrHealthCheckerInvalid) + if h.Active.HTTP != nil { + if err := h.Active.HTTP.Validate(); err != nil { + errs = errors.Join(errs, err) + } + } + if h.Active.TCP != nil { + if err := h.Active.TCP.Validate(); err != nil { + errs = errors.Join(errs, err) + } + } } - if h.HTTP != nil { - if err := h.HTTP.Validate(); err != nil { - errs = errors.Join(errs, err) + if h.Passive != nil { + if h.Passive.BaseEjectionTime != nil && h.Passive.BaseEjectionTime.Duration == 0 { + errs = errors.Join(errs, ErrOutlierDetectionBaseEjectionTimeInvalid) } - } - if h.TCP != nil { - if err := h.TCP.Validate(); err != nil { - errs = errors.Join(errs, err) + + if h.Passive.Interval != nil && h.Passive.Interval.Duration == 0 { + errs = errors.Join(errs, ErrOutlierDetectionIntervalInvalid) } } diff --git a/internal/ir/xds_test.go b/internal/ir/xds_test.go index c2a55a4ea2e..c9f2bed7411 100644 --- a/internal/ir/xds_test.go +++ b/internal/ir/xds_test.go @@ -1251,7 +1251,7 @@ func TestValidateHealthCheck(t *testing.T) { }{ { name: "invalid timeout", - input: HealthCheck{ + input: HealthCheck{&ActiveHealthCheck{ Timeout: &metav1.Duration{Duration: time.Duration(0)}, Interval: &metav1.Duration{Duration: time.Second}, UnhealthyThreshold: ptr.To[uint32](3), @@ -1260,12 +1260,14 @@ func TestValidateHealthCheck(t *testing.T) { Path: "/healthz", ExpectedStatuses: []HTTPStatus{200, 400}, }, + }, + &OutlierDetection{}, }, want: ErrHealthCheckTimeoutInvalid, }, { name: "invalid interval", - input: HealthCheck{ + input: HealthCheck{&ActiveHealthCheck{ Timeout: &metav1.Duration{Duration: time.Second}, Interval: &metav1.Duration{Duration: time.Duration(0)}, UnhealthyThreshold: ptr.To[uint32](3), @@ -1275,12 +1277,14 @@ func TestValidateHealthCheck(t *testing.T) { Method: ptr.To(http.MethodGet), ExpectedStatuses: []HTTPStatus{200, 400}, }, + }, + &OutlierDetection{}, }, want: ErrHealthCheckIntervalInvalid, }, { name: "invalid unhealthy threshold", - input: HealthCheck{ + input: HealthCheck{&ActiveHealthCheck{ Timeout: &metav1.Duration{Duration: time.Second}, Interval: &metav1.Duration{Duration: time.Second}, UnhealthyThreshold: ptr.To[uint32](0), @@ -1290,12 +1294,14 @@ func TestValidateHealthCheck(t *testing.T) { Method: ptr.To(http.MethodPatch), ExpectedStatuses: []HTTPStatus{200, 400}, }, + }, + &OutlierDetection{}, }, want: ErrHealthCheckUnhealthyThresholdInvalid, }, { name: "invalid healthy threshold", - input: HealthCheck{ + input: HealthCheck{&ActiveHealthCheck{ Timeout: &metav1.Duration{Duration: time.Second}, Interval: &metav1.Duration{Duration: time.Second}, UnhealthyThreshold: ptr.To[uint32](3), @@ -1305,12 +1311,14 @@ func TestValidateHealthCheck(t *testing.T) { Method: ptr.To(http.MethodPost), ExpectedStatuses: []HTTPStatus{200, 400}, }, + }, + &OutlierDetection{}, }, want: ErrHealthCheckHealthyThresholdInvalid, }, { name: "http-health-check: invalid path", - input: HealthCheck{ + input: HealthCheck{&ActiveHealthCheck{ Timeout: &metav1.Duration{Duration: time.Second}, Interval: &metav1.Duration{Duration: time.Second}, UnhealthyThreshold: ptr.To[uint32](3), @@ -1320,12 +1328,14 @@ func TestValidateHealthCheck(t *testing.T) { Method: ptr.To(http.MethodPut), ExpectedStatuses: []HTTPStatus{200, 400}, }, + }, + &OutlierDetection{}, }, want: ErrHCHTTPPathInvalid, }, { name: "http-health-check: invalid method", - input: HealthCheck{ + input: HealthCheck{&ActiveHealthCheck{ Timeout: &metav1.Duration{Duration: time.Second}, Interval: &metav1.Duration{Duration: time.Second}, UnhealthyThreshold: ptr.To(uint32(3)), @@ -1335,12 +1345,14 @@ func TestValidateHealthCheck(t *testing.T) { Method: ptr.To(http.MethodConnect), ExpectedStatuses: []HTTPStatus{200, 400}, }, + }, + &OutlierDetection{}, }, want: ErrHCHTTPMethodInvalid, }, { name: "http-health-check: invalid expected-statuses", - input: HealthCheck{ + input: HealthCheck{&ActiveHealthCheck{ Timeout: &metav1.Duration{Duration: time.Second}, Interval: &metav1.Duration{Duration: time.Second}, UnhealthyThreshold: ptr.To(uint32(3)), @@ -1350,12 +1362,14 @@ func TestValidateHealthCheck(t *testing.T) { Method: ptr.To(http.MethodDelete), ExpectedStatuses: []HTTPStatus{}, }, + }, + &OutlierDetection{}, }, want: ErrHCHTTPExpectedStatusesInvalid, }, { name: "http-health-check: invalid range", - input: HealthCheck{ + input: HealthCheck{&ActiveHealthCheck{ Timeout: &metav1.Duration{Duration: time.Second}, Interval: &metav1.Duration{Duration: time.Second}, UnhealthyThreshold: ptr.To(uint32(3)), @@ -1365,12 +1379,14 @@ func TestValidateHealthCheck(t *testing.T) { Method: ptr.To(http.MethodHead), ExpectedStatuses: []HTTPStatus{100, 600}, }, + }, + &OutlierDetection{}, }, want: ErrHTTPStatusInvalid, }, { name: "http-health-check: invalid expected-responses", - input: HealthCheck{ + input: HealthCheck{&ActiveHealthCheck{ Timeout: &metav1.Duration{Duration: time.Second}, Interval: &metav1.Duration{Duration: time.Second}, UnhealthyThreshold: ptr.To(uint32(3)), @@ -1384,12 +1400,14 @@ func TestValidateHealthCheck(t *testing.T) { Binary: []byte{'f', 'o', 'o'}, }, }, + }, + &OutlierDetection{}, }, want: ErrHealthCheckPayloadInvalid, }, { name: "tcp-health-check: invalid send payload", - input: HealthCheck{ + input: HealthCheck{&ActiveHealthCheck{ Timeout: &metav1.Duration{Duration: time.Second}, Interval: &metav1.Duration{Duration: time.Second}, UnhealthyThreshold: ptr.To(uint32(3)), @@ -1403,12 +1421,14 @@ func TestValidateHealthCheck(t *testing.T) { Text: ptr.To("foo"), }, }, + }, + &OutlierDetection{}, }, want: ErrHealthCheckPayloadInvalid, }, { name: "tcp-health-check: invalid receive payload", - input: HealthCheck{ + input: HealthCheck{&ActiveHealthCheck{ Timeout: &metav1.Duration{Duration: time.Second}, Interval: &metav1.Duration{Duration: time.Second}, UnhealthyThreshold: ptr.To(uint32(3)), @@ -1422,9 +1442,31 @@ func TestValidateHealthCheck(t *testing.T) { Binary: []byte{'f', 'o', 'o'}, }, }, + }, + &OutlierDetection{}, }, want: ErrHealthCheckPayloadInvalid, }, + { + name: "OutlierDetection invalid interval", + input: HealthCheck{&ActiveHealthCheck{}, + &OutlierDetection{ + Interval: &metav1.Duration{Duration: time.Duration(0)}, + BaseEjectionTime: &metav1.Duration{Duration: time.Second}, + }, + }, + want: ErrOutlierDetectionIntervalInvalid, + }, + { + name: "OutlierDetection invalid BaseEjectionTime", + input: HealthCheck{&ActiveHealthCheck{}, + &OutlierDetection{ + Interval: &metav1.Duration{Duration: time.Second}, + BaseEjectionTime: &metav1.Duration{Duration: time.Duration(0)}, + }, + }, + want: ErrOutlierDetectionBaseEjectionTimeInvalid, + }, } for i := range tests { test := tests[i] diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index 27456ccd5d7..a60b841001f 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -63,6 +63,51 @@ func (in *AccessLog) DeepCopy() *AccessLog { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveHealthCheck) DeepCopyInto(out *ActiveHealthCheck) { + *out = *in + if in.Timeout != nil { + in, out := &in.Timeout, &out.Timeout + *out = new(v1.Duration) + **out = **in + } + if in.Interval != nil { + in, out := &in.Interval, &out.Interval + *out = new(v1.Duration) + **out = **in + } + if in.UnhealthyThreshold != nil { + in, out := &in.UnhealthyThreshold, &out.UnhealthyThreshold + *out = new(uint32) + **out = **in + } + if in.HealthyThreshold != nil { + in, out := &in.HealthyThreshold, &out.HealthyThreshold + *out = new(uint32) + **out = **in + } + if in.HTTP != nil { + in, out := &in.HTTP, &out.HTTP + *out = new(HTTPHealthChecker) + (*in).DeepCopyInto(*out) + } + if in.TCP != nil { + in, out := &in.TCP, &out.TCP + *out = new(TCPHealthChecker) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveHealthCheck. +func (in *ActiveHealthCheck) DeepCopy() *ActiveHealthCheck { + if in == nil { + return nil + } + out := new(ActiveHealthCheck) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AddHeader) DeepCopyInto(out *AddHeader) { *out = *in @@ -865,34 +910,14 @@ func (in *HTTPTimeout) DeepCopy() *HTTPTimeout { // 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 - if in.Timeout != nil { - in, out := &in.Timeout, &out.Timeout - *out = new(v1.Duration) - **out = **in - } - if in.Interval != nil { - in, out := &in.Interval, &out.Interval - *out = new(v1.Duration) - **out = **in - } - if in.UnhealthyThreshold != nil { - in, out := &in.UnhealthyThreshold, &out.UnhealthyThreshold - *out = new(uint32) - **out = **in - } - if in.HealthyThreshold != nil { - in, out := &in.HealthyThreshold, &out.HealthyThreshold - *out = new(uint32) - **out = **in - } - if in.HTTP != nil { - in, out := &in.HTTP, &out.HTTP - *out = new(HTTPHealthChecker) + if in.Active != nil { + in, out := &in.Active, &out.Active + *out = new(ActiveHealthCheck) (*in).DeepCopyInto(*out) } - if in.TCP != nil { - in, out := &in.TCP, &out.TCP - *out = new(TCPHealthChecker) + if in.Passive != nil { + in, out := &in.Passive, &out.Passive + *out = new(OutlierDetection) (*in).DeepCopyInto(*out) } } @@ -1238,6 +1263,56 @@ func (in *OpenTelemetryAccessLog) DeepCopy() *OpenTelemetryAccessLog { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OutlierDetection) DeepCopyInto(out *OutlierDetection) { + *out = *in + if in.Interval != nil { + in, out := &in.Interval, &out.Interval + *out = new(v1.Duration) + **out = **in + } + if in.SplitExternalLocalOriginErrors != nil { + in, out := &in.SplitExternalLocalOriginErrors, &out.SplitExternalLocalOriginErrors + *out = new(bool) + **out = **in + } + if in.ConsecutiveLocalOriginFailures != nil { + in, out := &in.ConsecutiveLocalOriginFailures, &out.ConsecutiveLocalOriginFailures + *out = new(uint32) + **out = **in + } + if in.ConsecutiveGatewayErrors != nil { + in, out := &in.ConsecutiveGatewayErrors, &out.ConsecutiveGatewayErrors + *out = new(uint32) + **out = **in + } + if in.Consecutive5xxErrors != nil { + in, out := &in.Consecutive5xxErrors, &out.Consecutive5xxErrors + *out = new(uint32) + **out = **in + } + if in.BaseEjectionTime != nil { + in, out := &in.BaseEjectionTime, &out.BaseEjectionTime + *out = new(v1.Duration) + **out = **in + } + if in.MaxEjectionPercent != nil { + in, out := &in.MaxEjectionPercent, &out.MaxEjectionPercent + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OutlierDetection. +func (in *OutlierDetection) DeepCopy() *OutlierDetection { + if in == nil { + return nil + } + out := new(OutlierDetection) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PathSettings) DeepCopyInto(out *PathSettings) { *out = *in diff --git a/internal/xds/translator/cluster.go b/internal/xds/translator/cluster.go index abf0302cda8..3b2dee97941 100644 --- a/internal/xds/translator/cluster.go +++ b/internal/xds/translator/cluster.go @@ -136,44 +136,13 @@ func buildXdsCluster(args *xdsClusterArgs) *clusterv3.Cluster { cluster.LbPolicy = clusterv3.Cluster_MAGLEV } - if args.healthCheck != nil { - hc := &corev3.HealthCheck{ - Timeout: durationpb.New(args.healthCheck.Timeout.Duration), - Interval: durationpb.New(args.healthCheck.Interval.Duration), - } - if args.healthCheck.UnhealthyThreshold != nil { - hc.UnhealthyThreshold = wrapperspb.UInt32(*args.healthCheck.UnhealthyThreshold) - } - if args.healthCheck.HealthyThreshold != nil { - hc.HealthyThreshold = wrapperspb.UInt32(*args.healthCheck.HealthyThreshold) - } - if args.healthCheck.HTTP != nil { - httpChecker := &corev3.HealthCheck_HttpHealthCheck{ - Path: args.healthCheck.HTTP.Path, - } - if args.healthCheck.HTTP.Method != nil { - httpChecker.Method = corev3.RequestMethod(corev3.RequestMethod_value[*args.healthCheck.HTTP.Method]) - } - httpChecker.ExpectedStatuses = buildHTTPStatusRange(args.healthCheck.HTTP.ExpectedStatuses) - if receive := buildHealthCheckPayload(args.healthCheck.HTTP.ExpectedResponse); receive != nil { - httpChecker.Receive = append(httpChecker.Receive, receive) - } - hc.HealthChecker = &corev3.HealthCheck_HttpHealthCheck_{ - HttpHealthCheck: httpChecker, - } - } - if args.healthCheck.TCP != nil { - tcpChecker := &corev3.HealthCheck_TcpHealthCheck{ - Send: buildHealthCheckPayload(args.healthCheck.TCP.Send), - } - if receive := buildHealthCheckPayload(args.healthCheck.TCP.Receive); receive != nil { - tcpChecker.Receive = append(tcpChecker.Receive, receive) - } - hc.HealthChecker = &corev3.HealthCheck_TcpHealthCheck_{ - TcpHealthCheck: tcpChecker, - } - } - cluster.HealthChecks = []*corev3.HealthCheck{hc} + if args.healthCheck != nil && args.healthCheck.Active != nil { + cluster.HealthChecks = buildXdsHealthCheck(args.healthCheck.Active) + } + + if args.healthCheck != nil && args.healthCheck.Passive != nil { + cluster.OutlierDetection = buildXdsOutlierDetection(args.healthCheck.Passive) + } if args.circuitBreaker != nil { cluster.CircuitBreakers = buildXdsClusterCircuitBreaker(args.circuitBreaker) @@ -184,6 +153,74 @@ func buildXdsCluster(args *xdsClusterArgs) *clusterv3.Cluster { return cluster } +func buildXdsHealthCheck(healthcheck *ir.ActiveHealthCheck) []*corev3.HealthCheck { + hc := &corev3.HealthCheck{ + Timeout: durationpb.New(healthcheck.Timeout.Duration), + Interval: durationpb.New(healthcheck.Interval.Duration), + } + if healthcheck.UnhealthyThreshold != nil { + hc.UnhealthyThreshold = wrapperspb.UInt32(*healthcheck.UnhealthyThreshold) + } + if healthcheck.HealthyThreshold != nil { + hc.HealthyThreshold = wrapperspb.UInt32(*healthcheck.HealthyThreshold) + } + if healthcheck.HTTP != nil { + httpChecker := &corev3.HealthCheck_HttpHealthCheck{ + Path: healthcheck.HTTP.Path, + } + if healthcheck.HTTP.Method != nil { + httpChecker.Method = corev3.RequestMethod(corev3.RequestMethod_value[*healthcheck.HTTP.Method]) + } + httpChecker.ExpectedStatuses = buildHTTPStatusRange(healthcheck.HTTP.ExpectedStatuses) + if receive := buildHealthCheckPayload(healthcheck.HTTP.ExpectedResponse); receive != nil { + httpChecker.Receive = append(httpChecker.Receive, receive) + } + hc.HealthChecker = &corev3.HealthCheck_HttpHealthCheck_{ + HttpHealthCheck: httpChecker, + } + } + if healthcheck.TCP != nil { + tcpChecker := &corev3.HealthCheck_TcpHealthCheck{ + Send: buildHealthCheckPayload(healthcheck.TCP.Send), + } + if receive := buildHealthCheckPayload(healthcheck.TCP.Receive); receive != nil { + tcpChecker.Receive = append(tcpChecker.Receive, receive) + } + hc.HealthChecker = &corev3.HealthCheck_TcpHealthCheck_{ + TcpHealthCheck: tcpChecker, + } + } + return []*corev3.HealthCheck{hc} +} + +func buildXdsOutlierDetection(outlierDetection *ir.OutlierDetection) *clusterv3.OutlierDetection { + od := &clusterv3.OutlierDetection{ + BaseEjectionTime: durationpb.New(outlierDetection.BaseEjectionTime.Duration), + Interval: durationpb.New(outlierDetection.Interval.Duration), + } + if outlierDetection.SplitExternalLocalOriginErrors != nil { + od.SplitExternalLocalOriginErrors = *outlierDetection.SplitExternalLocalOriginErrors + } + + if outlierDetection.MaxEjectionPercent != nil && *outlierDetection.MaxEjectionPercent > 0 { + od.MaxEjectionPercent = wrapperspb.UInt32(uint32(*outlierDetection.MaxEjectionPercent)) + } + + if outlierDetection.ConsecutiveLocalOriginFailures != nil { + od.ConsecutiveLocalOriginFailure = wrapperspb.UInt32(*outlierDetection.ConsecutiveLocalOriginFailures) + } + + if outlierDetection.Consecutive5xxErrors != nil { + od.Consecutive_5Xx = wrapperspb.UInt32(*outlierDetection.Consecutive5xxErrors) + } + + if outlierDetection.ConsecutiveGatewayErrors != nil { + od.ConsecutiveGatewayFailure = wrapperspb.UInt32(*outlierDetection.ConsecutiveGatewayErrors) + } + + return od +} + // buildHTTPStatusRange converts an array of http status to an array of the range of http status. func buildHTTPStatusRange(irStatuses []ir.HTTPStatus) []*xdstype.Int64Range { if len(irStatuses) == 0 { 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 63fc5d6f43d..a634af2ef8f 100644 --- a/internal/xds/translator/testdata/in/xds-ir/health-check.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/health-check.yaml @@ -11,17 +11,22 @@ http: - name: "first-route" hostname: "*" healthCheck: - timeout: "500ms" - interval: "3s" - unhealthyThreshold: 3 - healthyThreshold: 1 - http: - path: "/healthz" - expectedResponse: - text: "ok" - expectedStatuses: - - 200 - - 300 + active: + timeout: "500ms" + interval: "3s" + unhealthyThreshold: 3 + healthyThreshold: 1 + http: + path: "/healthz" + expectedResponse: + text: "ok" + expectedStatuses: + - 200 + - 300 + passive: + baseEjectionTime: 180s + interval: 2s + maxEjectionPercent: 100 destination: name: "first-route-dest" settings: @@ -31,17 +36,22 @@ http: - name: "second-route" hostname: "*" healthCheck: - timeout: "1s" - interval: "5s" - unhealthyThreshold: 3 - healthyThreshold: 3 - http: - path: "/healthz" - expectedResponse: - binary: "cG9uZw==" - expectedStatuses: - - 200 - - 201 + active: + timeout: "1s" + interval: "5s" + unhealthyThreshold: 3 + healthyThreshold: 3 + http: + path: "/healthz" + expectedResponse: + binary: "cG9uZw==" + expectedStatuses: + - 200 + - 201 + passive: + baseEjectionTime: 180s + interval: 1s + maxEjectionPercent: 100 destination: name: "second-route-dest" settings: @@ -51,15 +61,20 @@ http: - name: "third-route" hostname: "*" healthCheck: - timeout: "1s" - interval: "5s" - unhealthyThreshold: 3 - healthyThreshold: 3 - tcp: - send: - text: "ping" - receive: - text: "pong" + active: + timeout: "1s" + interval: "5s" + unhealthyThreshold: 3 + healthyThreshold: 3 + tcp: + send: + text: "ping" + receive: + text: "pong" + passive: + baseEjectionTime: 160s + interval: 1s + maxEjectionPercent: 100 destination: name: "third-route-dest" settings: @@ -69,15 +84,20 @@ http: - name: "fourth-route" hostname: "*" healthCheck: - timeout: "1s" - interval: "5s" - unhealthyThreshold: 3 - healthyThreshold: 3 - tcp: - send: - binary: "cGluZw==" - receive: - binary: "cG9uZw==" + active: + timeout: "1s" + interval: "5s" + unhealthyThreshold: 3 + healthyThreshold: 3 + tcp: + send: + binary: "cGluZw==" + receive: + binary: "cG9uZw==" + passive: + baseEjectionTime: 180s + interval: 1s + maxEjectionPercent: 90 destination: name: "fourth-route-dest" settings: 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 628302cce16..6003509f196 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 @@ -23,7 +23,10 @@ unhealthyThreshold: 3 lbPolicy: LEAST_REQUEST name: first-route-dest - outlierDetection: {} + outlierDetection: + baseEjectionTime: 180s + interval: 2s + maxEjectionPercent: 100 perConnectionBufferLimitBytes: 32768 type: EDS - commonLbConfig: @@ -49,7 +52,10 @@ unhealthyThreshold: 3 lbPolicy: LEAST_REQUEST name: second-route-dest - outlierDetection: {} + outlierDetection: + baseEjectionTime: 180s + interval: 1s + maxEjectionPercent: 100 perConnectionBufferLimitBytes: 32768 type: EDS - commonLbConfig: @@ -73,7 +79,10 @@ unhealthyThreshold: 3 lbPolicy: LEAST_REQUEST name: third-route-dest - outlierDetection: {} + outlierDetection: + baseEjectionTime: 160s + interval: 1s + maxEjectionPercent: 100 perConnectionBufferLimitBytes: 32768 type: EDS - commonLbConfig: @@ -97,6 +106,9 @@ unhealthyThreshold: 3 lbPolicy: LEAST_REQUEST name: fourth-route-dest - outlierDetection: {} + outlierDetection: + baseEjectionTime: 180s + interval: 1s + maxEjectionPercent: 90 perConnectionBufferLimitBytes: 32768 type: EDS diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index b07c36f7411..b64355f7978 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -50,7 +50,7 @@ _Appears in:_ | Field | Type | Required | Description | | --- | --- | --- | --- | | `timeout` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#duration-v1-meta)_ | false | Timeout defines the time to wait for a health check response. | -| `interval` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#duration-v1-meta)_ | false | Interval defines the time between health checks. | +| `interval` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#duration-v1-meta)_ | false | Interval defines the time between active health checks. | | `unhealthyThreshold` | _integer_ | false | UnhealthyThreshold defines the number of unhealthy health checks required before a backend host is marked unhealthy. | | `healthyThreshold` | _integer_ | false | HealthyThreshold defines the number of healthy health checks required before a backend host is marked healthy. | | `type` | _[ActiveHealthCheckerType](#activehealthcheckertype)_ | true | Type defines the type of health checker. | @@ -1210,6 +1210,7 @@ _Appears in:_ | Field | Type | Required | Description | | --- | --- | --- | --- | | `active` | _[ActiveHealthCheck](#activehealthcheck)_ | false | Active health check configuration | +| `passive` | _[PassiveHealthCheck](#passivehealthcheck)_ | false | Passive passive check configuration | #### InfrastructureProviderType @@ -1591,6 +1592,26 @@ _Appears in:_ +#### PassiveHealthCheck + + + +PassiveHealthCheck defines the configuration for passive health checks in the context of Envoy's Outlier Detection, see https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/outlier + +_Appears in:_ +- [HealthCheck](#healthcheck) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `splitExternalLocalOriginErrors` | _boolean_ | false | SplitExternalLocalOriginErrors enables splitting of errors between external and local origin. | +| `interval` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#duration-v1-meta)_ | false | Interval defines the time between passive health checks. | +| `consecutiveLocalOriginFailures` | _integer_ | false | ConsecutiveLocalOriginFailures sets the number of consecutive local origin failures triggering ejection. Parameter takes effect only when split_external_local_origin_errors is set to true. | +| `consecutiveGatewayErrors` | _integer_ | false | ConsecutiveGatewayErrors sets the number of consecutive gateway errors triggering ejection. | +| `consecutive5XxErrors` | _integer_ | false | Consecutive5xxErrors sets the number of consecutive 5xx errors triggering ejection. | +| `baseEjectionTime` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#duration-v1-meta)_ | false | BaseEjectionTime defines the base duration for which a host will be ejected on consecutive failures. | +| `maxEjectionPercent` | _integer_ | false | MaxEjectionPercent sets the maximum percentage of hosts in a cluster that can be ejected. | + + #### PathEscapedSlashAction _Underlying type:_ _string_