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

bzp2010 pushed a commit to branch bzp/feat-record-gw-instance-status-accurately
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git

commit f3a264a916b4c53f10bde593aea62f67c96cd9f9
Author: bzp2010 <[email protected]>
AuthorDate: Thu Sep 3 23:14:44 2026 +0800

    feat: report instance unavailable to gateway proxy status
---
 api/v1alpha1/gatewayproxy_types.go                 |  14 +++
 api/v1alpha1/zz_generated.deepcopy.go              |  23 ++++
 .../bases/apisix.apache.org_gatewayproxies.yaml    |  72 +++++++++++
 config/rbac/role.yaml                              |   1 +
 internal/manager/controllers.go                    |   1 +
 internal/provider/apisix/status.go                 | 127 ++++++++++++++++---
 internal/provider/apisix/status_test.go            | 135 +++++++++++++++++++++
 test/e2e/crds/v2/status.go                         |  83 +++++++++++++
 8 files changed, 440 insertions(+), 16 deletions(-)

diff --git a/api/v1alpha1/gatewayproxy_types.go 
b/api/v1alpha1/gatewayproxy_types.go
index f9a5aa8e..ee5d5621 100644
--- a/api/v1alpha1/gatewayproxy_types.go
+++ b/api/v1alpha1/gatewayproxy_types.go
@@ -170,6 +170,7 @@ type ProviderService struct {
 }
 
 // +kubebuilder:object:root=true
+// +kubebuilder:subresource:status
 // GatewayProxy defines configuration for the gateway proxy instances used to 
route traffic to services.
 type GatewayProxy struct {
        metav1.TypeMeta   `json:",inline"`
@@ -178,6 +179,19 @@ type GatewayProxy struct {
        // GatewayProxySpec defines configuration of gateway proxy instances,
        // including networking settings, global plugins, and plugin metadata.
        Spec GatewayProxySpec `json:"spec,omitempty"`
+
+       // Status defines the current state of Gateway Proxy.
+       //
+       // +kubebuilder:default={conditions: {{type: "DataPlaneAvailable", 
status: "Unknown", reason:"Pending", message:"Waiting for controller", 
lastTransitionTime: "1970-01-01T00:00:00Z"}}}
+       // +optional
+       Status GatewayProxyStatus `json:"status,omitempty"`
+}
+
+// GatewayProxyStatus defines the observed state of GatewayProxy.
+type GatewayProxyStatus struct {
+       // Conditions describe the current state of the data plane instances 
this GatewayProxy addresses.
+       // +optional
+       Conditions []metav1.Condition `json:"conditions,omitempty"`
 }
 
 // +kubebuilder:object:root=true
diff --git a/api/v1alpha1/zz_generated.deepcopy.go 
b/api/v1alpha1/zz_generated.deepcopy.go
index 4c112170..22beb53e 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -461,6 +461,7 @@ func (in *GatewayProxy) DeepCopyInto(out *GatewayProxy) {
        out.TypeMeta = in.TypeMeta
        in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
        in.Spec.DeepCopyInto(&out.Spec)
+       in.Status.DeepCopyInto(&out.Status)
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, 
creating a new GatewayProxy.
@@ -588,6 +589,28 @@ func (in *GatewayProxySpec) DeepCopy() *GatewayProxySpec {
        return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, 
writing into out. in must be non-nil.
+func (in *GatewayProxyStatus) DeepCopyInto(out *GatewayProxyStatus) {
+       *out = *in
+       if in.Conditions != nil {
+               in, out := &in.Conditions, &out.Conditions
+               *out = make([]metav1.Condition, len(*in))
+               for i := range *in {
+                       (*in)[i].DeepCopyInto(&(*out)[i])
+               }
+       }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, 
creating a new GatewayProxyStatus.
+func (in *GatewayProxyStatus) DeepCopy() *GatewayProxyStatus {
+       if in == nil {
+               return nil
+       }
+       out := new(GatewayProxyStatus)
+       in.DeepCopyInto(out)
+       return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, 
writing into out. in must be non-nil.
 func (in *GatewayRef) DeepCopyInto(out *GatewayRef) {
        *out = *in
diff --git a/config/crd/bases/apisix.apache.org_gatewayproxies.yaml 
b/config/crd/bases/apisix.apache.org_gatewayproxies.yaml
index b2865255..67062d7c 100644
--- a/config/crd/bases/apisix.apache.org_gatewayproxies.yaml
+++ b/config/crd/bases/apisix.apache.org_gatewayproxies.yaml
@@ -203,6 +203,78 @@ spec:
             required:
             - provider
             type: object
+          status:
+            default:
+              conditions:
+              - lastTransitionTime: "1970-01-01T00:00:00Z"
+                message: Waiting for controller
+                reason: Pending
+                status: Unknown
+                type: DataPlaneAvailable
+            description: Status defines the current state of Gateway Proxy.
+            properties:
+              conditions:
+                description: |-
+                  Conditions describe the current state of the data plane 
instances this
+                  GatewayProxy addresses.
+                items:
+                  description: Condition contains details for one aspect of 
the current
+                    state of this API Resource.
+                  properties:
+                    lastTransitionTime:
+                      description: |-
+                        lastTransitionTime is the last time the condition 
transitioned from one status to another.
+                        This should be when the underlying condition changed.  
If that is not known, then using the time when the API field changed is 
acceptable.
+                      format: date-time
+                      type: string
+                    message:
+                      description: |-
+                        message is a human readable message indicating details 
about the transition.
+                        This may be an empty string.
+                      maxLength: 32768
+                      type: string
+                    observedGeneration:
+                      description: |-
+                        observedGeneration represents the .metadata.generation 
that the condition was set based upon.
+                        For instance, if .metadata.generation is currently 12, 
but the .status.conditions[x].observedGeneration is 9, the condition is out of 
date
+                        with respect to the current state of the instance.
+                      format: int64
+                      minimum: 0
+                      type: integer
+                    reason:
+                      description: |-
+                        reason contains a programmatic identifier indicating 
the reason for the condition's last transition.
+                        Producers of specific condition types may define 
expected values and meanings for this field,
+                        and whether the values are considered a guaranteed API.
+                        The value should be a CamelCase string.
+                        This field may not be empty.
+                      maxLength: 1024
+                      minLength: 1
+                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                      type: string
+                    status:
+                      description: status of the condition, one of True, 
False, Unknown.
+                      enum:
+                      - "True"
+                      - "False"
+                      - Unknown
+                      type: string
+                    type:
+                      description: type of condition in CamelCase or in 
foo.example.com/CamelCase.
+                      maxLength: 316
+                      pattern: 
^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - message
+                  - reason
+                  - status
+                  - type
+                  type: object
+                type: array
+            type: object
         type: object
     served: true
     storage: true
+    subresources:
+      status: {}
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index 86b93cc3..b8432ff2 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -53,6 +53,7 @@ rules:
   - apisixupstreams/status
   - backendtrafficpolicies/status
   - consumers/status
+  - gatewayproxies/status
   - httproutepolicies/status
   - l4routepolicies/status
   verbs:
diff --git a/internal/manager/controllers.go b/internal/manager/controllers.go
index 51ec3f91..5cc55435 100644
--- a/internal/manager/controllers.go
+++ b/internal/manager/controllers.go
@@ -71,6 +71,7 @@ import (
 // CustomResourceDefinition
 // 
+kubebuilder:rbac:groups=apisix.apache.org,resources=pluginconfigs,verbs=get;list;watch
 // 
+kubebuilder:rbac:groups=apisix.apache.org,resources=gatewayproxies,verbs=get;list;watch
+// 
+kubebuilder:rbac:groups=apisix.apache.org,resources=gatewayproxies/status,verbs=get;update
 // 
+kubebuilder:rbac:groups=apisix.apache.org,resources=consumers,verbs=get;list;watch
 // 
+kubebuilder:rbac:groups=apisix.apache.org,resources=consumers/status,verbs=get;update
 // 
+kubebuilder:rbac:groups=apisix.apache.org,resources=backendtrafficpolicies,verbs=get;list;watch
diff --git a/internal/provider/apisix/status.go 
b/internal/provider/apisix/status.go
index a1d857eb..7c888a0d 100644
--- a/internal/provider/apisix/status.go
+++ b/internal/provider/apisix/status.go
@@ -25,6 +25,8 @@ import (
        "sigs.k8s.io/controller-runtime/pkg/client"
        gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
 
+       adctypes "github.com/apache/apisix-ingress-controller/api/adc"
+       apiv1alpha1 "github.com/apache/apisix-ingress-controller/api/v1alpha1"
        apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
        "github.com/apache/apisix-ingress-controller/internal/controller/label"
        "github.com/apache/apisix-ingress-controller/internal/controller/status"
@@ -32,6 +34,16 @@ import (
        "github.com/apache/apisix-ingress-controller/internal/types"
 )
 
+// GatewayProxyConditionDataPlaneAvailable reports whether every APISIX 
instance a
+// GatewayProxy addresses took the last sync. It lands on the GatewayProxy 
rather than
+// a Gateway, an IngressClass, or a CRD, since GatewayProxy is what every path 
shares.
+const (
+       GatewayProxyConditionDataPlaneAvailable = "DataPlaneAvailable"
+
+       GatewayProxyReasonDataPlaneAvailable           = "DataPlaneAvailable"
+       GatewayProxyReasonDataPlaneInstanceUnavailable = 
"DataPlaneInstanceUnavailable"
+)
+
 // handleStatusUpdate updates resource conditions based on the latest sync 
results.
 //
 // It maintains a history of failed resources in d.statusUpdateMap.
@@ -42,32 +54,66 @@ import (
 func (d *apisixProvider) handleStatusUpdate(statusUpdateMap 
map[types.NamespacedNameKind][]string) {
        // Mark all resources in the current failure set as failed.
        for nnk, msgs := range statusUpdateMap {
-               d.updateStatus(nnk, cutils.NewConditionTypeAccepted(
-                       apiv2.ConditionReasonSyncFailed,
-                       false,
-                       0,
-                       strings.Join(msgs, "; "),
-               ))
+               d.updateStatus(nnk, failureCondition(nnk, strings.Join(msgs, "; 
")))
        }
 
        // Mark resources that exist only in the previous failure history as 
successful.
        for nnk := range d.statusUpdateMap {
                if _, ok := statusUpdateMap[nnk]; !ok {
-                       d.updateStatus(nnk, cutils.NewConditionTypeAccepted(
-                               apiv2.ConditionReasonAccepted,
-                               true,
-                               0,
-                               "",
-                       ))
+                       d.updateStatus(nnk, successCondition(nnk))
                }
        }
        // Update the failure history with the current failure set.
        d.statusUpdateMap = statusUpdateMap
 }
 
+// failureCondition and successCondition pick which condition a 
NamespacedNameKind
+// gets: GatewayProxyConditionDataPlaneAvailable for a GatewayProxy, the 
existing
+// Accepted/SyncFailed condition for everything else.
+func failureCondition(nnk types.NamespacedNameKind, msg string) 
metav1.Condition {
+       if nnk.Kind == types.KindGatewayProxy {
+               return newGatewayProxyDataPlaneAvailableCondition(false, 
GatewayProxyReasonDataPlaneInstanceUnavailable, msg)
+       }
+       return cutils.NewConditionTypeAccepted(apiv2.ConditionReasonSyncFailed, 
false, 0, msg)
+}
+
+func successCondition(nnk types.NamespacedNameKind) metav1.Condition {
+       if nnk.Kind == types.KindGatewayProxy {
+               return newGatewayProxyDataPlaneAvailableCondition(true, 
GatewayProxyReasonDataPlaneAvailable, "")
+       }
+       return cutils.NewConditionTypeAccepted(apiv2.ConditionReasonAccepted, 
true, 0, "")
+}
+
+func newGatewayProxyDataPlaneAvailableCondition(available bool, reason, msg 
string) metav1.Condition {
+       conditionStatus := metav1.ConditionFalse
+       if available {
+               conditionStatus = metav1.ConditionTrue
+       }
+       return metav1.Condition{
+               Type:               GatewayProxyConditionDataPlaneAvailable,
+               Status:             conditionStatus,
+               LastTransitionTime: metav1.Now(),
+               Reason:             reason,
+               Message:            cutils.TruncateConditionMessage(msg),
+       }
+}
+
 //nolint:gocyclo
 func (d *apisixProvider) updateStatus(nnk types.NamespacedNameKind, condition 
metav1.Condition) {
        switch nnk.Kind {
+       case types.KindGatewayProxy:
+               // Unlike the route kinds below, the condition lands on the 
GatewayProxy's own
+               // top-level Status.Conditions, not on a per-parent entry.
+               d.updater.Update(status.Update{
+                       NamespacedName: nnk.NamespacedName(),
+                       Resource:       &apiv1alpha1.GatewayProxy{},
+                       Mutator: status.MutatorFunc(func(obj client.Object) 
client.Object {
+                               cp := obj.(*apiv1alpha1.GatewayProxy).DeepCopy()
+                               condition.ObservedGeneration = 
cp.GetGeneration()
+                               cp.Status.Conditions = 
cutils.MergeCondition(cp.Status.Conditions, condition)
+                               return cp
+                       }),
+               })
        case types.KindApisixRoute:
                d.updater.Update(status.Update{
                        NamespacedName: nnk.NamespacedName(),
@@ -274,10 +320,59 @@ func (d *apisixProvider) resolveADCExecutionErrors(
        return statusUpdateMap
 }
 
+// handleEmptyFailedStatuses runs when nothing could be attributed to a 
specific
+// resource. A failed EndpointStatus entry means an unreachable or rejecting 
instance,
+// so it's marked on the GatewayProxy instead of smearing every resource; only 
a plain
+// transport error with no structured response at all falls back to smearing.
 func (d *apisixProvider) handleEmptyFailedStatuses(
        configName string,
        failedStatus types.ADCExecutionServerAddrError,
        statusUpdateMap map[types.NamespacedNameKind][]string,
+) {
+       if msg := unavailableEndpointsMessage(failedStatus.EndpointStatuses); 
msg != "" {
+               d.markGatewayProxyDataPlaneUnavailable(configName, msg, 
statusUpdateMap)
+               return
+       }
+       d.smearAllResources(configName, failedStatus.Error(), statusUpdateMap)
+}
+
+// unavailableEndpointsMessage summarizes every EndpointStatus entry that 
didn't
+// succeed, in the order given. Empty means none did (or there were none to 
check).
+func unavailableEndpointsMessage(endpoints []adctypes.EndpointStatus) string {
+       var failed []string
+       for _, ep := range endpoints {
+               if ep.Success {
+                       continue
+               }
+               failed = append(failed, fmt.Sprintf("%s: %s", ep.Server, 
ep.Reason))
+       }
+       if len(failed) == 0 {
+               return ""
+       }
+       return fmt.Sprintf("%d/%d gateway instance(s) failed to apply the last 
sync: %s",
+               len(failed), len(endpoints), strings.Join(failed, "; "))
+}
+
+// markGatewayProxyDataPlaneUnavailable marks the GatewayProxy configName 
names.
+func (d *apisixProvider) markGatewayProxyDataPlaneUnavailable(
+       configName string,
+       msg string,
+       statusUpdateMap map[types.NamespacedNameKind][]string,
+) {
+       var gatewayProxy types.NamespacedNameKind
+       if err := gatewayProxy.FromString(configName); err != nil {
+               d.log.Error(err, "failed to parse config name as a GatewayProxy 
key", "configName", configName)
+               return
+       }
+       statusUpdateMap[gatewayProxy] = append(statusUpdateMap[gatewayProxy], 
msg)
+}
+
+// smearAllResources is the last resort when nothing can be attributed: it 
marks every
+// resource under this config.
+func (d *apisixProvider) smearAllResources(
+       configName string,
+       msg string,
+       statusUpdateMap map[types.NamespacedNameKind][]string,
 ) {
        resource, err := d.client.GetResources(configName)
        if err != nil {
@@ -286,15 +381,15 @@ func (d *apisixProvider) handleEmptyFailedStatuses(
        }
 
        for _, obj := range resource.Services {
-               d.addResourceToStatusUpdateMap(obj.GetLabels(), 
failedStatus.Error(), statusUpdateMap)
+               d.addResourceToStatusUpdateMap(obj.GetLabels(), msg, 
statusUpdateMap)
        }
 
        for _, obj := range resource.Consumers {
-               d.addResourceToStatusUpdateMap(obj.GetLabels(), 
failedStatus.Error(), statusUpdateMap)
+               d.addResourceToStatusUpdateMap(obj.GetLabels(), msg, 
statusUpdateMap)
        }
 
        for _, obj := range resource.SSLs {
-               d.addResourceToStatusUpdateMap(obj.GetLabels(), 
failedStatus.Error(), statusUpdateMap)
+               d.addResourceToStatusUpdateMap(obj.GetLabels(), msg, 
statusUpdateMap)
        }
 
        globalRules, err := d.client.ListGlobalRules(configName)
@@ -303,7 +398,7 @@ func (d *apisixProvider) handleEmptyFailedStatuses(
                return
        }
        for _, rule := range globalRules {
-               d.addResourceToStatusUpdateMap(rule.GetLabels(), 
failedStatus.Error(), statusUpdateMap)
+               d.addResourceToStatusUpdateMap(rule.GetLabels(), msg, 
statusUpdateMap)
        }
 }
 
diff --git a/internal/provider/apisix/status_test.go 
b/internal/provider/apisix/status_test.go
new file mode 100644
index 00000000..d4c75fe2
--- /dev/null
+++ b/internal/provider/apisix/status_test.go
@@ -0,0 +1,135 @@
+// 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 apisix
+
+import (
+       "testing"
+
+       "github.com/go-logr/logr"
+       metav1 "k8s.io/apimachinery/pkg/apis/meta/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/internal/types"
+)
+
+func TestUnavailableEndpointsMessageEmptyWhenEverythingSucceeded(t *testing.T) 
{
+       msg := unavailableEndpointsMessage([]adctypes.EndpointStatus{
+               {Server: "http://apisix-1:9180";, Success: true},
+               {Server: "http://apisix-2:9180";, Success: true},
+       })
+       if msg != "" {
+               t.Fatalf("expected no message, got %q", msg)
+       }
+}
+
+func TestUnavailableEndpointsMessageEmptyWhenThereAreNoEndpoints(t *testing.T) 
{
+       if msg := unavailableEndpointsMessage(nil); msg != "" {
+               t.Fatalf("expected no message, got %q", msg)
+       }
+}
+
+func TestUnavailableEndpointsMessageSummarizesOnlyTheFailedOnes(t *testing.T) {
+       msg := unavailableEndpointsMessage([]adctypes.EndpointStatus{
+               {Server: "http://apisix-1:9180";, Success: true},
+               {Server: "http://apisix-2:9180";, Success: false, Reason: 
"connection refused"},
+       })
+       want := "1/2 gateway instance(s) failed to apply the last sync: 
http://apisix-2:9180: connection refused"
+       if msg != want {
+               t.Fatalf("got %q, want %q", msg, want)
+       }
+}
+
+func TestFailureConditionMarksGatewayProxyDataPlaneAvailableFalse(t 
*testing.T) {
+       nnk := types.NamespacedNameKind{Kind: types.KindGatewayProxy, 
Namespace: "ns", Name: "gp"}
+       c := failureCondition(nnk, "boom")
+
+       if c.Type != GatewayProxyConditionDataPlaneAvailable {
+               t.Errorf("Type = %q, want %q", c.Type, 
GatewayProxyConditionDataPlaneAvailable)
+       }
+       if c.Status != metav1.ConditionFalse {
+               t.Errorf("Status = %v, want False", c.Status)
+       }
+       if c.Reason != GatewayProxyReasonDataPlaneInstanceUnavailable {
+               t.Errorf("Reason = %q, want %q", c.Reason, 
GatewayProxyReasonDataPlaneInstanceUnavailable)
+       }
+       if c.Message != "boom" {
+               t.Errorf("Message = %q, want %q", c.Message, "boom")
+       }
+}
+
+func TestSuccessConditionMarksGatewayProxyDataPlaneAvailableTrue(t *testing.T) 
{
+       nnk := types.NamespacedNameKind{Kind: types.KindGatewayProxy, 
Namespace: "ns", Name: "gp"}
+       c := successCondition(nnk)
+
+       if c.Type != GatewayProxyConditionDataPlaneAvailable {
+               t.Errorf("Type = %q, want %q", c.Type, 
GatewayProxyConditionDataPlaneAvailable)
+       }
+       if c.Status != metav1.ConditionTrue {
+               t.Errorf("Status = %v, want True", c.Status)
+       }
+       if c.Reason != GatewayProxyReasonDataPlaneAvailable {
+               t.Errorf("Reason = %q, want %q", c.Reason, 
GatewayProxyReasonDataPlaneAvailable)
+       }
+}
+
+func TestFailureAndSuccessConditionKeepUsingAcceptedForNonGatewayProxyKinds(t 
*testing.T) {
+       nnk := types.NamespacedNameKind{Kind: types.KindApisixRoute, Namespace: 
"ns", Name: "route"}
+
+       failed := failureCondition(nnk, "boom")
+       if failed.Type != string(apiv2.ConditionTypeAccepted) {
+               t.Errorf("failure Type = %q, want %q", failed.Type, 
apiv2.ConditionTypeAccepted)
+       }
+       if failed.Reason != string(apiv2.ConditionReasonSyncFailed) {
+               t.Errorf("failure Reason = %q, want %q", failed.Reason, 
apiv2.ConditionReasonSyncFailed)
+       }
+
+       ok := successCondition(nnk)
+       if ok.Type != string(apiv2.ConditionTypeAccepted) {
+               t.Errorf("success Type = %q, want %q", ok.Type, 
apiv2.ConditionTypeAccepted)
+       }
+       if ok.Reason != string(apiv2.ConditionReasonAccepted) {
+               t.Errorf("success Reason = %q, want %q", ok.Reason, 
apiv2.ConditionReasonAccepted)
+       }
+}
+
+func TestMarkGatewayProxyDataPlaneUnavailableTargetsTheConfigNameItself(t 
*testing.T) {
+       d := &apisixProvider{log: logr.Discard()}
+       statusUpdateMap := map[types.NamespacedNameKind][]string{}
+
+       d.markGatewayProxyDataPlaneUnavailable("GatewayProxy/ns/gp", "boom", 
statusUpdateMap)
+
+       want := types.NamespacedNameKind{Kind: types.KindGatewayProxy, 
Namespace: "ns", Name: "gp"}
+       if got := statusUpdateMap[want]; len(got) != 1 || got[0] != "boom" {
+               t.Errorf("statusUpdateMap[%v] = %v, want [boom]", want, got)
+       }
+       if len(statusUpdateMap) != 1 {
+               t.Errorf("expected only the GatewayProxy itself to be marked, 
got %v", statusUpdateMap)
+       }
+}
+
+func TestMarkGatewayProxyDataPlaneUnavailableIgnoresAnUnparseableConfigName(t 
*testing.T) {
+       d := &apisixProvider{log: logr.Discard()}
+       statusUpdateMap := map[types.NamespacedNameKind][]string{}
+
+       d.markGatewayProxyDataPlaneUnavailable("not-a-valid-key", "boom", 
statusUpdateMap)
+
+       if len(statusUpdateMap) != 0 {
+               t.Errorf("expected nothing marked for an unparseable 
configName, got %v", statusUpdateMap)
+       }
+}
diff --git a/test/e2e/crds/v2/status.go b/test/e2e/crds/v2/status.go
index eaccb601..54bd4c06 100644
--- a/test/e2e/crds/v2/status.go
+++ b/test/e2e/crds/v2/status.go
@@ -220,6 +220,89 @@ spec:
                        })
                })
 
+               It("gateway proxy reports an unreachable data plane instance", 
func() {
+                       if os.Getenv("PROVIDER_TYPE") != "apisix-standalone" {
+                               Skip("EndpointStatus, and the GatewayProxy 
condition derived from it, only exists in apisix-standalone mode")
+                       }
+
+                       const name = "gateway-proxy-partial-instance"
+                       gatewayProxyYaml := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v1alpha1
+kind: GatewayProxy
+metadata:
+  name: %s
+  namespace: %s
+spec:
+  provider:
+    type: ControlPlane
+    controlPlane:
+      mode: apisix-standalone
+      endpoints:
+      - %s
+      - http://unreachable-instance.invalid:9180
+      auth:
+        type: AdminKey
+        adminKey:
+          value: "%s"
+`, name, s.Namespace(), s.Deployer.GetAdminEndpoint(), s.AdminKey())
+                       By("create GatewayProxy with one unreachable endpoint")
+                       err := s.CreateResourceFromString(gatewayProxyYaml)
+                       Expect(err).NotTo(HaveOccurred(), "creating 
GatewayProxy")
+
+                       ingressClassYaml := fmt.Sprintf(`
+apiVersion: networking.k8s.io/v1
+kind: IngressClass
+metadata:
+  name: %s
+spec:
+  controller: %s
+  parameters:
+    apiGroup: "apisix.apache.org"
+    kind: "GatewayProxy"
+    name: %s
+    namespace: %s
+    scope: Namespace
+`, name, s.GetControllerName(), name, s.Namespace())
+                       By("create IngressClass")
+                       err = 
s.CreateResourceFromStringWithNamespace(ingressClassYaml, "")
+                       Expect(err).NotTo(HaveOccurred(), "creating 
IngressClass")
+
+                       By("apply ApisixRoute through it")
+                       applier.MustApplyAPIv2(types.NamespacedName{Namespace: 
s.Namespace(), Name: "default"}, &apiv2.ApisixRoute{}, fmt.Sprintf(ar, 
s.Namespace(), name))
+
+                       By("check route in APISIX")
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method:  "GET",
+                               Path:    "/get",
+                               Headers: map[string]string{"Host": "httpbin"},
+                               Check:   scaffold.WithExpectedStatus(200),
+                       })
+
+                       By("check GatewayProxy status")
+                       s.RetryAssertion(func() string {
+                               output, _ := 
s.GetOutputFromString("gatewayproxy", name, "-o", "yaml", "-n", s.Namespace())
+                               return output
+                       }).Should(
+                               And(
+                                       ContainSubstring("type: 
DataPlaneAvailable"),
+                                       ContainSubstring(`status: "False"`),
+                                       ContainSubstring("reason: 
DataPlaneInstanceUnavailable"),
+                                       
ContainSubstring("unreachable-instance.invalid"),
+                               ),
+                       )
+
+                       By("check the ApisixRoute itself is not marked as 
failed")
+                       s.RetryAssertion(func() string {
+                               output, _ := s.GetOutputFromString("ar", 
"default", "-o", "yaml", "-n", s.Namespace())
+                               return output
+                       }).Should(
+                               And(
+                                       ContainSubstring(`status: "True"`),
+                                       ContainSubstring("reason: Accepted"),
+                               ),
+                       )
+               })
+
                It("update the same status only once", func() {
                        By("apply ApisixRoute")
                        applier.MustApplyAPIv2(types.NamespacedName{Namespace: 
s.Namespace(), Name: "default"}, &apiv2.ApisixRoute{}, fmt.Sprintf(ar, 
s.Namespace(), s.Namespace()))

Reply via email to