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