@@ -23,20 +23,26 @@ import (
2323 "io"
2424 "net/http"
2525 "net/http/httptest"
26+ "testing"
2627 "time"
2728
2829 nodedisruptionv1alpha1 "github.com/criteo/node-disruption-controller/api/v1alpha1"
2930 "github.com/criteo/node-disruption-controller/internal/appmgr"
3031 . "github.com/onsi/ginkgo/v2"
3132 . "github.com/onsi/gomega"
33+ "github.com/stretchr/testify/assert"
34+ "github.com/stretchr/testify/require"
3235 corev1 "k8s.io/api/core/v1"
3336 errorsk8s "k8s.io/apimachinery/pkg/api/errors"
3437 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
38+ "k8s.io/apimachinery/pkg/runtime"
3539 "k8s.io/apimachinery/pkg/types"
3640 "k8s.io/client-go/kubernetes/scheme"
3741
3842 ctrl "sigs.k8s.io/controller-runtime"
3943 "sigs.k8s.io/controller-runtime/pkg/client"
44+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
45+ "sigs.k8s.io/controller-runtime/pkg/client/interceptor"
4046)
4147
4248// +kubebuilder:docs-gen:collapse=Imports
@@ -1159,3 +1165,57 @@ var _ = Describe("NodeDisruption controller", func() {
11591165 })
11601166 })
11611167})
1168+
1169+ // TestReconcile_DoesNotErrorOnNotFoundStatusUpdate verifies that when the
1170+ // NodeDisruption is removed between Reconcile's Get and the status update
1171+ // (e.g., the object's finalizer was removed and it was deleted from etcd),
1172+ // Reconcile swallows the resulting NotFound error instead of bubbling it up to
1173+ // the controller-runtime workqueue (which would log it as a Reconciler error
1174+ // and trigger an unnecessary requeue).
1175+ func TestReconcile_DoesNotErrorOnNotFoundStatusUpdate (t * testing.T ) {
1176+ s := runtime .NewScheme ()
1177+ require .NoError (t , scheme .AddToScheme (s ))
1178+ require .NoError (t , nodedisruptionv1alpha1 .AddToScheme (s ))
1179+
1180+ nd := & nodedisruptionv1alpha1.NodeDisruption {
1181+ ObjectMeta : metav1.ObjectMeta {
1182+ Name : "vanishing-nd" ,
1183+ Namespace : "default" ,
1184+ Finalizers : []string {FinalizerName },
1185+ },
1186+ Spec : nodedisruptionv1alpha1.NodeDisruptionSpec {
1187+ NodeSelector : metav1.LabelSelector {},
1188+ Type : "maintenance" ,
1189+ },
1190+ }
1191+
1192+ notFound := errorsk8s .NewNotFound (
1193+ nodedisruptionv1alpha1 .GroupVersion .WithResource ("nodedisruptions" ).GroupResource (),
1194+ nd .Name ,
1195+ )
1196+
1197+ c := fake .NewClientBuilder ().
1198+ WithScheme (s ).
1199+ WithObjects (nd ).
1200+ WithStatusSubresource (& nodedisruptionv1alpha1.NodeDisruption {}).
1201+ WithInterceptorFuncs (interceptor.Funcs {
1202+ SubResourceUpdate : func (_ context.Context , _ client.Client , subResourceName string , _ client.Object , _ ... client.SubResourceUpdateOption ) error {
1203+ if subResourceName == "status" {
1204+ return notFound
1205+ }
1206+ return nil
1207+ },
1208+ }).
1209+ Build ()
1210+
1211+ r := & NodeDisruptionReconciler {
1212+ Client : c ,
1213+ Scheme : s ,
1214+ Config : NodeDisruptionReconcilerConfig {RetryInterval : time .Second },
1215+ }
1216+
1217+ _ , err := r .Reconcile (context .Background (), ctrl.Request {
1218+ NamespacedName : types.NamespacedName {Name : nd .Name , Namespace : nd .Namespace },
1219+ })
1220+ assert .NoError (t , err , "NotFound on status update must be swallowed by Reconcile" )
1221+ }
0 commit comments