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 }