diff --git a/api/v1alpha1/backendtrafficpolicy_types.go b/api/v1alpha1/backendtrafficpolicy_types.go
index fc998b66..9e861692 100644
--- a/api/v1alpha1/backendtrafficpolicy_types.go
+++ b/api/v1alpha1/backendtrafficpolicy_types.go
@@ -74,6 +74,13 @@ type BackendTrafficPolicySpec struct {
// UpstreamHost specifies the host of the Upstream request. Used only if
// passHost is set to `rewrite`.
Host Hostname `json:"upstreamHost,omitempty" yaml:"upstreamHost,omitempty"`
+
+ // HealthCheck defines active and passive health check configuration for
+ // the upstream backends. When configured, APISIX will probe backends
+ // (active) or monitor live traffic (passive) to detect and bypass
+ // unhealthy nodes.
+ // +optional
+ HealthCheck *HealthCheck `json:"healthCheck,omitempty" yaml:"healthCheck,omitempty"`
}
// LoadBalancer describes the load balancing parameters.
@@ -125,6 +132,139 @@ type BackendTrafficPolicyList struct {
Items []BackendTrafficPolicy `json:"items"`
}
+// HealthCheck defines the active and passive health check configuration for upstream nodes.
+type HealthCheck struct {
+ // Active health checks proactively send requests to upstream nodes to determine their availability.
+ // +kubebuilder:validation:Required
+ Active *ActiveHealthCheck `json:"active" yaml:"active"`
+ // Passive health checks evaluate upstream health based on observed traffic (timeouts, errors).
+ // +kubebuilder:validation:Optional
+ Passive *PassiveHealthCheck `json:"passive,omitempty" yaml:"passive,omitempty"`
+}
+
+// ActiveHealthCheck defines the active upstream health check configuration.
+type ActiveHealthCheck struct {
+ // Type is the health check type. Can be `http`, `https`, or `tcp`.
+ // +kubebuilder:validation:Enum=http;https;tcp;
+ // +kubebuilder:default=http
+ // +optional
+ Type string `json:"type,omitempty" yaml:"type,omitempty"`
+
+ // Timeout sets health check timeout.
+ // +optional
+ Timeout metav1.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"`
+
+ // Concurrency sets the number of targets to be checked at the same time.
+ // +kubebuilder:validation:Minimum=0
+ // +optional
+ Concurrency int `json:"concurrency,omitempty" yaml:"concurrency,omitempty"`
+
+ // Host sets the upstream host used in the health check request.
+ // +optional
+ Host string `json:"host,omitempty" yaml:"host,omitempty"`
+
+ // Port sets the port on the upstream node to probe.
+ // +kubebuilder:validation:Minimum=1
+ // +kubebuilder:validation:Maximum=65535
+ // +optional
+ Port int32 `json:"port,omitempty" yaml:"port,omitempty"`
+
+ // HTTPPath sets the HTTP path for the probe request.
+ // +optional
+ HTTPPath string `json:"httpPath,omitempty" yaml:"httpPath,omitempty"`
+
+ // StrictTLS controls whether TLS certificate validation is enforced.
+ // +optional
+ StrictTLS *bool `json:"strictTLS,omitempty" yaml:"strictTLS,omitempty"`
+
+ // RequestHeaders sets additional HTTP request headers for the probe.
+ // +optional
+ RequestHeaders []string `json:"requestHeaders,omitempty" yaml:"requestHeaders,omitempty"`
+
+ // Healthy configures the thresholds for marking a node healthy.
+ // +optional
+ Healthy *ActiveHealthCheckHealthy `json:"healthy,omitempty" yaml:"healthy,omitempty"`
+
+ // Unhealthy configures the thresholds for marking a node unhealthy.
+ // +optional
+ Unhealthy *ActiveHealthCheckUnhealthy `json:"unhealthy,omitempty" yaml:"unhealthy,omitempty"`
+}
+
+// PassiveHealthCheck defines passive health check configuration based on observed traffic.
+type PassiveHealthCheck struct {
+ // Type is the passive health check type. Can be `http`, `https`, or `tcp`.
+ // +kubebuilder:validation:Enum=http;https;tcp;
+ // +kubebuilder:default=http
+ // +optional
+ Type string `json:"type,omitempty" yaml:"type,omitempty"`
+
+ // Healthy defines conditions under which a node is considered healthy.
+ // +optional
+ Healthy *PassiveHealthCheckHealthy `json:"healthy,omitempty" yaml:"healthy,omitempty"`
+
+ // Unhealthy defines conditions under which a node is considered unhealthy.
+ // +optional
+ Unhealthy *PassiveHealthCheckUnhealthy `json:"unhealthy,omitempty" yaml:"unhealthy,omitempty"`
+}
+
+// ActiveHealthCheckHealthy defines thresholds for actively marking an upstream node healthy.
+type ActiveHealthCheckHealthy struct {
+ PassiveHealthCheckHealthy `json:",inline" yaml:",inline"`
+
+ // Interval defines the time between health check probes.
+ // Minimum is 1s.
+ Interval metav1.Duration `json:"interval,omitempty" yaml:"interval,omitempty"`
+}
+
+// ActiveHealthCheckUnhealthy defines thresholds for actively marking an upstream node unhealthy.
+type ActiveHealthCheckUnhealthy struct {
+ PassiveHealthCheckUnhealthy `json:",inline" yaml:",inline"`
+
+ // Interval defines the time between health check probes.
+ // Minimum is 1s.
+ Interval metav1.Duration `json:"interval,omitempty" yaml:"interval,omitempty"`
+}
+
+// PassiveHealthCheckHealthy defines conditions for passively marking a node healthy.
+type PassiveHealthCheckHealthy struct {
+ // HTTPCodes is the list of HTTP status codes considered healthy.
+ // +kubebuilder:validation:MinItems=1
+ // +optional
+ HTTPCodes []int `json:"httpCodes,omitempty" yaml:"httpCodes,omitempty"`
+
+ // Successes is the number of consecutive successful responses required to mark a node healthy.
+ // +kubebuilder:validation:Minimum=0
+ // +kubebuilder:validation:Maximum=254
+ // +optional
+ Successes int `json:"successes,omitempty" yaml:"successes,omitempty"`
+}
+
+// PassiveHealthCheckUnhealthy defines conditions for passively marking a node unhealthy.
+type PassiveHealthCheckUnhealthy struct {
+ // HTTPCodes is the list of HTTP status codes considered unhealthy.
+ // +kubebuilder:validation:MinItems=1
+ // +optional
+ HTTPCodes []int `json:"httpCodes,omitempty" yaml:"httpCodes,omitempty"`
+
+ // HTTPFailures is the number of HTTP failures to mark a node unhealthy.
+ // +kubebuilder:validation:Minimum=0
+ // +kubebuilder:validation:Maximum=254
+ // +optional
+ HTTPFailures int `json:"httpFailures,omitempty" yaml:"httpFailures,omitempty"`
+
+ // TCPFailures is the number of TCP failures to mark a node unhealthy.
+ // +kubebuilder:validation:Minimum=0
+ // +kubebuilder:validation:Maximum=254
+ // +optional
+ TCPFailures int `json:"tcpFailures,omitempty" yaml:"tcpFailures,omitempty"`
+
+ // Timeouts is the number of timeouts to mark a node unhealthy.
+ // +kubebuilder:validation:Minimum=1
+ // +kubebuilder:validation:Maximum=254
+ // +optional
+ Timeouts int `json:"timeouts,omitempty" yaml:"timeouts,omitempty"`
+}
+
func init() {
SchemeBuilder.Register(&BackendTrafficPolicy{}, &BackendTrafficPolicyList{})
}
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index f7b5383c..473a7b28 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -27,6 +27,76 @@ import (
"sigs.k8s.io/gateway-api/apis/v1alpha2"
)
+// 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
+ out.Timeout = in.Timeout
+ if in.StrictTLS != nil {
+ in, out := &in.StrictTLS, &out.StrictTLS
+ *out = new(bool)
+ **out = **in
+ }
+ if in.RequestHeaders != nil {
+ in, out := &in.RequestHeaders, &out.RequestHeaders
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+ if in.Healthy != nil {
+ in, out := &in.Healthy, &out.Healthy
+ *out = new(ActiveHealthCheckHealthy)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.Unhealthy != nil {
+ in, out := &in.Unhealthy, &out.Unhealthy
+ *out = new(ActiveHealthCheckUnhealthy)
+ (*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 *ActiveHealthCheckHealthy) DeepCopyInto(out *ActiveHealthCheckHealthy) {
+ *out = *in
+ in.PassiveHealthCheckHealthy.DeepCopyInto(&out.PassiveHealthCheckHealthy)
+ out.Interval = in.Interval
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveHealthCheckHealthy.
+func (in *ActiveHealthCheckHealthy) DeepCopy() *ActiveHealthCheckHealthy {
+ if in == nil {
+ return nil
+ }
+ out := new(ActiveHealthCheckHealthy)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ActiveHealthCheckUnhealthy) DeepCopyInto(out *ActiveHealthCheckUnhealthy) {
+ *out = *in
+ in.PassiveHealthCheckUnhealthy.DeepCopyInto(&out.PassiveHealthCheckUnhealthy)
+ out.Interval = in.Interval
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveHealthCheckUnhealthy.
+func (in *ActiveHealthCheckUnhealthy) DeepCopy() *ActiveHealthCheckUnhealthy {
+ if in == nil {
+ return nil
+ }
+ out := new(ActiveHealthCheckUnhealthy)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AdminKeyAuth) DeepCopyInto(out *AdminKeyAuth) {
*out = *in
@@ -172,6 +242,11 @@ func (in *BackendTrafficPolicySpec) DeepCopyInto(out *BackendTrafficPolicySpec)
*out = new(Timeout)
**out = **in
}
+ if in.HealthCheck != nil {
+ in, out := &in.HealthCheck, &out.HealthCheck
+ *out = new(HealthCheck)
+ (*in).DeepCopyInto(*out)
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendTrafficPolicySpec.
@@ -617,6 +692,31 @@ func (in *HTTPRoutePolicySpec) DeepCopy() *HTTPRoutePolicySpec {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HealthCheck) DeepCopyInto(out *HealthCheck) {
+ *out = *in
+ if in.Active != nil {
+ in, out := &in.Active, &out.Active
+ *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.
+func (in *HealthCheck) DeepCopy() *HealthCheck {
+ if in == nil {
+ return nil
+ }
+ out := new(HealthCheck)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LoadBalancer) DeepCopyInto(out *LoadBalancer) {
*out = *in
@@ -632,6 +732,71 @@ func (in *LoadBalancer) DeepCopy() *LoadBalancer {
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.Healthy != nil {
+ in, out := &in.Healthy, &out.Healthy
+ *out = new(PassiveHealthCheckHealthy)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.Unhealthy != nil {
+ in, out := &in.Unhealthy, &out.Unhealthy
+ *out = new(PassiveHealthCheckUnhealthy)
+ (*in).DeepCopyInto(*out)
+ }
+}
+
+// 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 *PassiveHealthCheckHealthy) DeepCopyInto(out *PassiveHealthCheckHealthy) {
+ *out = *in
+ if in.HTTPCodes != nil {
+ in, out := &in.HTTPCodes, &out.HTTPCodes
+ *out = make([]int, len(*in))
+ copy(*out, *in)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PassiveHealthCheckHealthy.
+func (in *PassiveHealthCheckHealthy) DeepCopy() *PassiveHealthCheckHealthy {
+ if in == nil {
+ return nil
+ }
+ out := new(PassiveHealthCheckHealthy)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PassiveHealthCheckUnhealthy) DeepCopyInto(out *PassiveHealthCheckUnhealthy) {
+ *out = *in
+ if in.HTTPCodes != nil {
+ in, out := &in.HTTPCodes, &out.HTTPCodes
+ *out = make([]int, len(*in))
+ copy(*out, *in)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PassiveHealthCheckUnhealthy.
+func (in *PassiveHealthCheckUnhealthy) DeepCopy() *PassiveHealthCheckUnhealthy {
+ if in == nil {
+ return nil
+ }
+ out := new(PassiveHealthCheckUnhealthy)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Plugin) DeepCopyInto(out *Plugin) {
*out = *in
diff --git a/config/crd/bases/apisix.apache.org_backendtrafficpolicies.yaml b/config/crd/bases/apisix.apache.org_backendtrafficpolicies.yaml
index 64c366a4..8b771c80 100644
--- a/config/crd/bases/apisix.apache.org_backendtrafficpolicies.yaml
+++ b/config/crd/bases/apisix.apache.org_backendtrafficpolicies.yaml
@@ -42,6 +42,181 @@ spec:
BackendTrafficPolicySpec defines traffic handling policies applied to backend services,
such as load balancing strategy, connection settings, and failover behavior.
properties:
+ healthCheck:
+ description: |-
+ HealthCheck defines active and passive health check configuration for
+ the upstream backends. When configured, APISIX will probe backends
+ (active) or monitor live traffic (passive) to detect and bypass
+ unhealthy nodes.
+ properties:
+ active:
+ description: Active health checks proactively send requests to
+ upstream nodes to determine their availability.
+ properties:
+ concurrency:
+ description: Concurrency sets the number of targets to be
+ checked at the same time.
+ minimum: 0
+ type: integer
+ healthy:
+ description: Healthy configures the thresholds for marking
+ a node healthy.
+ properties:
+ httpCodes:
+ description: HTTPCodes is the list of HTTP status codes
+ considered healthy.
+ items:
+ type: integer
+ minItems: 1
+ type: array
+ interval:
+ description: |-
+ Interval defines the time between health check probes.
+ Minimum is 1s.
+ type: string
+ successes:
+ description: Successes is the number of consecutive successful
+ responses required to mark a node healthy.
+ maximum: 254
+ minimum: 0
+ type: integer
+ type: object
+ host:
+ description: Host sets the upstream host used in the health
+ check request.
+ type: string
+ httpPath:
+ description: HTTPPath sets the HTTP path for the probe request.
+ type: string
+ port:
+ description: Port sets the port on the upstream node to probe.
+ format: int32
+ maximum: 65535
+ minimum: 1
+ type: integer
+ requestHeaders:
+ description: RequestHeaders sets additional HTTP request headers
+ for the probe.
+ items:
+ type: string
+ type: array
+ strictTLS:
+ description: StrictTLS controls whether TLS certificate validation
+ is enforced.
+ type: boolean
+ timeout:
+ description: Timeout sets health check timeout.
+ type: string
+ type:
+ default: http
+ description: Type is the health check type. Can be `http`,
+ `https`, or `tcp`.
+ enum:
+ - http
+ - https
+ - tcp
+ type: string
+ unhealthy:
+ description: Unhealthy configures the thresholds for marking
+ a node unhealthy.
+ properties:
+ httpCodes:
+ description: HTTPCodes is the list of HTTP status codes
+ considered unhealthy.
+ items:
+ type: integer
+ minItems: 1
+ type: array
+ httpFailures:
+ description: HTTPFailures is the number of HTTP failures
+ to mark a node unhealthy.
+ maximum: 254
+ minimum: 0
+ type: integer
+ interval:
+ description: |-
+ Interval defines the time between health check probes.
+ Minimum is 1s.
+ type: string
+ tcpFailures:
+ description: TCPFailures is the number of TCP failures
+ to mark a node unhealthy.
+ maximum: 254
+ minimum: 0
+ type: integer
+ timeouts:
+ description: Timeouts is the number of timeouts to mark
+ a node unhealthy.
+ maximum: 254
+ minimum: 1
+ type: integer
+ type: object
+ type: object
+ passive:
+ description: Passive health checks evaluate upstream health based
+ on observed traffic (timeouts, errors).
+ properties:
+ healthy:
+ description: Healthy defines conditions under which a node
+ is considered healthy.
+ properties:
+ httpCodes:
+ description: HTTPCodes is the list of HTTP status codes
+ considered healthy.
+ items:
+ type: integer
+ minItems: 1
+ type: array
+ successes:
+ description: Successes is the number of consecutive successful
+ responses required to mark a node healthy.
+ maximum: 254
+ minimum: 0
+ type: integer
+ type: object
+ type:
+ default: http
+ description: Type is the passive health check type. Can be
+ `http`, `https`, or `tcp`.
+ enum:
+ - http
+ - https
+ - tcp
+ type: string
+ unhealthy:
+ description: Unhealthy defines conditions under which a node
+ is considered unhealthy.
+ properties:
+ httpCodes:
+ description: HTTPCodes is the list of HTTP status codes
+ considered unhealthy.
+ items:
+ type: integer
+ minItems: 1
+ type: array
+ httpFailures:
+ description: HTTPFailures is the number of HTTP failures
+ to mark a node unhealthy.
+ maximum: 254
+ minimum: 0
+ type: integer
+ tcpFailures:
+ description: TCPFailures is the number of TCP failures
+ to mark a node unhealthy.
+ maximum: 254
+ minimum: 0
+ type: integer
+ timeouts:
+ description: Timeouts is the number of timeouts to mark
+ a node unhealthy.
+ maximum: 254
+ minimum: 1
+ type: integer
+ type: object
+ type: object
+ required:
+ - active
+ type: object
loadbalancer:
description: |-
LoadBalancer represents the load balancer configuration for Kubernetes Service.
diff --git a/docs/en/latest/reference/api-reference.md b/docs/en/latest/reference/api-reference.md
index 44b29323..21ba3cf9 100644
--- a/docs/en/latest/reference/api-reference.md
+++ b/docs/en/latest/reference/api-reference.md
@@ -103,6 +103,66 @@ PluginConfig defines plugin configuration.
### Types
This section describes the types used by the CRDs.
+#### ActiveHealthCheck
+
+
+ActiveHealthCheck defines the active upstream health check configuration.
+
+
+
+| Field | Description |
+| --- | --- |
+| `type` _string_ | Type is the health check type. Can be `http`, `https`, or `tcp`. |
+| `timeout` _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#duration-v1-meta)_ | Timeout sets health check timeout. |
+| `concurrency` _integer_ | Concurrency sets the number of targets to be checked at the same time. |
+| `host` _string_ | Host sets the upstream host used in the health check request. |
+| `port` _integer_ | Port sets the port on the upstream node to probe. |
+| `httpPath` _string_ | HTTPPath sets the HTTP path for the probe request. |
+| `strictTLS` _boolean_ | StrictTLS controls whether TLS certificate validation is enforced. |
+| `requestHeaders` _string array_ | RequestHeaders sets additional HTTP request headers for the probe. |
+| `healthy` _[ActiveHealthCheckHealthy](#activehealthcheckhealthy)_ | Healthy configures the thresholds for marking a node healthy. |
+| `unhealthy` _[ActiveHealthCheckUnhealthy](#activehealthcheckunhealthy)_ | Unhealthy configures the thresholds for marking a node unhealthy. |
+
+
+_Appears in:_
+- [HealthCheck](#healthcheck)
+
+#### ActiveHealthCheckHealthy
+
+
+ActiveHealthCheckHealthy defines thresholds for actively marking an upstream node healthy.
+
+
+
+| Field | Description |
+| --- | --- |
+| `httpCodes` _integer array_ | HTTPCodes is the list of HTTP status codes considered healthy. |
+| `successes` _integer_ | Successes is the number of consecutive successful responses required to mark a node healthy. |
+| `interval` _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#duration-v1-meta)_ | Interval defines the time between health check probes. Minimum is 1s. |
+
+
+_Appears in:_
+- [ActiveHealthCheck](#activehealthcheck)
+
+#### ActiveHealthCheckUnhealthy
+
+
+ActiveHealthCheckUnhealthy defines thresholds for actively marking an upstream node unhealthy.
+
+
+
+| Field | Description |
+| --- | --- |
+| `httpCodes` _integer array_ | HTTPCodes is the list of HTTP status codes considered unhealthy. |
+| `httpFailures` _integer_ | HTTPFailures is the number of HTTP failures to mark a node unhealthy. |
+| `tcpFailures` _integer_ | TCPFailures is the number of TCP failures to mark a node unhealthy. |
+| `timeouts` _integer_ | Timeouts is the number of timeouts to mark a node unhealthy. |
+| `interval` _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#duration-v1-meta)_ | Interval defines the time between health check probes. Minimum is 1s. |
+
+
+_Appears in:_
+- [ActiveHealthCheck](#activehealthcheck)
+
#### AdminKeyAuth
@@ -180,6 +240,7 @@ _Appears in:_
| `timeout` _[Timeout](#timeout)_ | Timeout sets the read, send, and connect timeouts to the upstream. |
| `passHost` _string_ | PassHost configures how the host header should be determined when a request is forwarded to the upstream. Default is `pass`. Can be `pass`, `node` or `rewrite`:
• `pass`: preserve the original Host header
• `node`: use the upstream node’s host
• `rewrite`: set to a custom host via `upstreamHost` |
| `upstreamHost` _[Hostname](#hostname)_ | UpstreamHost specifies the host of the Upstream request. Used only if passHost is set to `rewrite`. |
+| `healthCheck` _[HealthCheck](#healthcheck)_ | HealthCheck defines active and passive health check configuration for the upstream backends. When configured, APISIX will probe backends (active) or monitor live traffic (passive) to detect and bypass unhealthy nodes. |
_Appears in:_
@@ -344,6 +405,22 @@ HTTPRoutePolicySpec defines the desired state of HTTPRoutePolicy.
_Appears in:_
- [HTTPRoutePolicy](#httproutepolicy)
+#### HealthCheck
+
+
+HealthCheck defines the active and passive health check configuration for upstream nodes.
+
+
+
+| Field | Description |
+| --- | --- |
+| `active` _[ActiveHealthCheck](#activehealthcheck)_ | Active health checks proactively send requests to upstream nodes to determine their availability. |
+| `passive` _[PassiveHealthCheck](#passivehealthcheck)_ | Passive health checks evaluate upstream health based on observed traffic (timeouts, errors). |
+
+
+_Appears in:_
+- [BackendTrafficPolicySpec](#backendtrafficpolicyspec)
+
#### Hostname
_Base type:_ `string`
@@ -373,6 +450,59 @@ LoadBalancer describes the load balancing parameters.
_Appears in:_
- [BackendTrafficPolicySpec](#backendtrafficpolicyspec)
+#### PassiveHealthCheck
+
+
+PassiveHealthCheck defines passive health check configuration based on observed traffic.
+
+
+
+| Field | Description |
+| --- | --- |
+| `type` _string_ | Type is the passive health check type. Can be `http`, `https`, or `tcp`. |
+| `healthy` _[PassiveHealthCheckHealthy](#passivehealthcheckhealthy)_ | Healthy defines conditions under which a node is considered healthy. |
+| `unhealthy` _[PassiveHealthCheckUnhealthy](#passivehealthcheckunhealthy)_ | Unhealthy defines conditions under which a node is considered unhealthy. |
+
+
+_Appears in:_
+- [HealthCheck](#healthcheck)
+
+#### PassiveHealthCheckHealthy
+
+
+PassiveHealthCheckHealthy defines conditions for passively marking a node healthy.
+
+
+
+| Field | Description |
+| --- | --- |
+| `httpCodes` _integer array_ | HTTPCodes is the list of HTTP status codes considered healthy. |
+| `successes` _integer_ | Successes is the number of consecutive successful responses required to mark a node healthy. |
+
+
+_Appears in:_
+- [ActiveHealthCheckHealthy](#activehealthcheckhealthy)
+- [PassiveHealthCheck](#passivehealthcheck)
+
+#### PassiveHealthCheckUnhealthy
+
+
+PassiveHealthCheckUnhealthy defines conditions for passively marking a node unhealthy.
+
+
+
+| Field | Description |
+| --- | --- |
+| `httpCodes` _integer array_ | HTTPCodes is the list of HTTP status codes considered unhealthy. |
+| `httpFailures` _integer_ | HTTPFailures is the number of HTTP failures to mark a node unhealthy. |
+| `tcpFailures` _integer_ | TCPFailures is the number of TCP failures to mark a node unhealthy. |
+| `timeouts` _integer_ | Timeouts is the number of timeouts to mark a node unhealthy. |
+
+
+_Appears in:_
+- [ActiveHealthCheckUnhealthy](#activehealthcheckunhealthy)
+- [PassiveHealthCheck](#passivehealthcheck)
+
#### Plugin
diff --git a/internal/adc/translator/apisixconsumer_test.go b/internal/adc/translator/apisixconsumer_test.go
index e6a1ee4a..ff42eef7 100644
--- a/internal/adc/translator/apisixconsumer_test.go
+++ b/internal/adc/translator/apisixconsumer_test.go
@@ -49,7 +49,7 @@ func TestTranslateApisixConsumer_UsesMetadataLabelsWithoutOverwritingControllerL
},
},
Spec: apiv2.ApisixConsumerSpec{
- AuthParameter: apiv2.ApisixConsumerAuthParameter{
+ AuthParameter: &apiv2.ApisixConsumerAuthParameter{
BasicAuth: &apiv2.ApisixConsumerBasicAuth{
Value: &apiv2.ApisixConsumerBasicAuthValue{
Username: "demo",
diff --git a/internal/adc/translator/httproute_test.go b/internal/adc/translator/httproute_test.go
index 28fdea83..7b11e129 100644
--- a/internal/adc/translator/httproute_test.go
+++ b/internal/adc/translator/httproute_test.go
@@ -20,6 +20,7 @@ package translator
import (
"context"
"testing"
+ "time"
"github.com/go-logr/logr"
"github.com/stretchr/testify/assert"
@@ -32,6 +33,7 @@ import (
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+ adctypes "github.com/apache/apisix-ingress-controller/api/adc"
"github.com/apache/apisix-ingress-controller/api/v1alpha1"
apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
"github.com/apache/apisix-ingress-controller/internal/provider"
@@ -148,3 +150,162 @@ func TestTranslateHTTPRouteUpstreamScheme(t *testing.T) {
})
}
}
+
+func TestAttachBackendTrafficPolicyHealthCheck(t *testing.T) {
+ trueVal := true
+ falseVal := false
+
+ tests := []struct {
+ name string
+ policy *v1alpha1.BackendTrafficPolicy
+ wantChecks *adctypes.UpstreamHealthCheck
+ }{
+ {
+ name: "nil health check produces no checks",
+ policy: &v1alpha1.BackendTrafficPolicy{},
+ wantChecks: nil,
+ },
+ {
+ name: "active health check with all fields",
+ policy: &v1alpha1.BackendTrafficPolicy{
+ Spec: v1alpha1.BackendTrafficPolicySpec{
+ HealthCheck: &v1alpha1.HealthCheck{
+ Active: &v1alpha1.ActiveHealthCheck{
+ Type: "http",
+ Timeout: metav1.Duration{Duration: 3 * time.Second},
+ HTTPPath: "/healthz",
+ Concurrency: 10,
+ Host: "example.com",
+ Port: 8080,
+ StrictTLS: &trueVal,
+ RequestHeaders: []string{"X-Custom: value"},
+ Healthy: &v1alpha1.ActiveHealthCheckHealthy{
+ Interval: metav1.Duration{Duration: 5 * time.Second},
+ PassiveHealthCheckHealthy: v1alpha1.PassiveHealthCheckHealthy{
+ HTTPCodes: []int{200, 201},
+ Successes: 3,
+ },
+ },
+ Unhealthy: &v1alpha1.ActiveHealthCheckUnhealthy{
+ Interval: metav1.Duration{Duration: 2 * time.Second},
+ PassiveHealthCheckUnhealthy: v1alpha1.PassiveHealthCheckUnhealthy{
+ HTTPCodes: []int{500, 503},
+ HTTPFailures: 5,
+ TCPFailures: 2,
+ Timeouts: 3,
+ },
+ },
+ },
+ },
+ },
+ },
+ wantChecks: &adctypes.UpstreamHealthCheck{
+ Active: &adctypes.UpstreamActiveHealthCheck{
+ Type: "http",
+ Timeout: 3,
+ HTTPPath: "/healthz",
+ Concurrency: 10,
+ Host: "example.com",
+ Port: 8080,
+ HTTPSVerifyCertificate: true,
+ HTTPRequestHeaders: []string{"X-Custom: value"},
+ Healthy: adctypes.UpstreamActiveHealthCheckHealthy{
+ Interval: 5,
+ UpstreamPassiveHealthCheckHealthy: adctypes.UpstreamPassiveHealthCheckHealthy{
+ HTTPStatuses: []int{200, 201},
+ Successes: 3,
+ },
+ },
+ Unhealthy: adctypes.UpstreamActiveHealthCheckUnhealthy{
+ Interval: 2,
+ UpstreamPassiveHealthCheckUnhealthy: adctypes.UpstreamPassiveHealthCheckUnhealthy{
+ HTTPStatuses: []int{500, 503},
+ HTTPFailures: 5,
+ TCPFailures: 2,
+ Timeouts: 3,
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "strictTLS false disables certificate verification",
+ policy: &v1alpha1.BackendTrafficPolicy{
+ Spec: v1alpha1.BackendTrafficPolicySpec{
+ HealthCheck: &v1alpha1.HealthCheck{
+ Active: &v1alpha1.ActiveHealthCheck{
+ StrictTLS: &falseVal,
+ Healthy: &v1alpha1.ActiveHealthCheckHealthy{
+ Interval: metav1.Duration{Duration: 1 * time.Second},
+ },
+ },
+ },
+ },
+ },
+ wantChecks: &adctypes.UpstreamHealthCheck{
+ Active: &adctypes.UpstreamActiveHealthCheck{
+ Type: "http",
+ HTTPSVerifyCertificate: false,
+ Healthy: adctypes.UpstreamActiveHealthCheckHealthy{
+ Interval: 1,
+ },
+ },
+ },
+ },
+ {
+ name: "active and passive health checks together",
+ policy: &v1alpha1.BackendTrafficPolicy{
+ Spec: v1alpha1.BackendTrafficPolicySpec{
+ HealthCheck: &v1alpha1.HealthCheck{
+ Active: &v1alpha1.ActiveHealthCheck{
+ Type: "tcp",
+ Healthy: &v1alpha1.ActiveHealthCheckHealthy{
+ Interval: metav1.Duration{Duration: 1 * time.Second},
+ },
+ },
+ Passive: &v1alpha1.PassiveHealthCheck{
+ Type: "http",
+ Healthy: &v1alpha1.PassiveHealthCheckHealthy{
+ HTTPCodes: []int{200},
+ Successes: 2,
+ },
+ Unhealthy: &v1alpha1.PassiveHealthCheckUnhealthy{
+ HTTPCodes: []int{500},
+ HTTPFailures: 3,
+ },
+ },
+ },
+ },
+ },
+ wantChecks: &adctypes.UpstreamHealthCheck{
+ Active: &adctypes.UpstreamActiveHealthCheck{
+ Type: "tcp",
+ HTTPSVerifyCertificate: true,
+ Healthy: adctypes.UpstreamActiveHealthCheckHealthy{
+ Interval: 1,
+ },
+ },
+ Passive: &adctypes.UpstreamPassiveHealthCheck{
+ Type: "http",
+ Healthy: adctypes.UpstreamPassiveHealthCheckHealthy{
+ HTTPStatuses: []int{200},
+ Successes: 2,
+ },
+ Unhealthy: adctypes.UpstreamPassiveHealthCheckUnhealthy{
+ HTTPStatuses: []int{500},
+ HTTPFailures: 3,
+ },
+ },
+ },
+ },
+ }
+
+ translator := &Translator{Log: logr.Discard()}
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ups := adctypes.NewDefaultUpstream()
+ translator.attachBackendTrafficPolicyToUpstream(tt.policy, ups)
+ assert.Equal(t, tt.wantChecks, ups.Checks)
+ })
+ }
+}
diff --git a/internal/adc/translator/policies.go b/internal/adc/translator/policies.go
index 41706964..f948f238 100644
--- a/internal/adc/translator/policies.go
+++ b/internal/adc/translator/policies.go
@@ -24,6 +24,7 @@ import (
adctypes "github.com/apache/apisix-ingress-controller/api/adc"
"github.com/apache/apisix-ingress-controller/api/v1alpha1"
+ apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
)
func convertBackendRef(namespace, name, kind string) gatewayv1.BackendRef {
@@ -79,4 +80,92 @@ func (t *Translator) attachBackendTrafficPolicyToUpstream(policy *v1alpha1.Backe
upstream.HashOn = policy.Spec.LoadBalancer.HashOn
upstream.Key = policy.Spec.LoadBalancer.Key
}
+ if policy.Spec.HealthCheck != nil {
+ upstream.Checks = translateBTPHealthCheck(policy.Spec.HealthCheck)
+ }
+}
+
+func translateBTPHealthCheck(hc *v1alpha1.HealthCheck) *adctypes.UpstreamHealthCheck {
+ if hc == nil || (hc.Active == nil && hc.Passive == nil) {
+ return nil
+ }
+ result := &adctypes.UpstreamHealthCheck{}
+ if hc.Active != nil {
+ result.Active = translateBTPActiveHealthCheck(hc.Active)
+ }
+ if hc.Passive != nil {
+ result.Passive = translateBTPPassiveHealthCheck(hc.Passive)
+ }
+ return result
+}
+
+func translateBTPActiveHealthCheck(config *v1alpha1.ActiveHealthCheck) *adctypes.UpstreamActiveHealthCheck {
+ t := config.Type
+ if t == "" {
+ t = apiv2.HealthCheckHTTP
+ }
+ active := &adctypes.UpstreamActiveHealthCheck{
+ Type: t,
+ Timeout: int(config.Timeout.Seconds()),
+ Concurrency: config.Concurrency,
+ Host: config.Host,
+ Port: config.Port,
+ HTTPPath: config.HTTPPath,
+ HTTPSVerifyCertificate: config.StrictTLS == nil || *config.StrictTLS,
+ HTTPRequestHeaders: config.RequestHeaders,
+ }
+ if config.Healthy != nil {
+ interval := config.Healthy.Interval.Duration
+ if interval < apiv2.ActiveHealthCheckMinInterval {
+ interval = apiv2.ActiveHealthCheckMinInterval
+ }
+ active.Healthy = adctypes.UpstreamActiveHealthCheckHealthy{
+ Interval: int(interval.Seconds()),
+ UpstreamPassiveHealthCheckHealthy: adctypes.UpstreamPassiveHealthCheckHealthy{
+ HTTPStatuses: config.Healthy.HTTPCodes,
+ Successes: config.Healthy.Successes,
+ },
+ }
+ }
+ if config.Unhealthy != nil {
+ interval := config.Unhealthy.Interval.Duration
+ if interval < apiv2.ActiveHealthCheckMinInterval {
+ interval = apiv2.ActiveHealthCheckMinInterval
+ }
+ active.Unhealthy = adctypes.UpstreamActiveHealthCheckUnhealthy{
+ Interval: int(interval.Seconds()),
+ UpstreamPassiveHealthCheckUnhealthy: adctypes.UpstreamPassiveHealthCheckUnhealthy{
+ HTTPStatuses: config.Unhealthy.HTTPCodes,
+ HTTPFailures: config.Unhealthy.HTTPFailures,
+ TCPFailures: config.Unhealthy.TCPFailures,
+ Timeouts: config.Unhealthy.Timeouts,
+ },
+ }
+ }
+ return active
+}
+
+func translateBTPPassiveHealthCheck(config *v1alpha1.PassiveHealthCheck) *adctypes.UpstreamPassiveHealthCheck {
+ t := config.Type
+ if t == "" {
+ t = "http"
+ }
+ passive := &adctypes.UpstreamPassiveHealthCheck{
+ Type: t,
+ }
+ if config.Healthy != nil {
+ passive.Healthy = adctypes.UpstreamPassiveHealthCheckHealthy{
+ HTTPStatuses: config.Healthy.HTTPCodes,
+ Successes: config.Healthy.Successes,
+ }
+ }
+ if config.Unhealthy != nil {
+ passive.Unhealthy = adctypes.UpstreamPassiveHealthCheckUnhealthy{
+ HTTPStatuses: config.Unhealthy.HTTPCodes,
+ HTTPFailures: config.Unhealthy.HTTPFailures,
+ TCPFailures: config.Unhealthy.TCPFailures,
+ Timeouts: config.Unhealthy.Timeouts,
+ }
+ }
+ return passive
}
diff --git a/test/e2e/crds/v1alpha1/backendtrafficpolicy.go b/test/e2e/crds/v1alpha1/backendtrafficpolicy.go
index bfb2ef89..a1f938c2 100644
--- a/test/e2e/crds/v1alpha1/backendtrafficpolicy.go
+++ b/test/e2e/crds/v1alpha1/backendtrafficpolicy.go
@@ -18,6 +18,7 @@
package v1alpha1
import (
+ "context"
"fmt"
"time"
@@ -25,6 +26,7 @@ import (
. "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/types"
+ adctypes "github.com/apache/apisix-ingress-controller/api/adc"
"github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
)
@@ -57,6 +59,26 @@ spec:
- name: httpbin-service-e2e-test
port: 80
`
+ var gatewayBeforeEach = func() {
+ By("create GatewayProxy")
+ err = s.CreateResourceFromString(s.GetGatewayProxySpec())
+ Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy")
+ time.Sleep(5 * time.Second)
+
+ By("create GatewayClass")
+ err = s.CreateResourceFromString(s.GetGatewayClassYaml())
+ Expect(err).NotTo(HaveOccurred(), "creating GatewayClass")
+ time.Sleep(5 * time.Second)
+
+ By("create Gateway")
+ err = s.CreateResourceFromString(s.GetGatewayYaml())
+ Expect(err).NotTo(HaveOccurred(), "creating Gateway")
+ time.Sleep(5 * time.Second)
+
+ By("create HTTPRoute")
+ s.ApplyHTTPRoute(types.NamespacedName{Namespace: s.Namespace(), Name: "httpbin"}, fmt.Sprintf(defaultHTTPRoute, s.Namespace(), s.Namespace()))
+ }
+
Context("Rewrite Upstream Host", func() {
var createUpstreamHost = `
apiVersion: apisix.apache.org/v1alpha1
@@ -86,25 +108,7 @@ spec:
upstreamHost: httpbin.update.example.com
`
- BeforeEach(func() {
- By("create GatewayProxy")
- err = s.CreateResourceFromString(s.GetGatewayProxySpec())
- Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy")
- time.Sleep(5 * time.Second)
-
- By("create GatewayClass")
- err = s.CreateResourceFromString(s.GetGatewayClassYaml())
- Expect(err).NotTo(HaveOccurred(), "creating GatewayClass")
- time.Sleep(5 * time.Second)
-
- By("create Gateway")
- err = s.CreateResourceFromString(s.GetGatewayYaml())
- Expect(err).NotTo(HaveOccurred(), "creating Gateway")
- time.Sleep(5 * time.Second)
-
- By("create HTTPRoute")
- s.ApplyHTTPRoute(types.NamespacedName{Namespace: s.Namespace(), Name: "httpbin"}, fmt.Sprintf(defaultHTTPRoute, s.Namespace(), s.Namespace()))
- })
+ BeforeEach(gatewayBeforeEach)
It("should rewrite upstream host", func() {
s.ResourceApplied("BackendTrafficPolicy", "httpbin", createUpstreamHost, 1)
@@ -159,6 +163,178 @@ spec:
})
})
})
+
+ Context("Health Check", func() {
+ var policyWithActiveHealthCheck = `
+apiVersion: apisix.apache.org/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+ name: httpbin
+spec:
+ targetRefs:
+ - name: httpbin-service-e2e-test
+ kind: Service
+ group: ""
+ healthCheck:
+ active:
+ type: http
+ httpPath: /get
+ healthy:
+ httpCodes: [200]
+ interval: 1s
+ unhealthy:
+ httpCodes: [500]
+ httpFailures: 2
+ interval: 1s
+`
+
+ var policyWithActiveAndPassiveHealthCheck = `
+apiVersion: apisix.apache.org/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+ name: httpbin
+spec:
+ targetRefs:
+ - name: httpbin-service-e2e-test
+ kind: Service
+ group: ""
+ healthCheck:
+ active:
+ type: http
+ httpPath: /get
+ healthy:
+ httpCodes: [200]
+ interval: 1s
+ unhealthy:
+ httpCodes: [500]
+ httpFailures: 2
+ interval: 1s
+ passive:
+ type: http
+ healthy:
+ httpCodes: [200]
+ unhealthy:
+ httpCodes: [502, 503]
+ httpFailures: 3
+`
+
+ BeforeEach(gatewayBeforeEach)
+
+ It("should configure active health check on upstream", func() {
+ s.ResourceApplied("BackendTrafficPolicy", "httpbin", policyWithActiveHealthCheck, 1)
+
+ // Trigger some traffic so APISIX registers the upstream
+ s.RequestAssert(&scaffold.RequestAssert{
+ Method: "GET",
+ Path: "/get",
+ Host: "httpbin.org",
+ Checks: []scaffold.ResponseCheckFunc{
+ scaffold.WithExpectedStatus(200),
+ },
+ })
+ time.Sleep(2 * time.Second)
+
+ ups, err := s.DefaultDataplaneResource().Upstream().List(context.Background())
+ Expect(err).ToNot(HaveOccurred(), "listing upstreams")
+ Expect(ups).NotTo(BeEmpty(), "upstreams should not be empty")
+
+ var target *adctypes.Upstream
+ for _, u := range ups {
+ if u.Checks != nil {
+ target = u
+ break
+ }
+ }
+ Expect(target).NotTo(BeNil(), "upstream with health check should exist")
+ Expect(target.Checks.Active).NotTo(BeNil(), "active health check should be configured")
+ Expect(target.Checks.Active.HTTPPath).To(Equal("/get"), "active health check http path")
+ Expect(target.Checks.Active.Healthy.Interval).To(Equal(1), "active healthy interval")
+ Expect(target.Checks.Active.Healthy.HTTPStatuses).To(Equal([]int{200}), "active healthy http codes")
+ Expect(target.Checks.Active.Unhealthy.Interval).To(Equal(1), "active unhealthy interval")
+ Expect(target.Checks.Active.Unhealthy.HTTPFailures).To(Equal(2), "active unhealthy http failures")
+ Expect(target.Checks.Active.Unhealthy.HTTPStatuses).To(Equal([]int{500}), "active unhealthy http codes")
+ Expect(target.Checks.Passive).To(BeNil(), "passive health check should not be configured")
+ })
+
+ It("should configure active and passive health checks on upstream", func() {
+ s.ResourceApplied("BackendTrafficPolicy", "httpbin", policyWithActiveAndPassiveHealthCheck, 1)
+
+ // Trigger some traffic so APISIX registers the upstream
+ s.RequestAssert(&scaffold.RequestAssert{
+ Method: "GET",
+ Path: "/get",
+ Host: "httpbin.org",
+ Checks: []scaffold.ResponseCheckFunc{
+ scaffold.WithExpectedStatus(200),
+ },
+ })
+ time.Sleep(2 * time.Second)
+
+ ups, err := s.DefaultDataplaneResource().Upstream().List(context.Background())
+ Expect(err).ToNot(HaveOccurred(), "listing upstreams")
+ Expect(ups).NotTo(BeEmpty(), "upstreams should not be empty")
+
+ var target *adctypes.Upstream
+ for _, u := range ups {
+ if u.Checks != nil && u.Checks.Passive != nil {
+ target = u
+ break
+ }
+ }
+ Expect(target).NotTo(BeNil(), "upstream with active and passive health check should exist")
+
+ // Verify active health check
+ Expect(target.Checks.Active).NotTo(BeNil(), "active health check should be configured")
+ Expect(target.Checks.Active.HTTPPath).To(Equal("/get"), "active health check http path")
+ Expect(target.Checks.Active.Healthy.HTTPStatuses).To(Equal([]int{200}), "active healthy http codes")
+ Expect(target.Checks.Active.Unhealthy.HTTPFailures).To(Equal(2), "active unhealthy http failures")
+
+ // Verify passive health check
+ Expect(target.Checks.Passive.Healthy.HTTPStatuses).To(Equal([]int{200}), "passive healthy http codes")
+ Expect(target.Checks.Passive.Unhealthy.HTTPStatuses).To(Equal([]int{502, 503}), "passive unhealthy http codes")
+ Expect(target.Checks.Passive.Unhealthy.HTTPFailures).To(Equal(3), "passive unhealthy http failures")
+ })
+
+ It("should remove health check when policy is deleted", func() {
+ s.ResourceApplied("BackendTrafficPolicy", "httpbin", policyWithActiveHealthCheck, 1)
+
+ // Trigger traffic to establish upstream
+ s.RequestAssert(&scaffold.RequestAssert{
+ Method: "GET",
+ Path: "/get",
+ Host: "httpbin.org",
+ Checks: []scaffold.ResponseCheckFunc{
+ scaffold.WithExpectedStatus(200),
+ },
+ })
+ time.Sleep(2 * time.Second)
+
+ // Verify health check is present on the target upstream
+ ups, err := s.DefaultDataplaneResource().Upstream().List(context.Background())
+ Expect(err).ToNot(HaveOccurred())
+ hasHealthCheck := false
+ for _, u := range ups {
+ if u.Checks != nil {
+ hasHealthCheck = true
+ break
+ }
+ }
+ Expect(hasHealthCheck).To(BeTrue(), "upstream should have health check before policy deletion")
+
+ // Delete the policy
+ err = s.DeleteResourceFromString(policyWithActiveHealthCheck)
+ Expect(err).NotTo(HaveOccurred(), "deleting BackendTrafficPolicy")
+ time.Sleep(3 * time.Second)
+
+ // Verify health check is removed from the target upstream
+ ups, err = s.DefaultDataplaneResource().Upstream().List(context.Background())
+ Expect(err).ToNot(HaveOccurred())
+ Expect(ups).NotTo(BeEmpty(), "upstreams should still exist after policy deletion")
+ for _, u := range ups {
+ Expect(u.Checks).To(BeNil(), "upstream should not have health check after policy deletion")
+ }
+ })
+ })
})
var _ = Describe("Test BackendTrafficPolicy base on Ingress", Label("apisix.apache.org", "v1alpha1", "backendtrafficpolicy"), func() {