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() {