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