diff --git a/operators/constellation-node-operator/controllers/pendingnode_controller_test.go b/operators/constellation-node-operator/controllers/pendingnode_controller_test.go new file mode 100644 index 000000000..a09d9367a --- /dev/null +++ b/operators/constellation-node-operator/controllers/pendingnode_controller_test.go @@ -0,0 +1,251 @@ +package controllers + +import ( + "context" + "errors" + "net/http" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + 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/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + updatev1alpha1 "github.com/edgelesssys/constellation/operators/constellation-node-operator/api/v1alpha1" +) + +func TestNodeStateChangePredicate(t *testing.T) { + updateTestCases := map[string]struct { + event event.UpdateEvent + wantProcessing bool + }{ + "old object is not a node": { + event: event.UpdateEvent{ + ObjectNew: &corev1.Node{}, + }, + }, + "new object is not a node": { + event: event.UpdateEvent{ + ObjectOld: &corev1.Node{}, + }, + }, + "status is unchanged": { + event: event.UpdateEvent{ + ObjectOld: &corev1.Node{}, + ObjectNew: &corev1.Node{}, + }, + }, + "node became ready": { + event: event.UpdateEvent{ + ObjectOld: &corev1.Node{ + Status: corev1.NodeStatus{ + Conditions: []corev1.NodeCondition{ + {Type: corev1.NodeReady, Status: corev1.ConditionFalse}, + }, + }, + }, + ObjectNew: &corev1.Node{ + Status: corev1.NodeStatus{ + Conditions: []corev1.NodeCondition{ + {Type: corev1.NodeReady, Status: corev1.ConditionTrue}, + }, + }, + }, + }, + wantProcessing: true, + }, + "node acquired provider id": { + event: event.UpdateEvent{ + ObjectOld: &corev1.Node{}, + ObjectNew: &corev1.Node{ + Spec: corev1.NodeSpec{ + ProviderID: "provider-id", + }, + }, + }, + wantProcessing: true, + }, + } + + for name, tc := range updateTestCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + predicate := nodeStateChangePredicate() + assert.Equal(tc.wantProcessing, predicate.Update(tc.event)) + }) + } + + t.Run("create", func(t *testing.T) { + assert := assert.New(t) + predicate := nodeStateChangePredicate() + assert.True(predicate.Create(event.CreateEvent{})) + }) + + t.Run("delete", func(t *testing.T) { + assert := assert.New(t) + predicate := nodeStateChangePredicate() + assert.True(predicate.Delete(event.DeleteEvent{})) + }) + + t.Run("generic", func(t *testing.T) { + assert := assert.New(t) + predicate := nodeStateChangePredicate() + assert.False(predicate.Generic(event.GenericEvent{})) + }) +} + +func TestFindObjectsForNode(t *testing.T) { + testCases := map[string]struct { + pendingNode client.Object + listPendingNodesErr error + wantRequests []reconcile.Request + }{ + "getting the corresponding pending nodes fails": { + listPendingNodesErr: errors.New("get-pending-nodes-err"), + }, + "pending nodes reconcile request is returned": { + pendingNode: &updatev1alpha1.PendingNode{ + ObjectMeta: metav1.ObjectMeta{Name: "pending-node"}, + }, + wantRequests: []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Name: "pending-node", + }, + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + reconciler := PendingNodeReconciler{ + Client: newStubReaderClient(t, []runtime.Object{tc.pendingNode}, nil, tc.listPendingNodesErr), + } + requests := reconciler.findObjectsForNode(&corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pending-node", + }, + }) + assert.ElementsMatch(tc.wantRequests, requests) + }) + } +} + +func TestReachedGoal(t *testing.T) { + testCases := map[string]struct { + pendingNode updatev1alpha1.PendingNode + nodeState updatev1alpha1.CSPNodeState + getPendingNodeErr error + wantErr bool + wantGoalReached bool + }{ + "join: getting the corresponding k8s node fails": { + pendingNode: updatev1alpha1.PendingNode{ + ObjectMeta: metav1.ObjectMeta{Name: "pending-node"}, + Spec: updatev1alpha1.PendingNodeSpec{Goal: updatev1alpha1.NodeGoalJoin}, + }, + nodeState: updatev1alpha1.NodeStateReady, + getPendingNodeErr: errors.New("get-pending-node-err"), + wantErr: true, + }, + "join: node not found": { + pendingNode: updatev1alpha1.PendingNode{ + ObjectMeta: metav1.ObjectMeta{Name: "pending-node"}, + Spec: updatev1alpha1.PendingNodeSpec{Goal: updatev1alpha1.NodeGoalJoin}, + }, + nodeState: updatev1alpha1.NodeStateReady, + getPendingNodeErr: &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Status: "Failure", + Reason: "NotFound", + Code: http.StatusNotFound, + }, + }, + }, + "join: csp states node is not ready": { + pendingNode: updatev1alpha1.PendingNode{ + ObjectMeta: metav1.ObjectMeta{Name: "pending-node"}, + Spec: updatev1alpha1.PendingNodeSpec{Goal: updatev1alpha1.NodeGoalJoin}, + }, + nodeState: updatev1alpha1.NodeStateFailed, + }, + "join: node joined": { + pendingNode: updatev1alpha1.PendingNode{ + ObjectMeta: metav1.ObjectMeta{Name: "pending-node"}, + Spec: updatev1alpha1.PendingNodeSpec{Goal: updatev1alpha1.NodeGoalJoin}, + }, + nodeState: updatev1alpha1.NodeStateReady, + wantGoalReached: true, + }, + "leave: node still exists": { + pendingNode: updatev1alpha1.PendingNode{ + ObjectMeta: metav1.ObjectMeta{Name: "pending-node"}, + Spec: updatev1alpha1.PendingNodeSpec{Goal: updatev1alpha1.NodeGoalLeave}, + }, + nodeState: updatev1alpha1.NodeStateReady, + }, + "leave: node terminated": { + pendingNode: updatev1alpha1.PendingNode{ + ObjectMeta: metav1.ObjectMeta{Name: "pending-node"}, + Spec: updatev1alpha1.PendingNodeSpec{Goal: updatev1alpha1.NodeGoalLeave}, + }, + nodeState: updatev1alpha1.NodeStateTerminated, + wantGoalReached: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + reconciler := PendingNodeReconciler{ + Client: newStubReaderClient(t, []runtime.Object{&tc.pendingNode}, tc.getPendingNodeErr, nil), + } + reachedGoal, err := reconciler.reachedGoal(context.Background(), tc.pendingNode, tc.nodeState) + if tc.wantErr { + assert.Error(err) + return + } + require.NoError(err) + assert.Equal(tc.wantGoalReached, reachedGoal) + }) + } +} + +type stubNodeStateGetter struct { + sync.RWMutex + nodeState updatev1alpha1.CSPNodeState + nodeStateErr error + deleteNodeErr error +} + +func (g *stubNodeStateGetter) GetNodeState(ctx context.Context, providerID string) (updatev1alpha1.CSPNodeState, error) { + g.RLock() + defer g.RUnlock() + return g.nodeState, g.nodeStateErr +} + +func (g *stubNodeStateGetter) DeleteNode(ctx context.Context, providerID string) error { + g.RLock() + defer g.RUnlock() + return g.deleteNodeErr +} + +// thread safe methods to update the stub while in use + +func (g *stubNodeStateGetter) setNodeState(nodeState updatev1alpha1.CSPNodeState) { + g.Lock() + defer g.Unlock() + g.nodeState = nodeState +} diff --git a/operators/constellation-node-operator/go.mod b/operators/constellation-node-operator/go.mod index 797a368c7..cf67b2062 100644 --- a/operators/constellation-node-operator/go.mod +++ b/operators/constellation-node-operator/go.mod @@ -6,6 +6,7 @@ require ( github.com/medik8s/node-maintenance-operator v0.13.0 github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.18.1 + github.com/stretchr/testify v1.7.0 k8s.io/api v0.24.0 k8s.io/apimachinery v0.24.0 k8s.io/client-go v0.24.0 @@ -52,6 +53,7 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nxadm/tail v1.4.8 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.12.1 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.32.1 // indirect