Skip to content

Commit

Permalink
Fix panic when bindings reference a deleted cluster (#102)
Browse files Browse the repository at this point in the history
* Add integration test to reproduce panic loop

* Fix case where empty HTTP response caused panic in reconcile loop

* Fix another case where empty responses could cause a panic

* Refactor validateResponse functions

* Use default Ginkgo reporters

* Add integration test to repro panic when RMQ is down

* Fix case where non-existant RMQ would cause panic loop

* Reword test where cluster is nil
  • Loading branch information
coro authored Apr 12, 2021
1 parent 24dfb31 commit d4ffeeb
Show file tree
Hide file tree
Showing 10 changed files with 405 additions and 152 deletions.
7 changes: 7 additions & 0 deletions controllers/binding_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ func (r *BindingReconciler) declareBinding(ctx context.Context, client internal.
func (r *BindingReconciler) deleteBinding(ctx context.Context, client internal.RabbitMQClient, binding *topology.Binding) error {
logger := ctrl.LoggerFrom(ctx)

if client == nil || reflect.ValueOf(client).IsNil() {
logger.Info(noSuchRabbitDeletion, "binding", binding.Name)
r.Recorder.Event(binding, corev1.EventTypeNormal, "SuccessfulDelete", "successfully deleted binding")
return r.removeFinalizer(ctx, binding)
}

var info *rabbithole.BindingInfo
var err error
if binding.Spec.Arguments != nil {
Expand Down Expand Up @@ -166,6 +172,7 @@ func (r *BindingReconciler) deleteBinding(ctx context.Context, client internal.R
}

logger.Info("Successfully deleted binding")
r.Recorder.Event(binding, corev1.EventTypeNormal, "SuccessfulDelete", "successfully deleted binding")
return r.removeFinalizer(ctx, binding)
}

Expand Down
218 changes: 218 additions & 0 deletions controllers/binding_controller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
package controllers_test

import (
"bytes"
"errors"
"io/ioutil"
"net/http"
"time"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
. "github.com/onsi/gomega/gstruct"
topology "github.com/rabbitmq/messaging-topology-operator/api/v1alpha2"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)

var _ = Describe("bindingController", func() {
var binding topology.Binding
var bindingName string

JustBeforeEach(func() {
binding = topology.Binding{
ObjectMeta: metav1.ObjectMeta{
Name: bindingName,
Namespace: "default",
},
Spec: topology.BindingSpec{
RabbitmqClusterReference: topology.RabbitmqClusterReference{
Name: "example-rabbit",
},
},
}
})

When("creating a binding", func() {
When("the RabbitMQ Client returns a HTTP error response", func() {
BeforeEach(func() {
bindingName = "test-binding-http-error"
fakeRabbitMQClient.DeclareBindingReturns(&http.Response{
Status: "418 I'm a teapot",
StatusCode: 418,
}, errors.New("Some HTTP error"))
})

It("sets the status condition to indicate a failure to reconcile", func() {
Expect(client.Create(ctx, &binding)).To(Succeed())
EventuallyWithOffset(1, func() []topology.Condition {
_ = client.Get(
ctx,
types.NamespacedName{Name: binding.Name, Namespace: binding.Namespace},
&binding,
)

return binding.Status.Conditions
}, 10*time.Second, 1*time.Second).Should(ContainElement(MatchFields(IgnoreExtras, Fields{
"Type": Equal(topology.ConditionType("Ready")),
"Reason": Equal("FailedCreateOrUpdate"),
"Status": Equal(corev1.ConditionFalse),
"Message": ContainSubstring("Some HTTP error"),
})))
})
})

When("the RabbitMQ Client returns a Go error response", func() {
BeforeEach(func() {
bindingName = "test-binding-go-error"
fakeRabbitMQClient.DeclareBindingReturns(nil, errors.New("Hit a exception"))
})

It("sets the status condition to indicate a failure to reconcile", func() {
Expect(client.Create(ctx, &binding)).To(Succeed())
EventuallyWithOffset(1, func() []topology.Condition {
_ = client.Get(
ctx,
types.NamespacedName{Name: binding.Name, Namespace: binding.Namespace},
&binding,
)

return binding.Status.Conditions
}, 10*time.Second, 1*time.Second).Should(ContainElement(MatchFields(IgnoreExtras, Fields{
"Type": Equal(topology.ConditionType("Ready")),
"Reason": Equal("FailedCreateOrUpdate"),
"Status": Equal(corev1.ConditionFalse),
"Message": ContainSubstring("Hit a exception"),
})))
})
})

When("the RabbitMQ Client successfully creates a binding", func() {
BeforeEach(func() {
bindingName = "test-binding-success"
fakeRabbitMQClient.DeclareBindingReturns(&http.Response{
Status: "201 Created",
StatusCode: http.StatusCreated,
}, nil)
})

It("sets the status condition to indicate a success in reconciling", func() {
Expect(client.Create(ctx, &binding)).To(Succeed())
EventuallyWithOffset(1, func() []topology.Condition {
_ = client.Get(
ctx,
types.NamespacedName{Name: binding.Name, Namespace: binding.Namespace},
&binding,
)

return binding.Status.Conditions
}, 10*time.Second, 1*time.Second).Should(ContainElement(MatchFields(IgnoreExtras, Fields{
"Type": Equal(topology.ConditionType("Ready")),
"Reason": Equal("SuccessfulCreateOrUpdate"),
"Status": Equal(corev1.ConditionTrue),
})))
})
})
})

When("Deleting a binding", func() {
JustBeforeEach(func() {
fakeRabbitMQClient.DeclareBindingReturns(&http.Response{
Status: "201 Created",
StatusCode: http.StatusCreated,
}, nil)
Expect(client.Create(ctx, &binding)).To(Succeed())
EventuallyWithOffset(1, func() []topology.Condition {
_ = client.Get(
ctx,
types.NamespacedName{Name: binding.Name, Namespace: binding.Namespace},
&binding,
)

return binding.Status.Conditions
}, 10*time.Second, 1*time.Second).Should(ContainElement(MatchFields(IgnoreExtras, Fields{
"Type": Equal(topology.ConditionType("Ready")),
"Reason": Equal("SuccessfulCreateOrUpdate"),
"Status": Equal(corev1.ConditionTrue),
})))
})

When("the RabbitMQ Client returns a HTTP error response", func() {
BeforeEach(func() {
bindingName = "delete-binding-http-error"
fakeRabbitMQClient.DeleteBindingReturns(&http.Response{
Status: "502 Bad Gateway",
StatusCode: http.StatusBadGateway,
Body: ioutil.NopCloser(bytes.NewBufferString("Hello World")),
}, nil)
})

It("raises an event to indicate a failure to delete", func() {
Expect(client.Delete(ctx, &binding)).To(Succeed())
Consistently(func() bool {
err := client.Get(ctx, types.NamespacedName{Name: binding.Name, Namespace: binding.Namespace}, &topology.Binding{})
return apierrors.IsNotFound(err)
}, 5).Should(BeFalse())
Expect(observedEvents()).To(ContainElement("Warning FailedDelete failed to delete binding"))
})
})

When("the RabbitMQ Client returns a Go error response", func() {
BeforeEach(func() {
bindingName = "delete-binding-go-error"
fakeRabbitMQClient.DeleteBindingReturns(nil, errors.New("some error"))
})

It("raises an event to indicate a failure to delete", func() {
Expect(client.Delete(ctx, &binding)).To(Succeed())
Consistently(func() bool {
err := client.Get(ctx, types.NamespacedName{Name: binding.Name, Namespace: binding.Namespace}, &topology.Binding{})
return apierrors.IsNotFound(err)
}, 5).Should(BeFalse())
Expect(observedEvents()).To(ContainElement("Warning FailedDelete failed to delete binding"))
})
})

When("the RabbitMQ cluster is nil", func() {
BeforeEach(func() {
bindingName = "delete-binding-client-not-found-error"
})

JustBeforeEach(func() {
prepareNoSuchClusterError()
})

It("successfully deletes the binding regardless", func() {
Expect(client.Delete(ctx, &binding)).To(Succeed())
Eventually(func() bool {
err := client.Get(ctx, types.NamespacedName{Name: binding.Name, Namespace: binding.Namespace}, &topology.Binding{})
return apierrors.IsNotFound(err)
}, 5).Should(BeTrue())
Expect(observedEvents()).To(ContainElement("Normal SuccessfulDelete successfully deleted binding"))
})
})

When("the RabbitMQ Client successfully deletes a binding", func() {
BeforeEach(func() {
bindingName = "delete-binding-success"
fakeRabbitMQClient.DeleteBindingReturns(&http.Response{
Status: "204 No Content",
StatusCode: http.StatusNoContent,
}, nil)
})

It("raises an event to indicate a successful deletion", func() {
Expect(client.Delete(ctx, &binding)).To(Succeed())
Eventually(func() bool {
err := client.Get(ctx, types.NamespacedName{Name: binding.Name, Namespace: binding.Namespace}, &topology.Binding{})
return apierrors.IsNotFound(err)
}, 5).Should(BeTrue())
observedEvents := observedEvents()
Expect(observedEvents).NotTo(ContainElement("Warning FailedDelete failed to delete binding"))
Expect(observedEvents).To(ContainElement("Normal SuccessfulDelete successfully deleted binding"))
})
})
})
})
19 changes: 16 additions & 3 deletions controllers/controllers_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ package controllers_test

import (
"context"
"errors"
"path/filepath"
"testing"

Expand Down Expand Up @@ -40,9 +41,10 @@ var (
client runtimeClient.Client
clientSet *kubernetes.Clientset
ctx = context.Background()
fakeRabbitMQClient internalfakes.FakeRabbitMQClient
fakeRabbitMQClient *internalfakes.FakeRabbitMQClient
fakeRabbitMQClientError error
fakeRabbitMQClientFactory = func(ctx context.Context, c runtimeClient.Client, rmq topology.RabbitmqClusterReference, namespace string) (internal.RabbitMQClient, error) {
return &fakeRabbitMQClient, nil
return fakeRabbitMQClient, fakeRabbitMQClientError
}
fakeRecorder *record.FakeRecorder
)
Expand Down Expand Up @@ -135,7 +137,8 @@ var _ = BeforeSuite(func(done Done) {
}, 60)

var _ = BeforeEach(func() {
fakeRabbitMQClient = internalfakes.FakeRabbitMQClient{}
fakeRabbitMQClient = &internalfakes.FakeRabbitMQClient{}
fakeRabbitMQClientError = nil
})

var _ = AfterEach(func() {
Expand All @@ -157,3 +160,13 @@ func observedEvents() []string {
}
return events
}

func prepareClientError() {
fakeRabbitMQClient = nil
fakeRabbitMQClientError = errors.New("such a golang error")
}

func prepareNoSuchClusterError() {
fakeRabbitMQClient = nil
fakeRabbitMQClientError = internal.NoSuchRabbitmqClusterError
}
5 changes: 4 additions & 1 deletion controllers/exchange_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"context"
"encoding/json"
"errors"
"reflect"
"time"

"github.com/rabbitmq/messaging-topology-operator/internal"
Expand Down Expand Up @@ -142,8 +143,9 @@ func (r *ExchangeReconciler) addFinalizerIfNeeded(ctx context.Context, e *topolo
func (r *ExchangeReconciler) deleteExchange(ctx context.Context, client internal.RabbitMQClient, exchange *topology.Exchange) error {
logger := ctrl.LoggerFrom(ctx)

if client == nil {
if client == nil || reflect.ValueOf(client).IsNil() {
logger.Info(noSuchRabbitDeletion, "exchange", exchange.Name)
r.Recorder.Event(exchange, corev1.EventTypeNormal, "SuccessfulDelete", "successfully deleted exchange")
return r.removeFinalizer(ctx, exchange)
}

Expand All @@ -156,6 +158,7 @@ func (r *ExchangeReconciler) deleteExchange(ctx context.Context, client internal
logger.Error(err, msg, "exchange", exchange.Spec.Name)
return err
}
r.Recorder.Event(exchange, corev1.EventTypeNormal, "SuccessfulDelete", "successfully deleted exchange")
return r.removeFinalizer(ctx, exchange)
}

Expand Down
5 changes: 4 additions & 1 deletion controllers/permission_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"reflect"
"time"

"github.com/rabbitmq/messaging-topology-operator/internal"
Expand Down Expand Up @@ -122,8 +123,9 @@ func (r *PermissionReconciler) addFinalizerIfNeeded(ctx context.Context, permiss
func (r *PermissionReconciler) revokePermissions(ctx context.Context, client internal.RabbitMQClient, permission *topology.Permission) error {
logger := ctrl.LoggerFrom(ctx)

if client == nil {
if client == nil || reflect.ValueOf(client).IsNil() {
logger.Info(noSuchRabbitDeletion, "permission", permission.Name)
r.Recorder.Event(permission, corev1.EventTypeNormal, "SuccessfulDelete", "successfully deleted permission")
return r.removeFinalizer(ctx, permission)
}

Expand All @@ -136,6 +138,7 @@ func (r *PermissionReconciler) revokePermissions(ctx context.Context, client int
logger.Error(err, msg, "user", permission.Spec.User, "vhost", permission.Spec.Vhost)
return err
}
r.Recorder.Event(permission, corev1.EventTypeNormal, "SuccessfulDelete", "successfully deleted permission")
return r.removeFinalizer(ctx, permission)
}

Expand Down
5 changes: 4 additions & 1 deletion controllers/policy_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"context"
"encoding/json"
"errors"
"reflect"
"time"

"github.com/rabbitmq/messaging-topology-operator/internal"
Expand Down Expand Up @@ -142,8 +143,9 @@ func (r *PolicyReconciler) addFinalizerIfNeeded(ctx context.Context, policy *top
func (r *PolicyReconciler) deletePolicy(ctx context.Context, client internal.RabbitMQClient, policy *topology.Policy) error {
logger := ctrl.LoggerFrom(ctx)

if client == nil {
if client == nil || reflect.ValueOf(client).IsNil() {
logger.Info(noSuchRabbitDeletion, "policy", policy.Name)
r.Recorder.Event(policy, corev1.EventTypeNormal, "SuccessfulDelete", "successfully deleted policy")
return r.removeFinalizer(ctx, policy)
}

Expand All @@ -156,6 +158,7 @@ func (r *PolicyReconciler) deletePolicy(ctx context.Context, client internal.Rab
logger.Error(err, msg, "policy", policy.Spec.Name)
return err
}
r.Recorder.Event(policy, corev1.EventTypeNormal, "SuccessfulDelete", "successfully deleted policy")
return r.removeFinalizer(ctx, policy)
}

Expand Down
5 changes: 4 additions & 1 deletion controllers/queue_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"context"
"encoding/json"
"errors"
"reflect"
"time"

"github.com/go-logr/logr"
Expand Down Expand Up @@ -149,8 +150,9 @@ func (r *QueueReconciler) addFinalizerIfNeeded(ctx context.Context, q *topology.
func (r *QueueReconciler) deleteQueue(ctx context.Context, client internal.RabbitMQClient, q *topology.Queue) error {
logger := ctrl.LoggerFrom(ctx)

if client == nil {
if client == nil || reflect.ValueOf(client).IsNil() {
logger.Info(noSuchRabbitDeletion, "queue", q.Name)
r.Recorder.Event(q, corev1.EventTypeNormal, "SuccessfulDelete", "successfully deleted queue")
return r.removeFinalizer(ctx, q)
}

Expand All @@ -163,6 +165,7 @@ func (r *QueueReconciler) deleteQueue(ctx context.Context, client internal.Rabbi
logger.Error(err, msg, "queue", q.Spec.Name)
return err
}
r.Recorder.Event(q, corev1.EventTypeNormal, "SuccessfulDelete", "successfully deleted queue")
return r.removeFinalizer(ctx, q)
}

Expand Down
Loading

0 comments on commit d4ffeeb

Please sign in to comment.