fix: treat NotFound from pod delete in rolling update as success (#840)
Signed-off-by: Arunesh Dwivedi <arunesh.dwivedi@gmail.com>
Co-authored-by: Arunesh Dwivedi <arunesh.dwivedi@gmail.com>
diff --git a/controllers/events_test.go b/controllers/events_test.go
index f2cdb56..d2a5d1e 100644
--- a/controllers/events_test.go
+++ b/controllers/events_test.go
@@ -28,9 +28,11 @@
"github.com/go-logr/logr"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/runtime/schema"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -253,7 +255,87 @@
requireEvent(t, rec, corev1.EventTypeWarning, "PVCExpansionError")
}
-// TestReconcileSolrCloudBackupEmitsCloudNotReadyEvent verifies a BackupCloudNotReady warning is
+// TestDeletePodForUpdateTreatsNotFoundAsSuccess verifies that when the target
+// pod is already gone, DeletePodForUpdate does not report a PodUpdateError
+// warning. A NotFound from the delete is the desired postcondition (the pod is
+// absent and will be recreated), so it should surface a normal PodUpdate event.
+func TestDeletePodForUpdateTreatsNotFoundAsSuccess(t *testing.T) {
+ scheme := runtime.NewScheme()
+ if err := clientgoscheme.AddToScheme(scheme); err != nil {
+ t.Fatalf("could not add client-go types to scheme: %v", err)
+ }
+ if err := solrv1beta1.AddToScheme(scheme); err != nil {
+ t.Fatalf("could not add solr types to scheme: %v", err)
+ }
+
+ pod := &corev1.Pod{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-pod",
+ Namespace: "test",
+ UID: "test-uid",
+ },
+ }
+
+ // Force the fake client's Delete to return NotFound, simulating a pod that
+ // was already removed by a prior reconcile.
+ notFoundClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithInterceptorFuncs(interceptor.Funcs{
+ Delete: func(_ context.Context, _ client.WithWatch, _ client.Object, _ ...client.DeleteOption) error {
+ return apierrors.NewNotFound(schema.GroupResource{Resource: "pods"}, "test-pod")
+ },
+ }).
+ Build()
+
+ rec := record.NewFakeRecorder(8)
+ r := &SolrCloudReconciler{Client: notFoundClient, Recorder: rec}
+ instance := &solrv1beta1.SolrCloud{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "test"}}
+
+ if _, _, err := DeletePodForUpdate(context.Background(), r, instance, pod, false, logr.Discard()); err == nil {
+ t.Error("expected DeletePodForUpdate to return the NotFound delete error, got nil")
+ }
+ // A NotFound delete must surface a normal PodUpdate event, never a PodUpdateError warning.
+ requireEvent(t, rec, corev1.EventTypeNormal, "PodUpdate")
+}
+
+// TestDeletePodForUpdateReportsRealDeleteError verifies that a non-NotFound
+// delete failure is still surfaced as a PodUpdateError warning.
+func TestDeletePodForUpdateReportsRealDeleteError(t *testing.T) {
+ scheme := runtime.NewScheme()
+ if err := clientgoscheme.AddToScheme(scheme); err != nil {
+ t.Fatalf("could not add client-go types to scheme: %v", err)
+ }
+ if err := solrv1beta1.AddToScheme(scheme); err != nil {
+ t.Fatalf("could not add solr types to scheme: %v", err)
+ }
+
+ pod := &corev1.Pod{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-pod",
+ Namespace: "test",
+ UID: "test-uid",
+ },
+ }
+
+ realErrClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithInterceptorFuncs(interceptor.Funcs{
+ Delete: func(_ context.Context, _ client.WithWatch, _ client.Object, _ ...client.DeleteOption) error {
+ return errors.New("boom")
+ },
+ }).
+ Build()
+
+ rec := record.NewFakeRecorder(8)
+ r := &SolrCloudReconciler{Client: realErrClient, Recorder: rec}
+ instance := &solrv1beta1.SolrCloud{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "test"}}
+
+ if _, _, err := DeletePodForUpdate(context.Background(), r, instance, pod, false, logr.Discard()); err == nil {
+ t.Error("expected DeletePodForUpdate to return the delete error, got nil")
+ }
+ requireEvent(t, rec, corev1.EventTypeWarning, "PodUpdateError")
+}
+
// emitted when a backup is attempted against a repository that the SolrCloud has not yet marked
// available. A GCS repository is used so that EnsureDirectoryForBackup is a no-op (no pod exec).
func TestReconcileSolrCloudBackupEmitsCloudNotReadyEvent(t *testing.T) {
diff --git a/controllers/solr_pod_lifecycle_util.go b/controllers/solr_pod_lifecycle_util.go
index c794f07..9815dbd 100644
--- a/controllers/solr_pod_lifecycle_util.go
+++ b/controllers/solr_pod_lifecycle_util.go
@@ -23,6 +23,7 @@
"github.com/apache/solr-operator/controllers/util"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"time"
@@ -106,9 +107,17 @@
UID: &pod.UID,
})
if err != nil {
- logger.Error(err, "Error while killing solr pod for update", "pod", pod.Name)
- r.Recorder.Eventf(instance, corev1.EventTypeWarning, "PodUpdateError",
- "Error while deleting pod %s for an update: %v", pod.Name, err)
+ // A NotFound error means the pod is already gone, which is the
+ // desired postcondition for this update path. Treat it as success
+ // rather than reporting a spurious PodUpdateError.
+ if apierrors.IsNotFound(err) {
+ r.Recorder.Eventf(instance, corev1.EventTypeNormal, "PodUpdate",
+ "Pod %s already deleted; it will be recreated with the updated SolrCloud specification", pod.Name)
+ } else {
+ logger.Error(err, "Error while killing solr pod for update", "pod", pod.Name)
+ r.Recorder.Eventf(instance, corev1.EventTypeWarning, "PodUpdateError",
+ "Error while deleting pod %s for an update: %v", pod.Name, err)
+ }
} else {
r.Recorder.Eventf(instance, corev1.EventTypeNormal, "PodUpdate",
"Deleting pod %s so that it can be recreated with the updated SolrCloud specification", pod.Name)