You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by al...@apache.org on 2025/09/25 07:32:02 UTC

(apisix-ingress-controller) branch master updated: feat(apisixupstream): support healthcheck (#2574)

This is an automated email from the ASF dual-hosted git repository.

alinsran pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git


The following commit(s) were added to refs/heads/master by this push:
     new 7510e5c3 feat(apisixupstream): support healthcheck (#2574)
7510e5c3 is described below

commit 7510e5c377a3ca95d2fbcad19814cfc590685117
Author: AlinsRan <al...@apache.org>
AuthorDate: Thu Sep 25 15:31:57 2025 +0800

    feat(apisixupstream): support healthcheck (#2574)
---
 api/adc/types.go                                   |   2 +-
 api/v2/apisixupstream_types.go                     |  23 +++-
 .../bases/apisix.apache.org_apisixupstreams.yaml   |  56 ++++++---
 docs/en/latest/reference/api-reference.md          |  10 +-
 docs/en/latest/upgrade-guide.md                    |   1 -
 internal/adc/translator/apisixupstream.go          |  88 +++++++++++++
 internal/adc/translator/translator.go              |   1 -
 test/e2e/crds/v2/upstream.go                       | 138 +++++++++++++++++++++
 test/e2e/scaffold/adc.go                           |  45 +++++--
 9 files changed, 330 insertions(+), 34 deletions(-)

diff --git a/api/adc/types.go b/api/adc/types.go
index 5a8062db..b5b67410 100644
--- a/api/adc/types.go
+++ b/api/adc/types.go
@@ -205,7 +205,7 @@ type UpstreamActiveHealthCheck struct {
 	Host               string                             `json:"host,omitempty" yaml:"host,omitempty"`
 	Port               int32                              `json:"port,omitempty" yaml:"port,omitempty"`
 	HTTPPath           string                             `json:"http_path,omitempty" yaml:"http_path,omitempty"`
-	HTTPSVerifyCert    bool                               `json:"https_verify_certificate,omitempty" yaml:"https_verify_certificate,omitempty"`
+	HTTPSVerifyCert    bool                               `json:"https_verify_cert,omitempty" yaml:"https_verify_cert,omitempty"`
 	HTTPRequestHeaders []string                           `json:"req_headers,omitempty" yaml:"req_headers,omitempty"`
 	Healthy            UpstreamActiveHealthCheckHealthy   `json:"healthy,omitempty" yaml:"healthy,omitempty"`
 	Unhealthy          UpstreamActiveHealthCheckUnhealthy `json:"unhealthy,omitempty" yaml:"unhealthy,omitempty"`
diff --git a/api/v2/apisixupstream_types.go b/api/v2/apisixupstream_types.go
index c6578f91..aa052e61 100644
--- a/api/v2/apisixupstream_types.go
+++ b/api/v2/apisixupstream_types.go
@@ -91,7 +91,10 @@ type ApisixUpstreamExternalNode struct {
 	Weight *int `json:"weight,omitempty" yaml:"weight"`
 
 	// Port specifies the port number on which the external node is accepting traffic.
+	//
 	// +kubebuilder:validation:Optional
+	// +kubebuilder:validation:Minimum=1
+	// +kubebuilder:validation:Maximum=65535
 	Port *int `json:"port,omitempty" yaml:"port"`
 }
 
@@ -118,7 +121,6 @@ type ApisixUpstreamConfig struct {
 	Timeout *UpstreamTimeout `json:"timeout,omitempty" yaml:"timeout,omitempty"`
 
 	// HealthCheck defines the active and passive health check configuration for the upstream.
-	// Deprecated: no longer supported in standalone mode.
 	// +kubebuilder:validation:Optional
 	HealthCheck *HealthCheck `json:"healthCheck,omitempty" yaml:"healthCheck,omitempty"`
 
@@ -160,6 +162,8 @@ type PortLevelSettings struct {
 	ApisixUpstreamConfig `json:",inline" yaml:",inline"`
 
 	// Port is a Kubernetes Service port.
+	// +kubebuilder:validation:Minimum=1
+	// +kubebuilder:validation:Maximum=65535
 	Port int32 `json:"port" yaml:"port"`
 }
 
@@ -221,9 +225,11 @@ type Discovery struct {
 
 // 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:Optional
 	// +kubebuilder:validation:Enum=http;https;tcp;
-	// Type is the health check type. Can be `http`, `https`, or `tcp`.
+	// +kubebuilder:default=http
 	Type string `json:"type,omitempty" yaml:"type,omitempty"`
 	// Timeout sets health check timeout in seconds.
 	Timeout time.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"`
@@ -232,9 +238,10 @@ type ActiveHealthCheck struct {
 	Concurrency int `json:"concurrency,omitempty" yaml:"concurrency,omitempty"`
 	// Host sets the upstream host.
 	Host string `json:"host,omitempty" yaml:"host,omitempty"`
-	// +kubebuilder:validation:Minimum=0
-	// +kubebuilder:validation:Maximum=65535
 	// Port sets the upstream port.
+	//
+	// +kubebuilder:validation:Minimum=1
+	// +kubebuilder:validation:Maximum=65535
 	Port int32 `json:"port,omitempty" yaml:"port,omitempty"`
 	// HTTPPath sets the HTTP probe request path.
 	HTTPPath string `json:"httpPath,omitempty" yaml:"httpPath,omitempty"`
@@ -254,6 +261,10 @@ type ActiveHealthCheck struct {
 type PassiveHealthCheck struct {
 	// Type specifies the type of passive health check.
 	// Can be `http`, `https`, or `tcp`.
+	//
+	// +kubebuilder:validation:Optional
+	// +kubebuilder:validation:Enum=http;https;tcp;
+	// +kubebuilder:default=http
 	Type string `json:"type,omitempty" yaml:"type,omitempty"`
 	// Healthy defines the conditions under which an upstream node is considered healthy.
 	Healthy *PassiveHealthCheckHealthy `json:"healthy,omitempty" yaml:"healthy,omitempty"`
@@ -304,6 +315,10 @@ type PassiveHealthCheckUnhealthy struct {
 	// TCPFailures define the number of TCP failures to define an unhealthy target.
 	TCPFailures int `json:"tcpFailures,omitempty" yaml:"tcpFailures,omitempty"`
 	// Timeout sets health check timeout in seconds.
+	// https://github.com/apache/apisix/blob/0151d9e35bba63d7c316187272d88e19db0be634/apisix/schema_def.lua#L196
+	//
+	// +kubebuilder:validation:Minimum=1
+	// +kubebuilder:validation:Maximum=254
 	Timeouts int `json:"timeout,omitempty" yaml:"timeout,omitempty"`
 }
 
diff --git a/config/crd/bases/apisix.apache.org_apisixupstreams.yaml b/config/crd/bases/apisix.apache.org_apisixupstreams.yaml
index 37880aba..cae2ab96 100644
--- a/config/crd/bases/apisix.apache.org_apisixupstreams.yaml
+++ b/config/crd/bases/apisix.apache.org_apisixupstreams.yaml
@@ -81,6 +81,8 @@ spec:
                     port:
                       description: Port specifies the port number on which the external
                         node is accepting traffic.
+                      maximum: 65535
+                      minimum: 1
                       type: integer
                     type:
                       description: Type indicates the kind of external node. Can be
@@ -95,9 +97,8 @@ spec:
                 minItems: 1
                 type: array
               healthCheck:
-                description: |-
-                  HealthCheck defines the active and passive health check configuration for the upstream.
-                  Deprecated: no longer supported in standalone mode.
+                description: HealthCheck defines the active and passive health check
+                  configuration for the upstream.
                 properties:
                   active:
                     description: Active health checks proactively send requests to
@@ -140,7 +141,7 @@ spec:
                         description: Port sets the upstream port.
                         format: int32
                         maximum: 65535
-                        minimum: 0
+                        minimum: 1
                         type: integer
                       requestHeaders:
                         description: RequestHeaders sets the request headers.
@@ -155,6 +156,7 @@ spec:
                         format: int64
                         type: integer
                       type:
+                        default: http
                         description: Type is the health check type. Can be `http`,
                           `https`, or `tcp`.
                         enum:
@@ -190,7 +192,11 @@ spec:
                             minimum: 0
                             type: integer
                           timeout:
-                            description: Timeout sets health check timeout in seconds.
+                            description: |-
+                              Timeout sets health check timeout in seconds.
+                              https://github.com/apache/apisix/blob/0151d9e35bba63d7c316187272d88e19db0be634/apisix/schema_def.lua#L196
+                            maximum: 254
+                            minimum: 1
                             type: integer
                         type: object
                     type: object
@@ -217,9 +223,14 @@ spec:
                             type: integer
                         type: object
                       type:
+                        default: http
                         description: |-
                           Type specifies the type of passive health check.
                           Can be `http`, `https`, or `tcp`.
+                        enum:
+                        - http
+                        - https
+                        - tcp
                         type: string
                       unhealthy:
                         description: Unhealthy defines the conditions under which
@@ -245,7 +256,11 @@ spec:
                             minimum: 0
                             type: integer
                           timeout:
-                            description: Timeout sets health check timeout in seconds.
+                            description: |-
+                              Timeout sets health check timeout in seconds.
+                              https://github.com/apache/apisix/blob/0151d9e35bba63d7c316187272d88e19db0be634/apisix/schema_def.lua#L196
+                            maximum: 254
+                            minimum: 1
                             type: integer
                         type: object
                     type: object
@@ -344,9 +359,8 @@ spec:
                       - type
                       type: object
                     healthCheck:
-                      description: |-
-                        HealthCheck defines the active and passive health check configuration for the upstream.
-                        Deprecated: no longer supported in standalone mode.
+                      description: HealthCheck defines the active and passive health
+                        check configuration for the upstream.
                       properties:
                         active:
                           description: Active health checks proactively send requests
@@ -389,7 +403,7 @@ spec:
                               description: Port sets the upstream port.
                               format: int32
                               maximum: 65535
-                              minimum: 0
+                              minimum: 1
                               type: integer
                             requestHeaders:
                               description: RequestHeaders sets the request headers.
@@ -404,6 +418,7 @@ spec:
                               format: int64
                               type: integer
                             type:
+                              default: http
                               description: Type is the health check type. Can be `http`,
                                 `https`, or `tcp`.
                               enum:
@@ -439,8 +454,11 @@ spec:
                                   minimum: 0
                                   type: integer
                                 timeout:
-                                  description: Timeout sets health check timeout in
-                                    seconds.
+                                  description: |-
+                                    Timeout sets health check timeout in seconds.
+                                    https://github.com/apache/apisix/blob/0151d9e35bba63d7c316187272d88e19db0be634/apisix/schema_def.lua#L196
+                                  maximum: 254
+                                  minimum: 1
                                   type: integer
                               type: object
                           type: object
@@ -467,9 +485,14 @@ spec:
                                   type: integer
                               type: object
                             type:
+                              default: http
                               description: |-
                                 Type specifies the type of passive health check.
                                 Can be `http`, `https`, or `tcp`.
+                              enum:
+                              - http
+                              - https
+                              - tcp
                               type: string
                             unhealthy:
                               description: Unhealthy defines the conditions under
@@ -495,8 +518,11 @@ spec:
                                   minimum: 0
                                   type: integer
                                 timeout:
-                                  description: Timeout sets health check timeout in
-                                    seconds.
+                                  description: |-
+                                    Timeout sets health check timeout in seconds.
+                                    https://github.com/apache/apisix/blob/0151d9e35bba63d7c316187272d88e19db0be634/apisix/schema_def.lua#L196
+                                  maximum: 254
+                                  minimum: 1
                                   type: integer
                               type: object
                           type: object
@@ -559,6 +585,8 @@ spec:
                     port:
                       description: Port is a Kubernetes Service port.
                       format: int32
+                      maximum: 65535
+                      minimum: 1
                       type: integer
                     retries:
                       description: |-
diff --git a/docs/en/latest/reference/api-reference.md b/docs/en/latest/reference/api-reference.md
index 26e7183c..6a891cb8 100644
--- a/docs/en/latest/reference/api-reference.md
+++ b/docs/en/latest/reference/api-reference.md
@@ -661,7 +661,7 @@ UpstreamActiveHealthCheckHealthy defines the conditions used to actively determi
 | `httpCodes` _integer array_ | HTTPCodes define a list of HTTP status codes that are considered unhealthy. |
 | `httpFailures` _integer_ | HTTPFailures define the number of HTTP failures to define an unhealthy target. |
 | `tcpFailures` _integer_ | TCPFailures define the number of TCP failures to define an unhealthy target. |
-| `timeout` _integer_ | Timeout sets health check timeout in seconds. |
+| `timeout` _integer_ | Timeout sets health check timeout in seconds. https://github.com/apache/apisix/blob/0151d9e35bba63d7c316187272d88e19db0be634/apisix/schema_def.lua#L196 |
 | `interval` _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#duration-v1-meta)_ | Interval defines the time interval for checking targets, in seconds. |
 
 
@@ -1320,7 +1320,7 @@ ApisixUpstreamConfig defines configuration for upstream services.
 | `scheme` _string_ | Scheme is the protocol used to communicate with the upstream. Default is `http`. Can be `http`, `https`, `grpc`, or `grpcs`. |
 | `retries` _integer_ | Retries defines the number of retry attempts APISIX should make when a failure occurs. Failures include timeouts, network errors, or 5xx status codes. |
 | `timeout` _[UpstreamTimeout](#upstreamtimeout)_ | Timeout specifies the connection, send, and read timeouts for upstream requests. |
-| `healthCheck` _[HealthCheck](#healthcheck)_ | HealthCheck defines the active and passive health check configuration for the upstream. Deprecated: no longer supported in standalone mode. |
+| `healthCheck` _[HealthCheck](#healthcheck)_ | HealthCheck defines the active and passive health check configuration for the upstream. |
 | `tlsSecret` _[ApisixSecret](#apisixsecret)_ | TLSSecret references a Kubernetes Secret that contains the client certificate and key for mutual TLS when connecting to the upstream. |
 | `subsets` _[ApisixUpstreamSubset](#apisixupstreamsubset) array_ | Subsets defines labeled subsets of service endpoints, typically used for service versioning or canary deployments. |
 | `passHost` _string_ | PassHost configures how the host header should be determined when a request is forwarded to the upstream. Default is `pass`. Can be `pass`, `node` or `rewrite`:<br /> • `pass`: preserve the original Host header<br /> • `node`: use the upstream node’s host<br /> • `rewrite`: set to a custom host via upstreamHost |
@@ -1380,7 +1380,7 @@ definitions and custom configuration.
 | `scheme` _string_ | Scheme is the protocol used to communicate with the upstream. Default is `http`. Can be `http`, `https`, `grpc`, or `grpcs`. |
 | `retries` _integer_ | Retries defines the number of retry attempts APISIX should make when a failure occurs. Failures include timeouts, network errors, or 5xx status codes. |
 | `timeout` _[UpstreamTimeout](#upstreamtimeout)_ | Timeout specifies the connection, send, and read timeouts for upstream requests. |
-| `healthCheck` _[HealthCheck](#healthcheck)_ | HealthCheck defines the active and passive health check configuration for the upstream. Deprecated: no longer supported in standalone mode. |
+| `healthCheck` _[HealthCheck](#healthcheck)_ | HealthCheck defines the active and passive health check configuration for the upstream. |
 | `tlsSecret` _[ApisixSecret](#apisixsecret)_ | TLSSecret references a Kubernetes Secret that contains the client certificate and key for mutual TLS when connecting to the upstream. |
 | `subsets` _[ApisixUpstreamSubset](#apisixupstreamsubset) array_ | Subsets defines labeled subsets of service endpoints, typically used for service versioning or canary deployments. |
 | `passHost` _string_ | PassHost configures how the host header should be determined when a request is forwarded to the upstream. Default is `pass`. Can be `pass`, `node` or `rewrite`:<br /> • `pass`: preserve the original Host header<br /> • `node`: use the upstream node’s host<br /> • `rewrite`: set to a custom host via upstreamHost |
@@ -1528,7 +1528,7 @@ UpstreamPassiveHealthCheckUnhealthy defines the conditions used to passively det
 | `httpCodes` _integer array_ | HTTPCodes define a list of HTTP status codes that are considered unhealthy. |
 | `httpFailures` _integer_ | HTTPFailures define the number of HTTP failures to define an unhealthy target. |
 | `tcpFailures` _integer_ | TCPFailures define the number of TCP failures to define an unhealthy target. |
-| `timeout` _integer_ | Timeout sets health check timeout in seconds. |
+| `timeout` _integer_ | Timeout sets health check timeout in seconds. https://github.com/apache/apisix/blob/0151d9e35bba63d7c316187272d88e19db0be634/apisix/schema_def.lua#L196 |
 
 
 _Appears in:_
@@ -1550,7 +1550,7 @@ them if they are set on the port level.
 | `scheme` _string_ | Scheme is the protocol used to communicate with the upstream. Default is `http`. Can be `http`, `https`, `grpc`, or `grpcs`. |
 | `retries` _integer_ | Retries defines the number of retry attempts APISIX should make when a failure occurs. Failures include timeouts, network errors, or 5xx status codes. |
 | `timeout` _[UpstreamTimeout](#upstreamtimeout)_ | Timeout specifies the connection, send, and read timeouts for upstream requests. |
-| `healthCheck` _[HealthCheck](#healthcheck)_ | HealthCheck defines the active and passive health check configuration for the upstream. Deprecated: no longer supported in standalone mode. |
+| `healthCheck` _[HealthCheck](#healthcheck)_ | HealthCheck defines the active and passive health check configuration for the upstream. |
 | `tlsSecret` _[ApisixSecret](#apisixsecret)_ | TLSSecret references a Kubernetes Secret that contains the client certificate and key for mutual TLS when connecting to the upstream. |
 | `subsets` _[ApisixUpstreamSubset](#apisixupstreamsubset) array_ | Subsets defines labeled subsets of service endpoints, typically used for service versioning or canary deployments. |
 | `passHost` _string_ | PassHost configures how the host header should be determined when a request is forwarded to the upstream. Default is `pass`. Can be `pass`, `node` or `rewrite`:<br /> • `pass`: preserve the original Host header<br /> • `node`: use the upstream node’s host<br /> • `rewrite`: set to a custom host via upstreamHost |
diff --git a/docs/en/latest/upgrade-guide.md b/docs/en/latest/upgrade-guide.md
index eac2ebf6..068ef6a7 100644
--- a/docs/en/latest/upgrade-guide.md
+++ b/docs/en/latest/upgrade-guide.md
@@ -143,7 +143,6 @@ spec:
 Due to current limitations in the [ADC](https://github.com/api7/adc) component, the following fields are not yet supported:
 
 * `spec.discovery`: Service Discovery
-* `spec.healthCheck`: Health Checking
 
 More details: [ADC Backend Differences](https://github.com/api7/adc/blob/2449ca81e3c61169f8c1e59efb4c1173a766bce2/libs/backend-apisix-standalone/README.md#differences-in-upstream)
 
diff --git a/internal/adc/translator/apisixupstream.go b/internal/adc/translator/apisixupstream.go
index f4faf298..5a76025e 100644
--- a/internal/adc/translator/apisixupstream.go
+++ b/internal/adc/translator/apisixupstream.go
@@ -39,6 +39,7 @@ func (t *Translator) translateApisixUpstream(tctx *provider.TranslateContext, au
 		translateApisixUpstreamLoadBalancer,
 		translateApisixUpstreamRetriesAndTimeout,
 		translateApisixUpstreamPassHost,
+		translateUpstreamHealthCheck,
 	} {
 		if err = f(au, ups); err != nil {
 			return
@@ -252,3 +253,90 @@ func translateApisixUpstreamExternalNodesService(tctx *provider.TranslateContext
 
 	return nil
 }
+
+func translateUpstreamHealthCheck(au *apiv2.ApisixUpstream, ups *adc.Upstream) error {
+	if au == nil {
+		return nil
+	}
+	healcheck := au.Spec.HealthCheck
+	if healcheck == nil || (healcheck.Passive == nil && healcheck.Active == nil) {
+		return nil
+	}
+	var hc adc.UpstreamHealthCheck
+	if healcheck.Passive != nil {
+		hc.Passive = translateUpstreamPassiveHealthCheck(healcheck.Passive)
+	}
+
+	if healcheck.Active != nil {
+		active, err := translateUpstreamActiveHealthCheck(healcheck.Active)
+		if err != nil {
+			return err
+		}
+		hc.Active = active
+	}
+
+	ups.Checks = &hc
+	return nil
+}
+
+func translateUpstreamActiveHealthCheck(config *apiv2.ActiveHealthCheck) (*adc.UpstreamActiveHealthCheck, error) {
+	var active adc.UpstreamActiveHealthCheck
+	if config.Type == "" {
+		config.Type = apiv2.HealthCheckHTTP
+	}
+
+	active.Timeout = int(config.Timeout.Seconds())
+	active.Port = config.Port
+	active.Concurrency = config.Concurrency
+	active.Host = config.Host
+	active.HTTPPath = config.HTTPPath
+	active.HTTPRequestHeaders = config.RequestHeaders
+
+	if config.StrictTLS == nil || *config.StrictTLS {
+		active.HTTPSVerifyCert = true
+	}
+
+	if config.Healthy != nil {
+		active.Healthy.Successes = config.Healthy.Successes
+		active.Healthy.HTTPStatuses = config.Healthy.HTTPCodes
+
+		if config.Healthy.Interval.Duration < apiv2.ActiveHealthCheckMinInterval {
+			return nil, fmt.Errorf(`"healthCheck.active.healthy.interval" has invalid value`)
+		}
+		active.Healthy.Interval = int(config.Healthy.Interval.Seconds())
+	}
+
+	if config.Unhealthy != nil {
+		active.Unhealthy.HTTPFailures = config.Unhealthy.HTTPFailures
+		active.Unhealthy.TCPFailures = config.Unhealthy.TCPFailures
+		active.Unhealthy.Timeouts = config.Unhealthy.Timeouts
+		active.Unhealthy.HTTPStatuses = config.Unhealthy.HTTPCodes
+
+		if config.Unhealthy.Interval.Duration < apiv2.ActiveHealthCheckMinInterval {
+			return nil, fmt.Errorf(`"healthCheck.active.unhealthy.interval" has invalid value`)
+		}
+		active.Unhealthy.Interval = int(config.Unhealthy.Interval.Seconds())
+	}
+
+	return &active, nil
+}
+
+func translateUpstreamPassiveHealthCheck(config *apiv2.PassiveHealthCheck) *adc.UpstreamPassiveHealthCheck {
+	var passive adc.UpstreamPassiveHealthCheck
+	if config.Type == "" {
+		config.Type = apiv2.HealthCheckHTTP
+	}
+
+	if config.Healthy != nil {
+		passive.Healthy.Successes = config.Healthy.Successes
+		passive.Healthy.HTTPStatuses = config.Healthy.HTTPCodes
+	}
+
+	if config.Unhealthy != nil {
+		passive.Unhealthy.HTTPFailures = config.Unhealthy.HTTPFailures
+		passive.Unhealthy.TCPFailures = config.Unhealthy.TCPFailures
+		passive.Unhealthy.Timeouts = config.Unhealthy.Timeouts
+		passive.Unhealthy.HTTPStatuses = config.Unhealthy.HTTPCodes
+	}
+	return &passive
+}
diff --git a/internal/adc/translator/translator.go b/internal/adc/translator/translator.go
index 0d50661a..4c9bf0d8 100644
--- a/internal/adc/translator/translator.go
+++ b/internal/adc/translator/translator.go
@@ -34,7 +34,6 @@ func NewTranslator(log logr.Logger) *Translator {
 }
 
 type TranslateResult struct {
-	Routes         []*adctypes.Route
 	Services       []*adctypes.Service
 	SSL            []*adctypes.SSL
 	GlobalRules    adctypes.GlobalRule
diff --git a/test/e2e/crds/v2/upstream.go b/test/e2e/crds/v2/upstream.go
new file mode 100644
index 00000000..7d7d9a61
--- /dev/null
+++ b/test/e2e/crds/v2/upstream.go
@@ -0,0 +1,138 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package v2
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	"k8s.io/apimachinery/pkg/types"
+
+	apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
+	"github.com/apache/apisix-ingress-controller/test/e2e/framework"
+	"github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
+)
+
+var _ = Describe("Test ApisixUpstream", Label("apisix.apache.org", "v2", "apisixupstream"), func() {
+	var (
+		s       = scaffold.NewDefaultScaffold()
+		applier = framework.NewApplier(s.GinkgoT, s.K8sClient, s.CreateResourceFromString)
+	)
+	BeforeEach(func() {
+		By("create GatewayProxy")
+		err := s.CreateResourceFromString(s.GetGatewayProxySpec())
+		Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy")
+		time.Sleep(5 * time.Second)
+
+		By("create IngressClass")
+		err = s.CreateResourceFromStringWithNamespace(s.GetIngressClassYaml(), "")
+		Expect(err).NotTo(HaveOccurred(), "creating IngressClass")
+		time.Sleep(5 * time.Second)
+	})
+
+	Context("Health Check", func() {
+		It("active and passive", func() {
+			auWithHealthcheck := `
+apiVersion: apisix.apache.org/v2
+kind: ApisixUpstream
+metadata:
+  name: active
+spec:
+  ingressClassName: %s
+  externalNodes:
+  - type: Domain
+    name: httpbin-service-e2e-test
+  - type: Domain
+    name: invalid.httpbin.host
+  - type: Domain
+    name: invalid1.httpbin.host
+  retries: 1
+  healthCheck:
+    active:
+      type: http
+      httpPath: /ip
+      healthy:
+        httpCodes: [200]
+        interval: 1s
+      unhealthy:
+        httpFailures: 2
+        interval: 1s
+    passive:
+      healthy:
+        httpCodes: [200]
+      unhealthy:
+        httpCodes: [502]
+`
+			applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "active"},
+				&apiv2.ApisixUpstream{}, fmt.Sprintf(auWithHealthcheck, s.Namespace()))
+
+			ar := `
+apiVersion: apisix.apache.org/v2
+kind: ApisixRoute
+metadata:
+ name: httpbin-route
+spec:
+  ingressClassName: %s
+  http:
+  - name: rule1
+    match:
+      hosts:
+      - httpbin.org
+      paths:
+      - /*
+    upstreams:
+    - name: active
+`
+			applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "httpbin-route"},
+				&apiv2.ApisixRoute{}, fmt.Sprintf(ar, s.Namespace()))
+
+			By("triggering the health check")
+			s.RequestAssert(&scaffold.RequestAssert{
+				Method: "GET",
+				Path:   "/ip",
+				Host:   "httpbin.org",
+			})
+			time.Sleep(2 * time.Second)
+
+			ups, err := s.Deployer.DefaultDataplaneResource().Upstream().List(context.Background())
+			Expect(err).ToNot(HaveOccurred(), "listing upstreams")
+			Expect(ups).To(HaveLen(1), "the number of upstreams")
+			Expect(ups[0].Nodes).To(HaveLen(3), "the number of upstream nodes")
+			Expect(ups[0].Checks).ToNot(BeNil(), "the healthcheck configuration")
+			Expect(ups[0].Checks.Active).ToNot(BeNil(), "the active healthcheck configuration")
+			Expect(ups[0].Checks.Active.Healthy).ToNot(BeNil(), "the active healthy configuration")
+			Expect(ups[0].Checks.Active.Unhealthy).ToNot(BeNil(), "the active unhealthy configuration")
+			Expect(ups[0].Checks.Active.Healthy.Interval).To(Equal(1), "the healthy interval")
+			Expect(ups[0].Checks.Active.Healthy.HTTPStatuses).To(Equal([]int{200}), "the healthy http status")
+			Expect(ups[0].Checks.Active.Unhealthy.Interval).To(Equal(1), "the unhealthy interval")
+			Expect(ups[0].Checks.Active.Unhealthy.HTTPFailures).To(Equal(2), "the unhealthy http failures")
+			Expect(ups[0].Checks.Passive).ToNot(BeNil(), "the passive healthcheck configuration")
+			Expect(ups[0].Checks.Passive.Healthy).ToNot(BeNil(), "the passive healthy configuration")
+			Expect(ups[0].Checks.Passive.Unhealthy).ToNot(BeNil(), "the passive unhealthy configuration")
+			Expect(ups[0].Checks.Passive.Healthy.HTTPStatuses).To(Equal([]int{200}), "the passive healthy http status")
+			Expect(ups[0].Checks.Passive.Unhealthy.HTTPStatuses).To(Equal([]int{502}), "the passive unhealthy http status")
+
+			for range 100 {
+				s.NewAPISIXClient().GET("/ip").WithHost("httpbin.org").Expect().Status(200)
+			}
+		})
+	})
+})
diff --git a/test/e2e/scaffold/adc.go b/test/e2e/scaffold/adc.go
index 0a45cd33..0026e2e1 100644
--- a/test/e2e/scaffold/adc.go
+++ b/test/e2e/scaffold/adc.go
@@ -56,6 +56,7 @@ func init() {
 // DataplaneResource defines the interface for accessing dataplane resources
 type DataplaneResource interface {
 	Route() RouteResource
+	Upstream() UpstreamResource
 	Service() ServiceResource
 	SSL() SSLResource
 	Consumer() ConsumerResource
@@ -76,6 +77,11 @@ type SSLResource interface {
 	List(ctx context.Context) ([]*adctypes.SSL, error)
 }
 
+// UpstreamResource defines the interface for upstream resources
+type UpstreamResource interface {
+	List(ctx context.Context) ([]*adctypes.Upstream, error)
+}
+
 // ConsumerResource defines the interface for consumer resources
 type ConsumerResource interface {
 	List(ctx context.Context) ([]*adctypes.Consumer, error)
@@ -117,6 +123,10 @@ func (a *adcDataplaneResource) Consumer() ConsumerResource {
 	return &adcConsumerResource{a}
 }
 
+func (a *adcDataplaneResource) Upstream() UpstreamResource {
+	return &adcUpstreamResource{a}
+}
+
 func init() {
 	l, err := log.NewLogger(
 		log.WithOutputFile("stderr"),
@@ -191,14 +201,7 @@ func (a *adcDataplaneResource) dumpResources(ctx context.Context) (*translator.T
 		return nil, err
 	}
 
-	// Extract routes from services
-	var routes []*adctypes.Route
-	for _, service := range resources.Services {
-		routes = append(routes, service.Routes...)
-	}
-
 	result := &translator.TranslateResult{
-		Routes:         routes,
 		Services:       resources.Services,
 		SSL:            resources.SSLs,
 		GlobalRules:    resources.GlobalRules,
@@ -219,7 +222,11 @@ func (r *adcRouteResource) List(ctx context.Context) ([]*adctypes.Route, error)
 	if err != nil {
 		return nil, err
 	}
-	return result.Routes, nil
+	var routes []*adctypes.Route
+	for _, service := range result.Services {
+		routes = append(routes, service.Routes...)
+	}
+	return routes, nil
 }
 
 // adcServiceResource implements ServiceResource
@@ -260,3 +267,25 @@ func (c *adcConsumerResource) List(ctx context.Context) ([]*adctypes.Consumer, e
 	}
 	return result.Consumers, nil
 }
+
+type adcUpstreamResource struct {
+	*adcDataplaneResource
+}
+
+func (r *adcUpstreamResource) List(ctx context.Context) ([]*adctypes.Upstream, error) {
+	result, err := r.dumpResources(ctx)
+	if err != nil {
+		return nil, err
+	}
+	upstreams := make([]*adctypes.Upstream, 0, len(result.Services))
+	for _, svc := range result.Services {
+		if svc.Upstream != nil {
+			upstreams = append(upstreams, svc.Upstream)
+		}
+		if svc.Upstreams != nil {
+			upstreams = append(upstreams, svc.Upstreams...)
+		}
+	}
+
+	return upstreams, nil
+}