From db2c941d8ef393b5bfd5e2b6399fc48a91d6c5b5 Mon Sep 17 00:00:00 2001 From: rongxin Date: Tue, 12 May 2026 05:15:55 +0800 Subject: [PATCH 01/12] feat(api): add HealthCheck types to BackendTrafficPolicySpec --- api/v1alpha1/backendtrafficpolicy_types.go | 140 +++++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/api/v1alpha1/backendtrafficpolicy_types.go b/api/v1alpha1/backendtrafficpolicy_types.go index fc998b665..81a93bc01 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:"timeout,omitempty" yaml:"timeout,omitempty"` +} + func init() { SchemeBuilder.Register(&BackendTrafficPolicy{}, &BackendTrafficPolicyList{}) } From d66b84227873381e691a2e8b3f70f4f5361ba3a4 Mon Sep 17 00:00:00 2001 From: rongxin Date: Tue, 12 May 2026 05:16:58 +0800 Subject: [PATCH 02/12] chore: regenerate deepcopy for BackendTrafficPolicy health check types --- api/v1alpha1/zz_generated.deepcopy.go | 165 ++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index f7b5383c5..473a7b289 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 From 8e9cd6065721d32bae4313d8773f1cd16ba9442c Mon Sep 17 00:00:00 2001 From: rongxin Date: Tue, 12 May 2026 05:19:13 +0800 Subject: [PATCH 03/12] feat: translate BackendTrafficPolicy health checks to APISIX upstream --- internal/adc/translator/httproute_test.go | 161 ++++++++++++++++++++++ internal/adc/translator/policies.go | 80 +++++++++++ 2 files changed, 241 insertions(+) diff --git a/internal/adc/translator/httproute_test.go b/internal/adc/translator/httproute_test.go index 28fdea839..7b11e1290 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 417069640..ef9a77957 100644 --- a/internal/adc/translator/policies.go +++ b/internal/adc/translator/policies.go @@ -79,4 +79,84 @@ 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 { + active.Healthy = adctypes.UpstreamActiveHealthCheckHealthy{ + Interval: int(config.Healthy.Interval.Seconds()), + UpstreamPassiveHealthCheckHealthy: adctypes.UpstreamPassiveHealthCheckHealthy{ + HTTPStatuses: config.Healthy.HTTPCodes, + Successes: config.Healthy.Successes, + }, + } + } + if config.Unhealthy != nil { + active.Unhealthy = adctypes.UpstreamActiveHealthCheckUnhealthy{ + Interval: int(config.Unhealthy.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 } From 1e47d35aa736634e0ea2eb46da7eb3d23e161e9a Mon Sep 17 00:00:00 2001 From: rongxin Date: Tue, 12 May 2026 05:20:42 +0800 Subject: [PATCH 04/12] chore: regenerate CRD manifests with BackendTrafficPolicy health check fields --- ...six.apache.org_backendtrafficpolicies.yaml | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) diff --git a/config/crd/bases/apisix.apache.org_backendtrafficpolicies.yaml b/config/crd/bases/apisix.apache.org_backendtrafficpolicies.yaml index 64c366a43..52047d34b 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 + timeout: + 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 + timeout: + 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. From 0750e0446f29ea941cf83fe82aca26dcf3fe2f33 Mon Sep 17 00:00:00 2001 From: rongxin Date: Tue, 12 May 2026 09:01:53 +0800 Subject: [PATCH 05/12] test(e2e): add health check e2e tests for BackendTrafficPolicy --- .../e2e/crds/v1alpha1/backendtrafficpolicy.go | 213 ++++++++++++++++-- 1 file changed, 194 insertions(+), 19 deletions(-) diff --git a/test/e2e/crds/v1alpha1/backendtrafficpolicy.go b/test/e2e/crds/v1alpha1/backendtrafficpolicy.go index bfb2ef890..fcb5b84a8 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,177 @@ 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 + 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 + ups, err = s.DefaultDataplaneResource().Upstream().List(context.Background()) + Expect(err).ToNot(HaveOccurred()) + 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() { From f0e483f654d05a9d16e9cbfd2d0e75715e7eb7d4 Mon Sep 17 00:00:00 2001 From: rongxin Date: Wed, 13 May 2026 04:48:35 +0800 Subject: [PATCH 06/12] fix: address review comments on BackendTrafficPolicy health check - Fix Timeouts field JSON tag: timeout -> timeouts to match ADC type - Enforce minimum 1s interval in translateBTPActiveHealthCheck to prevent sub-second values from truncating to 0 (matching v2/apisixupstream behavior) - Scope e2e upstream assertions to httpbin-service-e2e-test by name - Regenerate CRD manifests and API reference docs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/v1alpha1/backendtrafficpolicy_types.go | 2 +- ...six.apache.org_backendtrafficpolicies.yaml | 4 +- docs/en/latest/reference/api-reference.md | 130 ++++ ...-05-12-backendtrafficpolicy-healthcheck.md | 580 ++++++++++++++++++ internal/adc/translator/policies.go | 16 +- .../e2e/crds/v1alpha1/backendtrafficpolicy.go | 15 +- 6 files changed, 736 insertions(+), 11 deletions(-) create mode 100644 docs/superpowers/plans/2026-05-12-backendtrafficpolicy-healthcheck.md diff --git a/api/v1alpha1/backendtrafficpolicy_types.go b/api/v1alpha1/backendtrafficpolicy_types.go index 81a93bc01..9e8616923 100644 --- a/api/v1alpha1/backendtrafficpolicy_types.go +++ b/api/v1alpha1/backendtrafficpolicy_types.go @@ -262,7 +262,7 @@ type PassiveHealthCheckUnhealthy struct { // +kubebuilder:validation:Minimum=1 // +kubebuilder:validation:Maximum=254 // +optional - Timeouts int `json:"timeout,omitempty" yaml:"timeout,omitempty"` + Timeouts int `json:"timeouts,omitempty" yaml:"timeouts,omitempty"` } func init() { diff --git a/config/crd/bases/apisix.apache.org_backendtrafficpolicies.yaml b/config/crd/bases/apisix.apache.org_backendtrafficpolicies.yaml index 52047d34b..8b771c804 100644 --- a/config/crd/bases/apisix.apache.org_backendtrafficpolicies.yaml +++ b/config/crd/bases/apisix.apache.org_backendtrafficpolicies.yaml @@ -144,7 +144,7 @@ spec: maximum: 254 minimum: 0 type: integer - timeout: + timeouts: description: Timeouts is the number of timeouts to mark a node unhealthy. maximum: 254 @@ -206,7 +206,7 @@ spec: maximum: 254 minimum: 0 type: integer - timeout: + timeouts: description: Timeouts is the number of timeouts to mark a node unhealthy. maximum: 254 diff --git a/docs/en/latest/reference/api-reference.md b/docs/en/latest/reference/api-reference.md index 44b29323c..21ba3cf99 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/docs/superpowers/plans/2026-05-12-backendtrafficpolicy-healthcheck.md b/docs/superpowers/plans/2026-05-12-backendtrafficpolicy-healthcheck.md new file mode 100644 index 000000000..3fb50895f --- /dev/null +++ b/docs/superpowers/plans/2026-05-12-backendtrafficpolicy-healthcheck.md @@ -0,0 +1,580 @@ +# BackendTrafficPolicy Health Check Support Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add active and passive upstream health check configuration support to `BackendTrafficPolicy` (Gateway API path), mirroring the capability already available in `ApisixUpstream`. + +**Architecture:** Define health check types independently in `api/v1alpha1` package; extend `BackendTrafficPolicySpec`; add translation logic in `internal/adc/translator/policies.go` that maps v1alpha1 types to ADC types (reusing patterns from `apisixupstream.go`). + +**Tech Stack:** Go, controller-gen (for DeepCopy generation), Kubernetes API conventions, APISIX ADC types. + +--- + +## File Map + +| File | Action | Purpose | +|------|--------|---------| +| `api/v1alpha1/backendtrafficpolicy_types.go` | Modify | Add `HealthCheck` field + new health check types | +| `api/v1alpha1/zz_generated.deepcopy.go` | Regenerate | Auto-generated DeepCopy for new types | +| `internal/adc/translator/policies.go` | Modify | Translate health check spec → ADC upstream | +| `internal/adc/translator/httproute_test.go` | Modify | Add unit tests for health check translation | + +--- + +### Task 1: Add Health Check Types and Field to BackendTrafficPolicySpec + +**Files:** +- Modify: `api/v1alpha1/backendtrafficpolicy_types.go` + +- [ ] **Step 1: Add `HealthCheck` field to `BackendTrafficPolicySpec`** + +In `api/v1alpha1/backendtrafficpolicy_types.go`, add the field after `Host`: + +```go +// 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"` +``` + +- [ ] **Step 2: Add health check type definitions** + +Append the following types at the end of `api/v1alpha1/backendtrafficpolicy_types.go` (before the `func init()` — but there is no init, so just append after the last type): + +```go +// 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:"timeout,omitempty" yaml:"timeout,omitempty"` +} +``` + +- [ ] **Step 3: Build to verify no syntax errors** + +```bash +go build ./api/v1alpha1/... +``` + +Expected: no errors. + +- [ ] **Step 4: Commit** + +```bash +git add api/v1alpha1/backendtrafficpolicy_types.go +git commit -m "feat(api): add HealthCheck types to BackendTrafficPolicySpec" +``` + +--- + +### Task 2: Regenerate DeepCopy Methods + +**Files:** +- Modify: `api/v1alpha1/zz_generated.deepcopy.go` (auto-generated) + +- [ ] **Step 1: Run controller-gen to regenerate DeepCopy** + +```bash +make generate +``` + +Expected: `api/v1alpha1/zz_generated.deepcopy.go` is updated with DeepCopy methods for `HealthCheck`, `ActiveHealthCheck`, `PassiveHealthCheck`, `ActiveHealthCheckHealthy`, `ActiveHealthCheckUnhealthy`, `PassiveHealthCheckHealthy`, `PassiveHealthCheckUnhealthy`. + +- [ ] **Step 2: Verify build** + +```bash +go build ./... +``` + +Expected: no errors. + +- [ ] **Step 3: Commit** + +```bash +git add api/v1alpha1/zz_generated.deepcopy.go +git commit -m "chore: regenerate deepcopy for BackendTrafficPolicy health check types" +``` + +--- + +### Task 3: Write Failing Tests for Health Check Translation + +**Files:** +- Modify: `internal/adc/translator/httproute_test.go` + +- [ ] **Step 1: Add test for active health check translation** + +Add the following test function to `internal/adc/translator/httproute_test.go`: + +```go +func TestAttachBackendTrafficPolicyHealthCheck(t *testing.T) { + trueVal := true + + 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 only", + policy: &v1alpha1.BackendTrafficPolicy{ + Spec: v1alpha1.BackendTrafficPolicySpec{ + HealthCheck: &v1alpha1.HealthCheck{ + Active: &v1alpha1.ActiveHealthCheck{ + Type: "http", + HTTPPath: "/healthz", + Concurrency: 10, + Host: "example.com", + Port: 8080, + StrictTLS: &trueVal, + 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", + HTTPPath: "/healthz", + Concurrency: 10, + Host: "example.com", + Port: 8080, + HTTPSVerifyCertificate: true, + 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: "passive health check only", + policy: &v1alpha1.BackendTrafficPolicy{ + Spec: v1alpha1.BackendTrafficPolicySpec{ + HealthCheck: &v1alpha1.HealthCheck{ + Active: &v1alpha1.ActiveHealthCheck{ + 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: "http", + 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) + }) + } +} +``` + +Note: you need to add the following imports to `httproute_test.go` if not already present: +- `"time"` +- `adctypes "github.com/apache/apisix-ingress-controller/api/adc"` + +- [ ] **Step 2: Run the test to confirm it fails** + +```bash +go test ./internal/adc/translator/... -run TestAttachBackendTrafficPolicyHealthCheck -v +``` + +Expected: compilation error or test failure (health check field not yet translated). + +- [ ] **Step 3: Commit failing tests** + +```bash +git add internal/adc/translator/httproute_test.go +git commit -m "test: add failing tests for BackendTrafficPolicy health check translation" +``` + +--- + +### Task 4: Implement Health Check Translation in policies.go + +**Files:** +- Modify: `internal/adc/translator/policies.go` + +- [ ] **Step 1: Add health check translation to `attachBackendTrafficPolicyToUpstream`** + +In `internal/adc/translator/policies.go`, update the `attachBackendTrafficPolicyToUpstream` function to call a new helper, and add the helper functions. The full updated file content: + +```go +func (t *Translator) attachBackendTrafficPolicyToUpstream(policy *v1alpha1.BackendTrafficPolicy, upstream *adctypes.Upstream) { + if policy == nil { + return + } + upstream.PassHost = policy.Spec.PassHost + upstream.UpstreamHost = string(policy.Spec.Host) + upstream.Scheme = policy.Spec.Scheme + if policy.Spec.Retries != nil { + upstream.Retries = new(int64) + *upstream.Retries = int64(*policy.Spec.Retries) + } + if policy.Spec.Timeout != nil { + upstream.Timeout = &adctypes.Timeout{ + Connect: int(policy.Spec.Timeout.Connect.Seconds()), + Read: int(policy.Spec.Timeout.Read.Seconds()), + Send: int(policy.Spec.Timeout.Send.Seconds()), + } + } + if policy.Spec.LoadBalancer != nil { + upstream.Type = adctypes.UpstreamType(policy.Spec.LoadBalancer.Type) + 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 { + active := &adctypes.UpstreamActiveHealthCheck{ + Type: config.Type, + Timeout: int(config.Timeout.Seconds()), + Concurrency: config.Concurrency, + Host: config.Host, + Port: config.Port, + HTTPPath: config.HTTPPath, + } + if config.Type == "" { + active.Type = "http" + } + if config.StrictTLS == nil || *config.StrictTLS { + active.HTTPSVerifyCertificate = true + } + if len(config.RequestHeaders) > 0 { + active.HTTPRequestHeaders = config.RequestHeaders + } + if config.Healthy != nil { + active.Healthy = adctypes.UpstreamActiveHealthCheckHealthy{ + Interval: int(config.Healthy.Interval.Seconds()), + UpstreamPassiveHealthCheckHealthy: adctypes.UpstreamPassiveHealthCheckHealthy{ + HTTPStatuses: config.Healthy.HTTPCodes, + Successes: config.Healthy.Successes, + }, + } + } + if config.Unhealthy != nil { + active.Unhealthy = adctypes.UpstreamActiveHealthCheckUnhealthy{ + Interval: int(config.Unhealthy.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 { + passive := &adctypes.UpstreamPassiveHealthCheck{ + Type: config.Type, + } + if config.Type == "" { + passive.Type = "http" + } + 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 +} +``` + +- [ ] **Step 2: Run the tests to verify they pass** + +```bash +go test ./internal/adc/translator/... -run TestAttachBackendTrafficPolicyHealthCheck -v +``` + +Expected: all test cases PASS. + +- [ ] **Step 3: Run the full translator test suite** + +```bash +go test ./internal/adc/translator/... -v +``` + +Expected: all tests PASS. + +- [ ] **Step 4: Commit** + +```bash +git add internal/adc/translator/policies.go +git commit -m "feat: translate BackendTrafficPolicy health check to APISIX upstream" +``` + +--- + +### Task 5: Verify Build and Full Test Suite + +- [ ] **Step 1: Run full build** + +```bash +go build ./... +``` + +Expected: no errors. + +- [ ] **Step 2: Run all unit tests** + +```bash +go test ./... +``` + +Expected: all tests pass. + +- [ ] **Step 3: Regenerate manifests (CRD YAML)** + +```bash +make manifests +``` + +Expected: CRD YAML in `config/crd/bases/` is updated to include `healthCheck` fields in `BackendTrafficPolicy`. + +- [ ] **Step 4: Commit CRD changes** + +```bash +git add config/crd/bases/ +git commit -m "chore: regenerate CRD manifests with BackendTrafficPolicy health check fields" +``` + +--- + +## Notes on ADC Type Field Names + +When writing translation code, the ADC types (`api/adc/types.go`) use these field names: + +- `UpstreamHealthCheck.Active` → `*UpstreamActiveHealthCheck` +- `UpstreamHealthCheck.Passive` → `*UpstreamPassiveHealthCheck` +- `UpstreamActiveHealthCheck.Healthy` → `UpstreamActiveHealthCheckHealthy` (value, not pointer) +- `UpstreamActiveHealthCheck.Unhealthy` → `UpstreamActiveHealthCheckUnhealthy` (value, not pointer) +- `UpstreamActiveHealthCheckHealthy` embeds `UpstreamPassiveHealthCheckHealthy` inline +- `UpstreamPassiveHealthCheckHealthy.HTTPStatuses` (not HTTPCodes) +- `UpstreamPassiveHealthCheckUnhealthy.HTTPStatuses` (not HTTPCodes) +- `Upstream.Checks` is the field for `*UpstreamHealthCheck` + +Verify exact field names against `api/adc/types.go` before compiling. diff --git a/internal/adc/translator/policies.go b/internal/adc/translator/policies.go index ef9a77957..8a4574470 100644 --- a/internal/adc/translator/policies.go +++ b/internal/adc/translator/policies.go @@ -18,6 +18,8 @@ package translator import ( + "time" + "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" @@ -26,6 +28,8 @@ import ( "github.com/apache/apisix-ingress-controller/api/v1alpha1" ) +const _minHealthCheckInterval = time.Second + func convertBackendRef(namespace, name, kind string) gatewayv1.BackendRef { backendRef := gatewayv1.BackendRef{} backendRef.Name = gatewayv1.ObjectName(name) @@ -114,8 +118,12 @@ func translateBTPActiveHealthCheck(config *v1alpha1.ActiveHealthCheck) *adctypes HTTPRequestHeaders: config.RequestHeaders, } if config.Healthy != nil { + interval := config.Healthy.Interval.Duration + if interval < _minHealthCheckInterval { + interval = _minHealthCheckInterval + } active.Healthy = adctypes.UpstreamActiveHealthCheckHealthy{ - Interval: int(config.Healthy.Interval.Seconds()), + Interval: int(interval.Seconds()), UpstreamPassiveHealthCheckHealthy: adctypes.UpstreamPassiveHealthCheckHealthy{ HTTPStatuses: config.Healthy.HTTPCodes, Successes: config.Healthy.Successes, @@ -123,8 +131,12 @@ func translateBTPActiveHealthCheck(config *v1alpha1.ActiveHealthCheck) *adctypes } } if config.Unhealthy != nil { + interval := config.Unhealthy.Interval.Duration + if interval < _minHealthCheckInterval { + interval = _minHealthCheckInterval + } active.Unhealthy = adctypes.UpstreamActiveHealthCheckUnhealthy{ - Interval: int(config.Unhealthy.Interval.Seconds()), + Interval: int(interval.Seconds()), UpstreamPassiveHealthCheckUnhealthy: adctypes.UpstreamPassiveHealthCheckUnhealthy{ HTTPStatuses: config.Unhealthy.HTTPCodes, HTTPFailures: config.Unhealthy.HTTPFailures, diff --git a/test/e2e/crds/v1alpha1/backendtrafficpolicy.go b/test/e2e/crds/v1alpha1/backendtrafficpolicy.go index fcb5b84a8..b50799d8c 100644 --- a/test/e2e/crds/v1alpha1/backendtrafficpolicy.go +++ b/test/e2e/crds/v1alpha1/backendtrafficpolicy.go @@ -20,6 +20,7 @@ package v1alpha1 import ( "context" "fmt" + "strings" "time" . "github.com/onsi/ginkgo/v2" @@ -240,7 +241,7 @@ spec: var target *adctypes.Upstream for _, u := range ups { - if u.Checks != nil { + if strings.Contains(u.Name, "httpbin-service-e2e-test") && u.Checks != nil { target = u break } @@ -276,7 +277,7 @@ spec: var target *adctypes.Upstream for _, u := range ups { - if u.Checks != nil && u.Checks.Passive != nil { + if strings.Contains(u.Name, "httpbin-service-e2e-test") && u.Checks != nil && u.Checks.Passive != nil { target = u break } @@ -309,12 +310,12 @@ spec: }) time.Sleep(2 * time.Second) - // Verify health check is present + // 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 { + if strings.Contains(u.Name, "httpbin-service-e2e-test") && u.Checks != nil { hasHealthCheck = true break } @@ -326,11 +327,13 @@ spec: Expect(err).NotTo(HaveOccurred(), "deleting BackendTrafficPolicy") time.Sleep(3 * time.Second) - // Verify health check is removed + // Verify health check is removed from the target upstream ups, err = s.DefaultDataplaneResource().Upstream().List(context.Background()) Expect(err).ToNot(HaveOccurred()) for _, u := range ups { - Expect(u.Checks).To(BeNil(), "upstream should not have health check after policy deletion") + if strings.Contains(u.Name, "httpbin-service-e2e-test") { + Expect(u.Checks).To(BeNil(), "upstream should not have health check after policy deletion") + } } }) }) From c76e9b2dc81558aba294f6ecb7d8cf9863bf783a Mon Sep 17 00:00:00 2001 From: rongxin Date: Wed, 13 May 2026 04:48:41 +0800 Subject: [PATCH 07/12] chore: remove accidentally staged plan file Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...-05-12-backendtrafficpolicy-healthcheck.md | 580 ------------------ 1 file changed, 580 deletions(-) delete mode 100644 docs/superpowers/plans/2026-05-12-backendtrafficpolicy-healthcheck.md diff --git a/docs/superpowers/plans/2026-05-12-backendtrafficpolicy-healthcheck.md b/docs/superpowers/plans/2026-05-12-backendtrafficpolicy-healthcheck.md deleted file mode 100644 index 3fb50895f..000000000 --- a/docs/superpowers/plans/2026-05-12-backendtrafficpolicy-healthcheck.md +++ /dev/null @@ -1,580 +0,0 @@ -# BackendTrafficPolicy Health Check Support Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add active and passive upstream health check configuration support to `BackendTrafficPolicy` (Gateway API path), mirroring the capability already available in `ApisixUpstream`. - -**Architecture:** Define health check types independently in `api/v1alpha1` package; extend `BackendTrafficPolicySpec`; add translation logic in `internal/adc/translator/policies.go` that maps v1alpha1 types to ADC types (reusing patterns from `apisixupstream.go`). - -**Tech Stack:** Go, controller-gen (for DeepCopy generation), Kubernetes API conventions, APISIX ADC types. - ---- - -## File Map - -| File | Action | Purpose | -|------|--------|---------| -| `api/v1alpha1/backendtrafficpolicy_types.go` | Modify | Add `HealthCheck` field + new health check types | -| `api/v1alpha1/zz_generated.deepcopy.go` | Regenerate | Auto-generated DeepCopy for new types | -| `internal/adc/translator/policies.go` | Modify | Translate health check spec → ADC upstream | -| `internal/adc/translator/httproute_test.go` | Modify | Add unit tests for health check translation | - ---- - -### Task 1: Add Health Check Types and Field to BackendTrafficPolicySpec - -**Files:** -- Modify: `api/v1alpha1/backendtrafficpolicy_types.go` - -- [ ] **Step 1: Add `HealthCheck` field to `BackendTrafficPolicySpec`** - -In `api/v1alpha1/backendtrafficpolicy_types.go`, add the field after `Host`: - -```go -// 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"` -``` - -- [ ] **Step 2: Add health check type definitions** - -Append the following types at the end of `api/v1alpha1/backendtrafficpolicy_types.go` (before the `func init()` — but there is no init, so just append after the last type): - -```go -// 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:"timeout,omitempty" yaml:"timeout,omitempty"` -} -``` - -- [ ] **Step 3: Build to verify no syntax errors** - -```bash -go build ./api/v1alpha1/... -``` - -Expected: no errors. - -- [ ] **Step 4: Commit** - -```bash -git add api/v1alpha1/backendtrafficpolicy_types.go -git commit -m "feat(api): add HealthCheck types to BackendTrafficPolicySpec" -``` - ---- - -### Task 2: Regenerate DeepCopy Methods - -**Files:** -- Modify: `api/v1alpha1/zz_generated.deepcopy.go` (auto-generated) - -- [ ] **Step 1: Run controller-gen to regenerate DeepCopy** - -```bash -make generate -``` - -Expected: `api/v1alpha1/zz_generated.deepcopy.go` is updated with DeepCopy methods for `HealthCheck`, `ActiveHealthCheck`, `PassiveHealthCheck`, `ActiveHealthCheckHealthy`, `ActiveHealthCheckUnhealthy`, `PassiveHealthCheckHealthy`, `PassiveHealthCheckUnhealthy`. - -- [ ] **Step 2: Verify build** - -```bash -go build ./... -``` - -Expected: no errors. - -- [ ] **Step 3: Commit** - -```bash -git add api/v1alpha1/zz_generated.deepcopy.go -git commit -m "chore: regenerate deepcopy for BackendTrafficPolicy health check types" -``` - ---- - -### Task 3: Write Failing Tests for Health Check Translation - -**Files:** -- Modify: `internal/adc/translator/httproute_test.go` - -- [ ] **Step 1: Add test for active health check translation** - -Add the following test function to `internal/adc/translator/httproute_test.go`: - -```go -func TestAttachBackendTrafficPolicyHealthCheck(t *testing.T) { - trueVal := true - - 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 only", - policy: &v1alpha1.BackendTrafficPolicy{ - Spec: v1alpha1.BackendTrafficPolicySpec{ - HealthCheck: &v1alpha1.HealthCheck{ - Active: &v1alpha1.ActiveHealthCheck{ - Type: "http", - HTTPPath: "/healthz", - Concurrency: 10, - Host: "example.com", - Port: 8080, - StrictTLS: &trueVal, - 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", - HTTPPath: "/healthz", - Concurrency: 10, - Host: "example.com", - Port: 8080, - HTTPSVerifyCertificate: true, - 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: "passive health check only", - policy: &v1alpha1.BackendTrafficPolicy{ - Spec: v1alpha1.BackendTrafficPolicySpec{ - HealthCheck: &v1alpha1.HealthCheck{ - Active: &v1alpha1.ActiveHealthCheck{ - 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: "http", - 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) - }) - } -} -``` - -Note: you need to add the following imports to `httproute_test.go` if not already present: -- `"time"` -- `adctypes "github.com/apache/apisix-ingress-controller/api/adc"` - -- [ ] **Step 2: Run the test to confirm it fails** - -```bash -go test ./internal/adc/translator/... -run TestAttachBackendTrafficPolicyHealthCheck -v -``` - -Expected: compilation error or test failure (health check field not yet translated). - -- [ ] **Step 3: Commit failing tests** - -```bash -git add internal/adc/translator/httproute_test.go -git commit -m "test: add failing tests for BackendTrafficPolicy health check translation" -``` - ---- - -### Task 4: Implement Health Check Translation in policies.go - -**Files:** -- Modify: `internal/adc/translator/policies.go` - -- [ ] **Step 1: Add health check translation to `attachBackendTrafficPolicyToUpstream`** - -In `internal/adc/translator/policies.go`, update the `attachBackendTrafficPolicyToUpstream` function to call a new helper, and add the helper functions. The full updated file content: - -```go -func (t *Translator) attachBackendTrafficPolicyToUpstream(policy *v1alpha1.BackendTrafficPolicy, upstream *adctypes.Upstream) { - if policy == nil { - return - } - upstream.PassHost = policy.Spec.PassHost - upstream.UpstreamHost = string(policy.Spec.Host) - upstream.Scheme = policy.Spec.Scheme - if policy.Spec.Retries != nil { - upstream.Retries = new(int64) - *upstream.Retries = int64(*policy.Spec.Retries) - } - if policy.Spec.Timeout != nil { - upstream.Timeout = &adctypes.Timeout{ - Connect: int(policy.Spec.Timeout.Connect.Seconds()), - Read: int(policy.Spec.Timeout.Read.Seconds()), - Send: int(policy.Spec.Timeout.Send.Seconds()), - } - } - if policy.Spec.LoadBalancer != nil { - upstream.Type = adctypes.UpstreamType(policy.Spec.LoadBalancer.Type) - 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 { - active := &adctypes.UpstreamActiveHealthCheck{ - Type: config.Type, - Timeout: int(config.Timeout.Seconds()), - Concurrency: config.Concurrency, - Host: config.Host, - Port: config.Port, - HTTPPath: config.HTTPPath, - } - if config.Type == "" { - active.Type = "http" - } - if config.StrictTLS == nil || *config.StrictTLS { - active.HTTPSVerifyCertificate = true - } - if len(config.RequestHeaders) > 0 { - active.HTTPRequestHeaders = config.RequestHeaders - } - if config.Healthy != nil { - active.Healthy = adctypes.UpstreamActiveHealthCheckHealthy{ - Interval: int(config.Healthy.Interval.Seconds()), - UpstreamPassiveHealthCheckHealthy: adctypes.UpstreamPassiveHealthCheckHealthy{ - HTTPStatuses: config.Healthy.HTTPCodes, - Successes: config.Healthy.Successes, - }, - } - } - if config.Unhealthy != nil { - active.Unhealthy = adctypes.UpstreamActiveHealthCheckUnhealthy{ - Interval: int(config.Unhealthy.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 { - passive := &adctypes.UpstreamPassiveHealthCheck{ - Type: config.Type, - } - if config.Type == "" { - passive.Type = "http" - } - 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 -} -``` - -- [ ] **Step 2: Run the tests to verify they pass** - -```bash -go test ./internal/adc/translator/... -run TestAttachBackendTrafficPolicyHealthCheck -v -``` - -Expected: all test cases PASS. - -- [ ] **Step 3: Run the full translator test suite** - -```bash -go test ./internal/adc/translator/... -v -``` - -Expected: all tests PASS. - -- [ ] **Step 4: Commit** - -```bash -git add internal/adc/translator/policies.go -git commit -m "feat: translate BackendTrafficPolicy health check to APISIX upstream" -``` - ---- - -### Task 5: Verify Build and Full Test Suite - -- [ ] **Step 1: Run full build** - -```bash -go build ./... -``` - -Expected: no errors. - -- [ ] **Step 2: Run all unit tests** - -```bash -go test ./... -``` - -Expected: all tests pass. - -- [ ] **Step 3: Regenerate manifests (CRD YAML)** - -```bash -make manifests -``` - -Expected: CRD YAML in `config/crd/bases/` is updated to include `healthCheck` fields in `BackendTrafficPolicy`. - -- [ ] **Step 4: Commit CRD changes** - -```bash -git add config/crd/bases/ -git commit -m "chore: regenerate CRD manifests with BackendTrafficPolicy health check fields" -``` - ---- - -## Notes on ADC Type Field Names - -When writing translation code, the ADC types (`api/adc/types.go`) use these field names: - -- `UpstreamHealthCheck.Active` → `*UpstreamActiveHealthCheck` -- `UpstreamHealthCheck.Passive` → `*UpstreamPassiveHealthCheck` -- `UpstreamActiveHealthCheck.Healthy` → `UpstreamActiveHealthCheckHealthy` (value, not pointer) -- `UpstreamActiveHealthCheck.Unhealthy` → `UpstreamActiveHealthCheckUnhealthy` (value, not pointer) -- `UpstreamActiveHealthCheckHealthy` embeds `UpstreamPassiveHealthCheckHealthy` inline -- `UpstreamPassiveHealthCheckHealthy.HTTPStatuses` (not HTTPCodes) -- `UpstreamPassiveHealthCheckUnhealthy.HTTPStatuses` (not HTTPCodes) -- `Upstream.Checks` is the field for `*UpstreamHealthCheck` - -Verify exact field names against `api/adc/types.go` before compiling. From 5dc2c36318f41435953e2e8c04c621a746b3f622 Mon Sep 17 00:00:00 2001 From: rongxin Date: Wed, 13 May 2026 05:31:29 +0800 Subject: [PATCH 08/12] test: fix e2e upstream health check assertions Upstream Name is cleared when embedded in a service object (httproute translator sets Name="" for single-backend upstreams). Filtering by name always fails; rely on Checks field instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/e2e/crds/v1alpha1/backendtrafficpolicy.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/test/e2e/crds/v1alpha1/backendtrafficpolicy.go b/test/e2e/crds/v1alpha1/backendtrafficpolicy.go index b50799d8c..08ffebad8 100644 --- a/test/e2e/crds/v1alpha1/backendtrafficpolicy.go +++ b/test/e2e/crds/v1alpha1/backendtrafficpolicy.go @@ -20,7 +20,6 @@ package v1alpha1 import ( "context" "fmt" - "strings" "time" . "github.com/onsi/ginkgo/v2" @@ -241,7 +240,7 @@ spec: var target *adctypes.Upstream for _, u := range ups { - if strings.Contains(u.Name, "httpbin-service-e2e-test") && u.Checks != nil { + if u.Checks != nil { target = u break } @@ -277,7 +276,7 @@ spec: var target *adctypes.Upstream for _, u := range ups { - if strings.Contains(u.Name, "httpbin-service-e2e-test") && u.Checks != nil && u.Checks.Passive != nil { + if u.Checks != nil && u.Checks.Passive != nil { target = u break } @@ -315,7 +314,7 @@ spec: Expect(err).ToNot(HaveOccurred()) hasHealthCheck := false for _, u := range ups { - if strings.Contains(u.Name, "httpbin-service-e2e-test") && u.Checks != nil { + if u.Checks != nil { hasHealthCheck = true break } @@ -331,9 +330,7 @@ spec: ups, err = s.DefaultDataplaneResource().Upstream().List(context.Background()) Expect(err).ToNot(HaveOccurred()) for _, u := range ups { - if strings.Contains(u.Name, "httpbin-service-e2e-test") { - Expect(u.Checks).To(BeNil(), "upstream should not have health check after policy deletion") - } + Expect(u.Checks).To(BeNil(), "upstream should not have health check after policy deletion") } }) }) From 307e47a406e734b731588b789869b1acd251b3df Mon Sep 17 00:00:00 2001 From: rongxin Date: Thu, 14 May 2026 04:11:27 +0800 Subject: [PATCH 09/12] fix: address follow-up review comments on health check translation - Reuse apiv2.ActiveHealthCheckMinInterval instead of duplicating the constant to avoid divergence - Assert upstreams list is non-empty before checking post-deletion state so the test fails explicitly if no upstreams are found Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- internal/adc/translator/policies.go | 13 +++++-------- test/e2e/crds/v1alpha1/backendtrafficpolicy.go | 1 + 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/internal/adc/translator/policies.go b/internal/adc/translator/policies.go index 8a4574470..1d5492d01 100644 --- a/internal/adc/translator/policies.go +++ b/internal/adc/translator/policies.go @@ -18,18 +18,15 @@ package translator import ( - "time" - "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" adctypes "github.com/apache/apisix-ingress-controller/api/adc" + apiv2 "github.com/apache/apisix-ingress-controller/api/v2" "github.com/apache/apisix-ingress-controller/api/v1alpha1" ) -const _minHealthCheckInterval = time.Second - func convertBackendRef(namespace, name, kind string) gatewayv1.BackendRef { backendRef := gatewayv1.BackendRef{} backendRef.Name = gatewayv1.ObjectName(name) @@ -119,8 +116,8 @@ func translateBTPActiveHealthCheck(config *v1alpha1.ActiveHealthCheck) *adctypes } if config.Healthy != nil { interval := config.Healthy.Interval.Duration - if interval < _minHealthCheckInterval { - interval = _minHealthCheckInterval + if interval < apiv2.ActiveHealthCheckMinInterval { + interval = apiv2.ActiveHealthCheckMinInterval } active.Healthy = adctypes.UpstreamActiveHealthCheckHealthy{ Interval: int(interval.Seconds()), @@ -132,8 +129,8 @@ func translateBTPActiveHealthCheck(config *v1alpha1.ActiveHealthCheck) *adctypes } if config.Unhealthy != nil { interval := config.Unhealthy.Interval.Duration - if interval < _minHealthCheckInterval { - interval = _minHealthCheckInterval + if interval < apiv2.ActiveHealthCheckMinInterval { + interval = apiv2.ActiveHealthCheckMinInterval } active.Unhealthy = adctypes.UpstreamActiveHealthCheckUnhealthy{ Interval: int(interval.Seconds()), diff --git a/test/e2e/crds/v1alpha1/backendtrafficpolicy.go b/test/e2e/crds/v1alpha1/backendtrafficpolicy.go index 08ffebad8..a1f938c27 100644 --- a/test/e2e/crds/v1alpha1/backendtrafficpolicy.go +++ b/test/e2e/crds/v1alpha1/backendtrafficpolicy.go @@ -329,6 +329,7 @@ spec: // 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") } From 9181362910690ab1ac8611fee529960378da76e4 Mon Sep 17 00:00:00 2001 From: rongxin Date: Thu, 14 May 2026 11:56:39 +0800 Subject: [PATCH 10/12] fix: use pointer for ApisixConsumerAuthParameter in test The AuthParameter field is declared as *ApisixConsumerAuthParameter (pointer), but the test was using a non-pointer value literal. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- internal/adc/translator/apisixconsumer_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/adc/translator/apisixconsumer_test.go b/internal/adc/translator/apisixconsumer_test.go index e6a1ee4ab..ff42eef7d 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", From 1d6fe8cffd7d309002bcb61b3526b5c2ea038198 Mon Sep 17 00:00:00 2001 From: rongxin Date: Thu, 14 May 2026 14:02:13 +0800 Subject: [PATCH 11/12] chore: fix gofmt formatting in policies.go Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- internal/adc/translator/policies.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/adc/translator/policies.go b/internal/adc/translator/policies.go index 1d5492d01..726fbface 100644 --- a/internal/adc/translator/policies.go +++ b/internal/adc/translator/policies.go @@ -23,8 +23,8 @@ import ( gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" adctypes "github.com/apache/apisix-ingress-controller/api/adc" - apiv2 "github.com/apache/apisix-ingress-controller/api/v2" "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 { From a29fd792f4a2a95cae858b0a04938332f451e851 Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Fri, 15 May 2026 09:05:08 +0800 Subject: [PATCH 12/12] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- internal/adc/translator/policies.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/adc/translator/policies.go b/internal/adc/translator/policies.go index 726fbface..f948f238c 100644 --- a/internal/adc/translator/policies.go +++ b/internal/adc/translator/policies.go @@ -102,7 +102,7 @@ func translateBTPHealthCheck(hc *v1alpha1.HealthCheck) *adctypes.UpstreamHealthC func translateBTPActiveHealthCheck(config *v1alpha1.ActiveHealthCheck) *adctypes.UpstreamActiveHealthCheck { t := config.Type if t == "" { - t = "http" + t = apiv2.HealthCheckHTTP } active := &adctypes.UpstreamActiveHealthCheck{ Type: t,