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  }