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  }