package application import ( "context" stderrors "errors" "fmt" "io" "slices" "strconv" "strings" "sync/atomic" "testing" "time" "k8s.io/apimachinery/pkg/labels" "github.com/argoproj/gitops-engine/pkg/health" synccommon "github.com/argoproj/gitops-engine/pkg/sync/common" "github.com/argoproj/gitops-engine/pkg/utils/kube" "github.com/argoproj/gitops-engine/pkg/utils/kube/kubetest" "github.com/argoproj/pkg/v2/sync" "github.com/golang-jwt/jwt/v5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" appsv1 "k8s.io/api/apps/v1" k8sbatchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/rest" kubetesting "k8s.io/client-go/testing" k8scache "k8s.io/client-go/tools/cache" "k8s.io/utils/ptr" "sigs.k8s.io/yaml" "github.com/argoproj/argo-cd/v3/common" "github.com/argoproj/argo-cd/v3/pkg/apiclient/application" "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/reposerver/apiclient" "github.com/argoproj/argo-cd/v3/reposerver/apiclient/mocks" servercache "github.com/argoproj/argo-cd/v3/server/cache" "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/cache" "github.com/argoproj/argo-cd/v3/util/cache/appstate" "github.com/argoproj/argo-cd/v3/util/db" "github.com/argoproj/argo-cd/v3/util/grpc" "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() type broadcasterMock struct { objects []runtime.Object } func (b broadcasterMock) Subscribe(ch chan *v1alpha1.ApplicationWatchEvent, _ ...func(event *v1alpha1.ApplicationWatchEvent) bool) func() { // Simulate the broadcaster notifying the subscriber of an application update. // The second parameter to Subscribe is filters. For the purposes of tests, we ignore the filters. Future tests // might require implementing those. go func() { for _, obj := range b.objects { app, ok := obj.(*v1alpha1.Application) if ok { oldVersion, err := strconv.Atoi(app.ResourceVersion) if err != nil { oldVersion = 0 } clonedApp := app.DeepCopy() clonedApp.ResourceVersion = strconv.Itoa(oldVersion + 1) ch <- &v1alpha1.ApplicationWatchEvent{Type: watch.Added, Application: *clonedApp} } } }() return func() {} } func (broadcasterMock) OnAdd(any, bool) {} func (broadcasterMock) OnUpdate(any, any) {} func (broadcasterMock) OnDelete(any) {} func fakeRepo() *v1alpha1.Repository { return &v1alpha1.Repository{ Repo: fakeRepoURL, } } func fakeCluster() *v1alpha1.Cluster { return &v1alpha1.Cluster{ Server: "https://cluster-api.example.com", Name: "fake-cluster", Config: v1alpha1.ClusterConfig{}, } } func fakeResolveRevisionResponse() *apiclient.ResolveRevisionResponse { return &apiclient.ResolveRevisionResponse{ Revision: "f9ba9e98119bf8c1176fbd65dbae26a71d044add", AmbiguousRevision: "HEAD (f9ba9e98119bf8c1176fbd65dbae26a71d044add)", } } func fakeResolveRevisionResponseHelm() *apiclient.ResolveRevisionResponse { return &apiclient.ResolveRevisionResponse{ Revision: "0.7.*", AmbiguousRevision: "0.7.* (0.7.2)", } } func fakeRepoServerClient(isHelm bool) *mocks.RepoServerServiceClient { mockRepoServiceClient := &mocks.RepoServerServiceClient{} mockRepoServiceClient.EXPECT().GenerateManifest(mock.Anything, mock.Anything).Return(&apiclient.ManifestResponse{}, nil) mockRepoServiceClient.EXPECT().GetAppDetails(mock.Anything, mock.Anything).Return(&apiclient.RepoAppDetailsResponse{}, nil) mockRepoServiceClient.EXPECT().TestRepository(mock.Anything, mock.Anything).Return(&apiclient.TestRepositoryResponse{}, nil) mockRepoServiceClient.EXPECT().GetRevisionMetadata(mock.Anything, mock.Anything).Return(&v1alpha1.RevisionMetadata{}, nil) mockWithFilesClient := &mocks.RepoServerService_GenerateManifestWithFilesClient{} mockWithFilesClient.EXPECT().Send(mock.Anything).Return(nil).Maybe() mockWithFilesClient.EXPECT().CloseAndRecv().Return(&apiclient.ManifestResponse{}, nil).Maybe() mockRepoServiceClient.EXPECT().GenerateManifestWithFiles(mock.Anything, mock.Anything).Return(mockWithFilesClient, nil) mockRepoServiceClient.EXPECT().GetRevisionChartDetails(mock.Anything, mock.Anything).Return(&v1alpha1.ChartDetails{}, nil) if isHelm { mockRepoServiceClient.EXPECT().ResolveRevision(mock.Anything, mock.Anything).Return(fakeResolveRevisionResponseHelm(), nil) } else { mockRepoServiceClient.EXPECT().ResolveRevision(mock.Anything, mock.Anything).Return(fakeResolveRevisionResponse(), nil) } return mockRepoServiceClient } // return an ApplicationServiceServer which returns fake data func newTestAppServer(t *testing.T, objects ...runtime.Object) *Server { t.Helper() f := func(enf *rbac.Enforcer) { _ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV) enf.SetDefaultRole("role:admin") } return newTestAppServerWithEnforcerConfigure(t, f, map[string]string{}, objects...) } func newTestAppServerWithEnforcerConfigure(t *testing.T, f func(*rbac.Enforcer), additionalConfig map[string]string, objects ...runtime.Object) *Server { 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", }, }, Data: additionalConfig, }, &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) mockRepoClient := &mocks.Clientset{RepoServerServiceClient: fakeRepoServerClient(false)} defaultProj := &v1alpha1.AppProject{ ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "default"}, Spec: v1alpha1.AppProjectSpec{ SourceRepos: []string{"*"}, Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, }, } myProj := &v1alpha1.AppProject{ ObjectMeta: metav1.ObjectMeta{Name: "my-proj", Namespace: "default"}, Spec: v1alpha1.AppProjectSpec{ SourceRepos: []string{"*"}, Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, }, } projWithSyncWindows := &v1alpha1.AppProject{ ObjectMeta: metav1.ObjectMeta{Name: "proj-maint", Namespace: "default"}, Spec: v1alpha1.AppProjectSpec{ SourceRepos: []string{"*"}, Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, SyncWindows: v1alpha1.SyncWindows{}, }, } matchingWindow := &v1alpha1.SyncWindow{ Kind: "allow", Schedule: "* * * * *", Duration: "1h", Applications: []string{"test-app"}, } projWithSyncWindows.Spec.SyncWindows = append(projWithSyncWindows.Spec.SyncWindows, matchingWindow) objects = append(objects, defaultProj, myProj, projWithSyncWindows) fakeAppsClientset := apps.NewSimpleClientset(objects...) factory := appinformer.NewSharedInformerFactoryWithOptions(fakeAppsClientset, 0, appinformer.WithNamespace(""), 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) settingsMgr := settings.NewSettingsManager(ctx, kubeclientset, testNamespace) // 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(t.Context()) go appInformer.Run(ctx.Done()) if !k8scache.WaitForCacheSync(ctx.Done(), appInformer.HasSynced) { panic("Timed out waiting for caches to sync") } 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") } broadcaster := broadcasterMock{ objects: objects, } appStateCache := appstate.NewCache(cache.NewCache(cache.NewInMemoryCache(time.Hour)), time.Hour) // pre-populate the app cache for _, obj := range objects { app, ok := obj.(*v1alpha1.Application) if ok { err := appStateCache.SetAppManagedResources(app.Name, []*v1alpha1.ResourceDiff{}) require.NoError(t, err) // Pre-populate the resource tree based on the app's resources. nodes := make([]v1alpha1.ResourceNode, len(app.Status.Resources)) for i, res := range app.Status.Resources { nodes[i] = v1alpha1.ResourceNode{ ResourceRef: v1alpha1.ResourceRef{ Group: res.Group, Kind: res.Kind, Version: res.Version, Name: res.Name, Namespace: res.Namespace, UID: "fake", }, } } err = appStateCache.SetAppResourcesTree(app.Name, &v1alpha1.ApplicationTree{ Nodes: nodes, }) require.NoError(t, err) } } appCache := servercache.NewCache(appStateCache, time.Hour, time.Hour) kubectl := &kubetest.MockKubectlCmd{} kubectl = kubectl.WithGetResourceFunc(func(_ context.Context, _ *rest.Config, gvk schema.GroupVersionKind, name string, namespace string) (*unstructured.Unstructured, error) { for _, obj := range objects { if obj.GetObjectKind().GroupVersionKind().GroupKind() == gvk.GroupKind() { if obj, ok := obj.(*unstructured.Unstructured); ok && obj.GetName() == name && obj.GetNamespace() == namespace { return obj, nil } } } return nil, nil }) server, _ := NewServer( testNamespace, kubeclientset, fakeAppsClientset, factory.Argoproj().V1alpha1().Applications().Lister(), appInformer, broadcaster, mockRepoClient, appCache, kubectl, db, enforcer, sync.NewKeyLock(), settingsMgr, projInformer, []string{}, testEnableEventList, true, ) return server.(*Server) } // return an ApplicationServiceServer which returns fake data func newTestAppServerWithBenchmark(b *testing.B, objects ...runtime.Object) *Server { b.Helper() f := func(enf *rbac.Enforcer) { _ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV) enf.SetDefaultRole("role:admin") } return newTestAppServerWithEnforcerConfigureWithBenchmark(b, f, objects...) } func newTestAppServerWithEnforcerConfigureWithBenchmark(b *testing.B, f func(*rbac.Enforcer), objects ...runtime.Object) *Server { b.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 := b.Context() db := db.NewDB(testNamespace, settings.NewSettingsManager(ctx, kubeclientset, testNamespace), kubeclientset) _, err := db.CreateRepository(ctx, fakeRepo()) require.NoError(b, err) _, err = db.CreateCluster(ctx, fakeCluster()) require.NoError(b, err) mockRepoClient := &mocks.Clientset{RepoServerServiceClient: fakeRepoServerClient(false)} defaultProj := &v1alpha1.AppProject{ ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "default"}, Spec: v1alpha1.AppProjectSpec{ SourceRepos: []string{"*"}, Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, }, } myProj := &v1alpha1.AppProject{ ObjectMeta: metav1.ObjectMeta{Name: "my-proj", Namespace: "default"}, Spec: v1alpha1.AppProjectSpec{ SourceRepos: []string{"*"}, Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, }, } projWithSyncWindows := &v1alpha1.AppProject{ ObjectMeta: metav1.ObjectMeta{Name: "proj-maint", Namespace: "default"}, Spec: v1alpha1.AppProjectSpec{ SourceRepos: []string{"*"}, Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, SyncWindows: v1alpha1.SyncWindows{}, }, } matchingWindow := &v1alpha1.SyncWindow{ Kind: "allow", Schedule: "* * * * *", Duration: "1h", Applications: []string{"test-app"}, } projWithSyncWindows.Spec.SyncWindows = append(projWithSyncWindows.Spec.SyncWindows, matchingWindow) objects = append(objects, defaultProj, myProj, projWithSyncWindows) fakeAppsClientset := apps.NewSimpleClientset(objects...) factory := appinformer.NewSharedInformerFactoryWithOptions(fakeAppsClientset, 0, appinformer.WithNamespace(""), 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) settingsMgr := settings.NewSettingsManager(ctx, kubeclientset, testNamespace) // populate the app informer with the fake objects appInformer := factory.Argoproj().V1alpha1().Applications().Informer() go appInformer.Run(ctx.Done()) if !k8scache.WaitForCacheSync(ctx.Done(), appInformer.HasSynced) { panic("Timed out waiting for caches to sync") } 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") } broadcaster := broadcasterMock{ objects: objects, } appStateCache := appstate.NewCache(cache.NewCache(cache.NewInMemoryCache(time.Hour)), time.Hour) // pre-populate the app cache for _, obj := range objects { app, ok := obj.(*v1alpha1.Application) if ok { err := appStateCache.SetAppManagedResources(app.Name, []*v1alpha1.ResourceDiff{}) require.NoError(b, err) // Pre-populate the resource tree based on the app's resources. nodes := make([]v1alpha1.ResourceNode, len(app.Status.Resources)) for i, res := range app.Status.Resources { nodes[i] = v1alpha1.ResourceNode{ ResourceRef: v1alpha1.ResourceRef{ Group: res.Group, Kind: res.Kind, Version: res.Version, Name: res.Name, Namespace: res.Namespace, UID: "fake", }, } } err = appStateCache.SetAppResourcesTree(app.Name, &v1alpha1.ApplicationTree{ Nodes: nodes, }) require.NoError(b, err) } } appCache := servercache.NewCache(appStateCache, time.Hour, time.Hour) kubectl := &kubetest.MockKubectlCmd{} kubectl = kubectl.WithGetResourceFunc(func(_ context.Context, _ *rest.Config, gvk schema.GroupVersionKind, name string, namespace string) (*unstructured.Unstructured, error) { for _, obj := range objects { if obj.GetObjectKind().GroupVersionKind().GroupKind() == gvk.GroupKind() { if obj, ok := obj.(*unstructured.Unstructured); ok && obj.GetName() == name && obj.GetNamespace() == namespace { return obj, nil } } } return nil, nil }) server, _ := NewServer( testNamespace, kubeclientset, fakeAppsClientset, factory.Argoproj().V1alpha1().Applications().Lister(), appInformer, broadcaster, mockRepoClient, appCache, kubectl, db, enforcer, sync.NewKeyLock(), settingsMgr, projInformer, []string{}, testEnableEventList, true, ) return server.(*Server) } const fakeApp = ` apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: test-app namespace: default spec: source: path: some/path repoURL: https://github.com/argoproj/argocd-example-apps.git targetRevision: HEAD ksonnet: environment: default destination: namespace: ` + test.FakeDestNamespace + ` server: https://cluster-api.example.com ` const fakeAppWithDestName = ` apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: test-app namespace: default spec: source: path: some/path repoURL: https://github.com/argoproj/argocd-example-apps.git targetRevision: HEAD ksonnet: environment: default destination: namespace: ` + test.FakeDestNamespace + ` name: fake-cluster ` const fakeAppWithAnnotations = ` apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: test-app namespace: default annotations: test.annotation: test spec: source: path: some/path repoURL: https://github.com/argoproj/argocd-example-apps.git targetRevision: HEAD ksonnet: environment: default destination: namespace: ` + test.FakeDestNamespace + ` server: https://cluster-api.example.com ` func newTestAppWithDestName(opts ...func(app *v1alpha1.Application)) *v1alpha1.Application { return createTestApp(fakeAppWithDestName, opts...) } func newTestApp(opts ...func(app *v1alpha1.Application)) *v1alpha1.Application { return createTestApp(fakeApp, opts...) } func newMultiSourceTestApp(opts ...func(app *v1alpha1.Application)) *v1alpha1.Application { multiSourceApp := newTestApp(opts...) multiSourceApp.Name = "multi-source-app" multiSourceApp.Spec = v1alpha1.ApplicationSpec{ Sources: []v1alpha1.ApplicationSource{ { RepoURL: "https://github.com/argoproj/argocd-example-apps.git", Path: "helm-guestbook", TargetRevision: "appbranch1", }, { RepoURL: "https://github.com/argoproj/argocd-example-apps.git", Path: "kustomize-guestbook", TargetRevision: "appbranch2", }, }, Destination: v1alpha1.ApplicationDestination{ Server: "https://cluster-api.example.com", Namespace: test.FakeDestNamespace, }, } return multiSourceApp } func newTestAppWithAnnotations(opts ...func(app *v1alpha1.Application)) *v1alpha1.Application { return createTestApp(fakeAppWithAnnotations, opts...) } func createTestApp(testApp string, opts ...func(app *v1alpha1.Application)) *v1alpha1.Application { var app v1alpha1.Application err := yaml.Unmarshal([]byte(testApp), &app) if err != nil { panic(err) } for i := range opts { opts[i](&app) } return &app } type TestServerStream struct { ctx context.Context appName string headerSent bool project string } func (t *TestServerStream) SetHeader(metadata.MD) error { return nil } func (t *TestServerStream) SendHeader(metadata.MD) error { return nil } func (t *TestServerStream) SetTrailer(metadata.MD) {} func (t *TestServerStream) Context() context.Context { return t.ctx } func (t *TestServerStream) SendMsg(_ any) error { return nil } func (t *TestServerStream) RecvMsg(_ any) error { return nil } func (t *TestServerStream) SendAndClose(_ *apiclient.ManifestResponse) error { return nil } func (t *TestServerStream) Recv() (*application.ApplicationManifestQueryWithFilesWrapper, error) { if !t.headerSent { t.headerSent = true return &application.ApplicationManifestQueryWithFilesWrapper{Part: &application.ApplicationManifestQueryWithFilesWrapper_Query{ Query: &application.ApplicationManifestQueryWithFiles{ Name: ptr.To(t.appName), Project: ptr.To(t.project), Checksum: ptr.To(""), }, }}, nil } return nil, io.EOF } func (t *TestServerStream) ServerStream() TestServerStream { return TestServerStream{} } type TestResourceTreeServer struct { ctx context.Context } func (t *TestResourceTreeServer) Send(_ *v1alpha1.ApplicationTree) error { return nil } func (t *TestResourceTreeServer) SetHeader(metadata.MD) error { return nil } func (t *TestResourceTreeServer) SendHeader(metadata.MD) error { return nil } func (t *TestResourceTreeServer) SetTrailer(metadata.MD) {} func (t *TestResourceTreeServer) Context() context.Context { return t.ctx } func (t *TestResourceTreeServer) SendMsg(_ any) error { return nil } func (t *TestResourceTreeServer) RecvMsg(_ any) error { return nil } type TestPodLogsServer struct { ctx context.Context } func (t *TestPodLogsServer) Send(_ *application.LogEntry) error { return nil } func (t *TestPodLogsServer) SetHeader(metadata.MD) error { return nil } func (t *TestPodLogsServer) SendHeader(metadata.MD) error { return nil } func (t *TestPodLogsServer) SetTrailer(metadata.MD) {} func (t *TestPodLogsServer) Context() context.Context { return t.ctx } func (t *TestPodLogsServer) SendMsg(_ any) error { return nil } func (t *TestPodLogsServer) RecvMsg(_ any) error { return nil } func TestNoAppEnumeration(t *testing.T) { // This test ensures that malicious users can't infer the existence or non-existence of Applications by inspecting // error messages. The errors for "app does not exist" must be the same as errors for "you aren't allowed to // interact with this app." // These tests are only important on API calls where the full app RBAC name (project, namespace, and name) is _not_ // known based on the query parameters. For example, the Create call cannot leak existence of Applications, because // the Application's project, namespace, and name are all specified in the API call. The call can be rejected // immediately if the user does not have access. But the Delete endpoint may be called with just the Application // name. So we cannot return a different error message for "does not exist" and "you don't have delete permissions," // because the user could infer that the Application exists if they do not get the "does not exist" message. For // endpoints that do not require the full RBAC name, we must return a generic "permission denied" for both "does not // exist" and "no access." f := func(enf *rbac.Enforcer) { _ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV) enf.SetDefaultRole("role:none") } deployment := appsv1.Deployment{ TypeMeta: metav1.TypeMeta{ APIVersion: "apps/v1", Kind: "Deployment", }, ObjectMeta: metav1.ObjectMeta{ Name: "test", Namespace: "test", }, } testApp := newTestApp(func(app *v1alpha1.Application) { app.Name = "test" app.Status.Resources = []v1alpha1.ResourceStatus{ { Group: deployment.GroupVersionKind().Group, Kind: deployment.GroupVersionKind().Kind, Version: deployment.GroupVersionKind().Version, Name: deployment.Name, Namespace: deployment.Namespace, Status: "Synced", }, } app.Status.History = []v1alpha1.RevisionHistory{ { ID: 0, Source: v1alpha1.ApplicationSource{ TargetRevision: "something-old", }, }, } }) testHelmApp := newTestApp(func(app *v1alpha1.Application) { app.Name = "test-helm" app.Spec.Source.Path = "" app.Spec.Source.Chart = "test" app.Status.Resources = []v1alpha1.ResourceStatus{ { Group: deployment.GroupVersionKind().Group, Kind: deployment.GroupVersionKind().Kind, Version: deployment.GroupVersionKind().Version, Name: deployment.Name, Namespace: deployment.Namespace, Status: "Synced", }, } app.Status.History = []v1alpha1.RevisionHistory{ { ID: 0, Source: v1alpha1.ApplicationSource{ TargetRevision: "something-old", }, }, } }) testAppMulti := newTestApp(func(app *v1alpha1.Application) { app.Name = "test-multi" app.Spec.Sources = v1alpha1.ApplicationSources{ v1alpha1.ApplicationSource{ TargetRevision: "something-old", }, v1alpha1.ApplicationSource{ TargetRevision: "something-old", }, } app.Status.Resources = []v1alpha1.ResourceStatus{ { Group: deployment.GroupVersionKind().Group, Kind: deployment.GroupVersionKind().Kind, Version: deployment.GroupVersionKind().Version, Name: deployment.Name, Namespace: deployment.Namespace, Status: "Synced", }, } app.Status.History = []v1alpha1.RevisionHistory{ { ID: 1, Sources: v1alpha1.ApplicationSources{ v1alpha1.ApplicationSource{ TargetRevision: "something-old", }, v1alpha1.ApplicationSource{ TargetRevision: "something-old", }, }, }, } }) testDeployment := kube.MustToUnstructured(&deployment) appServer := newTestAppServerWithEnforcerConfigure(t, f, map[string]string{}, testApp, testHelmApp, testAppMulti, testDeployment) noRoleCtx := t.Context() //nolint:staticcheck adminCtx := context.WithValue(noRoleCtx, "claims", &jwt.MapClaims{"groups": []string{"admin"}}) t.Run("Get", func(t *testing.T) { _, err := appServer.Get(adminCtx, &application.ApplicationQuery{Name: ptr.To("test")}) require.NoError(t, err) _, err = appServer.Get(noRoleCtx, &application.ApplicationQuery{Name: ptr.To("test")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.Get(adminCtx, &application.ApplicationQuery{Name: ptr.To("doest-not-exist")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.Get(adminCtx, &application.ApplicationQuery{Name: ptr.To("doest-not-exist"), Project: []string{"test"}}) assert.EqualError(t, err, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", "when the request specifies a project, we can return the standard k8s error message") }) t.Run("GetManifests", func(t *testing.T) { _, err := appServer.GetManifests(adminCtx, &application.ApplicationManifestQuery{Name: ptr.To("test")}) require.NoError(t, err) _, err = appServer.GetManifests(noRoleCtx, &application.ApplicationManifestQuery{Name: ptr.To("test")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.GetManifests(adminCtx, &application.ApplicationManifestQuery{Name: ptr.To("doest-not-exist")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.GetManifests(adminCtx, &application.ApplicationManifestQuery{Name: ptr.To("doest-not-exist"), Project: ptr.To("test")}) assert.EqualError(t, err, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", "when the request specifies a project, we can return the standard k8s error message") }) t.Run("ListResourceEvents", func(t *testing.T) { _, err := appServer.ListResourceEvents(adminCtx, &application.ApplicationResourceEventsQuery{Name: ptr.To("test")}) require.NoError(t, err) _, err = appServer.ListResourceEvents(noRoleCtx, &application.ApplicationResourceEventsQuery{Name: ptr.To("test")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.ListResourceEvents(adminCtx, &application.ApplicationResourceEventsQuery{Name: ptr.To("doest-not-exist")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.ListResourceEvents(adminCtx, &application.ApplicationResourceEventsQuery{Name: ptr.To("doest-not-exist"), Project: ptr.To("test")}) assert.EqualError(t, err, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", "when the request specifies a project, we can return the standard k8s error message") }) t.Run("UpdateSpec", func(t *testing.T) { _, err := appServer.UpdateSpec(adminCtx, &application.ApplicationUpdateSpecRequest{Name: ptr.To("test"), Spec: &v1alpha1.ApplicationSpec{ Destination: v1alpha1.ApplicationDestination{Namespace: "default", Server: "https://cluster-api.example.com"}, Source: &v1alpha1.ApplicationSource{RepoURL: "https://some-fake-source", Path: "."}, }}) require.NoError(t, err) _, err = appServer.UpdateSpec(noRoleCtx, &application.ApplicationUpdateSpecRequest{Name: ptr.To("test"), Spec: &v1alpha1.ApplicationSpec{ Destination: v1alpha1.ApplicationDestination{Namespace: "default", Server: "https://cluster-api.example.com"}, Source: &v1alpha1.ApplicationSource{RepoURL: "https://some-fake-source", Path: "."}, }}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.UpdateSpec(adminCtx, &application.ApplicationUpdateSpecRequest{Name: ptr.To("doest-not-exist"), Spec: &v1alpha1.ApplicationSpec{ Destination: v1alpha1.ApplicationDestination{Namespace: "default", Server: "https://cluster-api.example.com"}, Source: &v1alpha1.ApplicationSource{RepoURL: "https://some-fake-source", Path: "."}, }}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.UpdateSpec(adminCtx, &application.ApplicationUpdateSpecRequest{Name: ptr.To("doest-not-exist"), Project: ptr.To("test"), Spec: &v1alpha1.ApplicationSpec{ Destination: v1alpha1.ApplicationDestination{Namespace: "default", Server: "https://cluster-api.example.com"}, Source: &v1alpha1.ApplicationSource{RepoURL: "https://some-fake-source", Path: "."}, }}) assert.EqualError(t, err, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", "when the request specifies a project, we can return the standard k8s error message") }) t.Run("Patch", func(t *testing.T) { _, err := appServer.Patch(adminCtx, &application.ApplicationPatchRequest{Name: ptr.To("test"), Patch: ptr.To(`[{"op": "replace", "path": "/spec/source/path", "value": "foo"}]`)}) require.NoError(t, err) _, err = appServer.Patch(noRoleCtx, &application.ApplicationPatchRequest{Name: ptr.To("test"), Patch: ptr.To(`[{"op": "replace", "path": "/spec/source/path", "value": "foo"}]`)}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.Patch(adminCtx, &application.ApplicationPatchRequest{Name: ptr.To("doest-not-exist")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.Patch(adminCtx, &application.ApplicationPatchRequest{Name: ptr.To("doest-not-exist"), Project: ptr.To("test")}) assert.EqualError(t, err, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", "when the request specifies a project, we can return the standard k8s error message") }) t.Run("GetResource", func(t *testing.T) { _, err := appServer.GetResource(adminCtx, &application.ApplicationResourceRequest{Name: ptr.To("test"), ResourceName: ptr.To("test"), Group: ptr.To("apps"), Kind: ptr.To("Deployment"), Namespace: ptr.To("test")}) require.NoError(t, err) _, err = appServer.GetResource(noRoleCtx, &application.ApplicationResourceRequest{Name: ptr.To("test"), ResourceName: ptr.To("test"), Group: ptr.To("apps"), Kind: ptr.To("Deployment"), Namespace: ptr.To("test")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.GetResource(adminCtx, &application.ApplicationResourceRequest{Name: ptr.To("doest-not-exist"), ResourceName: ptr.To("test"), Group: ptr.To("apps"), Kind: ptr.To("Deployment"), Namespace: ptr.To("test")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.GetResource(adminCtx, &application.ApplicationResourceRequest{Name: ptr.To("doest-not-exist"), Project: ptr.To("test"), ResourceName: ptr.To("test"), Group: ptr.To("apps"), Kind: ptr.To("Deployment"), Namespace: ptr.To("test")}) assert.EqualError(t, err, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", "when the request specifies a project, we can return the standard k8s error message") }) t.Run("PatchResource", func(t *testing.T) { _, err := appServer.PatchResource(adminCtx, &application.ApplicationResourcePatchRequest{Name: ptr.To("test"), ResourceName: ptr.To("test"), Group: ptr.To("apps"), Kind: ptr.To("Deployment"), Namespace: ptr.To("test"), Patch: ptr.To(`[{"op": "replace", "path": "/spec/replicas", "value": 3}]`)}) // This will always throw an error, because the kubectl mock for PatchResource is hard-coded to return nil. // The best we can do is to confirm we get past the permission check. assert.NotEqual(t, common.PermissionDeniedAPIError.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.PatchResource(noRoleCtx, &application.ApplicationResourcePatchRequest{Name: ptr.To("test"), ResourceName: ptr.To("test"), Group: ptr.To("apps"), Kind: ptr.To("Deployment"), Namespace: ptr.To("test"), Patch: ptr.To(`[{"op": "replace", "path": "/spec/replicas", "value": 3}]`)}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.PatchResource(adminCtx, &application.ApplicationResourcePatchRequest{Name: ptr.To("doest-not-exist"), ResourceName: ptr.To("test"), Group: ptr.To("apps"), Kind: ptr.To("Deployment"), Namespace: ptr.To("test"), Patch: ptr.To(`[{"op": "replace", "path": "/spec/replicas", "value": 3}]`)}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.PatchResource(adminCtx, &application.ApplicationResourcePatchRequest{Name: ptr.To("doest-not-exist"), Project: ptr.To("test"), ResourceName: ptr.To("test"), Group: ptr.To("apps"), Kind: ptr.To("Deployment"), Namespace: ptr.To("test"), Patch: ptr.To(`[{"op": "replace", "path": "/spec/replicas", "value": 3}]`)}) assert.EqualError(t, err, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", "when the request specifies a project, we can return the standard k8s error message") }) t.Run("DeleteResource", func(t *testing.T) { _, err := appServer.DeleteResource(adminCtx, &application.ApplicationResourceDeleteRequest{Name: ptr.To("test"), ResourceName: ptr.To("test"), Group: ptr.To("apps"), Kind: ptr.To("Deployment"), Namespace: ptr.To("test")}) require.NoError(t, err) _, err = appServer.DeleteResource(noRoleCtx, &application.ApplicationResourceDeleteRequest{Name: ptr.To("test"), ResourceName: ptr.To("test"), Group: ptr.To("apps"), Kind: ptr.To("Deployment"), Namespace: ptr.To("test")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.DeleteResource(adminCtx, &application.ApplicationResourceDeleteRequest{Name: ptr.To("doest-not-exist"), ResourceName: ptr.To("test"), Group: ptr.To("apps"), Kind: ptr.To("Deployment"), Namespace: ptr.To("test")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.DeleteResource(adminCtx, &application.ApplicationResourceDeleteRequest{Name: ptr.To("doest-not-exist"), Project: ptr.To("test"), ResourceName: ptr.To("test"), Group: ptr.To("apps"), Kind: ptr.To("Deployment"), Namespace: ptr.To("test")}) assert.EqualError(t, err, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", "when the request specifies a project, we can return the standard k8s error message") }) t.Run("ResourceTree", func(t *testing.T) { _, err := appServer.ResourceTree(adminCtx, &application.ResourcesQuery{ApplicationName: ptr.To("test")}) require.NoError(t, err) _, err = appServer.ResourceTree(noRoleCtx, &application.ResourcesQuery{ApplicationName: ptr.To("test")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.ResourceTree(adminCtx, &application.ResourcesQuery{ApplicationName: ptr.To("doest-not-exist")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.ResourceTree(adminCtx, &application.ResourcesQuery{ApplicationName: ptr.To("doest-not-exist"), Project: ptr.To("test")}) assert.EqualError(t, err, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", "when the request specifies a project, we can return the standard k8s error message") }) t.Run("RevisionMetadata", func(t *testing.T) { _, err := appServer.RevisionMetadata(adminCtx, &application.RevisionMetadataQuery{Name: ptr.To("test")}) require.NoError(t, err) _, err = appServer.RevisionMetadata(adminCtx, &application.RevisionMetadataQuery{Name: ptr.To("test-multi"), SourceIndex: ptr.To(int32(0)), VersionId: ptr.To(int32(1))}) require.NoError(t, err) _, err = appServer.RevisionMetadata(noRoleCtx, &application.RevisionMetadataQuery{Name: ptr.To("test")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.RevisionMetadata(adminCtx, &application.RevisionMetadataQuery{Name: ptr.To("doest-not-exist")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.RevisionMetadata(adminCtx, &application.RevisionMetadataQuery{Name: ptr.To("doest-not-exist"), Project: ptr.To("test")}) assert.EqualError(t, err, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", "when the request specifies a project, we can return the standard k8s error message") }) t.Run("RevisionChartDetails", func(t *testing.T) { _, err := appServer.RevisionChartDetails(adminCtx, &application.RevisionMetadataQuery{Name: ptr.To("test-helm")}) require.NoError(t, err) _, err = appServer.RevisionChartDetails(noRoleCtx, &application.RevisionMetadataQuery{Name: ptr.To("test-helm")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.RevisionChartDetails(adminCtx, &application.RevisionMetadataQuery{Name: ptr.To("doest-not-exist")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.RevisionChartDetails(adminCtx, &application.RevisionMetadataQuery{Name: ptr.To("doest-not-exist"), Project: ptr.To("test")}) assert.EqualError(t, err, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", "when the request specifies a project, we can return the standard k8s error message") }) t.Run("ManagedResources", func(t *testing.T) { _, err := appServer.ManagedResources(adminCtx, &application.ResourcesQuery{ApplicationName: ptr.To("test")}) require.NoError(t, err) _, err = appServer.ManagedResources(noRoleCtx, &application.ResourcesQuery{ApplicationName: ptr.To("test")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.ManagedResources(adminCtx, &application.ResourcesQuery{ApplicationName: ptr.To("doest-not-exist")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.ManagedResources(adminCtx, &application.ResourcesQuery{ApplicationName: ptr.To("doest-not-exist"), Project: ptr.To("test")}) assert.EqualError(t, err, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", "when the request specifies a project, we can return the standard k8s error message") }) t.Run("Sync", func(t *testing.T) { _, err := appServer.Sync(adminCtx, &application.ApplicationSyncRequest{Name: ptr.To("test")}) require.NoError(t, err) _, err = appServer.Sync(noRoleCtx, &application.ApplicationSyncRequest{Name: ptr.To("test")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.Sync(adminCtx, &application.ApplicationSyncRequest{Name: ptr.To("doest-not-exist")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.Sync(adminCtx, &application.ApplicationSyncRequest{Name: ptr.To("doest-not-exist"), Project: ptr.To("test")}) assert.EqualError(t, err, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", "when the request specifies a project, we can return the standard k8s error message") }) t.Run("TerminateOperation", func(t *testing.T) { // The sync operation is already started from the previous test. We just need to set the field that the // controller would set if this were an actual Argo CD environment. setSyncRunningOperationState(t, appServer) _, err := appServer.TerminateOperation(adminCtx, &application.OperationTerminateRequest{Name: ptr.To("test")}) require.NoError(t, err) _, err = appServer.TerminateOperation(noRoleCtx, &application.OperationTerminateRequest{Name: ptr.To("test")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.TerminateOperation(adminCtx, &application.OperationTerminateRequest{Name: ptr.To("doest-not-exist")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.TerminateOperation(adminCtx, &application.OperationTerminateRequest{Name: ptr.To("doest-not-exist"), Project: ptr.To("test")}) assert.EqualError(t, err, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", "when the request specifies a project, we can return the standard k8s error message") }) t.Run("Rollback", func(t *testing.T) { unsetSyncRunningOperationState(t, appServer) _, err := appServer.Rollback(adminCtx, &application.ApplicationRollbackRequest{Name: ptr.To("test")}) require.NoError(t, err) _, err = appServer.Rollback(adminCtx, &application.ApplicationRollbackRequest{Name: ptr.To("test-multi"), Id: ptr.To(int64(1))}) require.NoError(t, err) _, err = appServer.Rollback(noRoleCtx, &application.ApplicationRollbackRequest{Name: ptr.To("test")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.Rollback(adminCtx, &application.ApplicationRollbackRequest{Name: ptr.To("doest-not-exist")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.Rollback(adminCtx, &application.ApplicationRollbackRequest{Name: ptr.To("doest-not-exist"), Project: ptr.To("test")}) assert.EqualError(t, err, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", "when the request specifies a project, we can return the standard k8s error message") }) t.Run("ListResourceActions", func(t *testing.T) { _, err := appServer.ListResourceActions(adminCtx, &application.ApplicationResourceRequest{Name: ptr.To("test"), ResourceName: ptr.To("test"), Group: ptr.To("apps"), Kind: ptr.To("Deployment"), Namespace: ptr.To("test")}) require.NoError(t, err) _, err = appServer.ListResourceActions(noRoleCtx, &application.ApplicationResourceRequest{Name: ptr.To("test")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.ListResourceActions(noRoleCtx, &application.ApplicationResourceRequest{Group: ptr.To("argoproj.io"), Kind: ptr.To("Application"), Name: ptr.To("test")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.ListResourceActions(adminCtx, &application.ApplicationResourceRequest{Name: ptr.To("doest-not-exist")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.ListResourceActions(adminCtx, &application.ApplicationResourceRequest{Name: ptr.To("doest-not-exist"), Project: ptr.To("test")}) assert.EqualError(t, err, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", "when the request specifies a project, we can return the standard k8s error message") }) //nolint:staticcheck,SA1019 // RunResourceAction is deprecated, but we still need to support it for backward compatibility. t.Run("RunResourceAction", func(t *testing.T) { _, err := appServer.RunResourceAction(adminCtx, &application.ResourceActionRunRequest{Name: ptr.To("test"), ResourceName: ptr.To("test"), Group: ptr.To("apps"), Kind: ptr.To("Deployment"), Namespace: ptr.To("test"), Action: ptr.To("restart")}) require.NoError(t, err) _, err = appServer.RunResourceAction(noRoleCtx, &application.ResourceActionRunRequest{Name: ptr.To("test")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.RunResourceAction(noRoleCtx, &application.ResourceActionRunRequest{Group: ptr.To("argoproj.io"), Kind: ptr.To("Application"), Name: ptr.To("test")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.RunResourceAction(adminCtx, &application.ResourceActionRunRequest{Name: ptr.To("doest-not-exist")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.RunResourceAction(adminCtx, &application.ResourceActionRunRequest{Name: ptr.To("doest-not-exist"), Project: ptr.To("test")}) assert.EqualError(t, err, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", "when the request specifies a project, we can return the standard k8s error message") }) t.Run("RunResourceActionV2", func(t *testing.T) { _, err := appServer.RunResourceActionV2(adminCtx, &application.ResourceActionRunRequestV2{Name: ptr.To("test"), ResourceName: ptr.To("test"), Group: ptr.To("apps"), Kind: ptr.To("Deployment"), Namespace: ptr.To("test"), Action: ptr.To("restart")}) require.NoError(t, err) _, err = appServer.RunResourceActionV2(noRoleCtx, &application.ResourceActionRunRequestV2{Name: ptr.To("test")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.RunResourceActionV2(noRoleCtx, &application.ResourceActionRunRequestV2{Group: ptr.To("argoproj.io"), Kind: ptr.To("Application"), Name: ptr.To("test")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.RunResourceActionV2(adminCtx, &application.ResourceActionRunRequestV2{Name: ptr.To("doest-not-exist")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.RunResourceActionV2(adminCtx, &application.ResourceActionRunRequestV2{Name: ptr.To("doest-not-exist"), Project: ptr.To("test")}) assert.EqualError(t, err, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", "when the request specifies a project, we can return the standard k8s error message") }) t.Run("GetApplicationSyncWindows", func(t *testing.T) { _, err := appServer.GetApplicationSyncWindows(adminCtx, &application.ApplicationSyncWindowsQuery{Name: ptr.To("test")}) require.NoError(t, err) _, err = appServer.GetApplicationSyncWindows(noRoleCtx, &application.ApplicationSyncWindowsQuery{Name: ptr.To("test")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.GetApplicationSyncWindows(adminCtx, &application.ApplicationSyncWindowsQuery{Name: ptr.To("doest-not-exist")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.GetApplicationSyncWindows(adminCtx, &application.ApplicationSyncWindowsQuery{Name: ptr.To("doest-not-exist"), Project: ptr.To("test")}) assert.EqualError(t, err, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", "when the request specifies a project, we can return the standard k8s error message") }) t.Run("GetManifestsWithFiles", func(t *testing.T) { err := appServer.GetManifestsWithFiles(&TestServerStream{ctx: adminCtx, appName: "test"}) require.NoError(t, err) err = appServer.GetManifestsWithFiles(&TestServerStream{ctx: noRoleCtx, appName: "test"}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") err = appServer.GetManifestsWithFiles(&TestServerStream{ctx: adminCtx, appName: "does-not-exist"}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") err = appServer.GetManifestsWithFiles(&TestServerStream{ctx: adminCtx, appName: "does-not-exist", project: "test"}) assert.EqualError(t, err, "rpc error: code = NotFound desc = applications.argoproj.io \"does-not-exist\" not found", "when the request specifies a project, we can return the standard k8s error message") }) t.Run("WatchResourceTree", func(t *testing.T) { err := appServer.WatchResourceTree(&application.ResourcesQuery{ApplicationName: ptr.To("test")}, &TestResourceTreeServer{ctx: adminCtx}) require.NoError(t, err) err = appServer.WatchResourceTree(&application.ResourcesQuery{ApplicationName: ptr.To("test")}, &TestResourceTreeServer{ctx: noRoleCtx}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") err = appServer.WatchResourceTree(&application.ResourcesQuery{ApplicationName: ptr.To("does-not-exist")}, &TestResourceTreeServer{ctx: adminCtx}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") err = appServer.WatchResourceTree(&application.ResourcesQuery{ApplicationName: ptr.To("does-not-exist"), Project: ptr.To("test")}, &TestResourceTreeServer{ctx: adminCtx}) assert.EqualError(t, err, "rpc error: code = NotFound desc = applications.argoproj.io \"does-not-exist\" not found", "when the request specifies a project, we can return the standard k8s error message") }) t.Run("PodLogs", func(t *testing.T) { err := appServer.PodLogs(&application.ApplicationPodLogsQuery{Name: ptr.To("test")}, &TestPodLogsServer{ctx: adminCtx}) require.NoError(t, err) err = appServer.PodLogs(&application.ApplicationPodLogsQuery{Name: ptr.To("test")}, &TestPodLogsServer{ctx: noRoleCtx}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") err = appServer.PodLogs(&application.ApplicationPodLogsQuery{Name: ptr.To("does-not-exist")}, &TestPodLogsServer{ctx: adminCtx}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") err = appServer.PodLogs(&application.ApplicationPodLogsQuery{Name: ptr.To("does-not-exist"), Project: ptr.To("test")}, &TestPodLogsServer{ctx: adminCtx}) assert.EqualError(t, err, "rpc error: code = NotFound desc = applications.argoproj.io \"does-not-exist\" not found", "when the request specifies a project, we can return the standard k8s error message") }) t.Run("ListLinks", func(t *testing.T) { _, err := appServer.ListLinks(adminCtx, &application.ListAppLinksRequest{Name: ptr.To("test")}) require.NoError(t, err) _, err = appServer.ListLinks(noRoleCtx, &application.ListAppLinksRequest{Name: ptr.To("test")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.ListLinks(adminCtx, &application.ListAppLinksRequest{Name: ptr.To("does-not-exist")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.ListLinks(adminCtx, &application.ListAppLinksRequest{Name: ptr.To("does-not-exist"), Project: ptr.To("test")}) assert.EqualError(t, err, "rpc error: code = NotFound desc = applications.argoproj.io \"does-not-exist\" not found", "when the request specifies a project, we can return the standard k8s error message") }) t.Run("ListResourceLinks", func(t *testing.T) { _, err := appServer.ListResourceLinks(adminCtx, &application.ApplicationResourceRequest{Name: ptr.To("test"), ResourceName: ptr.To("test"), Group: ptr.To("apps"), Kind: ptr.To("Deployment"), Namespace: ptr.To("test")}) require.NoError(t, err) _, err = appServer.ListResourceLinks(noRoleCtx, &application.ApplicationResourceRequest{Name: ptr.To("test"), ResourceName: ptr.To("test"), Group: ptr.To("apps"), Kind: ptr.To("Deployment"), Namespace: ptr.To("test")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.ListResourceLinks(adminCtx, &application.ApplicationResourceRequest{Name: ptr.To("does-not-exist"), ResourceName: ptr.To("test"), Group: ptr.To("apps"), Kind: ptr.To("Deployment"), Namespace: ptr.To("test")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.ListResourceLinks(adminCtx, &application.ApplicationResourceRequest{Name: ptr.To("does-not-exist"), ResourceName: ptr.To("test"), Group: ptr.To("apps"), Kind: ptr.To("Deployment"), Namespace: ptr.To("test"), Project: ptr.To("test")}) assert.EqualError(t, err, "rpc error: code = NotFound desc = applications.argoproj.io \"does-not-exist\" not found", "when the request specifies a project, we can return the standard k8s error message") }) // Do this last so other stuff doesn't fail. t.Run("Delete", func(t *testing.T) { _, err := appServer.Delete(adminCtx, &application.ApplicationDeleteRequest{Name: ptr.To("test")}) require.NoError(t, err) _, err = appServer.Delete(noRoleCtx, &application.ApplicationDeleteRequest{Name: ptr.To("test")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.Delete(adminCtx, &application.ApplicationDeleteRequest{Name: ptr.To("doest-not-exist")}) require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.Delete(adminCtx, &application.ApplicationDeleteRequest{Name: ptr.To("doest-not-exist"), Project: ptr.To("test")}) assert.EqualError(t, err, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", "when the request specifies a project, we can return the standard k8s error message") }) } // setSyncRunningOperationState simulates starting a sync operation on the given app. func setSyncRunningOperationState(t *testing.T, appServer *Server) { t.Helper() appIf := appServer.appclientset.ArgoprojV1alpha1().Applications("default") app, err := appIf.Get(t.Context(), "test", metav1.GetOptions{}) require.NoError(t, err) // This sets the status that would be set by the controller usually. app.Status.OperationState = &v1alpha1.OperationState{Phase: synccommon.OperationRunning, Operation: v1alpha1.Operation{Sync: &v1alpha1.SyncOperation{}}} _, err = appIf.Update(t.Context(), app, metav1.UpdateOptions{}) require.NoError(t, err) } // unsetSyncRunningOperationState simulates finishing a sync operation on the given app. func unsetSyncRunningOperationState(t *testing.T, appServer *Server) { t.Helper() appIf := appServer.appclientset.ArgoprojV1alpha1().Applications("default") app, err := appIf.Get(t.Context(), "test", metav1.GetOptions{}) require.NoError(t, err) app.Operation = nil app.Status.OperationState = nil _, err = appIf.Update(t.Context(), app, metav1.UpdateOptions{}) require.NoError(t, err) } func TestListAppsInNamespaceWithLabels(t *testing.T) { appServer := newTestAppServer(t, newTestApp(func(app *v1alpha1.Application) { app.Name = "App1" app.Namespace = "test-namespace" app.SetLabels(map[string]string{"key1": "value1", "key2": "value1"}) }), newTestApp(func(app *v1alpha1.Application) { app.Name = "App2" app.Namespace = "test-namespace" app.SetLabels(map[string]string{"key1": "value2"}) }), newTestApp(func(app *v1alpha1.Application) { app.Name = "App3" app.Namespace = "test-namespace" app.SetLabels(map[string]string{"key1": "value3"}) })) appServer.ns = "test-namespace" appQuery := application.ApplicationQuery{} namespace := "test-namespace" appQuery.AppNamespace = &namespace testListAppsWithLabels(t, appQuery, appServer) } func TestListAppsInDefaultNSWithLabels(t *testing.T) { appServer := newTestAppServer(t, newTestApp(func(app *v1alpha1.Application) { app.Name = "App1" app.SetLabels(map[string]string{"key1": "value1", "key2": "value1"}) }), newTestApp(func(app *v1alpha1.Application) { app.Name = "App2" app.SetLabels(map[string]string{"key1": "value2"}) }), newTestApp(func(app *v1alpha1.Application) { app.Name = "App3" app.SetLabels(map[string]string{"key1": "value3"}) })) appQuery := application.ApplicationQuery{} testListAppsWithLabels(t, appQuery, appServer) } func testListAppsWithLabels(t *testing.T, appQuery application.ApplicationQuery, appServer *Server) { t.Helper() validTests := []struct { testName string label string expectedResult []string }{ { testName: "Equality based filtering using '=' operator", label: "key1=value1", expectedResult: []string{"App1"}, }, { testName: "Equality based filtering using '==' operator", label: "key1==value1", expectedResult: []string{"App1"}, }, { testName: "Equality based filtering using '!=' operator", label: "key1!=value1", expectedResult: []string{"App2", "App3"}, }, { testName: "Set based filtering using 'in' operator", label: "key1 in (value1, value3)", expectedResult: []string{"App1", "App3"}, }, { testName: "Set based filtering using 'notin' operator", label: "key1 notin (value1, value3)", expectedResult: []string{"App2"}, }, { testName: "Set based filtering using 'exists' operator", label: "key1", expectedResult: []string{"App1", "App2", "App3"}, }, { testName: "Set based filtering using 'not exists' operator", label: "!key2", expectedResult: []string{"App2", "App3"}, }, } // test valid scenarios for _, validTest := range validTests { t.Run(validTest.testName, func(t *testing.T) { appQuery.Selector = &validTest.label res, err := appServer.List(t.Context(), &appQuery) 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 0 { f := func(enf *rbac.Enforcer) { _ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV) enf.SetDefaultRole("role:admin") } formatInt := strconv.FormatInt(maxPodLogsToRender[0], 10) appServer := newTestAppServerWithEnforcerConfigure(t, f, map[string]string{"server.maxPodLogsToRender": formatInt}, runtimeObjects...) return appServer, adminCtx } appServer := newTestAppServer(t, runtimeObjects...) return appServer, adminCtx } // refreshAnnotationRemover runs an infinite loop until it detects and removes refresh annotation or given context is done func refreshAnnotationRemover(t *testing.T, ctx context.Context, patched *int32, appServer *Server, appName string, ch chan string) { t.Helper() for ctx.Err() == nil { aName, appNs := argo.ParseFromQualifiedName(appName, appServer.ns) a, err := appServer.appLister.Applications(appNs).Get(aName) require.NoError(t, err) if a.GetAnnotations() != nil && a.GetAnnotations()[v1alpha1.AnnotationKeyRefresh] != "" { a.SetAnnotations(map[string]string{}) a.SetResourceVersion("999") _, err = appServer.appclientset.ArgoprojV1alpha1().Applications(a.Namespace).Update( t.Context(), a, metav1.UpdateOptions{}) require.NoError(t, err) atomic.AddInt32(patched, 1) ch <- "" } time.Sleep(100 * time.Millisecond) } } func TestGetAppRefresh_NormalRefresh(t *testing.T) { ctx, cancel := context.WithCancel(t.Context()) defer cancel() testApp := newTestApp() testApp.ResourceVersion = "1" appServer := newTestAppServer(t, testApp) var patched int32 ch := make(chan string, 1) go refreshAnnotationRemover(t, ctx, &patched, appServer, testApp.Name, ch) _, err := appServer.Get(t.Context(), &application.ApplicationQuery{ Name: &testApp.Name, Refresh: ptr.To(string(v1alpha1.RefreshTypeNormal)), }) require.NoError(t, err) select { case <-ch: assert.Equal(t, int32(1), atomic.LoadInt32(&patched)) case <-time.After(10 * time.Second): assert.Fail(t, "Out of time ( 10 seconds )") } } func TestGetAppRefresh_HardRefresh(t *testing.T) { ctx, cancel := context.WithCancel(t.Context()) defer cancel() testApp := newTestApp() testApp.ResourceVersion = "1" appServer := newTestAppServer(t, testApp) var getAppDetailsQuery *apiclient.RepoServerAppDetailsQuery mockRepoServiceClient := &mocks.RepoServerServiceClient{} mockRepoServiceClient.EXPECT().GetAppDetails(mock.Anything, mock.MatchedBy(func(q *apiclient.RepoServerAppDetailsQuery) bool { getAppDetailsQuery = q return true })).Return(&apiclient.RepoAppDetailsResponse{}, nil) appServer.repoClientset = &mocks.Clientset{RepoServerServiceClient: mockRepoServiceClient} var patched int32 ch := make(chan string, 1) go refreshAnnotationRemover(t, ctx, &patched, appServer, testApp.Name, ch) _, err := appServer.Get(t.Context(), &application.ApplicationQuery{ Name: &testApp.Name, Refresh: ptr.To(string(v1alpha1.RefreshTypeHard)), }) require.NoError(t, err) require.NotNil(t, getAppDetailsQuery) assert.True(t, getAppDetailsQuery.NoCache) assert.Equal(t, testApp.Spec.Source, getAppDetailsQuery.Source) require.NoError(t, err) select { case <-ch: assert.Equal(t, int32(1), atomic.LoadInt32(&patched)) case <-time.After(10 * time.Second): assert.Fail(t, "Out of time ( 10 seconds )") } } func TestGetApp_HealthStatusPropagation(t *testing.T) { newServerWithTree := func(t *testing.T) (*Server, *v1alpha1.Application) { t.Helper() cacheClient := cache.NewCache(cache.NewInMemoryCache(1 * time.Hour)) testApp := newTestApp() testApp.Status.ResourceHealthSource = v1alpha1.ResourceHealthLocationAppTree testApp.Status.Resources = []v1alpha1.ResourceStatus{ { Group: "apps", Kind: "Deployment", Name: "guestbook", Namespace: "default", }, } appServer := newTestAppServer(t, testApp) appStateCache := appstate.NewCache(cacheClient, time.Minute) appInstanceName := testApp.InstanceName(appServer.appNamespaceOrDefault(testApp.Namespace)) err := appStateCache.SetAppResourcesTree(appInstanceName, &v1alpha1.ApplicationTree{ Nodes: []v1alpha1.ResourceNode{{ ResourceRef: v1alpha1.ResourceRef{ Group: "apps", Kind: "Deployment", Name: "guestbook", Namespace: "default", }, Health: &v1alpha1.HealthStatus{Status: health.HealthStatusDegraded}, }}, }) require.NoError(t, err) appServer.cache = servercache.NewCache(appStateCache, time.Minute, time.Minute) return appServer, testApp } t.Run("propagated health status on get with no refresh", func(t *testing.T) { appServer, testApp := newServerWithTree(t) fetchedApp, err := appServer.Get(t.Context(), &application.ApplicationQuery{ Name: &testApp.Name, }) require.NoError(t, err) assert.Equal(t, health.HealthStatusDegraded, fetchedApp.Status.Resources[0].Health.Status) }) t.Run("propagated health status on normal refresh", func(t *testing.T) { appServer, testApp := newServerWithTree(t) var patched int32 ch := make(chan string, 1) ctx, cancel := context.WithCancel(t.Context()) defer cancel() go refreshAnnotationRemover(t, ctx, &patched, appServer, testApp.Name, ch) fetchedApp, err := appServer.Get(t.Context(), &application.ApplicationQuery{ Name: &testApp.Name, Refresh: ptr.To(string(v1alpha1.RefreshTypeNormal)), }) require.NoError(t, err) select { case <-ch: assert.Equal(t, int32(1), atomic.LoadInt32(&patched)) case <-time.After(10 * time.Second): assert.Fail(t, "Out of time ( 10 seconds )") } assert.Equal(t, health.HealthStatusDegraded, fetchedApp.Status.Resources[0].Health.Status) }) t.Run("propagated health status on hard refresh", func(t *testing.T) { appServer, testApp := newServerWithTree(t) var patched int32 ch := make(chan string, 1) ctx, cancel := context.WithCancel(t.Context()) defer cancel() go refreshAnnotationRemover(t, ctx, &patched, appServer, testApp.Name, ch) fetchedApp, err := appServer.Get(t.Context(), &application.ApplicationQuery{ Name: &testApp.Name, Refresh: ptr.To(string(v1alpha1.RefreshTypeHard)), }) require.NoError(t, err) select { case <-ch: assert.Equal(t, int32(1), atomic.LoadInt32(&patched)) case <-time.After(10 * time.Second): assert.Fail(t, "Out of time ( 10 seconds )") } assert.Equal(t, health.HealthStatusDegraded, fetchedApp.Status.Resources[0].Health.Status) }) } func TestInferResourcesStatusHealth(t *testing.T) { cacheClient := cache.NewCache(cache.NewInMemoryCache(1 * time.Hour)) testApp := newTestApp() testApp.Status.ResourceHealthSource = v1alpha1.ResourceHealthLocationAppTree testApp.Status.Resources = []v1alpha1.ResourceStatus{{ Group: "apps", Kind: "Deployment", Name: "guestbook", Namespace: "default", }, { Group: "apps", Kind: "StatefulSet", Name: "guestbook-stateful", Namespace: "default", }} appServer := newTestAppServer(t, testApp) appStateCache := appstate.NewCache(cacheClient, time.Minute) err := appStateCache.SetAppResourcesTree(testApp.Name, &v1alpha1.ApplicationTree{Nodes: []v1alpha1.ResourceNode{{ ResourceRef: v1alpha1.ResourceRef{ Group: "apps", Kind: "Deployment", Name: "guestbook", Namespace: "default", }, Health: &v1alpha1.HealthStatus{ Status: health.HealthStatusDegraded, }, }}}) require.NoError(t, err) appServer.cache = servercache.NewCache(appStateCache, time.Minute, time.Minute) appServer.inferResourcesStatusHealth(testApp) assert.Equal(t, health.HealthStatusDegraded, testApp.Status.Resources[0].Health.Status) assert.Nil(t, testApp.Status.Resources[1].Health) } func TestInferResourcesStatusHealthWithAppInAnyNamespace(t *testing.T) { cacheClient := cache.NewCache(cache.NewInMemoryCache(1 * time.Hour)) testApp := newTestApp() testApp.Namespace = "otherNamespace" testApp.Status.ResourceHealthSource = v1alpha1.ResourceHealthLocationAppTree testApp.Status.Resources = []v1alpha1.ResourceStatus{{ Group: "apps", Kind: "Deployment", Name: "guestbook", Namespace: "otherNamespace", }, { Group: "apps", Kind: "StatefulSet", Name: "guestbook-stateful", Namespace: "otherNamespace", }} appServer := newTestAppServer(t, testApp) appStateCache := appstate.NewCache(cacheClient, time.Minute) err := appStateCache.SetAppResourcesTree("otherNamespace"+"_"+testApp.Name, &v1alpha1.ApplicationTree{Nodes: []v1alpha1.ResourceNode{{ ResourceRef: v1alpha1.ResourceRef{ Group: "apps", Kind: "Deployment", Name: "guestbook", Namespace: "otherNamespace", }, Health: &v1alpha1.HealthStatus{ Status: health.HealthStatusDegraded, }, }}}) require.NoError(t, err) appServer.cache = servercache.NewCache(appStateCache, time.Minute, time.Minute) appServer.inferResourcesStatusHealth(testApp) assert.Equal(t, health.HealthStatusDegraded, testApp.Status.Resources[0].Health.Status) assert.Nil(t, testApp.Status.Resources[1].Health) } func TestRunNewStyleResourceAction(t *testing.T) { cacheClient := cache.NewCache(cache.NewInMemoryCache(1 * time.Hour)) group := "batch" kind := "CronJob" version := "v1" resourceName := "my-cron-job" namespace := testNamespace action := "create-job" uid := "1" resources := []v1alpha1.ResourceStatus{{ Group: group, Kind: kind, Name: resourceName, Namespace: testNamespace, Version: version, }} appStateCache := appstate.NewCache(cacheClient, time.Minute) nodes := []v1alpha1.ResourceNode{{ ResourceRef: v1alpha1.ResourceRef{ Group: group, Kind: kind, Version: version, Name: resourceName, Namespace: testNamespace, UID: uid, }, }} createJobDenyingProj := &v1alpha1.AppProject{ ObjectMeta: metav1.ObjectMeta{Name: "createJobDenyingProj", Namespace: "default"}, Spec: v1alpha1.AppProjectSpec{ SourceRepos: []string{"*"}, Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, NamespaceResourceWhitelist: []metav1.GroupKind{{Group: "never", Kind: "mind"}}, }, } cronJob := k8sbatchv1.CronJob{ TypeMeta: metav1.TypeMeta{ APIVersion: "batch/v1", Kind: "CronJob", }, ObjectMeta: metav1.ObjectMeta{ Name: "my-cron-job", Namespace: testNamespace, Labels: map[string]string{ "some": "label", }, }, Spec: k8sbatchv1.CronJobSpec{ Schedule: "* * * * *", JobTemplate: k8sbatchv1.JobTemplateSpec{ Spec: k8sbatchv1.JobSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "hello", Image: "busybox:1.28", ImagePullPolicy: "IfNotPresent", Command: []string{"/bin/sh", "-c", "date; echo Hello from the Kubernetes cluster"}, }, }, RestartPolicy: corev1.RestartPolicyOnFailure, }, }, }, }, }, } t.Run("CreateOperationNotPermitted", func(t *testing.T) { testApp := newTestApp() testApp.Spec.Project = "createJobDenyingProj" testApp.Status.ResourceHealthSource = v1alpha1.ResourceHealthLocationAppTree testApp.Status.Resources = resources appServer := newTestAppServer(t, testApp, createJobDenyingProj, kube.MustToUnstructured(&cronJob)) appServer.cache = servercache.NewCache(appStateCache, time.Minute, time.Minute) err := appStateCache.SetAppResourcesTree(testApp.Name, &v1alpha1.ApplicationTree{Nodes: nodes}) require.NoError(t, err) appResponse, runErr := appServer.RunResourceActionV2(t.Context(), &application.ResourceActionRunRequestV2{ Name: &testApp.Name, Namespace: &namespace, Action: &action, AppNamespace: &testApp.Namespace, ResourceName: &resourceName, Version: &version, Group: &group, Kind: &kind, }) require.ErrorContains(t, runErr, "is not permitted to manage") assert.Nil(t, appResponse) }) t.Run("CreateOperationPermitted", func(t *testing.T) { testApp := newTestApp() testApp.Status.ResourceHealthSource = v1alpha1.ResourceHealthLocationAppTree testApp.Status.Resources = resources appServer := newTestAppServer(t, testApp, kube.MustToUnstructured(&cronJob)) appServer.cache = servercache.NewCache(appStateCache, time.Minute, time.Minute) err := appStateCache.SetAppResourcesTree(testApp.Name, &v1alpha1.ApplicationTree{Nodes: nodes}) require.NoError(t, err) appResponse, runErr := appServer.RunResourceActionV2(t.Context(), &application.ResourceActionRunRequestV2{ Name: &testApp.Name, Namespace: &namespace, Action: &action, AppNamespace: &testApp.Namespace, ResourceName: &resourceName, Version: &version, Group: &group, Kind: &kind, }) require.NoError(t, runErr) assert.NotNil(t, appResponse) }) } func TestRunOldStyleResourceAction(t *testing.T) { cacheClient := cache.NewCache(cache.NewInMemoryCache(1 * time.Hour)) group := "apps" kind := "Deployment" version := "v1" resourceName := "nginx-deploy" namespace := testNamespace action := "pause" uid := "2" resources := []v1alpha1.ResourceStatus{{ Group: group, Kind: kind, Name: resourceName, Namespace: testNamespace, Version: version, }} appStateCache := appstate.NewCache(cacheClient, time.Minute) nodes := []v1alpha1.ResourceNode{{ ResourceRef: v1alpha1.ResourceRef{ Group: group, Kind: kind, Version: version, Name: resourceName, Namespace: testNamespace, UID: uid, }, }} deployment := appsv1.Deployment{ TypeMeta: metav1.TypeMeta{ APIVersion: "apps/v1", Kind: "Deployment", }, ObjectMeta: metav1.ObjectMeta{ Name: "nginx-deploy", Namespace: testNamespace, }, } t.Run("DefaultPatchOperation", func(t *testing.T) { testApp := newTestApp() testApp.Status.ResourceHealthSource = v1alpha1.ResourceHealthLocationAppTree testApp.Status.Resources = resources // appServer := newTestAppServer(t, testApp, returnDeployment()) appServer := newTestAppServer(t, testApp, kube.MustToUnstructured(&deployment)) appServer.cache = servercache.NewCache(appStateCache, time.Minute, time.Minute) err := appStateCache.SetAppResourcesTree(testApp.Name, &v1alpha1.ApplicationTree{Nodes: nodes}) require.NoError(t, err) appResponse, runErr := appServer.RunResourceActionV2(t.Context(), &application.ResourceActionRunRequestV2{ Name: &testApp.Name, Namespace: &namespace, Action: &action, AppNamespace: &testApp.Namespace, ResourceName: &resourceName, Version: &version, Group: &group, Kind: &kind, }) require.NoError(t, runErr) assert.NotNil(t, appResponse) }) } func TestIsApplicationPermitted(t *testing.T) { t.Run("Incorrect project", func(t *testing.T) { testApp := newTestApp() appServer := newTestAppServer(t, testApp) projects := map[string]bool{"test-app": false} permitted := appServer.isApplicationPermitted(labels.Everything(), 0, nil, "test", "default", projects, *testApp) assert.False(t, permitted) }) t.Run("Version is incorrect", func(t *testing.T) { testApp := newTestApp() appServer := newTestAppServer(t, testApp) minVersion := 100000 testApp.ResourceVersion = strconv.Itoa(minVersion - 1) permitted := appServer.isApplicationPermitted(labels.Everything(), minVersion, nil, "test", "default", nil, *testApp) assert.False(t, permitted) }) t.Run("Application name is incorrect", func(t *testing.T) { testApp := newTestApp() appServer := newTestAppServer(t, testApp) appName := "test" permitted := appServer.isApplicationPermitted(labels.Everything(), 0, nil, appName, "default", nil, *testApp) assert.False(t, permitted) }) t.Run("Application namespace is incorrect", func(t *testing.T) { testApp := newTestApp() appServer := newTestAppServer(t, testApp) permitted := appServer.isApplicationPermitted(labels.Everything(), 0, nil, testApp.Name, "demo", nil, *testApp) assert.False(t, permitted) }) t.Run("Application is not part of enabled namespace", func(t *testing.T) { testApp := newTestApp() appServer := newTestAppServer(t, testApp) appServer.ns = "server-ns" appServer.enabledNamespaces = []string{"demo"} permitted := appServer.isApplicationPermitted(labels.Everything(), 0, nil, testApp.Name, testApp.Namespace, nil, *testApp) assert.False(t, permitted) }) t.Run("Application is part of enabled namespace", func(t *testing.T) { testApp := newTestApp() appServer := newTestAppServer(t, testApp) appServer.ns = "server-ns" appServer.enabledNamespaces = []string{testApp.Namespace} permitted := appServer.isApplicationPermitted(labels.Everything(), 0, nil, testApp.Name, testApp.Namespace, nil, *testApp) assert.True(t, permitted) }) } func TestAppNamespaceRestrictions(t *testing.T) { t.Parallel() t.Run("List applications in controller namespace", func(t *testing.T) { t.Parallel() testApp := newTestApp() appServer := newTestAppServer(t, testApp) apps, err := appServer.List(t.Context(), &application.ApplicationQuery{}) require.NoError(t, err) require.Len(t, apps.Items, 1) }) t.Run("List applications with non-allowed apps existing", func(t *testing.T) { t.Parallel() testApp1 := newTestApp() testApp1.Namespace = "argocd-1" appServer := newTestAppServer(t, testApp1) apps, err := appServer.List(t.Context(), &application.ApplicationQuery{}) require.NoError(t, err) require.Empty(t, apps.Items) }) t.Run("List applications with non-allowed apps existing and explicit ns request", func(t *testing.T) { t.Parallel() testApp1 := newTestApp() testApp2 := newTestApp() testApp2.Namespace = "argocd-1" appServer := newTestAppServer(t, testApp1, testApp2) apps, err := appServer.List(t.Context(), &application.ApplicationQuery{AppNamespace: ptr.To("argocd-1")}) require.NoError(t, err) require.Empty(t, apps.Items) }) t.Run("List applications with allowed apps in other namespaces", func(t *testing.T) { t.Parallel() testApp1 := newTestApp() testApp1.Namespace = "argocd-1" appServer := newTestAppServer(t, testApp1) appServer.enabledNamespaces = []string{"argocd-1"} apps, err := appServer.List(t.Context(), &application.ApplicationQuery{}) require.NoError(t, err) require.Len(t, apps.Items, 1) }) t.Run("Get application in control plane namespace", func(t *testing.T) { t.Parallel() testApp := newTestApp() appServer := newTestAppServer(t, testApp) app, err := appServer.Get(t.Context(), &application.ApplicationQuery{ Name: ptr.To("test-app"), }) require.NoError(t, err) assert.Equal(t, "test-app", app.GetName()) }) t.Run("Get application in other namespace when forbidden", func(t *testing.T) { t.Parallel() testApp := newTestApp() testApp.Namespace = "argocd-1" appServer := newTestAppServer(t, testApp) app, err := appServer.Get(t.Context(), &application.ApplicationQuery{ Name: ptr.To("test-app"), AppNamespace: ptr.To("argocd-1"), }) require.ErrorContains(t, err, "permission denied") require.Nil(t, app) }) t.Run("Get application in other namespace when allowed", func(t *testing.T) { t.Parallel() testApp := newTestApp() testApp.Namespace = "argocd-1" testApp.Spec.Project = "other-ns" otherNsProj := &v1alpha1.AppProject{ ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, Spec: v1alpha1.AppProjectSpec{ SourceRepos: []string{"*"}, Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, SourceNamespaces: []string{"argocd-1"}, }, } appServer := newTestAppServer(t, testApp, otherNsProj) appServer.enabledNamespaces = []string{"argocd-1"} app, err := appServer.Get(t.Context(), &application.ApplicationQuery{ Name: ptr.To("test-app"), AppNamespace: ptr.To("argocd-1"), }) require.NoError(t, err) require.NotNil(t, app) require.Equal(t, "argocd-1", app.Namespace) require.Equal(t, "test-app", app.Name) }) t.Run("Get application in other namespace when project is not allowed", func(t *testing.T) { t.Parallel() testApp := newTestApp() testApp.Namespace = "argocd-1" testApp.Spec.Project = "other-ns" otherNsProj := &v1alpha1.AppProject{ ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, Spec: v1alpha1.AppProjectSpec{ SourceRepos: []string{"*"}, Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, SourceNamespaces: []string{"argocd-2"}, }, } appServer := newTestAppServer(t, testApp, otherNsProj) appServer.enabledNamespaces = []string{"argocd-1"} app, err := appServer.Get(t.Context(), &application.ApplicationQuery{ Name: ptr.To("test-app"), AppNamespace: ptr.To("argocd-1"), }) require.Error(t, err) require.Nil(t, app) require.ErrorContains(t, err, "app is not allowed in project") }) t.Run("Create application in other namespace when allowed", func(t *testing.T) { t.Parallel() testApp := newTestApp() testApp.Namespace = "argocd-1" testApp.Spec.Project = "other-ns" otherNsProj := &v1alpha1.AppProject{ ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, Spec: v1alpha1.AppProjectSpec{ SourceRepos: []string{"*"}, Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, SourceNamespaces: []string{"argocd-1"}, }, } appServer := newTestAppServer(t, otherNsProj) appServer.enabledNamespaces = []string{"argocd-1"} app, err := appServer.Create(t.Context(), &application.ApplicationCreateRequest{ Application: testApp, }) require.NoError(t, err) require.NotNil(t, app) assert.Equal(t, "test-app", app.Name) assert.Equal(t, "argocd-1", app.Namespace) }) t.Run("Create application in other namespace when not allowed by project", func(t *testing.T) { t.Parallel() testApp := newTestApp() testApp.Namespace = "argocd-1" testApp.Spec.Project = "other-ns" otherNsProj := &v1alpha1.AppProject{ ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, Spec: v1alpha1.AppProjectSpec{ SourceRepos: []string{"*"}, Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, SourceNamespaces: []string{}, }, } appServer := newTestAppServer(t, otherNsProj) appServer.enabledNamespaces = []string{"argocd-1"} app, err := appServer.Create(t.Context(), &application.ApplicationCreateRequest{ Application: testApp, }) require.Error(t, err) require.Nil(t, app) require.ErrorContains(t, err, "app is not allowed in project") }) t.Run("Create application in other namespace when not allowed by configuration", func(t *testing.T) { t.Parallel() testApp := newTestApp() testApp.Namespace = "argocd-1" testApp.Spec.Project = "other-ns" otherNsProj := &v1alpha1.AppProject{ ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, Spec: v1alpha1.AppProjectSpec{ SourceRepos: []string{"*"}, Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, SourceNamespaces: []string{"argocd-1"}, }, } appServer := newTestAppServer(t, otherNsProj) appServer.enabledNamespaces = []string{"argocd-2"} app, err := appServer.Create(t.Context(), &application.ApplicationCreateRequest{ Application: testApp, }) require.Error(t, err) require.Nil(t, app) require.ErrorContains(t, err, "namespace 'argocd-1' is not permitted") }) t.Run("Get application sync window in other namespace when project is allowed", func(t *testing.T) { t.Parallel() testApp := newTestApp() testApp.Namespace = "argocd-1" testApp.Spec.Project = "other-ns" otherNsProj := &v1alpha1.AppProject{ ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, Spec: v1alpha1.AppProjectSpec{ SourceRepos: []string{"*"}, Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, SourceNamespaces: []string{"argocd-1"}, }, } appServer := newTestAppServer(t, testApp, otherNsProj) appServer.enabledNamespaces = []string{"argocd-1"} active, err := appServer.GetApplicationSyncWindows(t.Context(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name, AppNamespace: &testApp.Namespace}) require.NoError(t, err) assert.Empty(t, active.ActiveWindows) }) t.Run("Get application sync window in other namespace when project is not allowed", func(t *testing.T) { t.Parallel() testApp := newTestApp() testApp.Namespace = "argocd-1" testApp.Spec.Project = "other-ns" otherNsProj := &v1alpha1.AppProject{ ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, Spec: v1alpha1.AppProjectSpec{ SourceRepos: []string{"*"}, Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, SourceNamespaces: []string{"argocd-2"}, }, } appServer := newTestAppServer(t, testApp, otherNsProj) appServer.enabledNamespaces = []string{"argocd-1"} active, err := appServer.GetApplicationSyncWindows(t.Context(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name, AppNamespace: &testApp.Namespace}) require.Error(t, err) require.Nil(t, active) require.ErrorContains(t, err, "app is not allowed in project") }) t.Run("Get list of links in other namespace when project is not allowed", func(t *testing.T) { t.Parallel() testApp := newTestApp() testApp.Namespace = "argocd-1" testApp.Spec.Project = "other-ns" otherNsProj := &v1alpha1.AppProject{ ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, Spec: v1alpha1.AppProjectSpec{ SourceRepos: []string{"*"}, Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, SourceNamespaces: []string{"argocd-2"}, }, } appServer := newTestAppServer(t, testApp, otherNsProj) appServer.enabledNamespaces = []string{"argocd-1"} links, err := appServer.ListLinks(t.Context(), &application.ListAppLinksRequest{ Name: ptr.To("test-app"), Namespace: ptr.To("argocd-1"), }) require.Error(t, err) require.Nil(t, links) require.ErrorContains(t, err, "app is not allowed in project") }) t.Run("Get list of links in other namespace when project is allowed", func(t *testing.T) { t.Parallel() testApp := newTestApp() testApp.Namespace = "argocd-1" testApp.Spec.Project = "other-ns" otherNsProj := &v1alpha1.AppProject{ ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, Spec: v1alpha1.AppProjectSpec{ SourceRepos: []string{"*"}, Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, SourceNamespaces: []string{"argocd-1"}, }, } appServer := newTestAppServer(t, testApp, otherNsProj) appServer.enabledNamespaces = []string{"argocd-1"} links, err := appServer.ListLinks(t.Context(), &application.ListAppLinksRequest{ Name: ptr.To("test-app"), Namespace: ptr.To("argocd-1"), }) require.NoError(t, err) assert.Empty(t, links.Items) }) } func TestGetAmbiguousRevision_MultiSource(t *testing.T) { app := &v1alpha1.Application{ Spec: v1alpha1.ApplicationSpec{ Sources: []v1alpha1.ApplicationSource{ { TargetRevision: "revision1", }, { TargetRevision: "revision2", }, }, }, } syncReq := &application.ApplicationSyncRequest{ SourcePositions: []int64{1, 2}, Revisions: []string{"rev1", "rev2"}, } sourceIndex := 0 expected := "rev1" result := getAmbiguousRevision(app, syncReq, sourceIndex) assert.Equalf(t, expected, result, "Expected ambiguous revision to be %s, but got %s", expected, result) sourceIndex = 1 expected = "rev2" result = getAmbiguousRevision(app, syncReq, sourceIndex) assert.Equal(t, expected, result, "Expected ambiguous revision to be %s, but got %s", expected, result) // Test when app.Spec.HasMultipleSources() is false app.Spec = v1alpha1.ApplicationSpec{ Source: &v1alpha1.ApplicationSource{ TargetRevision: "revision3", }, Sources: nil, } syncReq = &application.ApplicationSyncRequest{ Revision: strToPtr("revision3"), } expected = "revision3" result = getAmbiguousRevision(app, syncReq, sourceIndex) assert.Equal(t, expected, result, "Expected ambiguous revision to be %s, but got %s", expected, result) } func TestGetAmbiguousRevision_SingleSource(t *testing.T) { app := &v1alpha1.Application{ Spec: v1alpha1.ApplicationSpec{ Source: &v1alpha1.ApplicationSource{ TargetRevision: "revision1", }, }, } syncReq := &application.ApplicationSyncRequest{ Revision: strToPtr("rev1"), } // Test when app.Spec.HasMultipleSources() is true sourceIndex := 1 expected := "rev1" result := getAmbiguousRevision(app, syncReq, sourceIndex) assert.Equalf(t, expected, result, "Expected ambiguous revision to be %s, but got %s", expected, result) } func TestServer_ResolveSourceRevisions_MultiSource(t *testing.T) { s := newTestAppServer(t) ctx := t.Context() a := &v1alpha1.Application{ Spec: v1alpha1.ApplicationSpec{ Sources: []v1alpha1.ApplicationSource{ { RepoURL: "https://github.com/example/repo.git", }, }, }, } syncReq := &application.ApplicationSyncRequest{ SourcePositions: []int64{1}, Revisions: []string{"HEAD"}, } revision, displayRevision, sourceRevisions, displayRevisions, err := s.resolveSourceRevisions(ctx, a, syncReq) require.NoError(t, err) assert.Empty(t, revision) assert.Empty(t, displayRevision) assert.Equal(t, []string{fakeResolveRevisionResponse().Revision}, sourceRevisions) assert.Equal(t, []string{fakeResolveRevisionResponse().AmbiguousRevision}, displayRevisions) } func TestServer_ResolveSourceRevisions_SingleSource(t *testing.T) { s := newTestAppServer(t) ctx := t.Context() a := &v1alpha1.Application{ Spec: v1alpha1.ApplicationSpec{ Source: &v1alpha1.ApplicationSource{ RepoURL: "https://github.com/example/repo.git", }, }, } syncReq := &application.ApplicationSyncRequest{ Revision: strToPtr("HEAD"), } revision, displayRevision, sourceRevisions, displayRevisions, err := s.resolveSourceRevisions(ctx, a, syncReq) require.NoError(t, err) assert.Equal(t, fakeResolveRevisionResponse().Revision, revision) assert.Equal(t, fakeResolveRevisionResponse().AmbiguousRevision, displayRevision) assert.Equal(t, []string(nil), sourceRevisions) assert.Equal(t, []string(nil), displayRevisions) } func Test_RevisionMetadata(t *testing.T) { t.Parallel() singleSourceApp := newTestApp() singleSourceApp.Name = "single-source-app" singleSourceApp.Spec = v1alpha1.ApplicationSpec{ Source: &v1alpha1.ApplicationSource{ RepoURL: "https://github.com/argoproj/argocd-example-apps.git", Path: "helm-guestbook", TargetRevision: "HEAD", }, } multiSourceApp := newTestApp() multiSourceApp.Name = "multi-source-app" multiSourceApp.Spec = v1alpha1.ApplicationSpec{ Sources: []v1alpha1.ApplicationSource{ { RepoURL: "https://github.com/argoproj/argocd-example-apps.git", Path: "helm-guestbook", TargetRevision: "HEAD", }, { RepoURL: "https://github.com/argoproj/argocd-example-apps.git", Path: "kustomize-guestbook", TargetRevision: "HEAD", }, }, } singleSourceHistory := []v1alpha1.RevisionHistory{ { ID: 1, Source: singleSourceApp.Spec.GetSource(), Revision: "a", }, } multiSourceHistory := []v1alpha1.RevisionHistory{ { ID: 1, Sources: multiSourceApp.Spec.GetSources(), Revisions: []string{"a", "b"}, }, } testCases := []struct { name string multiSource bool history *struct { matchesSourceType bool } sourceIndex *int32 versionId *int32 expectErrorContains *string }{ { name: "single-source app without history, no source index, no version ID", multiSource: false, }, { name: "single-source app without history, no source index, missing version ID", multiSource: false, versionId: ptr.To(int32(999)), expectErrorContains: ptr.To("the app has no history"), }, { name: "single source app without history, present source index, no version ID", multiSource: false, sourceIndex: ptr.To(int32(0)), }, { name: "single source app without history, invalid source index, no version ID", multiSource: false, sourceIndex: ptr.To(int32(999)), expectErrorContains: ptr.To("source index 999 not found"), }, { name: "single source app with matching history, no source index, no version ID", multiSource: false, history: &struct{ matchesSourceType bool }{true}, }, { name: "single source app with matching history, no source index, missing version ID", multiSource: false, history: &struct{ matchesSourceType bool }{true}, versionId: ptr.To(int32(999)), expectErrorContains: ptr.To("history not found for version ID 999"), }, { name: "single source app with matching history, no source index, present version ID", multiSource: false, history: &struct{ matchesSourceType bool }{true}, versionId: ptr.To(int32(1)), }, { name: "single source app with multi-source history, no source index, no version ID", multiSource: false, history: &struct{ matchesSourceType bool }{false}, }, { name: "single source app with multi-source history, no source index, missing version ID", multiSource: false, history: &struct{ matchesSourceType bool }{false}, versionId: ptr.To(int32(999)), expectErrorContains: ptr.To("history not found for version ID 999"), }, { name: "single source app with multi-source history, no source index, present version ID", multiSource: false, history: &struct{ matchesSourceType bool }{false}, versionId: ptr.To(int32(1)), }, { name: "single-source app with multi-source history, source index 1, no version ID", multiSource: false, sourceIndex: ptr.To(int32(1)), history: &struct{ matchesSourceType bool }{false}, // Since the user requested source index 1, but no version ID, we'll get an error when looking at the live // source, because the live source is single-source. expectErrorContains: ptr.To("there is only 1 source"), }, { name: "single-source app with multi-source history, invalid source index, no version ID", multiSource: false, sourceIndex: ptr.To(int32(999)), history: &struct{ matchesSourceType bool }{false}, expectErrorContains: ptr.To("source index 999 not found"), }, { name: "single-source app with multi-source history, valid source index, present version ID", multiSource: false, sourceIndex: ptr.To(int32(1)), history: &struct{ matchesSourceType bool }{false}, versionId: ptr.To(int32(1)), }, { name: "multi-source app without history, no source index, no version ID", multiSource: true, }, { name: "multi-source app without history, no source index, missing version ID", multiSource: true, versionId: ptr.To(int32(999)), expectErrorContains: ptr.To("the app has no history"), }, { name: "multi-source app without history, present source index, no version ID", multiSource: true, sourceIndex: ptr.To(int32(1)), }, { name: "multi-source app without history, invalid source index, no version ID", multiSource: true, sourceIndex: ptr.To(int32(999)), expectErrorContains: ptr.To("source index 999 not found"), }, { name: "multi-source app with matching history, no source index, no version ID", multiSource: true, history: &struct{ matchesSourceType bool }{true}, }, { name: "multi-source app with matching history, no source index, missing version ID", multiSource: true, history: &struct{ matchesSourceType bool }{true}, versionId: ptr.To(int32(999)), expectErrorContains: ptr.To("history not found for version ID 999"), }, { name: "multi-source app with matching history, no source index, present version ID", multiSource: true, history: &struct{ matchesSourceType bool }{true}, versionId: ptr.To(int32(1)), }, { name: "multi-source app with single-source history, no source index, no version ID", multiSource: true, history: &struct{ matchesSourceType bool }{false}, }, { name: "multi-source app with single-source history, no source index, missing version ID", multiSource: true, history: &struct{ matchesSourceType bool }{false}, versionId: ptr.To(int32(999)), expectErrorContains: ptr.To("history not found for version ID 999"), }, { name: "multi-source app with single-source history, no source index, present version ID", multiSource: true, history: &struct{ matchesSourceType bool }{false}, versionId: ptr.To(int32(1)), }, { name: "multi-source app with single-source history, source index 1, no version ID", multiSource: true, sourceIndex: ptr.To(int32(1)), history: &struct{ matchesSourceType bool }{false}, }, { name: "multi-source app with single-source history, invalid source index, no version ID", multiSource: true, sourceIndex: ptr.To(int32(999)), history: &struct{ matchesSourceType bool }{false}, expectErrorContains: ptr.To("source index 999 not found"), }, { name: "multi-source app with single-source history, valid source index, present version ID", multiSource: true, sourceIndex: ptr.To(int32(0)), history: &struct{ matchesSourceType bool }{false}, versionId: ptr.To(int32(1)), }, { name: "multi-source app with single-source history, source index 1, present version ID", multiSource: true, sourceIndex: ptr.To(int32(1)), history: &struct{ matchesSourceType bool }{false}, versionId: ptr.To(int32(1)), expectErrorContains: ptr.To("source index 1 not found"), }, } for _, tc := range testCases { tcc := tc t.Run(tcc.name, func(t *testing.T) { t.Parallel() app := singleSourceApp.DeepCopy() if tcc.multiSource { app = multiSourceApp.DeepCopy() } if tcc.history != nil { if tcc.history.matchesSourceType { if tcc.multiSource { app.Status.History = multiSourceHistory } else { app.Status.History = singleSourceHistory } } else { if tcc.multiSource { app.Status.History = singleSourceHistory } else { app.Status.History = multiSourceHistory } } } s := newTestAppServer(t, app) request := &application.RevisionMetadataQuery{ Name: ptr.To(app.Name), Revision: ptr.To("HEAD"), SourceIndex: tcc.sourceIndex, VersionId: tcc.versionId, } _, err := s.RevisionMetadata(t.Context(), request) if tcc.expectErrorContains != nil { require.ErrorContains(t, err, *tcc.expectErrorContains) } else { require.NoError(t, err) } }) } } func Test_DeepCopyInformers(t *testing.T) { t.Parallel() namespace := "test-namespace" var ro []runtime.Object appOne := newTestApp(func(app *v1alpha1.Application) { app.Name = "appOne" app.Namespace = namespace app.Spec = v1alpha1.ApplicationSpec{} }) appTwo := newTestApp(func(app *v1alpha1.Application) { app.Name = "appTwo" app.Namespace = namespace app.Spec = v1alpha1.ApplicationSpec{} }) appThree := newTestApp(func(app *v1alpha1.Application) { app.Name = "appThree" app.Namespace = namespace app.Spec = v1alpha1.ApplicationSpec{} }) ro = append(ro, appOne, appTwo, appThree) appls := []v1alpha1.Application{*appOne, *appTwo, *appThree} appSetOne := &v1alpha1.ApplicationSet{ ObjectMeta: metav1.ObjectMeta{Name: "appSetOne", Namespace: namespace}, Spec: v1alpha1.ApplicationSetSpec{}, } appSetTwo := &v1alpha1.ApplicationSet{ ObjectMeta: metav1.ObjectMeta{Name: "appSetTwo", Namespace: namespace}, Spec: v1alpha1.ApplicationSetSpec{}, } appSetThree := &v1alpha1.ApplicationSet{ ObjectMeta: metav1.ObjectMeta{Name: "appSetThree", Namespace: namespace}, Spec: v1alpha1.ApplicationSetSpec{}, } ro = append(ro, appSetOne, appSetTwo, appSetThree) appSets := []v1alpha1.ApplicationSet{*appSetOne, *appSetTwo, *appSetThree} appProjects := createAppProject("projOne", "projTwo", "projThree") for i := range appProjects { ro = append(ro, &appProjects[i]) } s := newTestAppServer(t, ro...) appList, err := s.appclientset.ArgoprojV1alpha1().Applications(namespace).List(t.Context(), metav1.ListOptions{}) require.NoError(t, err) assert.ElementsMatch(t, appls, appList.Items) sAppList := appList.Items slices.SortFunc(sAppList, func(a, b v1alpha1.Application) int { return strings.Compare(a.Name, b.Name) }) slices.SortFunc(appls, func(a, b v1alpha1.Application) int { return strings.Compare(a.Name, b.Name) }) // ensure there is a deep copy for i := range appls { assert.NotSame(t, &appls[i], &sAppList[i]) assert.NotSame(t, &appls[i].Spec, &sAppList[i].Spec) a, err := s.appclientset.ArgoprojV1alpha1().Applications(namespace).Get(t.Context(), sAppList[i].Name, metav1.GetOptions{}) require.NoError(t, err) assert.NotSame(t, a, &sAppList[i]) } appSetList, err := s.appclientset.ArgoprojV1alpha1().ApplicationSets(namespace).List(t.Context(), metav1.ListOptions{}) require.NoError(t, err) assert.ElementsMatch(t, appSets, appSetList.Items) sAppSetList := appSetList.Items slices.SortFunc(sAppSetList, func(a, b v1alpha1.ApplicationSet) int { return strings.Compare(a.Name, b.Name) }) slices.SortFunc(appSets, func(a, b v1alpha1.ApplicationSet) int { return strings.Compare(a.Name, b.Name) }) for i := range appSets { assert.NotSame(t, &appSets[i], &sAppSetList[i]) assert.NotSame(t, &appSets[i].Spec, &sAppSetList[i].Spec) a, err := s.appclientset.ArgoprojV1alpha1().ApplicationSets(namespace).Get(t.Context(), sAppSetList[i].Name, metav1.GetOptions{}) require.NoError(t, err) assert.NotSame(t, a, &sAppSetList[i]) } projList, err := s.appclientset.ArgoprojV1alpha1().AppProjects("deep-copy-ns").List(t.Context(), metav1.ListOptions{}) require.NoError(t, err) assert.ElementsMatch(t, appProjects, projList.Items) spList := projList.Items slices.SortFunc(spList, func(a, b v1alpha1.AppProject) int { return strings.Compare(a.Name, b.Name) }) slices.SortFunc(appProjects, func(a, b v1alpha1.AppProject) int { return strings.Compare(a.Name, b.Name) }) for i := range appProjects { assert.NotSame(t, &appProjects[i], &spList[i]) assert.NotSame(t, &appProjects[i].Spec, &spList[i].Spec) p, err := s.appclientset.ArgoprojV1alpha1().AppProjects("deep-copy-ns").Get(t.Context(), spList[i].Name, metav1.GetOptions{}) require.NoError(t, err) assert.NotSame(t, p, &spList[i]) } } func TestServerSideDiff(t *testing.T) { // Create test projects (avoid "default" which is already created by newTestAppServerWithEnforcerConfigure) testProj := &v1alpha1.AppProject{ ObjectMeta: metav1.ObjectMeta{Name: "test-project", Namespace: testNamespace}, Spec: v1alpha1.AppProjectSpec{ SourceRepos: []string{"*"}, Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, }, } forbiddenProj := &v1alpha1.AppProject{ ObjectMeta: metav1.ObjectMeta{Name: "forbidden-project", Namespace: testNamespace}, Spec: v1alpha1.AppProjectSpec{ SourceRepos: []string{"*"}, Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, }, } // Create test applications that will exist in the server testApp := newTestApp(func(app *v1alpha1.Application) { app.Name = "test-app" app.Namespace = testNamespace app.Spec.Project = "test-project" }) forbiddenApp := newTestApp(func(app *v1alpha1.Application) { app.Name = "forbidden-app" app.Namespace = testNamespace app.Spec.Project = "forbidden-project" }) appServer := newTestAppServer(t, testProj, forbiddenProj, testApp, forbiddenApp) t.Run("InputValidation", func(t *testing.T) { // Test missing application name query := &application.ApplicationServerSideDiffQuery{ AppName: ptr.To(""), // Empty name instead of nil AppNamespace: ptr.To(testNamespace), Project: ptr.To("test-project"), LiveResources: []*v1alpha1.ResourceDiff{}, TargetManifests: []string{}, } _, err := appServer.ServerSideDiff(t.Context(), query) require.Error(t, err) assert.Contains(t, err.Error(), "not found") // Test nil application name queryNil := &application.ApplicationServerSideDiffQuery{ AppName: nil, AppNamespace: ptr.To(testNamespace), Project: ptr.To("test-project"), LiveResources: []*v1alpha1.ResourceDiff{}, TargetManifests: []string{}, } _, err = appServer.ServerSideDiff(t.Context(), queryNil) assert.Error(t, err) // Should get an error when name is nil }) t.Run("InvalidManifest", func(t *testing.T) { // Test error handling for malformed JSON in target manifests query := &application.ApplicationServerSideDiffQuery{ AppName: ptr.To("test-app"), AppNamespace: ptr.To(testNamespace), Project: ptr.To("test-project"), LiveResources: []*v1alpha1.ResourceDiff{}, TargetManifests: []string{`invalid json`}, } _, err := appServer.ServerSideDiff(t.Context(), query) // Should return error for invalid JSON require.Error(t, err) assert.Contains(t, err.Error(), "error unmarshaling target manifest") }) t.Run("InvalidLiveState", func(t *testing.T) { // Test error handling for malformed JSON in live state liveResource := &v1alpha1.ResourceDiff{ Group: "apps", Kind: "Deployment", Namespace: "default", Name: "test-deployment", LiveState: `invalid json`, TargetState: "", Modified: true, } query := &application.ApplicationServerSideDiffQuery{ AppName: ptr.To("test-app"), AppNamespace: ptr.To(testNamespace), Project: ptr.To("test-project"), LiveResources: []*v1alpha1.ResourceDiff{liveResource}, TargetManifests: []string{`{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"name":"test"}}`}, } _, err := appServer.ServerSideDiff(t.Context(), query) // Should return error for invalid JSON in live state require.Error(t, err) assert.Contains(t, err.Error(), "error unmarshaling live state") }) t.Run("EmptyRequest", func(t *testing.T) { // Test with empty resources - should succeed without errors but no diffs query := &application.ApplicationServerSideDiffQuery{ AppName: ptr.To("test-app"), AppNamespace: ptr.To(testNamespace), Project: ptr.To("test-project"), LiveResources: []*v1alpha1.ResourceDiff{}, TargetManifests: []string{}, } resp, err := appServer.ServerSideDiff(t.Context(), query) // Should succeed with empty response require.NoError(t, err) assert.NotNil(t, resp) assert.False(t, *resp.Modified) assert.Empty(t, resp.Items) }) t.Run("MissingAppPermission", func(t *testing.T) { // Test RBAC enforcement query := &application.ApplicationServerSideDiffQuery{ AppName: ptr.To("nonexistent-app"), AppNamespace: ptr.To(testNamespace), Project: ptr.To("nonexistent-project"), LiveResources: []*v1alpha1.ResourceDiff{}, TargetManifests: []string{}, } _, err := appServer.ServerSideDiff(t.Context(), query) // Should fail with permission error since nonexistent-app doesn't exist require.Error(t, err) assert.Contains(t, err.Error(), "application") }) }