This is an automated email from the ASF dual-hosted git repository.
AlinsRan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git
The following commit(s) were added to refs/heads/master by this push:
new e461eb2e feat: support upstream health checks in BackendTrafficPolicy
(#2763)
e461eb2e is described below
commit e461eb2e330c44d831b96c59d2c4093653a2b5b8
Author: AlinsRan <[email protected]>
AuthorDate: Mon May 18 09:01:24 2026 +0800
feat: support upstream health checks in BackendTrafficPolicy (#2763)
---
api/v1alpha1/backendtrafficpolicy_types.go | 140 ++++++++++
api/v1alpha1/zz_generated.deepcopy.go | 165 +++++++++++
.../apisix.apache.org_backendtrafficpolicies.yaml | 175 ++++++++++++
docs/en/latest/reference/api-reference.md | 130 +++++++++
internal/adc/translator/apisixconsumer_test.go | 2 +-
internal/adc/translator/httproute_test.go | 311 +++++++++++++++++++++
internal/adc/translator/policies.go | 89 ++++++
test/e2e/crds/v1alpha1/backendtrafficpolicy.go | 216 ++++++++++++--
8 files changed, 1207 insertions(+), 21 deletions(-)
diff --git a/api/v1alpha1/backendtrafficpolicy_types.go
b/api/v1alpha1/backendtrafficpolicy_types.go
index 44d5239d..826ec854 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 cc03a549..9e1c02ff 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -26,6 +26,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
@@ -171,6 +241,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.
@@ -616,6 +691,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
@@ -631,6 +731,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 ed2cad68..c286a934 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 11357fb3..3d8987ac 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`:<br /> • `pass`: preserve the original Host
header<br /> • `node`: use the upstream node’s host<br /> • `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 7dbe131c..fe56b1ec 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
new file mode 100644
index 00000000..7b11e129
--- /dev/null
+++ b/internal/adc/translator/httproute_test.go
@@ -0,0 +1,311 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package translator
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/go-logr/logr"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ corev1 "k8s.io/api/core/v1"
+ discoveryv1 "k8s.io/api/discovery/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/types"
+ "k8s.io/utils/ptr"
+ 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"
+ internaltypes
"github.com/apache/apisix-ingress-controller/internal/types"
+)
+
+func TestTranslateHTTPRouteUpstreamScheme(t *testing.T) {
+ tests := []struct {
+ name string
+ appProtocol string
+ policyScheme string
+ wantScheme string
+ }{
+ {
+ name: "preserves backend traffic policy scheme",
+ appProtocol: internaltypes.AppProtocolHTTP,
+ policyScheme: apiv2.SchemeHTTPS,
+ wantScheme: apiv2.SchemeHTTPS,
+ },
+ {
+ name: "falls back to app protocol when scheme is
unset",
+ appProtocol: internaltypes.AppProtocolWSS,
+ wantScheme: apiv2.SchemeHTTPS,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ translator := NewTranslator(logr.Discard())
+ tctx :=
provider.NewDefaultTranslateContext(context.Background())
+
+ const (
+ namespace = "default"
+ serviceName = "backend"
+ portName = "web"
+ portNumber = int32(8443)
+ )
+
+ serviceKey := types.NamespacedName{Namespace:
namespace, Name: serviceName}
+ tctx.Services[serviceKey] = &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: serviceName,
+ Namespace: namespace,
+ },
+ Spec: corev1.ServiceSpec{
+ Ports: []corev1.ServicePort{{
+ Name: portName,
+ Port: portNumber,
+ AppProtocol:
ptr.To(tt.appProtocol),
+ }},
+ },
+ }
+ tctx.EndpointSlices[serviceKey] =
[]discoveryv1.EndpointSlice{{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "backend-1",
+ Namespace: namespace,
+ },
+ Ports: []discoveryv1.EndpointPort{{
+ Name: ptr.To(portName),
+ Port: ptr.To(portNumber),
+ }},
+ Endpoints: []discoveryv1.Endpoint{{
+ Addresses: []string{"10.0.0.1"},
+ Conditions:
discoveryv1.EndpointConditions{
+ Ready: ptr.To(true),
+ },
+ }},
+ }}
+
+ if tt.policyScheme != "" {
+ tctx.BackendTrafficPolicies[serviceKey] =
&v1alpha1.BackendTrafficPolicy{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "backend-policy",
+ Namespace: namespace,
+ },
+ Spec: v1alpha1.BackendTrafficPolicySpec{
+ TargetRefs:
[]v1alpha1.BackendPolicyTargetReferenceWithSectionName{{
+
LocalPolicyTargetReference: gatewayv1alpha2.LocalPolicyTargetReference{
+ Name:
gatewayv1alpha2.ObjectName(serviceName),
+ Kind:
gatewayv1alpha2.Kind(internaltypes.KindService),
+ },
+ }},
+ Scheme: tt.policyScheme,
+ },
+ }
+ }
+
+ route := &gatewayv1.HTTPRoute{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "demo",
+ Namespace: namespace,
+ },
+ Spec: gatewayv1.HTTPRouteSpec{
+ Rules: []gatewayv1.HTTPRouteRule{{
+ BackendRefs:
[]gatewayv1.HTTPBackendRef{{
+ BackendRef:
gatewayv1.BackendRef{
+
BackendObjectReference: gatewayv1.BackendObjectReference{
+ Name:
gatewayv1.ObjectName(serviceName),
+ Port:
ptr.To(gatewayv1.PortNumber(portNumber)),
+ },
+ },
+ }},
+ }},
+ },
+ }
+
+ result, err := translator.TranslateHTTPRoute(tctx,
route)
+ require.NoError(t, err)
+ require.Len(t, result.Services, 1)
+ require.NotNil(t, result.Services[0].Upstream)
+
+ assert.Equal(t, tt.wantScheme,
result.Services[0].Upstream.Scheme)
+ assert.Equal(t, "10.0.0.1",
result.Services[0].Upstream.Nodes[0].Host)
+ })
+ }
+}
+
+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..726fbfac 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 = "http"
+ }
+ 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 d26b6630..5704757e 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,26 +108,8 @@ spec:
upstreamHost: httpbin.update.example.com
`
- BeforeEach(func() {
- gatewayName := s.Namespace()
- By("create GatewayProxy")
- err =
s.CreateResourceFromString(s.GetGatewayProxySpec())
- Expect(err).NotTo(HaveOccurred(), "creating
GatewayProxy")
- time.Sleep(time.Second)
-
- By("create GatewayClass")
- err =
s.CreateResourceFromString(s.GetGatewayClassYaml())
- Expect(err).NotTo(HaveOccurred(), "creating
GatewayClass")
- time.Sleep(time.Second)
-
- By("create Gateway")
- err = s.CreateResourceFromString(s.GetGatewayYaml())
- Expect(err).NotTo(HaveOccurred(), "creating Gateway")
- time.Sleep(time.Second)
-
- By("create HTTPRoute")
- s.ApplyHTTPRoute(types.NamespacedName{Namespace:
s.Namespace(), Name: "httpbin"}, fmt.Sprintf(defaultHTTPRoute, gatewayName,
s.Namespace()))
- })
+ BeforeEach(gatewayBeforeEach)
+
It("should rewrite upstream host", func() {
s.ResourceApplied("BackendTrafficPolicy", "httpbin",
createUpstreamHost, 1)
s.RequestAssert(&scaffold.RequestAssert{
@@ -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() {