github.com/argoproj/argo-cd/v3@v3.2.1/controller/sharding/sharding_test.go (about) 1 package sharding 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "os" 8 "strconv" 9 "testing" 10 "time" 11 12 "github.com/stretchr/testify/assert" 13 "github.com/stretchr/testify/mock" 14 "github.com/stretchr/testify/require" 15 appsv1 "k8s.io/api/apps/v1" 16 corev1 "k8s.io/api/core/v1" 17 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 "k8s.io/apimachinery/pkg/runtime" 19 kubefake "k8s.io/client-go/kubernetes/fake" 20 "sigs.k8s.io/yaml" 21 22 "github.com/argoproj/argo-cd/v3/common" 23 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" 24 dbmocks "github.com/argoproj/argo-cd/v3/util/db/mocks" 25 "github.com/argoproj/argo-cd/v3/util/settings" 26 ) 27 28 func TestGetShardByID_NotEmptyID(t *testing.T) { 29 db := &dbmocks.ArgoDB{} 30 replicasCount := 1 31 db.On("GetApplicationControllerReplicas").Return(replicasCount) 32 assert.Equal(t, 0, LegacyDistributionFunction(replicasCount)(&v1alpha1.Cluster{ID: "1"})) 33 assert.Equal(t, 0, LegacyDistributionFunction(replicasCount)(&v1alpha1.Cluster{ID: "2"})) 34 assert.Equal(t, 0, LegacyDistributionFunction(replicasCount)(&v1alpha1.Cluster{ID: "3"})) 35 assert.Equal(t, 0, LegacyDistributionFunction(replicasCount)(&v1alpha1.Cluster{ID: "4"})) 36 } 37 38 func TestGetShardByID_EmptyID(t *testing.T) { 39 db := &dbmocks.ArgoDB{} 40 replicasCount := 1 41 db.On("GetApplicationControllerReplicas").Return(replicasCount) 42 distributionFunction := LegacyDistributionFunction 43 shard := distributionFunction(replicasCount)(&v1alpha1.Cluster{}) 44 assert.Equal(t, 0, shard) 45 } 46 47 func TestGetShardByID_NoReplicas(t *testing.T) { 48 db := &dbmocks.ArgoDB{} 49 db.On("GetApplicationControllerReplicas").Return(0) 50 distributionFunction := LegacyDistributionFunction 51 shard := distributionFunction(0)(&v1alpha1.Cluster{}) 52 assert.Equal(t, -1, shard) 53 } 54 55 func TestGetShardByID_NoReplicasUsingHashDistributionFunction(t *testing.T) { 56 db := &dbmocks.ArgoDB{} 57 db.On("GetApplicationControllerReplicas").Return(0) 58 distributionFunction := LegacyDistributionFunction 59 shard := distributionFunction(0)(&v1alpha1.Cluster{}) 60 assert.Equal(t, -1, shard) 61 } 62 63 func TestGetShardByID_NoReplicasUsingHashDistributionFunctionWithClusters(t *testing.T) { 64 clusters, db, cluster1, cluster2, cluster3, cluster4, cluster5 := createTestClusters() 65 // Test with replicas set to 0 66 db.On("GetApplicationControllerReplicas").Return(0) 67 t.Setenv(common.EnvControllerShardingAlgorithm, common.RoundRobinShardingAlgorithm) 68 distributionFunction := RoundRobinDistributionFunction(clusters, 0) 69 assert.Equal(t, -1, distributionFunction(nil)) 70 assert.Equal(t, -1, distributionFunction(&cluster1)) 71 assert.Equal(t, -1, distributionFunction(&cluster2)) 72 assert.Equal(t, -1, distributionFunction(&cluster3)) 73 assert.Equal(t, -1, distributionFunction(&cluster4)) 74 assert.Equal(t, -1, distributionFunction(&cluster5)) 75 } 76 77 func TestGetClusterFilterDefault(t *testing.T) { 78 // shardIndex := 1 // ensuring that a shard with index 1 will process all the clusters with an "even" id (2,4,6,...) 79 clusterAccessor, _, cluster1, cluster2, cluster3, cluster4, _ := createTestClusters() 80 os.Unsetenv(common.EnvControllerShardingAlgorithm) 81 replicasCount := 2 82 distributionFunction := RoundRobinDistributionFunction(clusterAccessor, replicasCount) 83 assert.Equal(t, 0, distributionFunction(nil)) 84 assert.Equal(t, 0, distributionFunction(&cluster1)) 85 assert.Equal(t, 1, distributionFunction(&cluster2)) 86 assert.Equal(t, 0, distributionFunction(&cluster3)) 87 assert.Equal(t, 1, distributionFunction(&cluster4)) 88 } 89 90 func TestGetClusterFilterLegacy(t *testing.T) { 91 // shardIndex := 1 // ensuring that a shard with index 1 will process all the clusters with an "even" id (2,4,6,...) 92 clusterAccessor, db, cluster1, cluster2, cluster3, cluster4, _ := createTestClusters() 93 replicasCount := 2 94 db.On("GetApplicationControllerReplicas").Return(replicasCount) 95 t.Setenv(common.EnvControllerShardingAlgorithm, common.LegacyShardingAlgorithm) 96 distributionFunction := RoundRobinDistributionFunction(clusterAccessor, replicasCount) 97 assert.Equal(t, 0, distributionFunction(nil)) 98 assert.Equal(t, 0, distributionFunction(&cluster1)) 99 assert.Equal(t, 1, distributionFunction(&cluster2)) 100 assert.Equal(t, 0, distributionFunction(&cluster3)) 101 assert.Equal(t, 1, distributionFunction(&cluster4)) 102 } 103 104 func TestGetClusterFilterUnknown(t *testing.T) { 105 clusterAccessor, db, cluster1, cluster2, cluster3, cluster4, _ := createTestClusters() 106 appAccessor, _, _, _, _, _ := createTestApps() 107 // Test with replicas set to 0 108 t.Setenv(common.EnvControllerReplicas, "2") 109 os.Unsetenv(common.EnvControllerShardingAlgorithm) 110 t.Setenv(common.EnvControllerShardingAlgorithm, "unknown") 111 replicasCount := 2 112 db.On("GetApplicationControllerReplicas").Return(replicasCount) 113 distributionFunction := GetDistributionFunction(clusterAccessor, appAccessor, "unknown", replicasCount) 114 assert.Equal(t, 0, distributionFunction(nil)) 115 assert.Equal(t, 0, distributionFunction(&cluster1)) 116 assert.Equal(t, 1, distributionFunction(&cluster2)) 117 assert.Equal(t, 0, distributionFunction(&cluster3)) 118 assert.Equal(t, 1, distributionFunction(&cluster4)) 119 } 120 121 func TestLegacyGetClusterFilterWithFixedShard(t *testing.T) { 122 // shardIndex := 1 // ensuring that a shard with index 1 will process all the clusters with an "even" id (2,4,6,...) 123 t.Setenv(common.EnvControllerReplicas, "5") 124 clusterAccessor, db, cluster1, cluster2, cluster3, cluster4, _ := createTestClusters() 125 appAccessor, _, _, _, _, _ := createTestApps() 126 replicasCount := 5 127 db.On("GetApplicationControllerReplicas").Return(replicasCount) 128 filter := GetDistributionFunction(clusterAccessor, appAccessor, common.DefaultShardingAlgorithm, replicasCount) 129 assert.Equal(t, 0, filter(nil)) 130 assert.Equal(t, 4, filter(&cluster1)) 131 assert.Equal(t, 1, filter(&cluster2)) 132 assert.Equal(t, 2, filter(&cluster3)) 133 assert.Equal(t, 2, filter(&cluster4)) 134 135 var fixedShard int64 = 4 136 cluster5 := &v1alpha1.Cluster{ID: "5", Shard: &fixedShard} 137 clusterAccessor = getClusterAccessor([]v1alpha1.Cluster{cluster1, cluster2, cluster2, cluster4, *cluster5}) 138 filter = GetDistributionFunction(clusterAccessor, appAccessor, common.DefaultShardingAlgorithm, replicasCount) 139 assert.Equal(t, int(fixedShard), filter(cluster5)) 140 141 fixedShard = 1 142 cluster5.Shard = &fixedShard 143 clusterAccessor = getClusterAccessor([]v1alpha1.Cluster{cluster1, cluster2, cluster2, cluster4, *cluster5}) 144 filter = GetDistributionFunction(clusterAccessor, appAccessor, common.DefaultShardingAlgorithm, replicasCount) 145 assert.Equal(t, int(fixedShard), filter(&v1alpha1.Cluster{ID: "4", Shard: &fixedShard})) 146 } 147 148 func TestRoundRobinGetClusterFilterWithFixedShard(t *testing.T) { 149 // shardIndex := 1 // ensuring that a shard with index 1 will process all the clusters with an "even" id (2,4,6,...) 150 t.Setenv(common.EnvControllerReplicas, "4") 151 clusterAccessor, db, cluster1, cluster2, cluster3, cluster4, _ := createTestClusters() 152 appAccessor, _, _, _, _, _ := createTestApps() 153 replicasCount := 4 154 db.On("GetApplicationControllerReplicas").Return(replicasCount) 155 156 filter := GetDistributionFunction(clusterAccessor, appAccessor, common.RoundRobinShardingAlgorithm, replicasCount) 157 assert.Equal(t, 0, filter(nil)) 158 assert.Equal(t, 0, filter(&cluster1)) 159 assert.Equal(t, 1, filter(&cluster2)) 160 assert.Equal(t, 2, filter(&cluster3)) 161 assert.Equal(t, 3, filter(&cluster4)) 162 163 // a cluster with a fixed shard should be processed by the specified exact 164 // same shard unless the specified shard index is greater than the number of replicas. 165 var fixedShard int64 = 1 166 cluster5 := v1alpha1.Cluster{Name: "cluster5", ID: "5", Shard: &fixedShard} 167 clusters := []v1alpha1.Cluster{cluster1, cluster2, cluster3, cluster4, cluster5} 168 clusterAccessor = getClusterAccessor(clusters) 169 filter = GetDistributionFunction(clusterAccessor, appAccessor, common.RoundRobinShardingAlgorithm, replicasCount) 170 assert.Equal(t, int(fixedShard), filter(&cluster5)) 171 172 fixedShard = 1 173 cluster5 = v1alpha1.Cluster{Name: "cluster5", ID: "5", Shard: &fixedShard} 174 clusters = []v1alpha1.Cluster{cluster1, cluster2, cluster3, cluster4, cluster5} 175 clusterAccessor = getClusterAccessor(clusters) 176 filter = GetDistributionFunction(clusterAccessor, appAccessor, common.RoundRobinShardingAlgorithm, replicasCount) 177 assert.Equal(t, int(fixedShard), filter(&v1alpha1.Cluster{Name: "cluster4", ID: "4", Shard: &fixedShard})) 178 } 179 180 func TestGetShardByIndexModuloReplicasCountDistributionFunction2(t *testing.T) { 181 clusters, db, cluster1, cluster2, cluster3, cluster4, cluster5 := createTestClusters() 182 183 t.Run("replicas set to 1", func(t *testing.T) { 184 replicasCount := 1 185 db.On("GetApplicationControllerReplicas").Return(replicasCount).Once() 186 distributionFunction := RoundRobinDistributionFunction(clusters, replicasCount) 187 assert.Equal(t, 0, distributionFunction(nil)) 188 assert.Equal(t, 0, distributionFunction(&cluster1)) 189 assert.Equal(t, 0, distributionFunction(&cluster2)) 190 assert.Equal(t, 0, distributionFunction(&cluster3)) 191 assert.Equal(t, 0, distributionFunction(&cluster4)) 192 assert.Equal(t, 0, distributionFunction(&cluster5)) 193 }) 194 195 t.Run("replicas set to 2", func(t *testing.T) { 196 replicasCount := 2 197 db.On("GetApplicationControllerReplicas").Return(replicasCount).Once() 198 distributionFunction := RoundRobinDistributionFunction(clusters, replicasCount) 199 assert.Equal(t, 0, distributionFunction(nil)) 200 assert.Equal(t, 0, distributionFunction(&cluster1)) 201 assert.Equal(t, 1, distributionFunction(&cluster2)) 202 assert.Equal(t, 0, distributionFunction(&cluster3)) 203 assert.Equal(t, 1, distributionFunction(&cluster4)) 204 assert.Equal(t, 0, distributionFunction(&cluster5)) 205 }) 206 207 t.Run("replicas set to 3", func(t *testing.T) { 208 replicasCount := 3 209 db.On("GetApplicationControllerReplicas").Return(replicasCount).Once() 210 distributionFunction := RoundRobinDistributionFunction(clusters, replicasCount) 211 assert.Equal(t, 0, distributionFunction(nil)) 212 assert.Equal(t, 0, distributionFunction(&cluster1)) 213 assert.Equal(t, 1, distributionFunction(&cluster2)) 214 assert.Equal(t, 2, distributionFunction(&cluster3)) 215 assert.Equal(t, 0, distributionFunction(&cluster4)) 216 assert.Equal(t, 1, distributionFunction(&cluster5)) 217 }) 218 } 219 220 func TestGetShardByIndexModuloReplicasCountDistributionFunctionWhenClusterNumberIsHigh(t *testing.T) { 221 // Unit test written to evaluate the cost of calling db.ListCluster on every call of distributionFunction 222 // Doing that allows to accept added and removed clusters on the fly. 223 // Initial tests where showing that under 1024 clusters, execution time was around 400ms 224 // and for 4096 clusters, execution time was under 9s 225 // The other implementation was giving almost linear time of 400ms up to 10'000 clusters 226 clusterPointers := []*v1alpha1.Cluster{} 227 for i := 0; i < 2048; i++ { 228 cluster := createCluster(fmt.Sprintf("cluster-%d", i), strconv.Itoa(i)) 229 clusterPointers = append(clusterPointers, &cluster) 230 } 231 replicasCount := 2 232 t.Setenv(common.EnvControllerReplicas, strconv.Itoa(replicasCount)) 233 _, db, _, _, _, _, _ := createTestClusters() 234 clusterAccessor := func() []*v1alpha1.Cluster { return clusterPointers } 235 db.On("GetApplicationControllerReplicas").Return(replicasCount) 236 distributionFunction := RoundRobinDistributionFunction(clusterAccessor, replicasCount) 237 for i, c := range clusterPointers { 238 assert.Equal(t, i%2, distributionFunction(c)) 239 } 240 } 241 242 func TestGetShardByIndexModuloReplicasCountDistributionFunctionWhenClusterIsAddedAndRemoved(t *testing.T) { 243 db := dbmocks.ArgoDB{} 244 cluster1 := createCluster("cluster1", "1") 245 cluster2 := createCluster("cluster2", "2") 246 cluster3 := createCluster("cluster3", "3") 247 cluster4 := createCluster("cluster4", "4") 248 cluster5 := createCluster("cluster5", "5") 249 cluster6 := createCluster("cluster6", "6") 250 251 clusters := []v1alpha1.Cluster{cluster1, cluster2, cluster3, cluster4, cluster5} 252 clusterAccessor := getClusterAccessor(clusters) 253 254 clusterList := &v1alpha1.ClusterList{Items: []v1alpha1.Cluster{cluster1, cluster2, cluster3, cluster4, cluster5}} 255 db.On("ListClusters", mock.Anything).Return(clusterList, nil) 256 // Test with replicas set to 2 257 replicasCount := 2 258 db.On("GetApplicationControllerReplicas").Return(replicasCount) 259 distributionFunction := RoundRobinDistributionFunction(clusterAccessor, replicasCount) 260 assert.Equal(t, 0, distributionFunction(nil)) 261 assert.Equal(t, 0, distributionFunction(&cluster1)) 262 assert.Equal(t, 1, distributionFunction(&cluster2)) 263 assert.Equal(t, 0, distributionFunction(&cluster3)) 264 assert.Equal(t, 1, distributionFunction(&cluster4)) 265 assert.Equal(t, 0, distributionFunction(&cluster5)) 266 assert.Equal(t, -1, distributionFunction(&cluster6)) // as cluster6 is not in the DB, this one should not have a shard assigned 267 268 // Now, the database knows cluster6. Shard should be assigned a proper shard 269 clusterList.Items = append(clusterList.Items, cluster6) 270 distributionFunction = RoundRobinDistributionFunction(getClusterAccessor(clusterList.Items), replicasCount) 271 assert.Equal(t, 1, distributionFunction(&cluster6)) 272 273 // Now, we remove the last added cluster, it should be unassigned as well 274 clusterList.Items = clusterList.Items[:len(clusterList.Items)-1] 275 distributionFunction = RoundRobinDistributionFunction(getClusterAccessor(clusterList.Items), replicasCount) 276 assert.Equal(t, -1, distributionFunction(&cluster6)) 277 } 278 279 func TestConsistentHashingWhenClusterIsAddedAndRemoved(t *testing.T) { 280 db := dbmocks.ArgoDB{} 281 clusterCount := 133 282 prefix := "cluster" 283 284 clusters := []v1alpha1.Cluster{} 285 for i := 0; i < clusterCount; i++ { 286 id := fmt.Sprintf("%06d", i) 287 cluster := fmt.Sprintf("%s-%s", prefix, id) 288 clusters = append(clusters, createCluster(cluster, id)) 289 } 290 clusterAccessor := getClusterAccessor(clusters) 291 appAccessor, _, _, _, _, _ := createTestApps() 292 clusterList := &v1alpha1.ClusterList{Items: clusters} 293 db.On("ListClusters", mock.Anything).Return(clusterList, nil) 294 // Test with replicas set to 3 295 replicasCount := 3 296 db.On("GetApplicationControllerReplicas").Return(replicasCount) 297 distributionFunction := ConsistentHashingWithBoundedLoadsDistributionFunction(clusterAccessor, appAccessor, replicasCount) 298 assert.Equal(t, 0, distributionFunction(nil)) 299 distributionMap := map[int]int{} 300 assignementMap := map[string]int{} 301 for i := 0; i < clusterCount; i++ { 302 assignedShard := distributionFunction(&clusters[i]) 303 assignementMap[clusters[i].ID] = assignedShard 304 distributionMap[assignedShard]++ 305 } 306 307 // We check that the distribution does not differ for more than 20% 308 var sum float64 309 sum = 0 310 for shard, count := range distributionMap { 311 if shard != -1 { 312 sum = (sum + float64(count)) 313 } 314 } 315 average := sum / float64(replicasCount) 316 failedTests := false 317 for shard, count := range distributionMap { 318 if shard != -1 { 319 if float64(count) > average*float64(1.1) || float64(count) < average*float64(0.9) { 320 fmt.Printf("Cluster distribution differs for more than 20%%: %d for shard %d (average: %f)\n", count, shard, average) 321 failedTests = true 322 } 323 if failedTests { 324 t.Fail() 325 } 326 } 327 } 328 329 // Now we will decrease the number of replicas to 2, and we should see only clusters that were attached to shard 2 to be reassigned 330 replicasCount = 2 331 distributionFunction = ConsistentHashingWithBoundedLoadsDistributionFunction(getClusterAccessor(clusterList.Items), appAccessor, replicasCount) 332 removedCluster := clusterList.Items[len(clusterList.Items)-1] 333 for i := 0; i < clusterCount; i++ { 334 c := &clusters[i] 335 assignedShard := distributionFunction(c) 336 prevıouslyAssignedShard := assignementMap[clusters[i].ID] 337 if prevıouslyAssignedShard != 2 && prevıouslyAssignedShard != assignedShard { 338 fmt.Printf("Previously assigned %s cluster has moved from replica %d to %d", c.ID, prevıouslyAssignedShard, assignedShard) 339 t.Fail() 340 } 341 } 342 // Now, we remove the last added cluster, it should be unassigned 343 removedCluster = clusterList.Items[len(clusterList.Items)-1] 344 clusterList.Items = clusterList.Items[:len(clusterList.Items)-1] 345 distributionFunction = ConsistentHashingWithBoundedLoadsDistributionFunction(getClusterAccessor(clusterList.Items), appAccessor, replicasCount) 346 assert.Equal(t, -1, distributionFunction(&removedCluster)) 347 } 348 349 func TestConsistentHashingWhenClusterWithZeroReplicas(t *testing.T) { 350 db := dbmocks.ArgoDB{} 351 clusters := []v1alpha1.Cluster{createCluster("cluster-01", "01")} 352 clusterAccessor := getClusterAccessor(clusters) 353 clusterList := &v1alpha1.ClusterList{Items: clusters} 354 db.On("ListClusters", mock.Anything).Return(clusterList, nil) 355 appAccessor, _, _, _, _, _ := createTestApps() 356 // Test with replicas set to 0 357 replicasCount := 0 358 db.On("GetApplicationControllerReplicas").Return(replicasCount) 359 distributionFunction := ConsistentHashingWithBoundedLoadsDistributionFunction(clusterAccessor, appAccessor, replicasCount) 360 assert.Equal(t, -1, distributionFunction(nil)) 361 } 362 363 func TestConsistentHashingWhenClusterWithFixedShard(t *testing.T) { 364 db := dbmocks.ArgoDB{} 365 var fixedShard int64 = 1 366 cluster := &v1alpha1.Cluster{ID: "1", Shard: &fixedShard} 367 clusters := []v1alpha1.Cluster{*cluster} 368 369 clusterAccessor := getClusterAccessor(clusters) 370 clusterList := &v1alpha1.ClusterList{Items: clusters} 371 db.On("ListClusters", mock.Anything).Return(clusterList, nil) 372 373 // Test with replicas set to 5 374 replicasCount := 5 375 db.On("GetApplicationControllerReplicas").Return(replicasCount) 376 appAccessor, _, _, _, _, _ := createTestApps() 377 distributionFunction := ConsistentHashingWithBoundedLoadsDistributionFunction(clusterAccessor, appAccessor, replicasCount) 378 assert.Equal(t, fixedShard, int64(distributionFunction(cluster))) 379 } 380 381 func TestGetShardByIndexModuloReplicasCountDistributionFunction(t *testing.T) { 382 clusters, db, cluster1, cluster2, _, _, _ := createTestClusters() 383 replicasCount := 2 384 db.On("GetApplicationControllerReplicas").Return(replicasCount) 385 distributionFunction := RoundRobinDistributionFunction(clusters, replicasCount) 386 387 // Test that the function returns the correct shard for cluster1 and cluster2 388 expectedShardForCluster1 := 0 389 expectedShardForCluster2 := 1 390 shardForCluster1 := distributionFunction(&cluster1) 391 shardForCluster2 := distributionFunction(&cluster2) 392 393 assert.Equal(t, expectedShardForCluster1, shardForCluster1, "Expected shard for cluster1 to be %d but got %d", expectedShardForCluster1, shardForCluster1) 394 assert.Equal(t, expectedShardForCluster2, shardForCluster2, "Expected shard for cluster2 to be %d but got %d", expectedShardForCluster2, shardForCluster2) 395 } 396 397 func TestInferShard(t *testing.T) { 398 // Override the os.Hostname function to return a specific hostname for testing 399 defer func() { osHostnameFunction = os.Hostname }() 400 401 expectedShard := 3 402 osHostnameFunction = func() (string, error) { return "example-shard-3", nil } 403 actualShard, _ := InferShard() 404 assert.Equal(t, expectedShard, actualShard) 405 406 osHostnameError := errors.New("cannot resolve hostname") 407 osHostnameFunction = func() (string, error) { return "exampleshard", osHostnameError } 408 _, err := InferShard() 409 require.Error(t, err) 410 assert.Equal(t, err, osHostnameError) 411 412 osHostnameFunction = func() (string, error) { return "exampleshard", nil } 413 _, err = InferShard() 414 require.NoError(t, err) 415 416 osHostnameFunction = func() (string, error) { return "example-shard", nil } 417 _, err = InferShard() 418 require.NoError(t, err) 419 } 420 421 func createTestClusters() (clusterAccessor, *dbmocks.ArgoDB, v1alpha1.Cluster, v1alpha1.Cluster, v1alpha1.Cluster, v1alpha1.Cluster, v1alpha1.Cluster) { 422 db := dbmocks.ArgoDB{} 423 cluster1 := createCluster("cluster1", "1") 424 cluster2 := createCluster("cluster2", "2") 425 cluster3 := createCluster("cluster3", "3") 426 cluster4 := createCluster("cluster4", "4") 427 cluster5 := createCluster("cluster5", "5") 428 429 clusters := []v1alpha1.Cluster{cluster1, cluster2, cluster3, cluster4, cluster5} 430 431 db.On("ListClusters", mock.Anything).Return(&v1alpha1.ClusterList{Items: []v1alpha1.Cluster{ 432 cluster1, cluster2, cluster3, cluster4, cluster5, 433 }}, nil) 434 return getClusterAccessor(clusters), &db, cluster1, cluster2, cluster3, cluster4, cluster5 435 } 436 437 func getClusterAccessor(clusters []v1alpha1.Cluster) clusterAccessor { 438 // Convert the array to a slice of pointers 439 clusterPointers := getClusterPointers(clusters) 440 clusterAccessor := func() []*v1alpha1.Cluster { return clusterPointers } 441 return clusterAccessor 442 } 443 444 func getClusterPointers(clusters []v1alpha1.Cluster) []*v1alpha1.Cluster { 445 var clusterPointers []*v1alpha1.Cluster 446 for i := range clusters { 447 clusterPointers = append(clusterPointers, &clusters[i]) 448 } 449 return clusterPointers 450 } 451 452 func createCluster(name string, id string) v1alpha1.Cluster { 453 cluster := v1alpha1.Cluster{ 454 Name: name, 455 ID: id, 456 Server: "https://kubernetes.default.svc?" + id, 457 } 458 return cluster 459 } 460 461 func Test_getDefaultShardMappingData(t *testing.T) { 462 expectedData := []shardApplicationControllerMapping{ 463 { 464 ShardNumber: 0, 465 ControllerName: "", 466 }, { 467 ShardNumber: 1, 468 ControllerName: "", 469 }, 470 } 471 472 shardMappingData := getDefaultShardMappingData(2) 473 assert.Equal(t, expectedData, shardMappingData) 474 } 475 476 func Test_generateDefaultShardMappingCM_NoPredefinedShard(t *testing.T) { 477 replicas := 2 478 expectedTime := metav1.Now() 479 defer func() { osHostnameFunction = os.Hostname }() 480 defer func() { heartbeatCurrentTime = metav1.Now }() 481 482 expectedMapping := []shardApplicationControllerMapping{ 483 { 484 ShardNumber: 0, 485 ControllerName: "test-example", 486 HeartbeatTime: expectedTime, 487 }, { 488 ShardNumber: 1, 489 }, 490 } 491 492 expectedMappingCM, err := json.Marshal(expectedMapping) 493 require.NoError(t, err) 494 495 expectedShadingCM := &corev1.ConfigMap{ 496 ObjectMeta: metav1.ObjectMeta{ 497 Name: common.ArgoCDAppControllerShardConfigMapName, 498 Namespace: "test", 499 }, 500 Data: map[string]string{ 501 "shardControllerMapping": string(expectedMappingCM), 502 }, 503 } 504 heartbeatCurrentTime = func() metav1.Time { return expectedTime } 505 osHostnameFunction = func() (string, error) { return "test-example", nil } 506 shardingCM, err := generateDefaultShardMappingCM("test", "test-example", replicas, -1) 507 require.NoError(t, err) 508 assert.Equal(t, expectedShadingCM, shardingCM) 509 } 510 511 func Test_generateDefaultShardMappingCM_PredefinedShard(t *testing.T) { 512 replicas := 2 513 expectedTime := metav1.Now() 514 defer func() { osHostnameFunction = os.Hostname }() 515 defer func() { heartbeatCurrentTime = metav1.Now }() 516 517 expectedMapping := []shardApplicationControllerMapping{ 518 { 519 ShardNumber: 0, 520 }, { 521 ShardNumber: 1, 522 ControllerName: "test-example", 523 HeartbeatTime: expectedTime, 524 }, 525 } 526 527 expectedMappingCM, err := json.Marshal(expectedMapping) 528 require.NoError(t, err) 529 530 expectedShadingCM := &corev1.ConfigMap{ 531 ObjectMeta: metav1.ObjectMeta{ 532 Name: common.ArgoCDAppControllerShardConfigMapName, 533 Namespace: "test", 534 }, 535 Data: map[string]string{ 536 "shardControllerMapping": string(expectedMappingCM), 537 }, 538 } 539 heartbeatCurrentTime = func() metav1.Time { return expectedTime } 540 osHostnameFunction = func() (string, error) { return "test-example", nil } 541 shardingCM, err := generateDefaultShardMappingCM("test", "test-example", replicas, 1) 542 require.NoError(t, err) 543 assert.Equal(t, expectedShadingCM, shardingCM) 544 } 545 546 func Test_getOrUpdateShardNumberForController(t *testing.T) { 547 expectedTime := metav1.Now() 548 549 testCases := []struct { 550 name string 551 shardApplicationControllerMapping []shardApplicationControllerMapping 552 hostname string 553 replicas int 554 shard int 555 expectedShard int 556 expectedShardMappingData []shardApplicationControllerMapping 557 }{ 558 { 559 name: "length of shard mapping less than number of replicas - Existing controller", 560 shardApplicationControllerMapping: []shardApplicationControllerMapping{ 561 { 562 ControllerName: "test-example", 563 ShardNumber: 0, 564 HeartbeatTime: metav1.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), 565 }, 566 }, 567 hostname: "test-example", 568 replicas: 2, 569 shard: -1, 570 expectedShard: 0, 571 expectedShardMappingData: []shardApplicationControllerMapping{ 572 { 573 ControllerName: "test-example", 574 ShardNumber: 0, 575 HeartbeatTime: expectedTime, 576 }, { 577 ControllerName: "", 578 ShardNumber: 1, 579 HeartbeatTime: metav1.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), 580 }, 581 }, 582 }, 583 { 584 name: "length of shard mapping less than number of replicas - New controller", 585 shardApplicationControllerMapping: []shardApplicationControllerMapping{ 586 { 587 ControllerName: "test-example", 588 ShardNumber: 0, 589 HeartbeatTime: expectedTime, 590 }, 591 }, 592 hostname: "test-example-1", 593 replicas: 2, 594 shard: -1, 595 expectedShard: 1, 596 expectedShardMappingData: []shardApplicationControllerMapping{ 597 { 598 ControllerName: "test-example", 599 ShardNumber: 0, 600 HeartbeatTime: expectedTime, 601 }, { 602 ControllerName: "test-example-1", 603 ShardNumber: 1, 604 HeartbeatTime: expectedTime, 605 }, 606 }, 607 }, 608 { 609 name: "length of shard mapping more than number of replicas", 610 shardApplicationControllerMapping: []shardApplicationControllerMapping{ 611 { 612 ControllerName: "test-example", 613 ShardNumber: 0, 614 HeartbeatTime: expectedTime, 615 }, { 616 ControllerName: "test-example-1", 617 ShardNumber: 1, 618 HeartbeatTime: expectedTime, 619 }, 620 }, 621 hostname: "test-example", 622 replicas: 1, 623 shard: -1, 624 expectedShard: 0, 625 expectedShardMappingData: []shardApplicationControllerMapping{ 626 { 627 ControllerName: "test-example", 628 ShardNumber: 0, 629 HeartbeatTime: expectedTime, 630 }, 631 }, 632 }, 633 { 634 name: "shard number is pre-specified and length of shard mapping less than number of replicas - Existing controller", 635 shardApplicationControllerMapping: []shardApplicationControllerMapping{ 636 { 637 ControllerName: "test-example-1", 638 ShardNumber: 1, 639 HeartbeatTime: metav1.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), 640 }, { 641 ControllerName: "test-example", 642 ShardNumber: 0, 643 HeartbeatTime: expectedTime, 644 }, 645 }, 646 hostname: "test-example-1", 647 replicas: 2, 648 shard: 1, 649 expectedShard: 1, 650 expectedShardMappingData: []shardApplicationControllerMapping{ 651 { 652 ControllerName: "test-example-1", 653 ShardNumber: 1, 654 HeartbeatTime: expectedTime, 655 }, { 656 ControllerName: "test-example", 657 ShardNumber: 0, 658 HeartbeatTime: expectedTime, 659 }, 660 }, 661 }, 662 { 663 name: "shard number is pre-specified and length of shard mapping less than number of replicas - New controller", 664 shardApplicationControllerMapping: []shardApplicationControllerMapping{ 665 { 666 ControllerName: "test-example", 667 ShardNumber: 0, 668 HeartbeatTime: expectedTime, 669 }, 670 }, 671 hostname: "test-example-1", 672 replicas: 2, 673 shard: 1, 674 expectedShard: 1, 675 expectedShardMappingData: []shardApplicationControllerMapping{ 676 { 677 ControllerName: "test-example", 678 ShardNumber: 0, 679 HeartbeatTime: expectedTime, 680 }, { 681 ControllerName: "test-example-1", 682 ShardNumber: 1, 683 HeartbeatTime: expectedTime, 684 }, 685 }, 686 }, 687 { 688 name: "shard number is pre-specified and length of shard mapping more than number of replicas", 689 shardApplicationControllerMapping: []shardApplicationControllerMapping{ 690 { 691 ControllerName: "test-example", 692 ShardNumber: 0, 693 HeartbeatTime: expectedTime, 694 }, { 695 ControllerName: "test-example-1", 696 ShardNumber: 1, 697 HeartbeatTime: expectedTime, 698 }, { 699 ControllerName: "test-example-2", 700 ShardNumber: 2, 701 HeartbeatTime: expectedTime, 702 }, 703 }, 704 hostname: "test-example", 705 replicas: 2, 706 shard: 1, 707 expectedShard: 1, 708 expectedShardMappingData: []shardApplicationControllerMapping{ 709 { 710 ControllerName: "", 711 ShardNumber: 0, 712 HeartbeatTime: metav1.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), 713 }, { 714 ControllerName: "test-example", 715 ShardNumber: 1, 716 HeartbeatTime: expectedTime, 717 }, 718 }, 719 }, 720 { 721 name: "updating heartbeat", 722 shardApplicationControllerMapping: []shardApplicationControllerMapping{ 723 { 724 ControllerName: "test-example", 725 ShardNumber: 0, 726 HeartbeatTime: expectedTime, 727 }, { 728 ControllerName: "test-example-1", 729 ShardNumber: 1, 730 HeartbeatTime: metav1.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), 731 }, 732 }, 733 hostname: "test-example-1", 734 replicas: 2, 735 shard: -1, 736 expectedShard: 1, 737 expectedShardMappingData: []shardApplicationControllerMapping{ 738 { 739 ControllerName: "test-example", 740 ShardNumber: 0, 741 HeartbeatTime: expectedTime, 742 }, { 743 ControllerName: "test-example-1", 744 ShardNumber: 1, 745 HeartbeatTime: expectedTime, 746 }, 747 }, 748 }, 749 { 750 name: "updating heartbeat - shard pre-defined", 751 shardApplicationControllerMapping: []shardApplicationControllerMapping{ 752 { 753 ControllerName: "test-example", 754 ShardNumber: 0, 755 HeartbeatTime: expectedTime, 756 }, { 757 ControllerName: "test-example-1", 758 ShardNumber: 1, 759 HeartbeatTime: metav1.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), 760 }, 761 }, 762 hostname: "test-example-1", 763 replicas: 2, 764 shard: 1, 765 expectedShard: 1, 766 expectedShardMappingData: []shardApplicationControllerMapping{ 767 { 768 ControllerName: "test-example", 769 ShardNumber: 0, 770 HeartbeatTime: expectedTime, 771 }, { 772 ControllerName: "test-example-1", 773 ShardNumber: 1, 774 HeartbeatTime: expectedTime, 775 }, 776 }, 777 }, 778 } 779 780 for _, tc := range testCases { 781 t.Run(tc.name, func(t *testing.T) { 782 defer func() { osHostnameFunction = os.Hostname }() 783 heartbeatCurrentTime = func() metav1.Time { return expectedTime } 784 shard, shardMappingData := getOrUpdateShardNumberForController(tc.shardApplicationControllerMapping, tc.hostname, tc.replicas, tc.shard) 785 assert.Equal(t, tc.expectedShard, shard) 786 assert.Equal(t, tc.expectedShardMappingData, shardMappingData) 787 }) 788 } 789 } 790 791 func TestGetClusterSharding(t *testing.T) { 792 IntPtr := func(i int32) *int32 { 793 return &i 794 } 795 796 deployment := &appsv1.Deployment{ 797 ObjectMeta: metav1.ObjectMeta{ 798 Name: common.DefaultApplicationControllerName, 799 Namespace: "argocd", 800 }, 801 Spec: appsv1.DeploymentSpec{ 802 Replicas: IntPtr(1), 803 }, 804 } 805 806 deploymentMultiReplicas := &appsv1.Deployment{ 807 ObjectMeta: metav1.ObjectMeta{ 808 Name: "argocd-application-controller-multi-replicas", 809 Namespace: "argocd", 810 }, 811 Spec: appsv1.DeploymentSpec{ 812 Replicas: IntPtr(3), 813 }, 814 } 815 816 objects := append([]runtime.Object{}, deployment, deploymentMultiReplicas) 817 kubeclientset := kubefake.NewSimpleClientset(objects...) 818 819 settingsMgr := settings.NewSettingsManager(t.Context(), kubeclientset, "argocd", settings.WithRepoOrClusterChangedHandler(func() { 820 })) 821 822 testCases := []struct { 823 name string 824 useDynamicSharding bool 825 envsSetter func(t *testing.T) 826 cleanup func() 827 expectedShard int 828 expectedReplicas int 829 expectedErr error 830 }{ 831 { 832 name: "Default sharding with statefulset", 833 envsSetter: func(t *testing.T) { 834 t.Helper() 835 t.Setenv(common.EnvControllerReplicas, "1") 836 }, 837 cleanup: func() {}, 838 useDynamicSharding: false, 839 expectedShard: 0, 840 expectedReplicas: 1, 841 expectedErr: nil, 842 }, 843 { 844 name: "Default sharding with deployment", 845 envsSetter: func(t *testing.T) { 846 t.Helper() 847 t.Setenv(common.EnvAppControllerName, common.DefaultApplicationControllerName) 848 }, 849 cleanup: func() {}, 850 useDynamicSharding: true, 851 expectedShard: 0, 852 expectedReplicas: 1, 853 expectedErr: nil, 854 }, 855 { 856 name: "Default sharding with deployment and multiple replicas", 857 envsSetter: func(t *testing.T) { 858 t.Helper() 859 t.Setenv(common.EnvAppControllerName, "argocd-application-controller-multi-replicas") 860 }, 861 cleanup: func() {}, 862 useDynamicSharding: true, 863 expectedShard: 0, 864 expectedReplicas: 3, 865 expectedErr: nil, 866 }, 867 { 868 name: "Statefulset multiple replicas", 869 envsSetter: func(t *testing.T) { 870 t.Helper() 871 t.Setenv(common.EnvControllerReplicas, "3") 872 osHostnameFunction = func() (string, error) { return "example-shard-3", nil } 873 }, 874 cleanup: func() { 875 osHostnameFunction = os.Hostname 876 }, 877 useDynamicSharding: false, 878 expectedShard: 3, 879 expectedReplicas: 3, 880 expectedErr: nil, 881 }, 882 { 883 name: "Explicit shard with statefulset and 1 replica", 884 envsSetter: func(t *testing.T) { 885 t.Helper() 886 t.Setenv(common.EnvControllerReplicas, "1") 887 t.Setenv(common.EnvControllerShard, "3") 888 }, 889 cleanup: func() {}, 890 useDynamicSharding: false, 891 expectedShard: 0, 892 expectedReplicas: 1, 893 expectedErr: nil, 894 }, 895 { 896 name: "Explicit shard with statefulset and 2 replica - and to high shard", 897 envsSetter: func(t *testing.T) { 898 t.Helper() 899 t.Setenv(common.EnvControllerReplicas, "2") 900 t.Setenv(common.EnvControllerShard, "3") 901 }, 902 cleanup: func() {}, 903 useDynamicSharding: false, 904 expectedShard: 0, 905 expectedReplicas: 2, 906 expectedErr: nil, 907 }, 908 { 909 name: "Explicit shard with statefulset and 2 replica", 910 envsSetter: func(t *testing.T) { 911 t.Helper() 912 t.Setenv(common.EnvControllerReplicas, "2") 913 t.Setenv(common.EnvControllerShard, "1") 914 }, 915 cleanup: func() {}, 916 useDynamicSharding: false, 917 expectedShard: 1, 918 expectedReplicas: 2, 919 expectedErr: nil, 920 }, 921 { 922 name: "Explicit shard with deployment", 923 envsSetter: func(t *testing.T) { 924 t.Helper() 925 t.Setenv(common.EnvControllerShard, "3") 926 }, 927 cleanup: func() {}, 928 useDynamicSharding: true, 929 expectedShard: 0, 930 expectedReplicas: 1, 931 expectedErr: nil, 932 }, 933 { 934 name: "Explicit shard with deployment and multiple replicas will read from configmap", 935 envsSetter: func(t *testing.T) { 936 t.Helper() 937 t.Setenv(common.EnvAppControllerName, "argocd-application-controller-multi-replicas") 938 t.Setenv(common.EnvControllerShard, "3") 939 }, 940 cleanup: func() {}, 941 useDynamicSharding: true, 942 expectedShard: 0, 943 expectedReplicas: 3, 944 expectedErr: nil, 945 }, 946 { 947 name: "Dynamic sharding but missing deployment", 948 envsSetter: func(t *testing.T) { 949 t.Helper() 950 t.Setenv(common.EnvAppControllerName, "missing-deployment") 951 }, 952 cleanup: func() {}, 953 useDynamicSharding: true, 954 expectedShard: 0, 955 expectedReplicas: 1, 956 expectedErr: errors.New("(dynamic cluster distribution) failed to get app controller deployment: deployments.apps \"missing-deployment\" not found"), 957 }, 958 } 959 960 for _, tc := range testCases { 961 t.Run(tc.name, func(t *testing.T) { 962 tc.envsSetter(t) 963 defer tc.cleanup() 964 shardingCache, err := GetClusterSharding(kubeclientset, settingsMgr, "round-robin", tc.useDynamicSharding) 965 966 if shardingCache != nil { 967 clusterSharding := shardingCache.(*ClusterSharding) 968 assert.Equal(t, tc.expectedShard, clusterSharding.Shard) 969 assert.Equal(t, tc.expectedReplicas, clusterSharding.Replicas) 970 } 971 972 if tc.expectedErr != nil { 973 assert.EqualError(t, err, tc.expectedErr.Error()) 974 } else { 975 require.NoError(t, err) 976 } 977 }) 978 } 979 } 980 981 func TestAppAwareCache(t *testing.T) { 982 _, _, cluster1, cluster2, cluster3, cluster4, cluster5 := createTestClusters() 983 _, app1, app2, app3, app4, app5 := createTestApps() 984 985 clusterList := getClusterPointers([]v1alpha1.Cluster{cluster1, cluster2, cluster3, cluster4, cluster5}) 986 appList := getAppPointers([]v1alpha1.Application{app1, app2, app3, app4, app5}) 987 988 getClusters := func() []*v1alpha1.Cluster { return clusterList } 989 getApps := func() []*v1alpha1.Application { return appList } 990 991 appDistribution := getAppDistribution(getClusters, getApps) 992 993 assert.Equal(t, int64(2), appDistribution["cluster1"]) 994 assert.Equal(t, int64(2), appDistribution["cluster2"]) 995 assert.Equal(t, int64(1), appDistribution["cluster3"]) 996 997 app6 := createApp("app6", "cluster4") 998 appList = append(appList, &app6) 999 1000 app1Update := createApp("app1", "cluster2") 1001 // replace app 1 1002 appList[0] = &app1Update 1003 1004 // Remove app 3 1005 appList = append(appList[:2], appList[3:]...) 1006 1007 appDistribution = getAppDistribution(getClusters, getApps) 1008 1009 assert.Equal(t, int64(1), appDistribution["cluster1"]) 1010 assert.Equal(t, int64(2), appDistribution["cluster2"]) 1011 assert.Equal(t, int64(1), appDistribution["cluster3"]) 1012 assert.Equal(t, int64(1), appDistribution["cluster4"]) 1013 } 1014 1015 func createTestApps() (appAccessor, v1alpha1.Application, v1alpha1.Application, v1alpha1.Application, v1alpha1.Application, v1alpha1.Application) { 1016 app1 := createApp("app1", "cluster1") 1017 app2 := createApp("app2", "cluster1") 1018 app3 := createApp("app3", "cluster2") 1019 app4 := createApp("app4", "cluster2") 1020 app5 := createApp("app5", "cluster3") 1021 1022 apps := []v1alpha1.Application{app1, app2, app3, app4, app5} 1023 1024 return getAppAccessor(apps), app1, app2, app3, app4, app5 1025 } 1026 1027 func getAppAccessor(apps []v1alpha1.Application) appAccessor { 1028 // Convert the array to a slice of pointers 1029 appPointers := getAppPointers(apps) 1030 appAccessor := func() []*v1alpha1.Application { return appPointers } 1031 return appAccessor 1032 } 1033 1034 func getAppPointers(apps []v1alpha1.Application) []*v1alpha1.Application { 1035 var appPointers []*v1alpha1.Application 1036 for i := range apps { 1037 appPointers = append(appPointers, &apps[i]) 1038 } 1039 return appPointers 1040 } 1041 1042 func createApp(name string, server string) v1alpha1.Application { 1043 testApp := ` 1044 apiVersion: argoproj.io/v1alpha1 1045 kind: Application 1046 metadata: 1047 name: ` + name + ` 1048 spec: 1049 destination: 1050 server: ` + server + ` 1051 ` 1052 1053 var app v1alpha1.Application 1054 err := yaml.Unmarshal([]byte(testApp), &app) 1055 if err != nil { 1056 panic(err) 1057 } 1058 return app 1059 }