github.com/argoproj/argo-cd/v2@v2.10.9/server/applicationset/applicationset_test.go (about)

     1  package applicationset
     2  
     3  import (
     4  	"context"
     5  	"testing"
     6  
     7  	"github.com/argoproj/pkg/sync"
     8  	"github.com/stretchr/testify/assert"
     9  	v1 "k8s.io/api/core/v1"
    10  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    11  	"k8s.io/apimachinery/pkg/runtime"
    12  	"k8s.io/client-go/kubernetes/fake"
    13  	k8scache "k8s.io/client-go/tools/cache"
    14  
    15  	"github.com/argoproj/argo-cd/v2/common"
    16  	"github.com/argoproj/argo-cd/v2/pkg/apiclient/applicationset"
    17  	appsv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
    18  	apps "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned/fake"
    19  	appinformer "github.com/argoproj/argo-cd/v2/pkg/client/informers/externalversions"
    20  	"github.com/argoproj/argo-cd/v2/server/rbacpolicy"
    21  	"github.com/argoproj/argo-cd/v2/util/assets"
    22  	"github.com/argoproj/argo-cd/v2/util/db"
    23  	"github.com/argoproj/argo-cd/v2/util/errors"
    24  	"github.com/argoproj/argo-cd/v2/util/rbac"
    25  	"github.com/argoproj/argo-cd/v2/util/settings"
    26  )
    27  
    28  const (
    29  	testNamespace = "default"
    30  	fakeRepoURL   = "https://git.com/repo.git"
    31  )
    32  
    33  func fakeRepo() *appsv1.Repository {
    34  	return &appsv1.Repository{
    35  		Repo: fakeRepoURL,
    36  	}
    37  }
    38  
    39  func fakeCluster() *appsv1.Cluster {
    40  	return &appsv1.Cluster{
    41  		Server: "https://cluster-api.example.com",
    42  		Name:   "fake-cluster",
    43  		Config: appsv1.ClusterConfig{},
    44  	}
    45  }
    46  
    47  // return an ApplicationServiceServer which returns fake data
    48  func newTestAppSetServer(objects ...runtime.Object) *Server {
    49  	f := func(enf *rbac.Enforcer) {
    50  		_ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV)
    51  		enf.SetDefaultRole("role:admin")
    52  	}
    53  	scopedNamespaces := ""
    54  	return newTestAppSetServerWithEnforcerConfigure(f, scopedNamespaces, objects...)
    55  }
    56  
    57  // return an ApplicationServiceServer which returns fake data
    58  func newTestNamespacedAppSetServer(objects ...runtime.Object) *Server {
    59  	f := func(enf *rbac.Enforcer) {
    60  		_ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV)
    61  		enf.SetDefaultRole("role:admin")
    62  	}
    63  	scopedNamespaces := "argocd"
    64  	return newTestAppSetServerWithEnforcerConfigure(f, scopedNamespaces, objects...)
    65  }
    66  
    67  func newTestAppSetServerWithEnforcerConfigure(f func(*rbac.Enforcer), namespace string, objects ...runtime.Object) *Server {
    68  	kubeclientset := fake.NewSimpleClientset(&v1.ConfigMap{
    69  		ObjectMeta: metav1.ObjectMeta{
    70  			Namespace: testNamespace,
    71  			Name:      "argocd-cm",
    72  			Labels: map[string]string{
    73  				"app.kubernetes.io/part-of": "argocd",
    74  			},
    75  		},
    76  	}, &v1.Secret{
    77  		ObjectMeta: metav1.ObjectMeta{
    78  			Name:      "argocd-secret",
    79  			Namespace: testNamespace,
    80  		},
    81  		Data: map[string][]byte{
    82  			"admin.password":   []byte("test"),
    83  			"server.secretkey": []byte("test"),
    84  		},
    85  	})
    86  	ctx := context.Background()
    87  	db := db.NewDB(testNamespace, settings.NewSettingsManager(ctx, kubeclientset, testNamespace), kubeclientset)
    88  	_, err := db.CreateRepository(ctx, fakeRepo())
    89  	errors.CheckError(err)
    90  	_, err = db.CreateCluster(ctx, fakeCluster())
    91  	errors.CheckError(err)
    92  
    93  	defaultProj := &appsv1.AppProject{
    94  		ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "default"},
    95  		Spec: appsv1.AppProjectSpec{
    96  			SourceRepos:  []string{"*"},
    97  			Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}},
    98  		},
    99  	}
   100  	myProj := &appsv1.AppProject{
   101  		ObjectMeta: metav1.ObjectMeta{Name: "my-proj", Namespace: "default"},
   102  		Spec: appsv1.AppProjectSpec{
   103  			SourceRepos:  []string{"*"},
   104  			Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}},
   105  		},
   106  	}
   107  
   108  	objects = append(objects, defaultProj, myProj)
   109  
   110  	fakeAppsClientset := apps.NewSimpleClientset(objects...)
   111  	factory := appinformer.NewSharedInformerFactoryWithOptions(fakeAppsClientset, 0, appinformer.WithNamespace(namespace), appinformer.WithTweakListOptions(func(options *metav1.ListOptions) {}))
   112  	fakeProjLister := factory.Argoproj().V1alpha1().AppProjects().Lister().AppProjects(testNamespace)
   113  
   114  	enforcer := rbac.NewEnforcer(kubeclientset, testNamespace, common.ArgoCDRBACConfigMapName, nil)
   115  	f(enforcer)
   116  	enforcer.SetClaimsEnforcerFunc(rbacpolicy.NewRBACPolicyEnforcer(enforcer, fakeProjLister).EnforceClaims)
   117  
   118  	settingsMgr := settings.NewSettingsManager(ctx, kubeclientset, testNamespace)
   119  
   120  	// populate the app informer with the fake objects
   121  	appInformer := factory.Argoproj().V1alpha1().Applications().Informer()
   122  	// TODO(jessesuen): probably should return cancel function so tests can stop background informer
   123  	//ctx, cancel := context.WithCancel(context.Background())
   124  	go appInformer.Run(ctx.Done())
   125  	if !k8scache.WaitForCacheSync(ctx.Done(), appInformer.HasSynced) {
   126  		panic("Timed out waiting for caches to sync")
   127  	}
   128  	// populate the appset informer with the fake objects
   129  	appsetInformer := factory.Argoproj().V1alpha1().ApplicationSets().Informer()
   130  	go appsetInformer.Run(ctx.Done())
   131  	if !k8scache.WaitForCacheSync(ctx.Done(), appsetInformer.HasSynced) {
   132  		panic("Timed out waiting for caches to sync")
   133  	}
   134  
   135  	projInformer := factory.Argoproj().V1alpha1().AppProjects().Informer()
   136  	go projInformer.Run(ctx.Done())
   137  	if !k8scache.WaitForCacheSync(ctx.Done(), projInformer.HasSynced) {
   138  		panic("Timed out waiting for caches to sync")
   139  	}
   140  
   141  	server := NewServer(
   142  		db,
   143  		kubeclientset,
   144  		enforcer,
   145  		fakeAppsClientset,
   146  		appInformer,
   147  		factory.Argoproj().V1alpha1().ApplicationSets().Lister(),
   148  		fakeProjLister,
   149  		settingsMgr,
   150  		testNamespace,
   151  		sync.NewKeyLock(),
   152  		[]string{testNamespace, "external-namespace"},
   153  	)
   154  	return server.(*Server)
   155  }
   156  
   157  func newTestAppSet(opts ...func(appset *appsv1.ApplicationSet)) *appsv1.ApplicationSet {
   158  	appset := appsv1.ApplicationSet{
   159  		ObjectMeta: metav1.ObjectMeta{
   160  			Namespace: testNamespace,
   161  		},
   162  		Spec: appsv1.ApplicationSetSpec{
   163  			Template: appsv1.ApplicationSetTemplate{
   164  				Spec: appsv1.ApplicationSpec{
   165  					Project: "default",
   166  				},
   167  			},
   168  		},
   169  	}
   170  	for i := range opts {
   171  		opts[i](&appset)
   172  	}
   173  	return &appset
   174  }
   175  
   176  func testListAppsetsWithLabels(t *testing.T, appsetQuery applicationset.ApplicationSetListQuery, appServer *Server) {
   177  	validTests := []struct {
   178  		testName       string
   179  		label          string
   180  		expectedResult []string
   181  	}{
   182  		{testName: "Equality based filtering using '=' operator",
   183  			label:          "key1=value1",
   184  			expectedResult: []string{"AppSet1"}},
   185  		{testName: "Equality based filtering using '==' operator",
   186  			label:          "key1==value1",
   187  			expectedResult: []string{"AppSet1"}},
   188  		{testName: "Equality based filtering using '!=' operator",
   189  			label:          "key1!=value1",
   190  			expectedResult: []string{"AppSet2", "AppSet3"}},
   191  		{testName: "Set based filtering using 'in' operator",
   192  			label:          "key1 in (value1, value3)",
   193  			expectedResult: []string{"AppSet1", "AppSet3"}},
   194  		{testName: "Set based filtering using 'notin' operator",
   195  			label:          "key1 notin (value1, value3)",
   196  			expectedResult: []string{"AppSet2"}},
   197  		{testName: "Set based filtering using 'exists' operator",
   198  			label:          "key1",
   199  			expectedResult: []string{"AppSet1", "AppSet2", "AppSet3"}},
   200  		{testName: "Set based filtering using 'not exists' operator",
   201  			label:          "!key2",
   202  			expectedResult: []string{"AppSet2", "AppSet3"}},
   203  	}
   204  	//test valid scenarios
   205  	for _, validTest := range validTests {
   206  		t.Run(validTest.testName, func(t *testing.T) {
   207  			appsetQuery.Selector = validTest.label
   208  			res, err := appServer.List(context.Background(), &appsetQuery)
   209  			assert.NoError(t, err)
   210  			apps := []string{}
   211  			for i := range res.Items {
   212  				apps = append(apps, res.Items[i].Name)
   213  			}
   214  			assert.Equal(t, validTest.expectedResult, apps)
   215  		})
   216  	}
   217  
   218  	invalidTests := []struct {
   219  		testName    string
   220  		label       string
   221  		errorMesage string
   222  	}{
   223  		{testName: "Set based filtering using '>' operator",
   224  			label:       "key1>value1",
   225  			errorMesage: "error parsing the selector"},
   226  		{testName: "Set based filtering using '<' operator",
   227  			label:       "key1<value1",
   228  			errorMesage: "error parsing the selector"},
   229  	}
   230  	//test invalid scenarios
   231  	for _, invalidTest := range invalidTests {
   232  		t.Run(invalidTest.testName, func(t *testing.T) {
   233  			appsetQuery.Selector = invalidTest.label
   234  			_, err := appServer.List(context.Background(), &appsetQuery)
   235  			assert.ErrorContains(t, err, invalidTest.errorMesage)
   236  		})
   237  	}
   238  }
   239  
   240  func TestListAppSetsInNamespaceWithLabels(t *testing.T) {
   241  	testNamespace := "test-namespace"
   242  	appSetServer := newTestAppSetServer(newTestAppSet(func(appset *appsv1.ApplicationSet) {
   243  		appset.Name = "AppSet1"
   244  		appset.ObjectMeta.Namespace = testNamespace
   245  		appset.SetLabels(map[string]string{"key1": "value1", "key2": "value1"})
   246  	}), newTestAppSet(func(appset *appsv1.ApplicationSet) {
   247  		appset.Name = "AppSet2"
   248  		appset.ObjectMeta.Namespace = testNamespace
   249  		appset.SetLabels(map[string]string{"key1": "value2"})
   250  	}), newTestAppSet(func(appset *appsv1.ApplicationSet) {
   251  		appset.Name = "AppSet3"
   252  		appset.ObjectMeta.Namespace = testNamespace
   253  		appset.SetLabels(map[string]string{"key1": "value3"})
   254  	}))
   255  	appSetServer.enabledNamespaces = []string{testNamespace}
   256  	appsetQuery := applicationset.ApplicationSetListQuery{AppsetNamespace: testNamespace}
   257  
   258  	testListAppsetsWithLabels(t, appsetQuery, appSetServer)
   259  }
   260  
   261  func TestListAppSetsInDefaultNSWithLabels(t *testing.T) {
   262  	appSetServer := newTestAppSetServer(newTestAppSet(func(appset *appsv1.ApplicationSet) {
   263  		appset.Name = "AppSet1"
   264  		appset.SetLabels(map[string]string{"key1": "value1", "key2": "value1"})
   265  	}), newTestAppSet(func(appset *appsv1.ApplicationSet) {
   266  		appset.Name = "AppSet2"
   267  		appset.SetLabels(map[string]string{"key1": "value2"})
   268  	}), newTestAppSet(func(appset *appsv1.ApplicationSet) {
   269  		appset.Name = "AppSet3"
   270  		appset.SetLabels(map[string]string{"key1": "value3"})
   271  	}))
   272  	appsetQuery := applicationset.ApplicationSetListQuery{}
   273  
   274  	testListAppsetsWithLabels(t, appsetQuery, appSetServer)
   275  }
   276  
   277  // This test covers https://github.com/argoproj/argo-cd/issues/15429
   278  // If the namespace isn't provided during listing action, argocd's
   279  // default namespace must be used and not all the namespaces
   280  func TestListAppSetsWithoutNamespace(t *testing.T) {
   281  	testNamespace := "test-namespace"
   282  	appSetServer := newTestNamespacedAppSetServer(newTestAppSet(func(appset *appsv1.ApplicationSet) {
   283  		appset.Name = "AppSet1"
   284  		appset.ObjectMeta.Namespace = testNamespace
   285  		appset.SetLabels(map[string]string{"key1": "value1", "key2": "value1"})
   286  	}), newTestAppSet(func(appset *appsv1.ApplicationSet) {
   287  		appset.Name = "AppSet2"
   288  		appset.ObjectMeta.Namespace = testNamespace
   289  		appset.SetLabels(map[string]string{"key1": "value2"})
   290  	}), newTestAppSet(func(appset *appsv1.ApplicationSet) {
   291  		appset.Name = "AppSet3"
   292  		appset.ObjectMeta.Namespace = testNamespace
   293  		appset.SetLabels(map[string]string{"key1": "value3"})
   294  	}))
   295  	appSetServer.enabledNamespaces = []string{testNamespace}
   296  	appsetQuery := applicationset.ApplicationSetListQuery{}
   297  
   298  	res, err := appSetServer.List(context.Background(), &appsetQuery)
   299  	assert.NoError(t, err)
   300  	assert.Equal(t, 0, len(res.Items))
   301  }
   302  
   303  func TestCreateAppSet(t *testing.T) {
   304  	testAppSet := newTestAppSet()
   305  	appServer := newTestAppSetServer()
   306  	testAppSet.Spec.Generators = []appsv1.ApplicationSetGenerator{
   307  		{
   308  			List: &appsv1.ListGenerator{},
   309  		},
   310  	}
   311  	createReq := applicationset.ApplicationSetCreateRequest{
   312  		Applicationset: testAppSet,
   313  	}
   314  	_, err := appServer.Create(context.Background(), &createReq)
   315  	assert.NoError(t, err)
   316  }
   317  
   318  func TestCreateAppSetTemplatedProject(t *testing.T) {
   319  	testAppSet := newTestAppSet()
   320  	appServer := newTestAppSetServer()
   321  	testAppSet.Spec.Template.Spec.Project = "{{ .project }}"
   322  	createReq := applicationset.ApplicationSetCreateRequest{
   323  		Applicationset: testAppSet,
   324  	}
   325  	_, err := appServer.Create(context.Background(), &createReq)
   326  	assert.Equal(t, "error validating ApplicationSets: the Argo CD API does not currently support creating ApplicationSets with templated `project` fields", err.Error())
   327  }
   328  
   329  func TestCreateAppSetWrongNamespace(t *testing.T) {
   330  	testAppSet := newTestAppSet()
   331  	appServer := newTestAppSetServer()
   332  	testAppSet.ObjectMeta.Namespace = "NOT-ALLOWED"
   333  	createReq := applicationset.ApplicationSetCreateRequest{
   334  		Applicationset: testAppSet,
   335  	}
   336  	_, err := appServer.Create(context.Background(), &createReq)
   337  
   338  	assert.Equal(t, "namespace 'NOT-ALLOWED' is not permitted", err.Error())
   339  }
   340  
   341  func TestGetAppSet(t *testing.T) {
   342  	appSet1 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
   343  		appset.Name = "AppSet1"
   344  	})
   345  
   346  	appSet2 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
   347  		appset.Name = "AppSet2"
   348  	})
   349  
   350  	appSet3 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
   351  		appset.Name = "AppSet3"
   352  	})
   353  
   354  	t.Run("Get in default namespace", func(t *testing.T) {
   355  
   356  		appSetServer := newTestAppSetServer(appSet1, appSet2, appSet3)
   357  
   358  		appsetQuery := applicationset.ApplicationSetGetQuery{Name: "AppSet1"}
   359  
   360  		res, err := appSetServer.Get(context.Background(), &appsetQuery)
   361  		assert.NoError(t, err)
   362  		assert.Equal(t, "AppSet1", res.Name)
   363  	})
   364  
   365  	t.Run("Get in named namespace", func(t *testing.T) {
   366  
   367  		appSetServer := newTestAppSetServer(appSet1, appSet2, appSet3)
   368  
   369  		appsetQuery := applicationset.ApplicationSetGetQuery{Name: "AppSet1", AppsetNamespace: testNamespace}
   370  
   371  		res, err := appSetServer.Get(context.Background(), &appsetQuery)
   372  		assert.NoError(t, err)
   373  		assert.Equal(t, "AppSet1", res.Name)
   374  	})
   375  
   376  	t.Run("Get in not allowed namespace", func(t *testing.T) {
   377  
   378  		appSetServer := newTestAppSetServer(appSet1, appSet2, appSet3)
   379  
   380  		appsetQuery := applicationset.ApplicationSetGetQuery{Name: "AppSet1", AppsetNamespace: "NOT-ALLOWED"}
   381  
   382  		_, err := appSetServer.Get(context.Background(), &appsetQuery)
   383  		assert.Equal(t, "namespace 'NOT-ALLOWED' is not permitted", err.Error())
   384  	})
   385  }
   386  
   387  func TestDeleteAppSet(t *testing.T) {
   388  	appSet1 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
   389  		appset.Name = "AppSet1"
   390  	})
   391  
   392  	appSet2 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
   393  		appset.Name = "AppSet2"
   394  	})
   395  
   396  	appSet3 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
   397  		appset.Name = "AppSet3"
   398  	})
   399  
   400  	t.Run("Delete in default namespace", func(t *testing.T) {
   401  
   402  		appSetServer := newTestAppSetServer(appSet1, appSet2, appSet3)
   403  
   404  		appsetQuery := applicationset.ApplicationSetDeleteRequest{Name: "AppSet1"}
   405  
   406  		res, err := appSetServer.Delete(context.Background(), &appsetQuery)
   407  		assert.NoError(t, err)
   408  		assert.Equal(t, &applicationset.ApplicationSetResponse{}, res)
   409  	})
   410  
   411  	t.Run("Delete in named namespace", func(t *testing.T) {
   412  
   413  		appSetServer := newTestAppSetServer(appSet1, appSet2, appSet3)
   414  
   415  		appsetQuery := applicationset.ApplicationSetDeleteRequest{Name: "AppSet1", AppsetNamespace: testNamespace}
   416  
   417  		res, err := appSetServer.Delete(context.Background(), &appsetQuery)
   418  		assert.NoError(t, err)
   419  		assert.Equal(t, &applicationset.ApplicationSetResponse{}, res)
   420  	})
   421  }
   422  
   423  func TestUpdateAppSet(t *testing.T) {
   424  	appSet := newTestAppSet(func(appset *appsv1.ApplicationSet) {
   425  		appset.ObjectMeta.Annotations = map[string]string{
   426  			"annotation-key1": "annotation-value1",
   427  			"annotation-key2": "annotation-value2",
   428  		}
   429  		appset.ObjectMeta.Labels = map[string]string{
   430  			"label-key1": "label-value1",
   431  			"label-key2": "label-value2",
   432  		}
   433  	})
   434  
   435  	newAppSet := newTestAppSet(func(appset *appsv1.ApplicationSet) {
   436  		appset.ObjectMeta.Annotations = map[string]string{
   437  			"annotation-key1": "annotation-value1-updated",
   438  		}
   439  		appset.ObjectMeta.Labels = map[string]string{
   440  			"label-key1": "label-value1-updated",
   441  		}
   442  	})
   443  
   444  	t.Run("Update merge", func(t *testing.T) {
   445  
   446  		appServer := newTestAppSetServer(appSet)
   447  
   448  		updated, err := appServer.updateAppSet(appSet, newAppSet, context.Background(), true)
   449  
   450  		assert.NoError(t, err)
   451  		assert.Equal(t, map[string]string{
   452  			"annotation-key1": "annotation-value1-updated",
   453  			"annotation-key2": "annotation-value2",
   454  		}, updated.Annotations)
   455  		assert.Equal(t, map[string]string{
   456  			"label-key1": "label-value1-updated",
   457  			"label-key2": "label-value2",
   458  		}, updated.Labels)
   459  	})
   460  
   461  	t.Run("Update no merge", func(t *testing.T) {
   462  
   463  		appServer := newTestAppSetServer(appSet)
   464  
   465  		updated, err := appServer.updateAppSet(appSet, newAppSet, context.Background(), false)
   466  
   467  		assert.NoError(t, err)
   468  		assert.Equal(t, map[string]string{
   469  			"annotation-key1": "annotation-value1-updated",
   470  		}, updated.Annotations)
   471  		assert.Equal(t, map[string]string{
   472  			"label-key1": "label-value1-updated",
   473  		}, updated.Labels)
   474  	})
   475  
   476  }