bzp2010 commented on code in PR #2856:
URL: 
https://github.com/apache/apisix-ingress-controller/pull/2856#discussion_r3990604036


##########
internal/provider/apisix/status.go:
##########
@@ -18,56 +18,232 @@
 package apisix
 
 import (
+       "context"
        "fmt"
        "strings"
 
+       corev1 "k8s.io/api/core/v1"
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        "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"
        cutils 
"github.com/apache/apisix-ingress-controller/internal/controller/utils"
        "github.com/apache/apisix-ingress-controller/internal/types"
 )
 
-// handleStatusUpdate updates resource conditions based on the latest sync 
results.
+// 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"
+)
+
+// updateStatusFromSyncResults updates every resource and GatewayProxy status 
this
+// round's sync results call for. results holds one entry per config sync() 
actually
+// reached pushConfig for this round, success (a zero-value 
types.ADCExecutionErrors) or
+// failure; a config sync() could not even build a SyncInput for is absent 
here entirely
+// and its status is left untouched this round, see sync().
 //
-// It maintains a history of failed resources in d.statusUpdateMap.
+// GatewayProxy's DataPlaneAvailable condition is recomputed and written fresh 
every
+// round directly from this round's result, never compared against any 
remembered
+// history: a config with no error this round is written True, one with any 
error is
+// written False. That is what makes a GatewayProxy that has always been 
healthy actually
+// get a True the first time, and what keeps a restart from leaving a stale 
False stuck
+// forever: the write only ever depends on this round's actual outcome.
 //
-// For resources in the current failure map (statusUpdateMap), it marks them 
as failed.
-// For resources that exist only in the previous failure history (i.e. not in 
this sync's failures),
-// it marks them as accepted (success).
-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, "; "),
-               ))
+// Resource status can't afford the same full recompute: a config's resource 
set can be
+// large, and rewriting every one of them every round even when nothing 
changed would be
+// wasteful. So resources keep a small persisted delta in d.resourceFailures 
instead:
+// newly (or still) failing resources are written SyncFailed, and any resource 
that was
+// failing last round but isn't failing this one gets its error explicitly 
cleared with
+// an Accepted write.
+func (d *apisixProvider) updateStatusFromSyncResults(ctx context.Context, 
results map[string]types.ADCExecutionErrors) {
+       resourceFailures := map[types.NamespacedNameKind][]string{}
+
+       for configName, execErrs := range results {
+               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)
+                       continue
+               }
+
+               gatewayProxyMsgs, failedEndpoints := 
d.classifySyncResult(configName, execErrs, resourceFailures)
+               if len(gatewayProxyMsgs) > 0 {
+                       d.updateStatus(gatewayProxy, 
failureCondition(gatewayProxy, strings.Join(gatewayProxyMsgs, "; ")))
+                       d.recordFailedEndpointEvents(ctx, gatewayProxy, 
failedEndpoints)
+               } else {
+                       d.updateStatus(gatewayProxy, 
successCondition(gatewayProxy))
+               }
+       }
+
+       d.applyResourceFailures(resourceFailures)
+       d.log.V(1).Info("updated status from sync results", "results", results, 
"resource_failures", resourceFailures)
+}
+
+// classifySyncResult splits one config's this-round result into what belongs 
on the
+// GatewayProxy (returned) and what belongs on specific Kubernetes resources 
(added into
+// resourceFailures). A FailedStatuses entry that resolves to a resource via 
its Event
+// goes there; everything else, no FailedStatuses at all, or a FailedStatuses 
entry
+// with no Event to resolve, which is what apisix-standalone's own 
endpoint-driven
+// rejections look like, is a GatewayProxy-level signal instead, using
+// EndpointStatuses for the message when there is one and the raw error 
otherwise.
+func (d *apisixProvider) classifySyncResult(
+       configName string,
+       execErrs types.ADCExecutionErrors,
+       resourceFailures map[types.NamespacedNameKind][]string,
+) (gatewayProxyMsgs []string, failedEndpoints []adctypes.EndpointStatus) {
+       unattributed := func(addrErr types.ADCExecutionServerAddrError) {
+               msg := unavailableEndpointsMessage(addrErr.EndpointStatuses)
+               if msg == "" {
+                       msg = addrErr.Error()
+               }
+               gatewayProxyMsgs = append(gatewayProxyMsgs, msg)
+               failedEndpoints = append(failedEndpoints, 
addrErr.EndpointStatuses...)
+       }
+
+       for _, execErr := range execErrs.Errors {
+               for _, addrErr := range execErr.FailedErrors {
+                       if len(addrErr.FailedStatuses) == 0 {
+                               unattributed(addrErr)
+                               continue
+                       }
+
+                       attributedAll := true
+                       for _, syncStatus := range addrErr.FailedStatuses {
+                               if syncStatus.Event.ResourceType == "" {
+                                       // standalone: this whole addrErr 
carries no per-resource attribution.
+                                       attributedAll = false
+                                       break
+                               }
+                               labels, err := 
d.store.GetResourceLabel(configName, syncStatus.Event.ResourceType, 
syncStatus.Event.ResourceID)
+                               if err != nil {
+                                       d.log.Error(err, "failed to get 
resource label",
+                                               "configName", configName, 
"resourceType", syncStatus.Event.ResourceType, "id", 
syncStatus.Event.ResourceID)
+                                       continue
+                               }
+                               resourceKey := types.NamespacedNameKind{
+                                       Name:      labels[label.LabelName],
+                                       Namespace: labels[label.LabelNamespace],
+                                       Kind:      labels[label.LabelKind],
+                               }
+                               msg := fmt.Sprintf("ServerAddr: %s, Error: %s", 
addrErr.ServerAddr, syncStatus.Reason)
+                               resourceFailures[resourceKey] = 
append(resourceFailures[resourceKey], msg)
+                       }
+                       if !attributedAll {
+                               unattributed(addrErr)
+                       }
+               }
+       }
+       return gatewayProxyMsgs, failedEndpoints
+}
+
+// applyResourceFailures writes this round's newly (or still) failing 
resources, and
+// clears the recorded error from any resource that was failing last round but 
isn't in
+// newFailures now. See updateStatusFromSyncResults for why resources use this 
delta
+// instead of GatewayProxy's full recompute.
+func (d *apisixProvider) applyResourceFailures(newFailures 
map[types.NamespacedNameKind][]string) {
+       for resourceKey, msgs := range newFailures {
+               d.updateStatus(resourceKey, failureCondition(resourceKey, 
strings.Join(msgs, "; ")))
+       }
+       for resourceKey := range d.resourceFailures {
+               if _, stillFailing := newFailures[resourceKey]; !stillFailing {
+                       d.updateStatus(resourceKey, 
successCondition(resourceKey))
+               }
+       }
+       d.resourceFailures = newFailures
+}
+
+// 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),
+       }
+}
+
+// recordFailedEndpointEvents fires one Warning event per failed 
EndpointStatus entry,
+// so each instance's own failure history (when it started, how often) is 
visible on
+// its own, not folded into everyone else's. The GatewayProxy is fetched fresh 
from the
+// API server first so the Event's involvedObject carries a real UID: a 
hand-built stub
+// with only Name/Namespace leaves that UID empty, and kubectl describe 
resolves events
+// by matching it, so an event against such a stub never shows up there.
+func (d *apisixProvider) recordFailedEndpointEvents(ctx context.Context, nnk 
types.NamespacedNameKind, endpoints []adctypes.EndpointStatus) {
+       if d.EventRecorder == nil {
+               return
+       }
+       hasFailure := false
+       for _, ep := range endpoints {
+               if !ep.Success {
+                       hasFailure = true
+                       break
+               }
+       }
+       if !hasFailure {
+               return
        }
 
-       // 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,
-                               "",
-                       ))
+       gatewayProxy := &apiv1alpha1.GatewayProxy{}
+       if err := d.K8sClient.Get(ctx, nnk.NamespacedName(), gatewayProxy); err 
!= nil {
+               d.log.Error(err, "failed to get GatewayProxy to record failed 
endpoint events", "name", nnk.Name, "namespace", nnk.Namespace)
+               return
+       }
+
+       for _, ep := range endpoints {
+               if ep.Success {
+                       continue
                }
+               d.EventRecorder.Event(gatewayProxy, corev1.EventTypeWarning, 
GatewayProxyReasonDataPlaneInstanceUnavailable,
+                       fmt.Sprintf("%s: %s", ep.Server, ep.Reason))
        }
-       // Update the failure history with the current failure set.
-       d.statusUpdateMap = statusUpdateMap
 }
 
 //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)

Review Comment:
   This will be fixed in a future PR; it's a completely unrelated issue.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to