constellation/operators/constellation-node-operator/controllers/pendingnode_controller_test.go
2022-09-22 09:10:19 +02:00

258 lines
6.9 KiB
Go

/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
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/v2/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
}