github.com/verrazzano/verrazzano@v1.7.0/application-operator/mcagent/mcagent_syncer_test.go (about)

     1  // Copyright (c) 2021, 2023, Oracle and/or its affiliates.
     2  // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
     3  
     4  package mcagent
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"reflect"
    10  	"strings"
    11  	"testing"
    12  	"time"
    13  
    14  	"sigs.k8s.io/controller-runtime/pkg/client"
    15  
    16  	"github.com/golang/mock/gomock"
    17  	asserts "github.com/stretchr/testify/assert"
    18  	"github.com/verrazzano/verrazzano/application-operator/apis/clusters/v1alpha1"
    19  	"github.com/verrazzano/verrazzano/application-operator/controllers/clusters"
    20  	"github.com/verrazzano/verrazzano/application-operator/mocks"
    21  	"go.uber.org/zap"
    22  	corev1 "k8s.io/api/core/v1"
    23  	"k8s.io/apimachinery/pkg/api/errors"
    24  	"k8s.io/apimachinery/pkg/runtime/schema"
    25  	"k8s.io/apimachinery/pkg/types"
    26  )
    27  
    28  const testMCAppConfigNamespace = "unit-mcappconfig-namespace"
    29  
    30  // TestSyncer_isThisCluster tests the isThisCluster method of Syncer
    31  func TestSyncer_isThisCluster(t *testing.T) {
    32  	tests := []struct {
    33  		name               string
    34  		managedClusterName string
    35  		placement          v1alpha1.Placement
    36  		want               bool
    37  	}{
    38  		{"same cluster single placement", "mycluster1", v1alpha1.Placement{Clusters: []v1alpha1.Cluster{{Name: "mycluster1"}}}, true},
    39  		{"same cluster multi-placement", "mycluster1", v1alpha1.Placement{Clusters: []v1alpha1.Cluster{{Name: "othercluster"}, {Name: "mycluster1"}}}, true},
    40  		{"different cluster single placement", "mycluster1", v1alpha1.Placement{Clusters: []v1alpha1.Cluster{{Name: "othercluster"}}}, false},
    41  		{"different cluster multi-placement", "mycluster1", v1alpha1.Placement{Clusters: []v1alpha1.Cluster{{Name: "othercluster"}, {Name: "mycluster2"}}}, false},
    42  	}
    43  	for _, tt := range tests {
    44  		t.Run(tt.name, func(t *testing.T) {
    45  			s := &Syncer{
    46  				ManagedClusterName: tt.managedClusterName,
    47  			}
    48  			if got := s.isThisCluster(tt.placement); got != tt.want {
    49  				t.Errorf("isThisCluster() = %v, want %v", got, tt.want)
    50  			}
    51  		})
    52  	}
    53  }
    54  
    55  // TestSyncer_processStatusUpdates tests the processStatusUpdates method of Syncer
    56  // GIVEN a syncer object created with a status updates channel
    57  // WHEN processStatusUpdates is called
    58  // THEN for every message written to the channel, a corresponding status update to admin cluster
    59  // is generated
    60  func TestSyncer_processStatusUpdates(t *testing.T) {
    61  	// Admin cluster mocks
    62  	adminMocker := gomock.NewController(t)
    63  	adminMock := mocks.NewMockClient(adminMocker)
    64  	statusMocker := gomock.NewController(t)
    65  	statusMock := mocks.NewMockClient(statusMocker)
    66  
    67  	statusUpdatesChan := make(chan clusters.StatusUpdateMessage, 5)
    68  
    69  	// write some messages to the status update channel for the agent to make sure
    70  	// they get discarded when there is no admin cluster to connect to
    71  	// write some messages to the status update channel for the agent to make sure
    72  	// they get discarded when there is no admin cluster to connect to
    73  	statusUpdates := makeStatusUpdateMessages()
    74  	for _, update := range statusUpdates {
    75  		statusUpdatesChan <- update
    76  	}
    77  
    78  	// Expect every status update that is in the statusUpdates array to be sent
    79  	// to the admin cluster
    80  	adminMock.EXPECT().Status().Times(len(statusUpdates)).Return(statusMock)
    81  	var updateMsgSecret *v1alpha1.MultiClusterSecret
    82  	var updateMsgAppConf *v1alpha1.MultiClusterApplicationConfiguration
    83  	for _, updateMsg := range statusUpdates {
    84  		// expect a GET on one multi cluster secret and one multicluster app config
    85  		if strings.Contains(reflect.TypeOf(updateMsg.Resource).String(), "MultiClusterSecret") {
    86  			updateMsgSecret = updateMsg.Resource.(*v1alpha1.MultiClusterSecret)
    87  		} else {
    88  			updateMsgAppConf = updateMsg.Resource.(*v1alpha1.MultiClusterApplicationConfiguration)
    89  		}
    90  	}
    91  
    92  	secretName := types.NamespacedName{Namespace: updateMsgSecret.GetNamespace(), Name: updateMsgSecret.GetName()}
    93  	appName := types.NamespacedName{Namespace: updateMsgAppConf.GetNamespace(), Name: updateMsgAppConf.GetName()}
    94  	adminMock.EXPECT().
    95  		Get(gomock.Any(), secretName, gomock.AssignableToTypeOf(&v1alpha1.MultiClusterSecret{}), gomock.Any()).
    96  		DoAndReturn(func(ctx context.Context, name types.NamespacedName, mcSecret *v1alpha1.MultiClusterSecret, opts ...client.GetOption) error {
    97  			asserts.Equal(t, secretName, name)
    98  			updateMsgSecret.DeepCopyInto(mcSecret)
    99  			return nil
   100  		})
   101  	adminMock.EXPECT().
   102  		Get(gomock.Any(), appName, gomock.Any(), gomock.Any()).
   103  		DoAndReturn(func(ctx context.Context, name types.NamespacedName, mcAppConf *v1alpha1.MultiClusterApplicationConfiguration, opts ...client.GetOption) error {
   104  			asserts.Equal(t, appName, name)
   105  			updateMsgAppConf.DeepCopyInto(mcAppConf)
   106  			return nil
   107  		})
   108  
   109  	statusMock.EXPECT().Update(gomock.Any(), gomock.AssignableToTypeOf(&v1alpha1.MultiClusterSecret{}))
   110  	statusMock.EXPECT().Update(gomock.Any(), gomock.AssignableToTypeOf(&v1alpha1.MultiClusterApplicationConfiguration{}))
   111  
   112  	// Make the request
   113  	s := &Syncer{
   114  		Context:             context.TODO(),
   115  		AdminClient:         adminMock,
   116  		ManagedClusterName:  "mycluster1",
   117  		StatusUpdateChannel: statusUpdatesChan,
   118  		Log:                 zap.S().With("statusUpdateUnitTest"),
   119  	}
   120  	s.processStatusUpdates()
   121  
   122  	statusMocker.Finish()
   123  	adminMocker.Finish()
   124  }
   125  
   126  // TestSyncer_processStatusUpdates_RetriesOnConflict tests whether the processStatusUpdates
   127  // method of Syncer retries status updates when a conflict is returned
   128  // GIVEN a syncer object created with a status updates channel
   129  // WHEN processStatusUpdates is called
   130  // THEN for every status update sent to admin cluster that fails with Conflict, there are
   131  // retryCount retries
   132  func TestSyncer_processStatusUpdates_RetriesOnConflict(t *testing.T) {
   133  	// Admin cluster mocks
   134  	adminMocker := gomock.NewController(t)
   135  	adminMock := mocks.NewMockClient(adminMocker)
   136  	statusMocker := gomock.NewController(t)
   137  	statusMock := mocks.NewMockClient(statusMocker)
   138  
   139  	// Reduce the retry delay to make the test faster
   140  	retryDelay = 500 * time.Millisecond
   141  
   142  	statusUpdatesChan := make(chan clusters.StatusUpdateMessage, 5)
   143  
   144  	// write some messages to the status update channel for the agent to make sure
   145  	// they get discarded when there is no admin cluster to connect to
   146  	// write some messages to the status update channel for the agent to make sure
   147  	// they get discarded when there is no admin cluster to connect to
   148  	statusUpdates := makeStatusUpdateMessages()
   149  	for _, update := range statusUpdates {
   150  		statusUpdatesChan <- update
   151  	}
   152  
   153  	// Expect every status update that is in the statusUpdates array to be sent
   154  	// to the admin cluster (and retried retryCount times)
   155  	adminMock.EXPECT().Status().Times(len(statusUpdates) * retryCount).Return(statusMock)
   156  	var updateMsgSecret *v1alpha1.MultiClusterSecret
   157  	var updateMsgAppConf *v1alpha1.MultiClusterApplicationConfiguration
   158  	for _, updateMsg := range statusUpdates {
   159  		// expect a GET on one multi cluster secret and one multicluster app config
   160  		if strings.Contains(reflect.TypeOf(updateMsg.Resource).String(), "MultiClusterSecret") {
   161  			updateMsgSecret = updateMsg.Resource.(*v1alpha1.MultiClusterSecret)
   162  		} else {
   163  			updateMsgAppConf = updateMsg.Resource.(*v1alpha1.MultiClusterApplicationConfiguration)
   164  		}
   165  	}
   166  
   167  	secretName := types.NamespacedName{Namespace: updateMsgSecret.GetNamespace(), Name: updateMsgSecret.GetName()}
   168  	appName := types.NamespacedName{Namespace: updateMsgAppConf.GetNamespace(), Name: updateMsgAppConf.GetName()}
   169  	adminMock.EXPECT().
   170  		Get(gomock.Any(), secretName, gomock.AssignableToTypeOf(&v1alpha1.MultiClusterSecret{}), gomock.Any()).
   171  		Times(retryCount).
   172  		DoAndReturn(func(ctx context.Context, name types.NamespacedName, mcSecret *v1alpha1.MultiClusterSecret, opts ...client.GetOption) error {
   173  			asserts.Equal(t, secretName, name)
   174  			updateMsgSecret.DeepCopyInto(mcSecret)
   175  			return nil
   176  		})
   177  	adminMock.EXPECT().
   178  		Get(gomock.Any(), appName, gomock.Any(), gomock.Any()).
   179  		Times(retryCount).
   180  		DoAndReturn(func(ctx context.Context, name types.NamespacedName, mcAppConf *v1alpha1.MultiClusterApplicationConfiguration, opts ...client.GetOption) error {
   181  			asserts.Equal(t, appName, name)
   182  			updateMsgAppConf.DeepCopyInto(mcAppConf)
   183  			return nil
   184  		})
   185  
   186  	conflictErr := errors.NewConflict(schema.GroupResource{Group: "", Resource: ""}, "someResName", fmt.Errorf("Some error"))
   187  	statusMock.EXPECT().
   188  		Update(gomock.Any(), gomock.AssignableToTypeOf(&v1alpha1.MultiClusterSecret{})).
   189  		Times(retryCount).
   190  		Return(conflictErr)
   191  	statusMock.EXPECT().
   192  		Update(gomock.Any(), gomock.AssignableToTypeOf(&v1alpha1.MultiClusterApplicationConfiguration{})).
   193  		Times(retryCount).
   194  		Return(conflictErr)
   195  
   196  	// Make the request
   197  	s := &Syncer{
   198  		Context:             context.TODO(),
   199  		AdminClient:         adminMock,
   200  		ManagedClusterName:  "mycluster1",
   201  		StatusUpdateChannel: statusUpdatesChan,
   202  		Log:                 zap.S().With("statusUpdateUnitTest"),
   203  	}
   204  	s.processStatusUpdates()
   205  
   206  	statusMocker.Finish()
   207  	adminMocker.Finish()
   208  }
   209  
   210  func makeStatusUpdateMessages() []clusters.StatusUpdateMessage {
   211  	secret := v1alpha1.MultiClusterSecret{}
   212  	secret.Name = "somesecret"
   213  	secret.Namespace = "somens"
   214  
   215  	appConfig := v1alpha1.MultiClusterApplicationConfiguration{}
   216  	appConfig.Name = "someappconf"
   217  	appConfig.Namespace = "appconfns"
   218  	msg1 := clusters.StatusUpdateMessage{
   219  		NewCondition:     v1alpha1.Condition{Type: v1alpha1.DeployFailed, Status: corev1.ConditionTrue, Message: "my msg 1"},
   220  		NewClusterStatus: v1alpha1.ClusterLevelStatus{Name: "cluster1", State: v1alpha1.Failed},
   221  		Resource:         &secret,
   222  	}
   223  	msg2 := clusters.StatusUpdateMessage{
   224  		NewCondition:     v1alpha1.Condition{Type: v1alpha1.DeployComplete, Status: corev1.ConditionTrue, Message: "my msg 2"},
   225  		NewClusterStatus: v1alpha1.ClusterLevelStatus{Name: "cluster1", State: v1alpha1.Succeeded},
   226  		Resource:         &appConfig,
   227  	}
   228  	return []clusters.StatusUpdateMessage{msg1, msg2}
   229  }