Files
argo-cd/server/applicationset/applicationset_test.go
2026-02-12 09:29:40 -05:00

894 lines
28 KiB
Go

package applicationset
import (
"sort"
"testing"
"sigs.k8s.io/controller-runtime/pkg/client"
cr_fake "sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/argoproj/argo-cd/gitops-engine/pkg/health"
"github.com/argoproj/pkg/v2/sync"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake"
k8scache "k8s.io/client-go/tools/cache"
"github.com/argoproj/argo-cd/v3/common"
"github.com/argoproj/argo-cd/v3/pkg/apiclient/applicationset"
appsv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
apps "github.com/argoproj/argo-cd/v3/pkg/client/clientset/versioned/fake"
appinformer "github.com/argoproj/argo-cd/v3/pkg/client/informers/externalversions"
"github.com/argoproj/argo-cd/v3/server/rbacpolicy"
"github.com/argoproj/argo-cd/v3/test"
"github.com/argoproj/argo-cd/v3/util/argo"
"github.com/argoproj/argo-cd/v3/util/assets"
"github.com/argoproj/argo-cd/v3/util/db"
"github.com/argoproj/argo-cd/v3/util/rbac"
"github.com/argoproj/argo-cd/v3/util/settings"
)
const (
testNamespace = "default"
fakeRepoURL = "https://git.com/repo.git"
)
var testEnableEventList []string = argo.DefaultEnableEventList()
func fakeRepo() *appsv1.Repository {
return &appsv1.Repository{
Repo: fakeRepoURL,
}
}
func fakeCluster() *appsv1.Cluster {
return &appsv1.Cluster{
Server: "https://cluster-api.example.com",
Name: "fake-cluster",
Config: appsv1.ClusterConfig{},
}
}
// newTestAppSetServer returns an ApplicationSetServer with fake data for testing
func newTestAppSetServer(t *testing.T, objects ...client.Object) *Server {
t.Helper()
server, _ := newTestAppSetServerWithK8sClient(t, objects...)
return server
}
// newTestAppSetServerWithK8sClient returns an ApplicationSetServer and the kubernetes clientset for testing.
// Use this variant when you need to create kubernetes resources (e.g., Events) after server creation.
func newTestAppSetServerWithK8sClient(t *testing.T, objects ...client.Object) (*Server, kubernetes.Interface) {
t.Helper()
f := func(enf *rbac.Enforcer) {
_ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV)
enf.SetDefaultRole("role:admin")
}
scopedNamespaces := ""
return newTestAppSetServerWithEnforcerConfigure(t, f, scopedNamespaces, objects...)
}
// newTestNamespacedAppSetServer returns an ApplicationSetServer with fake data for testing with namespaced scope
func newTestNamespacedAppSetServer(t *testing.T, objects ...client.Object) *Server {
t.Helper()
f := func(enf *rbac.Enforcer) {
_ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV)
enf.SetDefaultRole("role:admin")
}
scopedNamespaces := "argocd"
server, _ := newTestAppSetServerWithEnforcerConfigure(t, f, scopedNamespaces, objects...)
return server
}
func newTestAppSetServerWithEnforcerConfigure(t *testing.T, f func(*rbac.Enforcer), namespace string, objects ...client.Object) (*Server, kubernetes.Interface) {
t.Helper()
kubeclientset := fake.NewClientset(&corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: "argocd-cm",
Labels: map[string]string{
"app.kubernetes.io/part-of": "argocd",
},
},
}, &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "argocd-secret",
Namespace: testNamespace,
},
Data: map[string][]byte{
"admin.password": []byte("test"),
"server.secretkey": []byte("test"),
},
})
ctx := t.Context()
db := db.NewDB(testNamespace, settings.NewSettingsManager(ctx, kubeclientset, testNamespace), kubeclientset)
_, err := db.CreateRepository(ctx, fakeRepo())
require.NoError(t, err)
_, err = db.CreateCluster(ctx, fakeCluster())
require.NoError(t, err)
defaultProj := &appsv1.AppProject{
ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "default"},
Spec: appsv1.AppProjectSpec{
SourceRepos: []string{"*"},
Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}},
},
}
myProj := &appsv1.AppProject{
ObjectMeta: metav1.ObjectMeta{Name: "my-proj", Namespace: "default"},
Spec: appsv1.AppProjectSpec{
SourceRepos: []string{"*"},
Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}},
},
}
objects = append(objects, defaultProj, myProj)
runtimeObjects := make([]runtime.Object, len(objects))
for i := range objects {
runtimeObjects[i] = objects[i]
}
fakeAppsClientset := apps.NewSimpleClientset(runtimeObjects...)
factory := appinformer.NewSharedInformerFactoryWithOptions(fakeAppsClientset, 0, appinformer.WithNamespace(namespace), appinformer.WithTweakListOptions(func(_ *metav1.ListOptions) {}))
fakeProjLister := factory.Argoproj().V1alpha1().AppProjects().Lister().AppProjects(testNamespace)
enforcer := rbac.NewEnforcer(kubeclientset, testNamespace, common.ArgoCDRBACConfigMapName, nil)
f(enforcer)
enforcer.SetClaimsEnforcerFunc(rbacpolicy.NewRBACPolicyEnforcer(enforcer, fakeProjLister).EnforceClaims)
// populate the app informer with the fake objects
appInformer := factory.Argoproj().V1alpha1().Applications().Informer()
// TODO(jessesuen): probably should return cancel function so tests can stop background informer
// ctx, cancel := context.WithCancel(context.Background())
go appInformer.Run(ctx.Done())
if !k8scache.WaitForCacheSync(ctx.Done(), appInformer.HasSynced) {
panic("Timed out waiting for caches to sync")
}
// populate the appset informer with the fake objects
appsetInformer := factory.Argoproj().V1alpha1().ApplicationSets().Informer()
go appsetInformer.Run(ctx.Done())
if !k8scache.WaitForCacheSync(ctx.Done(), appsetInformer.HasSynced) {
t.Fatal("Timed out waiting for caches to sync")
}
scheme := runtime.NewScheme()
err = appsv1.AddToScheme(scheme)
require.NoError(t, err)
err = corev1.AddToScheme(scheme)
require.NoError(t, err)
// Add the fake cluster secret so the ClusterGenerator can find it via controller-runtime client
fakeClusterSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-cluster-api.example.com-2aborni",
Namespace: testNamespace,
Labels: map[string]string{
common.LabelKeySecretType: common.LabelValueSecretTypeCluster,
},
},
Data: map[string][]byte{
"name": []byte("fake-cluster"),
"server": []byte("https://cluster-api.example.com"),
"config": []byte("{}"),
},
}
objects = append(objects, fakeClusterSecret)
crClient := cr_fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build()
projInformer := factory.Argoproj().V1alpha1().AppProjects().Informer()
go projInformer.Run(ctx.Done())
if !k8scache.WaitForCacheSync(ctx.Done(), projInformer.HasSynced) {
panic("Timed out waiting for caches to sync")
}
clusterInformer, err := settings.NewClusterInformer(kubeclientset, testNamespace)
if err != nil {
t.Fatal(err)
}
defer test.StartInformer(clusterInformer)()
server := NewServer(
db,
kubeclientset,
nil,
crClient,
enforcer,
nil,
fakeAppsClientset,
appInformer,
factory.Argoproj().V1alpha1().ApplicationSets().Lister(),
testNamespace,
sync.NewKeyLock(),
[]string{testNamespace, "external-namespace"},
true,
true,
"",
[]string{},
true,
true,
testEnableEventList,
clusterInformer,
)
return server.(*Server), kubeclientset
}
func newTestAppSet(opts ...func(appset *appsv1.ApplicationSet)) *appsv1.ApplicationSet {
appset := appsv1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
},
Spec: appsv1.ApplicationSetSpec{
Template: appsv1.ApplicationSetTemplate{
Spec: appsv1.ApplicationSpec{
Project: "default",
},
},
},
}
for i := range opts {
opts[i](&appset)
}
return &appset
}
func testListAppsetsWithLabels(t *testing.T, appsetQuery applicationset.ApplicationSetListQuery, appServer *Server) {
t.Helper()
validTests := []struct {
testName string
label string
expectedResult []string
}{
{
testName: "Equality based filtering using '=' operator",
label: "key1=value1",
expectedResult: []string{"AppSet1"},
},
{
testName: "Equality based filtering using '==' operator",
label: "key1==value1",
expectedResult: []string{"AppSet1"},
},
{
testName: "Equality based filtering using '!=' operator",
label: "key1!=value1",
expectedResult: []string{"AppSet2", "AppSet3"},
},
{
testName: "Set based filtering using 'in' operator",
label: "key1 in (value1, value3)",
expectedResult: []string{"AppSet1", "AppSet3"},
},
{
testName: "Set based filtering using 'notin' operator",
label: "key1 notin (value1, value3)",
expectedResult: []string{"AppSet2"},
},
{
testName: "Set based filtering using 'exists' operator",
label: "key1",
expectedResult: []string{"AppSet1", "AppSet2", "AppSet3"},
},
{
testName: "Set based filtering using 'not exists' operator",
label: "!key2",
expectedResult: []string{"AppSet2", "AppSet3"},
},
}
// test valid scenarios
for _, validTest := range validTests {
t.Run(validTest.testName, func(t *testing.T) {
appsetQuery.Selector = validTest.label
res, err := appServer.List(t.Context(), &appsetQuery)
require.NoError(t, err)
apps := []string{}
for i := range res.Items {
apps = append(apps, res.Items[i].Name)
}
assert.Equal(t, validTest.expectedResult, apps)
})
}
invalidTests := []struct {
testName string
label string
errorMesage string
}{
{
testName: "Set based filtering using '>' operator",
label: "key1>value1",
errorMesage: "error parsing the selector",
},
{
testName: "Set based filtering using '<' operator",
label: "key1<value1",
errorMesage: "error parsing the selector",
},
}
// test invalid scenarios
for _, invalidTest := range invalidTests {
t.Run(invalidTest.testName, func(t *testing.T) {
appsetQuery.Selector = invalidTest.label
_, err := appServer.List(t.Context(), &appsetQuery)
assert.ErrorContains(t, err, invalidTest.errorMesage)
})
}
}
func TestListAppSetsInNamespaceWithLabels(t *testing.T) {
testNamespace := "test-namespace"
appSetServer := newTestAppSetServer(t, newTestAppSet(func(appset *appsv1.ApplicationSet) {
appset.Name = "AppSet1"
appset.Namespace = testNamespace
appset.SetLabels(map[string]string{"key1": "value1", "key2": "value1"})
}), newTestAppSet(func(appset *appsv1.ApplicationSet) {
appset.Name = "AppSet2"
appset.Namespace = testNamespace
appset.SetLabels(map[string]string{"key1": "value2"})
}), newTestAppSet(func(appset *appsv1.ApplicationSet) {
appset.Name = "AppSet3"
appset.Namespace = testNamespace
appset.SetLabels(map[string]string{"key1": "value3"})
}))
appSetServer.enabledNamespaces = []string{testNamespace}
appsetQuery := applicationset.ApplicationSetListQuery{AppsetNamespace: testNamespace}
testListAppsetsWithLabels(t, appsetQuery, appSetServer)
}
func TestListAppSetsInDefaultNSWithLabels(t *testing.T) {
appSetServer := newTestAppSetServer(t, newTestAppSet(func(appset *appsv1.ApplicationSet) {
appset.Name = "AppSet1"
appset.SetLabels(map[string]string{"key1": "value1", "key2": "value1"})
}), newTestAppSet(func(appset *appsv1.ApplicationSet) {
appset.Name = "AppSet2"
appset.SetLabels(map[string]string{"key1": "value2"})
}), newTestAppSet(func(appset *appsv1.ApplicationSet) {
appset.Name = "AppSet3"
appset.SetLabels(map[string]string{"key1": "value3"})
}))
appsetQuery := applicationset.ApplicationSetListQuery{}
testListAppsetsWithLabels(t, appsetQuery, appSetServer)
}
// This test covers https://github.com/argoproj/argo-cd/issues/15429
// If the namespace isn't provided during listing action, argocd's
// default namespace must be used and not all the namespaces
func TestListAppSetsWithoutNamespace(t *testing.T) {
testNamespace := "test-namespace"
appSetServer := newTestNamespacedAppSetServer(t, newTestAppSet(func(appset *appsv1.ApplicationSet) {
appset.Name = "AppSet1"
appset.Namespace = testNamespace
appset.SetLabels(map[string]string{"key1": "value1", "key2": "value1"})
}), newTestAppSet(func(appset *appsv1.ApplicationSet) {
appset.Name = "AppSet2"
appset.Namespace = testNamespace
appset.SetLabels(map[string]string{"key1": "value2"})
}), newTestAppSet(func(appset *appsv1.ApplicationSet) {
appset.Name = "AppSet3"
appset.Namespace = testNamespace
appset.SetLabels(map[string]string{"key1": "value3"})
}))
appSetServer.enabledNamespaces = []string{testNamespace}
appsetQuery := applicationset.ApplicationSetListQuery{}
res, err := appSetServer.List(t.Context(), &appsetQuery)
require.NoError(t, err)
assert.Empty(t, res.Items)
}
func TestCreateAppSet(t *testing.T) {
testAppSet := newTestAppSet()
appServer := newTestAppSetServer(t)
testAppSet.Spec.Generators = []appsv1.ApplicationSetGenerator{
{
List: &appsv1.ListGenerator{},
},
}
createReq := applicationset.ApplicationSetCreateRequest{
Applicationset: testAppSet,
}
_, err := appServer.Create(t.Context(), &createReq)
require.NoError(t, err)
}
func TestCreateAppSetTemplatedProject(t *testing.T) {
testAppSet := newTestAppSet()
appServer := newTestAppSetServer(t)
testAppSet.Spec.Template.Spec.Project = "{{ .project }}"
createReq := applicationset.ApplicationSetCreateRequest{
Applicationset: testAppSet,
}
_, err := appServer.Create(t.Context(), &createReq)
assert.EqualError(t, err, "error validating ApplicationSets: the Argo CD API does not currently support creating ApplicationSets with templated `project` fields")
}
func TestCreateAppSetWrongNamespace(t *testing.T) {
testAppSet := newTestAppSet()
appServer := newTestAppSetServer(t)
testAppSet.Namespace = "NOT-ALLOWED"
createReq := applicationset.ApplicationSetCreateRequest{
Applicationset: testAppSet,
}
_, err := appServer.Create(t.Context(), &createReq)
assert.EqualError(t, err, "namespace 'NOT-ALLOWED' is not permitted")
}
func TestCreateAppSetDryRun(t *testing.T) {
testAppSet := newTestAppSet()
appServer := newTestAppSetServer(t)
testAppSet.Spec.Template.Name = "{{name}}"
testAppSet.Spec.Generators = []appsv1.ApplicationSetGenerator{
{
List: &appsv1.ListGenerator{
Elements: []apiextensionsv1.JSON{{Raw: []byte(`{"name": "a"}`)}, {Raw: []byte(`{"name": "b"}`)}},
},
},
}
createReq := applicationset.ApplicationSetCreateRequest{
Applicationset: testAppSet,
DryRun: true,
}
result, err := appServer.Create(t.Context(), &createReq)
require.NoError(t, err)
assert.Len(t, result.Status.Resources, 2)
// Sort resulting application by name
sort.Slice(result.Status.Resources, func(i, j int) bool {
return result.Status.Resources[i].Name < result.Status.Resources[j].Name
})
assert.Equal(t, "a", result.Status.Resources[0].Name)
assert.Equal(t, testAppSet.Namespace, result.Status.Resources[0].Namespace)
assert.Equal(t, "b", result.Status.Resources[1].Name)
assert.Equal(t, testAppSet.Namespace, result.Status.Resources[1].Namespace)
}
func TestCreateAppSetDryRunWithDuplicate(t *testing.T) {
testAppSet := newTestAppSet()
appServer := newTestAppSetServer(t)
testAppSet.Spec.Template.Name = "{{name}}"
testAppSet.Spec.Generators = []appsv1.ApplicationSetGenerator{
{
List: &appsv1.ListGenerator{
Elements: []apiextensionsv1.JSON{{Raw: []byte(`{"name": "a"}`)}, {Raw: []byte(`{"name": "a"}`)}},
},
},
}
createReq := applicationset.ApplicationSetCreateRequest{
Applicationset: testAppSet,
DryRun: true,
}
result, err := appServer.Create(t.Context(), &createReq)
require.NoError(t, err)
assert.Len(t, result.Status.Resources, 1)
assert.Equal(t, "a", result.Status.Resources[0].Name)
assert.Equal(t, testAppSet.Namespace, result.Status.Resources[0].Namespace)
}
func TestGetAppSet(t *testing.T) {
appSet1 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
appset.Name = "AppSet1"
})
appSet2 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
appset.Name = "AppSet2"
})
appSet3 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
appset.Name = "AppSet3"
})
t.Run("Get in default namespace", func(t *testing.T) {
appSetServer := newTestAppSetServer(t, appSet1, appSet2, appSet3)
appsetQuery := applicationset.ApplicationSetGetQuery{Name: "AppSet1"}
res, err := appSetServer.Get(t.Context(), &appsetQuery)
require.NoError(t, err)
assert.Equal(t, "AppSet1", res.Name)
})
t.Run("Get in named namespace", func(t *testing.T) {
appSetServer := newTestAppSetServer(t, appSet1, appSet2, appSet3)
appsetQuery := applicationset.ApplicationSetGetQuery{Name: "AppSet1", AppsetNamespace: testNamespace}
res, err := appSetServer.Get(t.Context(), &appsetQuery)
require.NoError(t, err)
assert.Equal(t, "AppSet1", res.Name)
})
t.Run("Get in not allowed namespace", func(t *testing.T) {
appSetServer := newTestAppSetServer(t, appSet1, appSet2, appSet3)
appsetQuery := applicationset.ApplicationSetGetQuery{Name: "AppSet1", AppsetNamespace: "NOT-ALLOWED"}
_, err := appSetServer.Get(t.Context(), &appsetQuery)
assert.EqualError(t, err, "namespace 'NOT-ALLOWED' is not permitted")
})
}
func TestDeleteAppSet(t *testing.T) {
appSet1 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
appset.Name = "AppSet1"
})
appSet2 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
appset.Name = "AppSet2"
})
appSet3 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
appset.Name = "AppSet3"
})
t.Run("Delete in default namespace", func(t *testing.T) {
appSetServer := newTestAppSetServer(t, appSet1, appSet2, appSet3)
appsetQuery := applicationset.ApplicationSetDeleteRequest{Name: "AppSet1"}
res, err := appSetServer.Delete(t.Context(), &appsetQuery)
require.NoError(t, err)
assert.Equal(t, &applicationset.ApplicationSetResponse{}, res)
})
t.Run("Delete in named namespace", func(t *testing.T) {
appSetServer := newTestAppSetServer(t, appSet1, appSet2, appSet3)
appsetQuery := applicationset.ApplicationSetDeleteRequest{Name: "AppSet1", AppsetNamespace: testNamespace}
res, err := appSetServer.Delete(t.Context(), &appsetQuery)
require.NoError(t, err)
assert.Equal(t, &applicationset.ApplicationSetResponse{}, res)
})
}
func TestUpdateAppSet(t *testing.T) {
appSet := newTestAppSet(func(appset *appsv1.ApplicationSet) {
appset.Annotations = map[string]string{
"annotation-key1": "annotation-value1",
"annotation-key2": "annotation-value2",
}
appset.Labels = map[string]string{
"label-key1": "label-value1",
"label-key2": "label-value2",
}
appset.Finalizers = []string{"finalizer"}
})
newAppSet := newTestAppSet(func(appset *appsv1.ApplicationSet) {
appset.Annotations = map[string]string{
"annotation-key1": "annotation-value1-updated",
}
appset.Labels = map[string]string{
"label-key1": "label-value1-updated",
}
appset.Finalizers = []string{"finalizer-updated"}
})
t.Run("Update merge", func(t *testing.T) {
appServer := newTestAppSetServer(t, appSet)
updated, err := appServer.updateAppSet(t.Context(), appSet, newAppSet, true)
require.NoError(t, err)
assert.Equal(t, map[string]string{
"annotation-key1": "annotation-value1-updated",
"annotation-key2": "annotation-value2",
}, updated.Annotations)
assert.Equal(t, map[string]string{
"label-key1": "label-value1-updated",
"label-key2": "label-value2",
}, updated.Labels)
assert.Equal(t, []string{"finalizer-updated"}, updated.Finalizers)
})
t.Run("Update no merge", func(t *testing.T) {
appServer := newTestAppSetServer(t, appSet)
updated, err := appServer.updateAppSet(t.Context(), appSet, newAppSet, false)
require.NoError(t, err)
assert.Equal(t, map[string]string{
"annotation-key1": "annotation-value1-updated",
}, updated.Annotations)
assert.Equal(t, map[string]string{
"label-key1": "label-value1-updated",
}, updated.Labels)
})
}
func TestUpsertAppSet(t *testing.T) {
name := "test"
ns := "external-namespace"
appSet := newTestAppSet(func(appset *appsv1.ApplicationSet) {
appset.Annotations = map[string]string{
"annotation-key1": "annotation-value1",
"annotation-key2": "annotation-value2",
}
appset.Labels = map[string]string{
"label-key1": "label-value1",
"label-key2": "label-value2",
}
appset.Name = name
appset.Namespace = ns
appset.Finalizers = []string{"finalizer"}
})
updatedAppSet := newTestAppSet(func(appset *appsv1.ApplicationSet) {
appset.Annotations = map[string]string{
"annotation-key1": "annotation-value1-updated",
}
appset.Labels = map[string]string{
"label-key1": "label-value1-updated",
}
appset.Name = name
appset.Namespace = ns
appset.Finalizers = []string{"finalizer-updated"}
})
t.Run("Upsert", func(t *testing.T) {
appServer := newTestAppSetServer(t, appSet)
updated, err := appServer.Create(t.Context(), &applicationset.ApplicationSetCreateRequest{
Applicationset: updatedAppSet,
Upsert: true,
})
require.NoError(t, err)
assert.Equal(t, map[string]string{
"annotation-key1": "annotation-value1-updated",
"annotation-key2": "annotation-value2",
}, updated.Annotations)
assert.Equal(t, map[string]string{
"label-key1": "label-value1-updated",
"label-key2": "label-value2",
}, updated.Labels)
assert.Equal(t, []string{"finalizer-updated"}, updated.Finalizers)
})
}
func TestResourceTree(t *testing.T) {
appSet1 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
appset.Name = "AppSet1"
appset.Status.Resources = []appsv1.ResourceStatus{
{
Name: "app1",
Kind: "Application",
Group: "argoproj.io",
Version: "v1alpha1",
Namespace: "default",
Health: &appsv1.HealthStatus{
Status: health.HealthStatusHealthy,
Message: "OK",
},
Status: appsv1.SyncStatusCodeSynced,
},
}
})
appSet2 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
appset.Name = "AppSet2"
})
appSet3 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
appset.Name = "AppSet3"
})
expectedTree := &appsv1.ApplicationSetTree{
Nodes: []appsv1.ResourceNode{
{
ResourceRef: appsv1.ResourceRef{
Kind: "Application",
Group: "argoproj.io",
Version: "v1alpha1",
Namespace: "default",
Name: "app1",
},
ParentRefs: []appsv1.ResourceRef{
{
Kind: "ApplicationSet",
Group: "argoproj.io",
Version: "v1alpha1",
Namespace: "default",
Name: "AppSet1",
},
},
Health: &appsv1.HealthStatus{
Status: health.HealthStatusHealthy,
Message: "OK",
},
},
},
}
t.Run("ResourceTree in default namespace", func(t *testing.T) {
appSetServer := newTestAppSetServer(t, appSet1, appSet2, appSet3)
appsetQuery := applicationset.ApplicationSetTreeQuery{Name: "AppSet1"}
res, err := appSetServer.ResourceTree(t.Context(), &appsetQuery)
require.NoError(t, err)
assert.Equal(t, expectedTree, res)
})
t.Run("ResourceTree in named namespace", func(t *testing.T) {
appSetServer := newTestAppSetServer(t, appSet1, appSet2, appSet3)
appsetQuery := applicationset.ApplicationSetTreeQuery{Name: "AppSet1", AppsetNamespace: testNamespace}
res, err := appSetServer.ResourceTree(t.Context(), &appsetQuery)
require.NoError(t, err)
assert.Equal(t, expectedTree, res)
})
t.Run("ResourceTree in not allowed namespace", func(t *testing.T) {
appSetServer := newTestAppSetServer(t, appSet1, appSet2, appSet3)
appsetQuery := applicationset.ApplicationSetTreeQuery{Name: "AppSet1", AppsetNamespace: "NOT-ALLOWED"}
_, err := appSetServer.ResourceTree(t.Context(), &appsetQuery)
assert.EqualError(t, err, "namespace 'NOT-ALLOWED' is not permitted")
})
}
func TestListResourceEvents(t *testing.T) {
appSet1 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
appset.Name = "AppSet1"
appset.UID = "test-uid-123"
})
appSet2 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
appset.Name = "AppSet2"
})
t.Run("ListResourceEvents in default namespace", func(t *testing.T) {
appSetServer := newTestAppSetServer(t, appSet1, appSet2)
appsetQuery := applicationset.ApplicationSetGetQuery{Name: "AppSet1"}
res, err := appSetServer.ListResourceEvents(t.Context(), &appsetQuery)
require.NoError(t, err)
// No events exist in the fake client, so we expect an empty list
assert.Empty(t, res.Items)
})
t.Run("ListResourceEvents in named namespace", func(t *testing.T) {
appSetServer := newTestAppSetServer(t, appSet1, appSet2)
appsetQuery := applicationset.ApplicationSetGetQuery{Name: "AppSet1", AppsetNamespace: testNamespace}
res, err := appSetServer.ListResourceEvents(t.Context(), &appsetQuery)
require.NoError(t, err)
assert.Empty(t, res.Items)
})
t.Run("ListResourceEvents in not allowed namespace", func(t *testing.T) {
appSetServer := newTestAppSetServer(t, appSet1, appSet2)
appsetQuery := applicationset.ApplicationSetGetQuery{Name: "AppSet1", AppsetNamespace: "NOT-ALLOWED"}
_, err := appSetServer.ListResourceEvents(t.Context(), &appsetQuery)
assert.EqualError(t, err, "namespace 'NOT-ALLOWED' is not permitted")
})
t.Run("ListResourceEvents for non-existent appset", func(t *testing.T) {
appSetServer := newTestAppSetServer(t, appSet1, appSet2)
appsetQuery := applicationset.ApplicationSetGetQuery{Name: "DoesNotExist"}
_, err := appSetServer.ListResourceEvents(t.Context(), &appsetQuery)
require.Error(t, err)
assert.Contains(t, err.Error(), "error getting ApplicationSet")
})
t.Run("ListResourceEvents with events returns non-empty list", func(t *testing.T) {
appSetServer, kubeclientset := newTestAppSetServerWithK8sClient(t, appSet1, appSet2)
// Create events after server creation using the kubeclientset
_, err := kubeclientset.CoreV1().Events(testNamespace).Create(t.Context(), &corev1.Event{
ObjectMeta: metav1.ObjectMeta{
Name: "appset1-event-1",
Namespace: testNamespace,
},
InvolvedObject: corev1.ObjectReference{
Name: "AppSet1",
Namespace: testNamespace,
UID: "test-uid-123",
},
Reason: "Created",
Message: "ApplicationSet created successfully",
Type: corev1.EventTypeNormal,
}, metav1.CreateOptions{})
require.NoError(t, err)
_, err = kubeclientset.CoreV1().Events(testNamespace).Create(t.Context(), &corev1.Event{
ObjectMeta: metav1.ObjectMeta{
Name: "appset1-event-2",
Namespace: testNamespace,
},
InvolvedObject: corev1.ObjectReference{
Name: "AppSet1",
Namespace: testNamespace,
UID: "test-uid-123",
},
Reason: "Updated",
Message: "ApplicationSet updated successfully",
Type: corev1.EventTypeNormal,
}, metav1.CreateOptions{})
require.NoError(t, err)
appsetQuery := applicationset.ApplicationSetGetQuery{Name: "AppSet1"}
res, err := appSetServer.ListResourceEvents(t.Context(), &appsetQuery)
require.NoError(t, err)
assert.NotEmpty(t, res.Items)
assert.Len(t, res.Items, 2)
// Verify the returned events have the expected content
eventNames := []string{res.Items[0].Name, res.Items[1].Name}
assert.Contains(t, eventNames, "appset1-event-1")
assert.Contains(t, eventNames, "appset1-event-2")
})
}
func TestAppSet_Generate_Cluster(t *testing.T) {
appSet1 := newTestAppSet(func(appset *appsv1.ApplicationSet) {
appset.Name = "AppSet1"
appset.Spec.Template.Name = "{{name}}"
appset.Spec.Generators = []appsv1.ApplicationSetGenerator{
{
Clusters: &appsv1.ClusterGenerator{},
},
}
})
t.Run("Generate in default namespace", func(t *testing.T) {
appSetServer := newTestAppSetServer(t, appSet1)
appsetQuery := applicationset.ApplicationSetGenerateRequest{
ApplicationSet: appSet1,
}
res, err := appSetServer.Generate(t.Context(), &appsetQuery)
require.NoError(t, err)
require.Len(t, res.Applications, 2)
assert.Equal(t, "fake-cluster", res.Applications[0].Name)
assert.Equal(t, "in-cluster", res.Applications[1].Name)
})
t.Run("Generate in different namespace", func(t *testing.T) {
appSetServer := newTestAppSetServer(t, appSet1)
appSet1Ns := appSet1.DeepCopy()
appSet1Ns.Namespace = "external-namespace"
appsetQuery := applicationset.ApplicationSetGenerateRequest{ApplicationSet: appSet1Ns}
res, err := appSetServer.Generate(t.Context(), &appsetQuery)
require.NoError(t, err)
require.Len(t, res.Applications, 2)
assert.Equal(t, "fake-cluster", res.Applications[0].Name)
assert.Equal(t, "in-cluster", res.Applications[1].Name)
})
t.Run("Generate in not allowed namespace", func(t *testing.T) {
appSetServer := newTestAppSetServer(t, appSet1)
appSet1Ns := appSet1.DeepCopy()
appSet1Ns.Namespace = "NOT-ALLOWED"
appsetQuery := applicationset.ApplicationSetGenerateRequest{ApplicationSet: appSet1Ns}
_, err := appSetServer.Generate(t.Context(), &appsetQuery)
assert.EqualError(t, err, "namespace 'NOT-ALLOWED' is not permitted")
})
}