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 6e2efa1b87d11dc03a11380546f674acc4fa652d Author: bzp2010 <[email protected]> AuthorDate: Fri Sep 4 00:05:27 2026 +0800 feat: create per-endpoint event and link to gateway proxy --- internal/manager/run.go | 1 + internal/provider/apisix/status.go | 43 ++++++++++++++++++++++-- internal/provider/apisix/status_test.go | 59 +++++++++++++++++++++++++++++++-- internal/provider/options.go | 6 ++++ test/e2e/crds/v2/status.go | 13 ++++++++ 5 files changed, 118 insertions(+), 4 deletions(-) diff --git a/internal/manager/run.go b/internal/manager/run.go index b08ae92f..18fd0c75 100644 --- a/internal/manager/run.go +++ b/internal/manager/run.go @@ -223,6 +223,7 @@ func Run(ctx context.Context, logger logr.Logger) error { SyncPeriod: config.ControllerConfig.ProviderConfig.SyncPeriod.Duration, InitSyncDelay: config.ControllerConfig.ProviderConfig.InitSyncDelay.Duration, ListenerPortMatchMode: config.ControllerConfig.ListenerPortMatchMode, + EventRecorder: mgr.GetEventRecorderFor("apisix-provider"), } provider, err := provider.New(providerType, logger, updater.Writer(), readier, providerOptions) if err != nil { diff --git a/internal/provider/apisix/status.go b/internal/provider/apisix/status.go index 7c888a0d..8f12eb70 100644 --- a/internal/provider/apisix/status.go +++ b/internal/provider/apisix/status.go @@ -21,6 +21,7 @@ import ( "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" @@ -61,6 +62,9 @@ func (d *apisixProvider) handleStatusUpdate(statusUpdateMap map[types.Namespaced for nnk := range d.statusUpdateMap { if _, ok := statusUpdateMap[nnk]; !ok { d.updateStatus(nnk, successCondition(nnk)) + if nnk.Kind == types.KindGatewayProxy { + d.recordGatewayProxyRecoveredEvent(nnk) + } } } // Update the failure history with the current failure set. @@ -98,6 +102,38 @@ func newGatewayProxyDataPlaneAvailableCondition(available bool, reason, msg stri } } +// recordGatewayProxyRecoveredEvent fires once a GatewayProxy leaves the failure +// history entirely, i.e. every instance took the last sync. +func (d *apisixProvider) recordGatewayProxyRecoveredEvent(nnk types.NamespacedNameKind) { + if d.EventRecorder == nil { + return + } + gatewayProxy := &apiv1alpha1.GatewayProxy{ + ObjectMeta: metav1.ObjectMeta{Name: nnk.Name, Namespace: nnk.Namespace}, + } + d.EventRecorder.Event(gatewayProxy, corev1.EventTypeNormal, GatewayProxyReasonDataPlaneAvailable, + "every data plane instance took the last sync") +} + +// 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. +func (d *apisixProvider) recordFailedEndpointEvents(nnk types.NamespacedNameKind, endpoints []adctypes.EndpointStatus) { + if d.EventRecorder == nil { + return + } + gatewayProxy := &apiv1alpha1.GatewayProxy{ + ObjectMeta: metav1.ObjectMeta{Name: nnk.Name, Namespace: nnk.Namespace}, + } + for _, ep := range endpoints { + if ep.Success { + continue + } + d.EventRecorder.Event(gatewayProxy, corev1.EventTypeWarning, GatewayProxyReasonDataPlaneInstanceUnavailable, + fmt.Sprintf("%s: %s", ep.Server, ep.Reason)) + } +} + //nolint:gocyclo func (d *apisixProvider) updateStatus(nnk types.NamespacedNameKind, condition metav1.Condition) { switch nnk.Kind { @@ -330,7 +366,7 @@ func (d *apisixProvider) handleEmptyFailedStatuses( statusUpdateMap map[types.NamespacedNameKind][]string, ) { if msg := unavailableEndpointsMessage(failedStatus.EndpointStatuses); msg != "" { - d.markGatewayProxyDataPlaneUnavailable(configName, msg, statusUpdateMap) + d.markGatewayProxyDataPlaneUnavailable(configName, msg, failedStatus.EndpointStatuses, statusUpdateMap) return } d.smearAllResources(configName, failedStatus.Error(), statusUpdateMap) @@ -353,10 +389,12 @@ func unavailableEndpointsMessage(endpoints []adctypes.EndpointStatus) string { len(failed), len(endpoints), strings.Join(failed, "; ")) } -// markGatewayProxyDataPlaneUnavailable marks the GatewayProxy configName names. +// markGatewayProxyDataPlaneUnavailable marks the GatewayProxy configName names, and +// fires one event per failed endpoint. func (d *apisixProvider) markGatewayProxyDataPlaneUnavailable( configName string, msg string, + endpoints []adctypes.EndpointStatus, statusUpdateMap map[types.NamespacedNameKind][]string, ) { var gatewayProxy types.NamespacedNameKind @@ -365,6 +403,7 @@ func (d *apisixProvider) markGatewayProxyDataPlaneUnavailable( return } statusUpdateMap[gatewayProxy] = append(statusUpdateMap[gatewayProxy], msg) + d.recordFailedEndpointEvents(gatewayProxy, endpoints) } // smearAllResources is the last resort when nothing can be attributed: it marks every diff --git a/internal/provider/apisix/status_test.go b/internal/provider/apisix/status_test.go index d4c75fe2..79198fdc 100644 --- a/internal/provider/apisix/status_test.go +++ b/internal/provider/apisix/status_test.go @@ -18,10 +18,12 @@ package apisix import ( + "strings" "testing" "github.com/go-logr/logr" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/record" adctypes "github.com/apache/apisix-ingress-controller/api/adc" apiv2 "github.com/apache/apisix-ingress-controller/api/v2" @@ -112,7 +114,7 @@ func TestMarkGatewayProxyDataPlaneUnavailableTargetsTheConfigNameItself(t *testi d := &apisixProvider{log: logr.Discard()} statusUpdateMap := map[types.NamespacedNameKind][]string{} - d.markGatewayProxyDataPlaneUnavailable("GatewayProxy/ns/gp", "boom", statusUpdateMap) + d.markGatewayProxyDataPlaneUnavailable("GatewayProxy/ns/gp", "boom", nil, statusUpdateMap) want := types.NamespacedNameKind{Kind: types.KindGatewayProxy, Namespace: "ns", Name: "gp"} if got := statusUpdateMap[want]; len(got) != 1 || got[0] != "boom" { @@ -127,9 +129,62 @@ func TestMarkGatewayProxyDataPlaneUnavailableIgnoresAnUnparseableConfigName(t *t d := &apisixProvider{log: logr.Discard()} statusUpdateMap := map[types.NamespacedNameKind][]string{} - d.markGatewayProxyDataPlaneUnavailable("not-a-valid-key", "boom", statusUpdateMap) + d.markGatewayProxyDataPlaneUnavailable("not-a-valid-key", "boom", nil, statusUpdateMap) if len(statusUpdateMap) != 0 { t.Errorf("expected nothing marked for an unparseable configName, got %v", statusUpdateMap) } } + +func TestRecordFailedEndpointEventsFiresOneWarningPerFailedEndpoint(t *testing.T) { + recorder := record.NewFakeRecorder(2) + d := &apisixProvider{log: logr.Discard()} + d.EventRecorder = recorder + nnk := types.NamespacedNameKind{Kind: types.KindGatewayProxy, Namespace: "ns", Name: "gp"} + + d.recordFailedEndpointEvents(nnk, []adctypes.EndpointStatus{ + {Server: "http://apisix-1:9180", Success: true}, + {Server: "http://apisix-2:9180", Success: false, Reason: "connection refused"}, + {Server: "http://apisix-3:9180", Success: false, Reason: "TLS handshake failed"}, + }) + + first := <-recorder.Events + if !strings.Contains(first, "Warning") || !strings.Contains(first, "DataPlaneInstanceUnavailable") || + !strings.Contains(first, "http://apisix-2:9180: connection refused") { + t.Errorf("first event = %q, want the apisix-2 failure", first) + } + + second := <-recorder.Events + if !strings.Contains(second, "Warning") || !strings.Contains(second, "DataPlaneInstanceUnavailable") || + !strings.Contains(second, "http://apisix-3:9180: TLS handshake failed") { + t.Errorf("second event = %q, want the apisix-3 failure", second) + } + + select { + case e := <-recorder.Events: + t.Errorf("unexpected third event %q, the successful endpoint should not fire one", e) + default: + } +} + +func TestRecordFailedEndpointEventsNoopsWithoutARecorder(t *testing.T) { + d := &apisixProvider{log: logr.Discard()} + nnk := types.NamespacedNameKind{Kind: types.KindGatewayProxy, Namespace: "ns", Name: "gp"} + + // Must not panic when no EventRecorder was configured. + d.recordFailedEndpointEvents(nnk, []adctypes.EndpointStatus{{Server: "http://apisix-1:9180", Success: false}}) +} + +func TestRecordGatewayProxyRecoveredEventFiresNormal(t *testing.T) { + recorder := record.NewFakeRecorder(1) + d := &apisixProvider{log: logr.Discard()} + d.EventRecorder = recorder + nnk := types.NamespacedNameKind{Kind: types.KindGatewayProxy, Namespace: "ns", Name: "gp"} + + d.recordGatewayProxyRecoveredEvent(nnk) + + event := <-recorder.Events + if !strings.Contains(event, "Normal") || !strings.Contains(event, "DataPlaneAvailable") { + t.Errorf("recovery event = %q, want it to contain Normal and DataPlaneAvailable", event) + } +} diff --git a/internal/provider/options.go b/internal/provider/options.go index c47e7ce9..7465f520 100644 --- a/internal/provider/options.go +++ b/internal/provider/options.go @@ -20,6 +20,8 @@ package provider import ( "time" + "k8s.io/client-go/tools/record" + "github.com/apache/apisix-ingress-controller/internal/controller/config" ) @@ -34,6 +36,7 @@ type Options struct { DefaultBackendMode string DefaultResolveEndpoints bool ListenerPortMatchMode config.ListenerPortMatchMode + EventRecorder record.EventRecorder } func (o *Options) ApplyToList(lo *Options) { @@ -55,6 +58,9 @@ func (o *Options) ApplyToList(lo *Options) { if o.ListenerPortMatchMode != "" { lo.ListenerPortMatchMode = o.ListenerPortMatchMode } + if o.EventRecorder != nil { + lo.EventRecorder = o.EventRecorder + } } func (o *Options) ApplyOptions(opts []Option) *Options { diff --git a/test/e2e/crds/v2/status.go b/test/e2e/crds/v2/status.go index 54bd4c06..3c06a0c2 100644 --- a/test/e2e/crds/v2/status.go +++ b/test/e2e/crds/v2/status.go @@ -291,6 +291,19 @@ spec: ), ) + By("check a Warning event was recorded on the GatewayProxy") + s.RetryAssertion(func() string { + output, _ := s.GetOutputFromString("events", + "--field-selector", "involvedObject.kind=GatewayProxy,involvedObject.name="+name, + "-n", s.Namespace()) + return output + }).Should( + And( + ContainSubstring("Warning"), + ContainSubstring("DataPlaneInstanceUnavailable"), + ), + ) + By("check the ApisixRoute itself is not marked as failed") s.RetryAssertion(func() string { output, _ := s.GetOutputFromString("ar", "default", "-o", "yaml", "-n", s.Namespace())
