github.com/argoproj/argo-cd/v3@v3.2.1/server/application/application_test.go (about) 1 package application 2 3 import ( 4 "context" 5 stderrors "errors" 6 "fmt" 7 "io" 8 "slices" 9 "strconv" 10 "strings" 11 "sync/atomic" 12 "testing" 13 "time" 14 15 "k8s.io/apimachinery/pkg/labels" 16 17 "github.com/argoproj/gitops-engine/pkg/health" 18 synccommon "github.com/argoproj/gitops-engine/pkg/sync/common" 19 "github.com/argoproj/gitops-engine/pkg/utils/kube" 20 "github.com/argoproj/gitops-engine/pkg/utils/kube/kubetest" 21 "github.com/argoproj/pkg/v2/sync" 22 "github.com/golang-jwt/jwt/v5" 23 "github.com/stretchr/testify/assert" 24 "github.com/stretchr/testify/mock" 25 "github.com/stretchr/testify/require" 26 "google.golang.org/grpc/codes" 27 "google.golang.org/grpc/metadata" 28 "google.golang.org/grpc/status" 29 appsv1 "k8s.io/api/apps/v1" 30 k8sbatchv1 "k8s.io/api/batch/v1" 31 corev1 "k8s.io/api/core/v1" 32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 34 "k8s.io/apimachinery/pkg/runtime" 35 "k8s.io/apimachinery/pkg/runtime/schema" 36 "k8s.io/apimachinery/pkg/watch" 37 "k8s.io/client-go/kubernetes/fake" 38 "k8s.io/client-go/rest" 39 kubetesting "k8s.io/client-go/testing" 40 k8scache "k8s.io/client-go/tools/cache" 41 "k8s.io/utils/ptr" 42 "sigs.k8s.io/yaml" 43 44 "github.com/argoproj/argo-cd/v3/common" 45 "github.com/argoproj/argo-cd/v3/pkg/apiclient/application" 46 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" 47 apps "github.com/argoproj/argo-cd/v3/pkg/client/clientset/versioned/fake" 48 appinformer "github.com/argoproj/argo-cd/v3/pkg/client/informers/externalversions" 49 "github.com/argoproj/argo-cd/v3/reposerver/apiclient" 50 "github.com/argoproj/argo-cd/v3/reposerver/apiclient/mocks" 51 servercache "github.com/argoproj/argo-cd/v3/server/cache" 52 "github.com/argoproj/argo-cd/v3/server/rbacpolicy" 53 "github.com/argoproj/argo-cd/v3/test" 54 "github.com/argoproj/argo-cd/v3/util/argo" 55 "github.com/argoproj/argo-cd/v3/util/assets" 56 "github.com/argoproj/argo-cd/v3/util/cache" 57 "github.com/argoproj/argo-cd/v3/util/cache/appstate" 58 "github.com/argoproj/argo-cd/v3/util/db" 59 "github.com/argoproj/argo-cd/v3/util/grpc" 60 "github.com/argoproj/argo-cd/v3/util/rbac" 61 "github.com/argoproj/argo-cd/v3/util/settings" 62 ) 63 64 const ( 65 testNamespace = "default" 66 fakeRepoURL = "https://git.com/repo.git" 67 ) 68 69 var testEnableEventList []string = argo.DefaultEnableEventList() 70 71 type broadcasterMock struct { 72 objects []runtime.Object 73 } 74 75 func (b broadcasterMock) Subscribe(ch chan *v1alpha1.ApplicationWatchEvent, _ ...func(event *v1alpha1.ApplicationWatchEvent) bool) func() { 76 // Simulate the broadcaster notifying the subscriber of an application update. 77 // The second parameter to Subscribe is filters. For the purposes of tests, we ignore the filters. Future tests 78 // might require implementing those. 79 go func() { 80 for _, obj := range b.objects { 81 app, ok := obj.(*v1alpha1.Application) 82 if ok { 83 oldVersion, err := strconv.Atoi(app.ResourceVersion) 84 if err != nil { 85 oldVersion = 0 86 } 87 clonedApp := app.DeepCopy() 88 clonedApp.ResourceVersion = strconv.Itoa(oldVersion + 1) 89 ch <- &v1alpha1.ApplicationWatchEvent{Type: watch.Added, Application: *clonedApp} 90 } 91 } 92 }() 93 return func() {} 94 } 95 96 func (broadcasterMock) OnAdd(any, bool) {} 97 func (broadcasterMock) OnUpdate(any, any) {} 98 func (broadcasterMock) OnDelete(any) {} 99 100 func fakeRepo() *v1alpha1.Repository { 101 return &v1alpha1.Repository{ 102 Repo: fakeRepoURL, 103 } 104 } 105 106 func fakeCluster() *v1alpha1.Cluster { 107 return &v1alpha1.Cluster{ 108 Server: "https://cluster-api.example.com", 109 Name: "fake-cluster", 110 Config: v1alpha1.ClusterConfig{}, 111 } 112 } 113 114 func fakeAppList() *apiclient.AppList { 115 return &apiclient.AppList{ 116 Apps: map[string]string{ 117 "some/path": "Ksonnet", 118 }, 119 } 120 } 121 122 func fakeResolveRevisionResponse() *apiclient.ResolveRevisionResponse { 123 return &apiclient.ResolveRevisionResponse{ 124 Revision: "f9ba9e98119bf8c1176fbd65dbae26a71d044add", 125 AmbiguousRevision: "HEAD (f9ba9e98119bf8c1176fbd65dbae26a71d044add)", 126 } 127 } 128 129 func fakeResolveRevisionResponseHelm() *apiclient.ResolveRevisionResponse { 130 return &apiclient.ResolveRevisionResponse{ 131 Revision: "0.7.*", 132 AmbiguousRevision: "0.7.* (0.7.2)", 133 } 134 } 135 136 func fakeRepoServerClient(isHelm bool) *mocks.RepoServerServiceClient { 137 mockRepoServiceClient := mocks.RepoServerServiceClient{} 138 mockRepoServiceClient.On("GetProcessableApps", mock.Anything, mock.Anything).Return(fakeAppList(), nil) 139 mockRepoServiceClient.On("GenerateManifest", mock.Anything, mock.Anything).Return(&apiclient.ManifestResponse{}, nil) 140 mockRepoServiceClient.On("GetAppDetails", mock.Anything, mock.Anything).Return(&apiclient.RepoAppDetailsResponse{}, nil) 141 mockRepoServiceClient.On("TestRepository", mock.Anything, mock.Anything).Return(&apiclient.TestRepositoryResponse{}, nil) 142 mockRepoServiceClient.On("GetRevisionMetadata", mock.Anything, mock.Anything).Return(&v1alpha1.RevisionMetadata{}, nil) 143 mockWithFilesClient := &mocks.RepoServerService_GenerateManifestWithFilesClient{} 144 mockWithFilesClient.On("Send", mock.Anything).Return(nil) 145 mockWithFilesClient.On("CloseAndRecv").Return(&apiclient.ManifestResponse{}, nil) 146 mockRepoServiceClient.On("GenerateManifestWithFiles", mock.Anything, mock.Anything).Return(mockWithFilesClient, nil) 147 mockRepoServiceClient.On("GetRevisionChartDetails", mock.Anything, mock.Anything).Return(&v1alpha1.ChartDetails{}, nil) 148 149 if isHelm { 150 mockRepoServiceClient.On("ResolveRevision", mock.Anything, mock.Anything).Return(fakeResolveRevisionResponseHelm(), nil) 151 } else { 152 mockRepoServiceClient.On("ResolveRevision", mock.Anything, mock.Anything).Return(fakeResolveRevisionResponse(), nil) 153 } 154 155 return &mockRepoServiceClient 156 } 157 158 // return an ApplicationServiceServer which returns fake data 159 func newTestAppServer(t *testing.T, objects ...runtime.Object) *Server { 160 t.Helper() 161 f := func(enf *rbac.Enforcer) { 162 _ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV) 163 enf.SetDefaultRole("role:admin") 164 } 165 return newTestAppServerWithEnforcerConfigure(t, f, map[string]string{}, objects...) 166 } 167 168 func newTestAppServerWithEnforcerConfigure(t *testing.T, f func(*rbac.Enforcer), additionalConfig map[string]string, objects ...runtime.Object) *Server { 169 t.Helper() 170 kubeclientset := fake.NewClientset(&corev1.ConfigMap{ 171 ObjectMeta: metav1.ObjectMeta{ 172 Namespace: testNamespace, 173 Name: "argocd-cm", 174 Labels: map[string]string{ 175 "app.kubernetes.io/part-of": "argocd", 176 }, 177 }, 178 Data: additionalConfig, 179 }, &corev1.Secret{ 180 ObjectMeta: metav1.ObjectMeta{ 181 Name: "argocd-secret", 182 Namespace: testNamespace, 183 }, 184 Data: map[string][]byte{ 185 "admin.password": []byte("test"), 186 "server.secretkey": []byte("test"), 187 }, 188 }) 189 ctx := t.Context() 190 db := db.NewDB(testNamespace, settings.NewSettingsManager(ctx, kubeclientset, testNamespace), kubeclientset) 191 _, err := db.CreateRepository(ctx, fakeRepo()) 192 require.NoError(t, err) 193 _, err = db.CreateCluster(ctx, fakeCluster()) 194 require.NoError(t, err) 195 196 mockRepoClient := &mocks.Clientset{RepoServerServiceClient: fakeRepoServerClient(false)} 197 198 defaultProj := &v1alpha1.AppProject{ 199 ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "default"}, 200 Spec: v1alpha1.AppProjectSpec{ 201 SourceRepos: []string{"*"}, 202 Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 203 }, 204 } 205 206 myProj := &v1alpha1.AppProject{ 207 ObjectMeta: metav1.ObjectMeta{Name: "my-proj", Namespace: "default"}, 208 Spec: v1alpha1.AppProjectSpec{ 209 SourceRepos: []string{"*"}, 210 Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 211 }, 212 } 213 projWithSyncWindows := &v1alpha1.AppProject{ 214 ObjectMeta: metav1.ObjectMeta{Name: "proj-maint", Namespace: "default"}, 215 Spec: v1alpha1.AppProjectSpec{ 216 SourceRepos: []string{"*"}, 217 Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 218 SyncWindows: v1alpha1.SyncWindows{}, 219 }, 220 } 221 matchingWindow := &v1alpha1.SyncWindow{ 222 Kind: "allow", 223 Schedule: "* * * * *", 224 Duration: "1h", 225 Applications: []string{"test-app"}, 226 } 227 projWithSyncWindows.Spec.SyncWindows = append(projWithSyncWindows.Spec.SyncWindows, matchingWindow) 228 229 objects = append(objects, defaultProj, myProj, projWithSyncWindows) 230 231 fakeAppsClientset := apps.NewSimpleClientset(objects...) 232 factory := appinformer.NewSharedInformerFactoryWithOptions(fakeAppsClientset, 0, appinformer.WithNamespace(""), appinformer.WithTweakListOptions(func(_ *metav1.ListOptions) {})) 233 fakeProjLister := factory.Argoproj().V1alpha1().AppProjects().Lister().AppProjects(testNamespace) 234 235 enforcer := rbac.NewEnforcer(kubeclientset, testNamespace, common.ArgoCDRBACConfigMapName, nil) 236 f(enforcer) 237 enforcer.SetClaimsEnforcerFunc(rbacpolicy.NewRBACPolicyEnforcer(enforcer, fakeProjLister).EnforceClaims) 238 239 settingsMgr := settings.NewSettingsManager(ctx, kubeclientset, testNamespace) 240 241 // populate the app informer with the fake objects 242 appInformer := factory.Argoproj().V1alpha1().Applications().Informer() 243 // TODO(jessesuen): probably should return cancel function so tests can stop background informer 244 // ctx, cancel := context.WithCancel(t.Context()) 245 go appInformer.Run(ctx.Done()) 246 if !k8scache.WaitForCacheSync(ctx.Done(), appInformer.HasSynced) { 247 panic("Timed out waiting for caches to sync") 248 } 249 250 projInformer := factory.Argoproj().V1alpha1().AppProjects().Informer() 251 go projInformer.Run(ctx.Done()) 252 if !k8scache.WaitForCacheSync(ctx.Done(), projInformer.HasSynced) { 253 panic("Timed out waiting for caches to sync") 254 } 255 256 broadcaster := broadcasterMock{ 257 objects: objects, 258 } 259 260 appStateCache := appstate.NewCache(cache.NewCache(cache.NewInMemoryCache(time.Hour)), time.Hour) 261 // pre-populate the app cache 262 for _, obj := range objects { 263 app, ok := obj.(*v1alpha1.Application) 264 if ok { 265 err := appStateCache.SetAppManagedResources(app.Name, []*v1alpha1.ResourceDiff{}) 266 require.NoError(t, err) 267 268 // Pre-populate the resource tree based on the app's resources. 269 nodes := make([]v1alpha1.ResourceNode, len(app.Status.Resources)) 270 for i, res := range app.Status.Resources { 271 nodes[i] = v1alpha1.ResourceNode{ 272 ResourceRef: v1alpha1.ResourceRef{ 273 Group: res.Group, 274 Kind: res.Kind, 275 Version: res.Version, 276 Name: res.Name, 277 Namespace: res.Namespace, 278 UID: "fake", 279 }, 280 } 281 } 282 err = appStateCache.SetAppResourcesTree(app.Name, &v1alpha1.ApplicationTree{ 283 Nodes: nodes, 284 }) 285 require.NoError(t, err) 286 } 287 } 288 appCache := servercache.NewCache(appStateCache, time.Hour, time.Hour) 289 290 kubectl := &kubetest.MockKubectlCmd{} 291 kubectl = kubectl.WithGetResourceFunc(func(_ context.Context, _ *rest.Config, gvk schema.GroupVersionKind, name string, namespace string) (*unstructured.Unstructured, error) { 292 for _, obj := range objects { 293 if obj.GetObjectKind().GroupVersionKind().GroupKind() == gvk.GroupKind() { 294 if obj, ok := obj.(*unstructured.Unstructured); ok && obj.GetName() == name && obj.GetNamespace() == namespace { 295 return obj, nil 296 } 297 } 298 } 299 return nil, nil 300 }) 301 302 server, _ := NewServer( 303 testNamespace, 304 kubeclientset, 305 fakeAppsClientset, 306 factory.Argoproj().V1alpha1().Applications().Lister(), 307 appInformer, 308 broadcaster, 309 mockRepoClient, 310 appCache, 311 kubectl, 312 db, 313 enforcer, 314 sync.NewKeyLock(), 315 settingsMgr, 316 projInformer, 317 []string{}, 318 testEnableEventList, 319 true, 320 ) 321 return server.(*Server) 322 } 323 324 // return an ApplicationServiceServer which returns fake data 325 func newTestAppServerWithBenchmark(b *testing.B, objects ...runtime.Object) *Server { 326 b.Helper() 327 f := func(enf *rbac.Enforcer) { 328 _ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV) 329 enf.SetDefaultRole("role:admin") 330 } 331 return newTestAppServerWithEnforcerConfigureWithBenchmark(b, f, objects...) 332 } 333 334 func newTestAppServerWithEnforcerConfigureWithBenchmark(b *testing.B, f func(*rbac.Enforcer), objects ...runtime.Object) *Server { 335 b.Helper() 336 kubeclientset := fake.NewClientset(&corev1.ConfigMap{ 337 ObjectMeta: metav1.ObjectMeta{ 338 Namespace: testNamespace, 339 Name: "argocd-cm", 340 Labels: map[string]string{ 341 "app.kubernetes.io/part-of": "argocd", 342 }, 343 }, 344 }, &corev1.Secret{ 345 ObjectMeta: metav1.ObjectMeta{ 346 Name: "argocd-secret", 347 Namespace: testNamespace, 348 }, 349 Data: map[string][]byte{ 350 "admin.password": []byte("test"), 351 "server.secretkey": []byte("test"), 352 }, 353 }) 354 ctx := b.Context() 355 db := db.NewDB(testNamespace, settings.NewSettingsManager(ctx, kubeclientset, testNamespace), kubeclientset) 356 _, err := db.CreateRepository(ctx, fakeRepo()) 357 require.NoError(b, err) 358 _, err = db.CreateCluster(ctx, fakeCluster()) 359 require.NoError(b, err) 360 361 mockRepoClient := &mocks.Clientset{RepoServerServiceClient: fakeRepoServerClient(false)} 362 363 defaultProj := &v1alpha1.AppProject{ 364 ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "default"}, 365 Spec: v1alpha1.AppProjectSpec{ 366 SourceRepos: []string{"*"}, 367 Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 368 }, 369 } 370 myProj := &v1alpha1.AppProject{ 371 ObjectMeta: metav1.ObjectMeta{Name: "my-proj", Namespace: "default"}, 372 Spec: v1alpha1.AppProjectSpec{ 373 SourceRepos: []string{"*"}, 374 Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 375 }, 376 } 377 projWithSyncWindows := &v1alpha1.AppProject{ 378 ObjectMeta: metav1.ObjectMeta{Name: "proj-maint", Namespace: "default"}, 379 Spec: v1alpha1.AppProjectSpec{ 380 SourceRepos: []string{"*"}, 381 Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 382 SyncWindows: v1alpha1.SyncWindows{}, 383 }, 384 } 385 matchingWindow := &v1alpha1.SyncWindow{ 386 Kind: "allow", 387 Schedule: "* * * * *", 388 Duration: "1h", 389 Applications: []string{"test-app"}, 390 } 391 projWithSyncWindows.Spec.SyncWindows = append(projWithSyncWindows.Spec.SyncWindows, matchingWindow) 392 393 objects = append(objects, defaultProj, myProj, projWithSyncWindows) 394 395 fakeAppsClientset := apps.NewSimpleClientset(objects...) 396 factory := appinformer.NewSharedInformerFactoryWithOptions(fakeAppsClientset, 0, appinformer.WithNamespace(""), appinformer.WithTweakListOptions(func(_ *metav1.ListOptions) {})) 397 fakeProjLister := factory.Argoproj().V1alpha1().AppProjects().Lister().AppProjects(testNamespace) 398 399 enforcer := rbac.NewEnforcer(kubeclientset, testNamespace, common.ArgoCDRBACConfigMapName, nil) 400 f(enforcer) 401 enforcer.SetClaimsEnforcerFunc(rbacpolicy.NewRBACPolicyEnforcer(enforcer, fakeProjLister).EnforceClaims) 402 403 settingsMgr := settings.NewSettingsManager(ctx, kubeclientset, testNamespace) 404 405 // populate the app informer with the fake objects 406 appInformer := factory.Argoproj().V1alpha1().Applications().Informer() 407 408 go appInformer.Run(ctx.Done()) 409 if !k8scache.WaitForCacheSync(ctx.Done(), appInformer.HasSynced) { 410 panic("Timed out waiting for caches to sync") 411 } 412 413 projInformer := factory.Argoproj().V1alpha1().AppProjects().Informer() 414 go projInformer.Run(ctx.Done()) 415 if !k8scache.WaitForCacheSync(ctx.Done(), projInformer.HasSynced) { 416 panic("Timed out waiting for caches to sync") 417 } 418 419 broadcaster := broadcasterMock{ 420 objects: objects, 421 } 422 423 appStateCache := appstate.NewCache(cache.NewCache(cache.NewInMemoryCache(time.Hour)), time.Hour) 424 // pre-populate the app cache 425 for _, obj := range objects { 426 app, ok := obj.(*v1alpha1.Application) 427 if ok { 428 err := appStateCache.SetAppManagedResources(app.Name, []*v1alpha1.ResourceDiff{}) 429 require.NoError(b, err) 430 431 // Pre-populate the resource tree based on the app's resources. 432 nodes := make([]v1alpha1.ResourceNode, len(app.Status.Resources)) 433 for i, res := range app.Status.Resources { 434 nodes[i] = v1alpha1.ResourceNode{ 435 ResourceRef: v1alpha1.ResourceRef{ 436 Group: res.Group, 437 Kind: res.Kind, 438 Version: res.Version, 439 Name: res.Name, 440 Namespace: res.Namespace, 441 UID: "fake", 442 }, 443 } 444 } 445 err = appStateCache.SetAppResourcesTree(app.Name, &v1alpha1.ApplicationTree{ 446 Nodes: nodes, 447 }) 448 require.NoError(b, err) 449 } 450 } 451 appCache := servercache.NewCache(appStateCache, time.Hour, time.Hour) 452 453 kubectl := &kubetest.MockKubectlCmd{} 454 kubectl = kubectl.WithGetResourceFunc(func(_ context.Context, _ *rest.Config, gvk schema.GroupVersionKind, name string, namespace string) (*unstructured.Unstructured, error) { 455 for _, obj := range objects { 456 if obj.GetObjectKind().GroupVersionKind().GroupKind() == gvk.GroupKind() { 457 if obj, ok := obj.(*unstructured.Unstructured); ok && obj.GetName() == name && obj.GetNamespace() == namespace { 458 return obj, nil 459 } 460 } 461 } 462 return nil, nil 463 }) 464 465 server, _ := NewServer( 466 testNamespace, 467 kubeclientset, 468 fakeAppsClientset, 469 factory.Argoproj().V1alpha1().Applications().Lister(), 470 appInformer, 471 broadcaster, 472 mockRepoClient, 473 appCache, 474 kubectl, 475 db, 476 enforcer, 477 sync.NewKeyLock(), 478 settingsMgr, 479 projInformer, 480 []string{}, 481 testEnableEventList, 482 true, 483 ) 484 return server.(*Server) 485 } 486 487 const fakeApp = ` 488 apiVersion: argoproj.io/v1alpha1 489 kind: Application 490 metadata: 491 name: test-app 492 namespace: default 493 spec: 494 source: 495 path: some/path 496 repoURL: https://github.com/argoproj/argocd-example-apps.git 497 targetRevision: HEAD 498 ksonnet: 499 environment: default 500 destination: 501 namespace: ` + test.FakeDestNamespace + ` 502 server: https://cluster-api.example.com 503 ` 504 505 const fakeAppWithDestName = ` 506 apiVersion: argoproj.io/v1alpha1 507 kind: Application 508 metadata: 509 name: test-app 510 namespace: default 511 spec: 512 source: 513 path: some/path 514 repoURL: https://github.com/argoproj/argocd-example-apps.git 515 targetRevision: HEAD 516 ksonnet: 517 environment: default 518 destination: 519 namespace: ` + test.FakeDestNamespace + ` 520 name: fake-cluster 521 ` 522 523 const fakeAppWithAnnotations = ` 524 apiVersion: argoproj.io/v1alpha1 525 kind: Application 526 metadata: 527 name: test-app 528 namespace: default 529 annotations: 530 test.annotation: test 531 spec: 532 source: 533 path: some/path 534 repoURL: https://github.com/argoproj/argocd-example-apps.git 535 targetRevision: HEAD 536 ksonnet: 537 environment: default 538 destination: 539 namespace: ` + test.FakeDestNamespace + ` 540 server: https://cluster-api.example.com 541 ` 542 543 func newTestAppWithDestName(opts ...func(app *v1alpha1.Application)) *v1alpha1.Application { 544 return createTestApp(fakeAppWithDestName, opts...) 545 } 546 547 func newTestApp(opts ...func(app *v1alpha1.Application)) *v1alpha1.Application { 548 return createTestApp(fakeApp, opts...) 549 } 550 551 func newTestAppWithAnnotations(opts ...func(app *v1alpha1.Application)) *v1alpha1.Application { 552 return createTestApp(fakeAppWithAnnotations, opts...) 553 } 554 555 func createTestApp(testApp string, opts ...func(app *v1alpha1.Application)) *v1alpha1.Application { 556 var app v1alpha1.Application 557 err := yaml.Unmarshal([]byte(testApp), &app) 558 if err != nil { 559 panic(err) 560 } 561 for i := range opts { 562 opts[i](&app) 563 } 564 return &app 565 } 566 567 type TestServerStream struct { 568 ctx context.Context 569 appName string 570 headerSent bool 571 project string 572 } 573 574 func (t *TestServerStream) SetHeader(metadata.MD) error { 575 return nil 576 } 577 578 func (t *TestServerStream) SendHeader(metadata.MD) error { 579 return nil 580 } 581 582 func (t *TestServerStream) SetTrailer(metadata.MD) {} 583 584 func (t *TestServerStream) Context() context.Context { 585 return t.ctx 586 } 587 588 func (t *TestServerStream) SendMsg(_ any) error { 589 return nil 590 } 591 592 func (t *TestServerStream) RecvMsg(_ any) error { 593 return nil 594 } 595 596 func (t *TestServerStream) SendAndClose(_ *apiclient.ManifestResponse) error { 597 return nil 598 } 599 600 func (t *TestServerStream) Recv() (*application.ApplicationManifestQueryWithFilesWrapper, error) { 601 if !t.headerSent { 602 t.headerSent = true 603 return &application.ApplicationManifestQueryWithFilesWrapper{Part: &application.ApplicationManifestQueryWithFilesWrapper_Query{ 604 Query: &application.ApplicationManifestQueryWithFiles{ 605 Name: ptr.To(t.appName), 606 Project: ptr.To(t.project), 607 Checksum: ptr.To(""), 608 }, 609 }}, nil 610 } 611 return nil, io.EOF 612 } 613 614 func (t *TestServerStream) ServerStream() TestServerStream { 615 return TestServerStream{} 616 } 617 618 type TestResourceTreeServer struct { 619 ctx context.Context 620 } 621 622 func (t *TestResourceTreeServer) Send(_ *v1alpha1.ApplicationTree) error { 623 return nil 624 } 625 626 func (t *TestResourceTreeServer) SetHeader(metadata.MD) error { 627 return nil 628 } 629 630 func (t *TestResourceTreeServer) SendHeader(metadata.MD) error { 631 return nil 632 } 633 634 func (t *TestResourceTreeServer) SetTrailer(metadata.MD) {} 635 636 func (t *TestResourceTreeServer) Context() context.Context { 637 return t.ctx 638 } 639 640 func (t *TestResourceTreeServer) SendMsg(_ any) error { 641 return nil 642 } 643 644 func (t *TestResourceTreeServer) RecvMsg(_ any) error { 645 return nil 646 } 647 648 type TestPodLogsServer struct { 649 ctx context.Context 650 } 651 652 func (t *TestPodLogsServer) Send(_ *application.LogEntry) error { 653 return nil 654 } 655 656 func (t *TestPodLogsServer) SetHeader(metadata.MD) error { 657 return nil 658 } 659 660 func (t *TestPodLogsServer) SendHeader(metadata.MD) error { 661 return nil 662 } 663 664 func (t *TestPodLogsServer) SetTrailer(metadata.MD) {} 665 666 func (t *TestPodLogsServer) Context() context.Context { 667 return t.ctx 668 } 669 670 func (t *TestPodLogsServer) SendMsg(_ any) error { 671 return nil 672 } 673 674 func (t *TestPodLogsServer) RecvMsg(_ any) error { 675 return nil 676 } 677 678 func TestNoAppEnumeration(t *testing.T) { 679 // This test ensures that malicious users can't infer the existence or non-existence of Applications by inspecting 680 // error messages. The errors for "app does not exist" must be the same as errors for "you aren't allowed to 681 // interact with this app." 682 683 // These tests are only important on API calls where the full app RBAC name (project, namespace, and name) is _not_ 684 // known based on the query parameters. For example, the Create call cannot leak existence of Applications, because 685 // the Application's project, namespace, and name are all specified in the API call. The call can be rejected 686 // immediately if the user does not have access. But the Delete endpoint may be called with just the Application 687 // name. So we cannot return a different error message for "does not exist" and "you don't have delete permissions," 688 // because the user could infer that the Application exists if they do not get the "does not exist" message. For 689 // endpoints that do not require the full RBAC name, we must return a generic "permission denied" for both "does not 690 // exist" and "no access." 691 692 f := func(enf *rbac.Enforcer) { 693 _ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV) 694 enf.SetDefaultRole("role:none") 695 } 696 deployment := appsv1.Deployment{ 697 TypeMeta: metav1.TypeMeta{ 698 APIVersion: "apps/v1", 699 Kind: "Deployment", 700 }, 701 ObjectMeta: metav1.ObjectMeta{ 702 Name: "test", 703 Namespace: "test", 704 }, 705 } 706 testApp := newTestApp(func(app *v1alpha1.Application) { 707 app.Name = "test" 708 app.Status.Resources = []v1alpha1.ResourceStatus{ 709 { 710 Group: deployment.GroupVersionKind().Group, 711 Kind: deployment.GroupVersionKind().Kind, 712 Version: deployment.GroupVersionKind().Version, 713 Name: deployment.Name, 714 Namespace: deployment.Namespace, 715 Status: "Synced", 716 }, 717 } 718 app.Status.History = []v1alpha1.RevisionHistory{ 719 { 720 ID: 0, 721 Source: v1alpha1.ApplicationSource{ 722 TargetRevision: "something-old", 723 }, 724 }, 725 } 726 }) 727 testHelmApp := newTestApp(func(app *v1alpha1.Application) { 728 app.Name = "test-helm" 729 app.Spec.Source.Path = "" 730 app.Spec.Source.Chart = "test" 731 app.Status.Resources = []v1alpha1.ResourceStatus{ 732 { 733 Group: deployment.GroupVersionKind().Group, 734 Kind: deployment.GroupVersionKind().Kind, 735 Version: deployment.GroupVersionKind().Version, 736 Name: deployment.Name, 737 Namespace: deployment.Namespace, 738 Status: "Synced", 739 }, 740 } 741 app.Status.History = []v1alpha1.RevisionHistory{ 742 { 743 ID: 0, 744 Source: v1alpha1.ApplicationSource{ 745 TargetRevision: "something-old", 746 }, 747 }, 748 } 749 }) 750 testAppMulti := newTestApp(func(app *v1alpha1.Application) { 751 app.Name = "test-multi" 752 app.Spec.Sources = v1alpha1.ApplicationSources{ 753 v1alpha1.ApplicationSource{ 754 TargetRevision: "something-old", 755 }, 756 v1alpha1.ApplicationSource{ 757 TargetRevision: "something-old", 758 }, 759 } 760 app.Status.Resources = []v1alpha1.ResourceStatus{ 761 { 762 Group: deployment.GroupVersionKind().Group, 763 Kind: deployment.GroupVersionKind().Kind, 764 Version: deployment.GroupVersionKind().Version, 765 Name: deployment.Name, 766 Namespace: deployment.Namespace, 767 Status: "Synced", 768 }, 769 } 770 app.Status.History = []v1alpha1.RevisionHistory{ 771 { 772 ID: 1, 773 Sources: v1alpha1.ApplicationSources{ 774 v1alpha1.ApplicationSource{ 775 TargetRevision: "something-old", 776 }, 777 v1alpha1.ApplicationSource{ 778 TargetRevision: "something-old", 779 }, 780 }, 781 }, 782 } 783 }) 784 testDeployment := kube.MustToUnstructured(&deployment) 785 appServer := newTestAppServerWithEnforcerConfigure(t, f, map[string]string{}, testApp, testHelmApp, testAppMulti, testDeployment) 786 787 noRoleCtx := t.Context() 788 //nolint:staticcheck 789 adminCtx := context.WithValue(noRoleCtx, "claims", &jwt.MapClaims{"groups": []string{"admin"}}) 790 791 t.Run("Get", func(t *testing.T) { 792 _, err := appServer.Get(adminCtx, &application.ApplicationQuery{Name: ptr.To("test")}) 793 require.NoError(t, err) 794 _, err = appServer.Get(noRoleCtx, &application.ApplicationQuery{Name: ptr.To("test")}) 795 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 796 _, err = appServer.Get(adminCtx, &application.ApplicationQuery{Name: ptr.To("doest-not-exist")}) 797 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 798 _, err = appServer.Get(adminCtx, &application.ApplicationQuery{Name: ptr.To("doest-not-exist"), Project: []string{"test"}}) 799 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") 800 }) 801 802 t.Run("GetManifests", func(t *testing.T) { 803 _, err := appServer.GetManifests(adminCtx, &application.ApplicationManifestQuery{Name: ptr.To("test")}) 804 require.NoError(t, err) 805 _, err = appServer.GetManifests(noRoleCtx, &application.ApplicationManifestQuery{Name: ptr.To("test")}) 806 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 807 _, err = appServer.GetManifests(adminCtx, &application.ApplicationManifestQuery{Name: ptr.To("doest-not-exist")}) 808 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 809 _, err = appServer.GetManifests(adminCtx, &application.ApplicationManifestQuery{Name: ptr.To("doest-not-exist"), Project: ptr.To("test")}) 810 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") 811 }) 812 813 t.Run("ListResourceEvents", func(t *testing.T) { 814 _, err := appServer.ListResourceEvents(adminCtx, &application.ApplicationResourceEventsQuery{Name: ptr.To("test")}) 815 require.NoError(t, err) 816 _, err = appServer.ListResourceEvents(noRoleCtx, &application.ApplicationResourceEventsQuery{Name: ptr.To("test")}) 817 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 818 _, err = appServer.ListResourceEvents(adminCtx, &application.ApplicationResourceEventsQuery{Name: ptr.To("doest-not-exist")}) 819 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 820 _, err = appServer.ListResourceEvents(adminCtx, &application.ApplicationResourceEventsQuery{Name: ptr.To("doest-not-exist"), Project: ptr.To("test")}) 821 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") 822 }) 823 824 t.Run("UpdateSpec", func(t *testing.T) { 825 _, err := appServer.UpdateSpec(adminCtx, &application.ApplicationUpdateSpecRequest{Name: ptr.To("test"), Spec: &v1alpha1.ApplicationSpec{ 826 Destination: v1alpha1.ApplicationDestination{Namespace: "default", Server: "https://cluster-api.example.com"}, 827 Source: &v1alpha1.ApplicationSource{RepoURL: "https://some-fake-source", Path: "."}, 828 }}) 829 require.NoError(t, err) 830 _, err = appServer.UpdateSpec(noRoleCtx, &application.ApplicationUpdateSpecRequest{Name: ptr.To("test"), Spec: &v1alpha1.ApplicationSpec{ 831 Destination: v1alpha1.ApplicationDestination{Namespace: "default", Server: "https://cluster-api.example.com"}, 832 Source: &v1alpha1.ApplicationSource{RepoURL: "https://some-fake-source", Path: "."}, 833 }}) 834 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 835 _, err = appServer.UpdateSpec(adminCtx, &application.ApplicationUpdateSpecRequest{Name: ptr.To("doest-not-exist"), Spec: &v1alpha1.ApplicationSpec{ 836 Destination: v1alpha1.ApplicationDestination{Namespace: "default", Server: "https://cluster-api.example.com"}, 837 Source: &v1alpha1.ApplicationSource{RepoURL: "https://some-fake-source", Path: "."}, 838 }}) 839 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 840 _, err = appServer.UpdateSpec(adminCtx, &application.ApplicationUpdateSpecRequest{Name: ptr.To("doest-not-exist"), Project: ptr.To("test"), Spec: &v1alpha1.ApplicationSpec{ 841 Destination: v1alpha1.ApplicationDestination{Namespace: "default", Server: "https://cluster-api.example.com"}, 842 Source: &v1alpha1.ApplicationSource{RepoURL: "https://some-fake-source", Path: "."}, 843 }}) 844 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") 845 }) 846 847 t.Run("Patch", func(t *testing.T) { 848 _, err := appServer.Patch(adminCtx, &application.ApplicationPatchRequest{Name: ptr.To("test"), Patch: ptr.To(`[{"op": "replace", "path": "/spec/source/path", "value": "foo"}]`)}) 849 require.NoError(t, err) 850 _, err = appServer.Patch(noRoleCtx, &application.ApplicationPatchRequest{Name: ptr.To("test"), Patch: ptr.To(`[{"op": "replace", "path": "/spec/source/path", "value": "foo"}]`)}) 851 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 852 _, err = appServer.Patch(adminCtx, &application.ApplicationPatchRequest{Name: ptr.To("doest-not-exist")}) 853 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 854 _, err = appServer.Patch(adminCtx, &application.ApplicationPatchRequest{Name: ptr.To("doest-not-exist"), Project: ptr.To("test")}) 855 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") 856 }) 857 858 t.Run("GetResource", func(t *testing.T) { 859 _, 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")}) 860 require.NoError(t, err) 861 _, 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")}) 862 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 863 _, 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")}) 864 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 865 _, 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")}) 866 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") 867 }) 868 869 t.Run("PatchResource", func(t *testing.T) { 870 _, 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}]`)}) 871 // This will always throw an error, because the kubectl mock for PatchResource is hard-coded to return nil. 872 // The best we can do is to confirm we get past the permission check. 873 assert.NotEqual(t, common.PermissionDeniedAPIError.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 874 _, 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}]`)}) 875 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 876 _, 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}]`)}) 877 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 878 _, 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}]`)}) 879 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") 880 }) 881 882 t.Run("DeleteResource", func(t *testing.T) { 883 _, 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")}) 884 require.NoError(t, err) 885 _, 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")}) 886 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 887 _, 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")}) 888 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 889 _, 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")}) 890 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") 891 }) 892 893 t.Run("ResourceTree", func(t *testing.T) { 894 _, err := appServer.ResourceTree(adminCtx, &application.ResourcesQuery{ApplicationName: ptr.To("test")}) 895 require.NoError(t, err) 896 _, err = appServer.ResourceTree(noRoleCtx, &application.ResourcesQuery{ApplicationName: ptr.To("test")}) 897 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 898 _, err = appServer.ResourceTree(adminCtx, &application.ResourcesQuery{ApplicationName: ptr.To("doest-not-exist")}) 899 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 900 _, err = appServer.ResourceTree(adminCtx, &application.ResourcesQuery{ApplicationName: ptr.To("doest-not-exist"), Project: ptr.To("test")}) 901 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") 902 }) 903 904 t.Run("RevisionMetadata", func(t *testing.T) { 905 _, err := appServer.RevisionMetadata(adminCtx, &application.RevisionMetadataQuery{Name: ptr.To("test")}) 906 require.NoError(t, err) 907 _, err = appServer.RevisionMetadata(adminCtx, &application.RevisionMetadataQuery{Name: ptr.To("test-multi"), SourceIndex: ptr.To(int32(0)), VersionId: ptr.To(int32(1))}) 908 require.NoError(t, err) 909 _, err = appServer.RevisionMetadata(noRoleCtx, &application.RevisionMetadataQuery{Name: ptr.To("test")}) 910 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 911 _, err = appServer.RevisionMetadata(adminCtx, &application.RevisionMetadataQuery{Name: ptr.To("doest-not-exist")}) 912 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 913 _, err = appServer.RevisionMetadata(adminCtx, &application.RevisionMetadataQuery{Name: ptr.To("doest-not-exist"), Project: ptr.To("test")}) 914 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") 915 }) 916 917 t.Run("RevisionChartDetails", func(t *testing.T) { 918 _, err := appServer.RevisionChartDetails(adminCtx, &application.RevisionMetadataQuery{Name: ptr.To("test-helm")}) 919 require.NoError(t, err) 920 _, err = appServer.RevisionChartDetails(noRoleCtx, &application.RevisionMetadataQuery{Name: ptr.To("test-helm")}) 921 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 922 _, err = appServer.RevisionChartDetails(adminCtx, &application.RevisionMetadataQuery{Name: ptr.To("doest-not-exist")}) 923 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 924 _, err = appServer.RevisionChartDetails(adminCtx, &application.RevisionMetadataQuery{Name: ptr.To("doest-not-exist"), Project: ptr.To("test")}) 925 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") 926 }) 927 928 t.Run("ManagedResources", func(t *testing.T) { 929 _, err := appServer.ManagedResources(adminCtx, &application.ResourcesQuery{ApplicationName: ptr.To("test")}) 930 require.NoError(t, err) 931 _, err = appServer.ManagedResources(noRoleCtx, &application.ResourcesQuery{ApplicationName: ptr.To("test")}) 932 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 933 _, err = appServer.ManagedResources(adminCtx, &application.ResourcesQuery{ApplicationName: ptr.To("doest-not-exist")}) 934 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 935 _, err = appServer.ManagedResources(adminCtx, &application.ResourcesQuery{ApplicationName: ptr.To("doest-not-exist"), Project: ptr.To("test")}) 936 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") 937 }) 938 939 t.Run("Sync", func(t *testing.T) { 940 _, err := appServer.Sync(adminCtx, &application.ApplicationSyncRequest{Name: ptr.To("test")}) 941 require.NoError(t, err) 942 _, err = appServer.Sync(noRoleCtx, &application.ApplicationSyncRequest{Name: ptr.To("test")}) 943 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 944 _, err = appServer.Sync(adminCtx, &application.ApplicationSyncRequest{Name: ptr.To("doest-not-exist")}) 945 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 946 _, err = appServer.Sync(adminCtx, &application.ApplicationSyncRequest{Name: ptr.To("doest-not-exist"), Project: ptr.To("test")}) 947 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") 948 }) 949 950 t.Run("TerminateOperation", func(t *testing.T) { 951 // The sync operation is already started from the previous test. We just need to set the field that the 952 // controller would set if this were an actual Argo CD environment. 953 setSyncRunningOperationState(t, appServer) 954 _, err := appServer.TerminateOperation(adminCtx, &application.OperationTerminateRequest{Name: ptr.To("test")}) 955 require.NoError(t, err) 956 _, err = appServer.TerminateOperation(noRoleCtx, &application.OperationTerminateRequest{Name: ptr.To("test")}) 957 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 958 _, err = appServer.TerminateOperation(adminCtx, &application.OperationTerminateRequest{Name: ptr.To("doest-not-exist")}) 959 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 960 _, err = appServer.TerminateOperation(adminCtx, &application.OperationTerminateRequest{Name: ptr.To("doest-not-exist"), Project: ptr.To("test")}) 961 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") 962 }) 963 964 t.Run("Rollback", func(t *testing.T) { 965 unsetSyncRunningOperationState(t, appServer) 966 _, err := appServer.Rollback(adminCtx, &application.ApplicationRollbackRequest{Name: ptr.To("test")}) 967 require.NoError(t, err) 968 _, err = appServer.Rollback(adminCtx, &application.ApplicationRollbackRequest{Name: ptr.To("test-multi"), Id: ptr.To(int64(1))}) 969 require.NoError(t, err) 970 _, err = appServer.Rollback(noRoleCtx, &application.ApplicationRollbackRequest{Name: ptr.To("test")}) 971 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 972 _, err = appServer.Rollback(adminCtx, &application.ApplicationRollbackRequest{Name: ptr.To("doest-not-exist")}) 973 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 974 _, err = appServer.Rollback(adminCtx, &application.ApplicationRollbackRequest{Name: ptr.To("doest-not-exist"), Project: ptr.To("test")}) 975 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") 976 }) 977 978 t.Run("ListResourceActions", func(t *testing.T) { 979 _, 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")}) 980 require.NoError(t, err) 981 _, err = appServer.ListResourceActions(noRoleCtx, &application.ApplicationResourceRequest{Name: ptr.To("test")}) 982 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 983 _, err = appServer.ListResourceActions(noRoleCtx, &application.ApplicationResourceRequest{Group: ptr.To("argoproj.io"), Kind: ptr.To("Application"), Name: ptr.To("test")}) 984 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 985 _, err = appServer.ListResourceActions(adminCtx, &application.ApplicationResourceRequest{Name: ptr.To("doest-not-exist")}) 986 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 987 _, err = appServer.ListResourceActions(adminCtx, &application.ApplicationResourceRequest{Name: ptr.To("doest-not-exist"), Project: ptr.To("test")}) 988 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") 989 }) 990 991 //nolint:staticcheck,SA1019 // RunResourceAction is deprecated, but we still need to support it for backward compatibility. 992 t.Run("RunResourceAction", func(t *testing.T) { 993 _, 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")}) 994 require.NoError(t, err) 995 _, err = appServer.RunResourceAction(noRoleCtx, &application.ResourceActionRunRequest{Name: ptr.To("test")}) 996 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 997 _, err = appServer.RunResourceAction(noRoleCtx, &application.ResourceActionRunRequest{Group: ptr.To("argoproj.io"), Kind: ptr.To("Application"), Name: ptr.To("test")}) 998 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 999 _, err = appServer.RunResourceAction(adminCtx, &application.ResourceActionRunRequest{Name: ptr.To("doest-not-exist")}) 1000 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 1001 _, err = appServer.RunResourceAction(adminCtx, &application.ResourceActionRunRequest{Name: ptr.To("doest-not-exist"), Project: ptr.To("test")}) 1002 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") 1003 }) 1004 1005 t.Run("RunResourceActionV2", func(t *testing.T) { 1006 _, 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")}) 1007 require.NoError(t, err) 1008 _, err = appServer.RunResourceActionV2(noRoleCtx, &application.ResourceActionRunRequestV2{Name: ptr.To("test")}) 1009 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 1010 _, err = appServer.RunResourceActionV2(noRoleCtx, &application.ResourceActionRunRequestV2{Group: ptr.To("argoproj.io"), Kind: ptr.To("Application"), Name: ptr.To("test")}) 1011 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 1012 _, err = appServer.RunResourceActionV2(adminCtx, &application.ResourceActionRunRequestV2{Name: ptr.To("doest-not-exist")}) 1013 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 1014 _, err = appServer.RunResourceActionV2(adminCtx, &application.ResourceActionRunRequestV2{Name: ptr.To("doest-not-exist"), Project: ptr.To("test")}) 1015 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") 1016 }) 1017 1018 t.Run("GetApplicationSyncWindows", func(t *testing.T) { 1019 _, err := appServer.GetApplicationSyncWindows(adminCtx, &application.ApplicationSyncWindowsQuery{Name: ptr.To("test")}) 1020 require.NoError(t, err) 1021 _, err = appServer.GetApplicationSyncWindows(noRoleCtx, &application.ApplicationSyncWindowsQuery{Name: ptr.To("test")}) 1022 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 1023 _, err = appServer.GetApplicationSyncWindows(adminCtx, &application.ApplicationSyncWindowsQuery{Name: ptr.To("doest-not-exist")}) 1024 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 1025 _, err = appServer.GetApplicationSyncWindows(adminCtx, &application.ApplicationSyncWindowsQuery{Name: ptr.To("doest-not-exist"), Project: ptr.To("test")}) 1026 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") 1027 }) 1028 1029 t.Run("GetManifestsWithFiles", func(t *testing.T) { 1030 err := appServer.GetManifestsWithFiles(&TestServerStream{ctx: adminCtx, appName: "test"}) 1031 require.NoError(t, err) 1032 err = appServer.GetManifestsWithFiles(&TestServerStream{ctx: noRoleCtx, appName: "test"}) 1033 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 1034 err = appServer.GetManifestsWithFiles(&TestServerStream{ctx: adminCtx, appName: "does-not-exist"}) 1035 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 1036 err = appServer.GetManifestsWithFiles(&TestServerStream{ctx: adminCtx, appName: "does-not-exist", project: "test"}) 1037 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") 1038 }) 1039 1040 t.Run("WatchResourceTree", func(t *testing.T) { 1041 err := appServer.WatchResourceTree(&application.ResourcesQuery{ApplicationName: ptr.To("test")}, &TestResourceTreeServer{ctx: adminCtx}) 1042 require.NoError(t, err) 1043 err = appServer.WatchResourceTree(&application.ResourcesQuery{ApplicationName: ptr.To("test")}, &TestResourceTreeServer{ctx: noRoleCtx}) 1044 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 1045 err = appServer.WatchResourceTree(&application.ResourcesQuery{ApplicationName: ptr.To("does-not-exist")}, &TestResourceTreeServer{ctx: adminCtx}) 1046 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 1047 err = appServer.WatchResourceTree(&application.ResourcesQuery{ApplicationName: ptr.To("does-not-exist"), Project: ptr.To("test")}, &TestResourceTreeServer{ctx: adminCtx}) 1048 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") 1049 }) 1050 1051 t.Run("PodLogs", func(t *testing.T) { 1052 err := appServer.PodLogs(&application.ApplicationPodLogsQuery{Name: ptr.To("test")}, &TestPodLogsServer{ctx: adminCtx}) 1053 require.NoError(t, err) 1054 err = appServer.PodLogs(&application.ApplicationPodLogsQuery{Name: ptr.To("test")}, &TestPodLogsServer{ctx: noRoleCtx}) 1055 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 1056 err = appServer.PodLogs(&application.ApplicationPodLogsQuery{Name: ptr.To("does-not-exist")}, &TestPodLogsServer{ctx: adminCtx}) 1057 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 1058 err = appServer.PodLogs(&application.ApplicationPodLogsQuery{Name: ptr.To("does-not-exist"), Project: ptr.To("test")}, &TestPodLogsServer{ctx: adminCtx}) 1059 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") 1060 }) 1061 1062 t.Run("ListLinks", func(t *testing.T) { 1063 _, err := appServer.ListLinks(adminCtx, &application.ListAppLinksRequest{Name: ptr.To("test")}) 1064 require.NoError(t, err) 1065 _, err = appServer.ListLinks(noRoleCtx, &application.ListAppLinksRequest{Name: ptr.To("test")}) 1066 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 1067 _, err = appServer.ListLinks(adminCtx, &application.ListAppLinksRequest{Name: ptr.To("does-not-exist")}) 1068 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 1069 _, err = appServer.ListLinks(adminCtx, &application.ListAppLinksRequest{Name: ptr.To("does-not-exist"), Project: ptr.To("test")}) 1070 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") 1071 }) 1072 1073 t.Run("ListResourceLinks", func(t *testing.T) { 1074 _, 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")}) 1075 require.NoError(t, err) 1076 _, 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")}) 1077 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 1078 _, 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")}) 1079 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 1080 _, 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")}) 1081 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") 1082 }) 1083 1084 // Do this last so other stuff doesn't fail. 1085 t.Run("Delete", func(t *testing.T) { 1086 _, err := appServer.Delete(adminCtx, &application.ApplicationDeleteRequest{Name: ptr.To("test")}) 1087 require.NoError(t, err) 1088 _, err = appServer.Delete(noRoleCtx, &application.ApplicationDeleteRequest{Name: ptr.To("test")}) 1089 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 1090 _, err = appServer.Delete(adminCtx, &application.ApplicationDeleteRequest{Name: ptr.To("doest-not-exist")}) 1091 require.EqualError(t, err, common.PermissionDeniedAPIError.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 1092 _, err = appServer.Delete(adminCtx, &application.ApplicationDeleteRequest{Name: ptr.To("doest-not-exist"), Project: ptr.To("test")}) 1093 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") 1094 }) 1095 } 1096 1097 // setSyncRunningOperationState simulates starting a sync operation on the given app. 1098 func setSyncRunningOperationState(t *testing.T, appServer *Server) { 1099 t.Helper() 1100 appIf := appServer.appclientset.ArgoprojV1alpha1().Applications("default") 1101 app, err := appIf.Get(t.Context(), "test", metav1.GetOptions{}) 1102 require.NoError(t, err) 1103 // This sets the status that would be set by the controller usually. 1104 app.Status.OperationState = &v1alpha1.OperationState{Phase: synccommon.OperationRunning, Operation: v1alpha1.Operation{Sync: &v1alpha1.SyncOperation{}}} 1105 _, err = appIf.Update(t.Context(), app, metav1.UpdateOptions{}) 1106 require.NoError(t, err) 1107 } 1108 1109 // unsetSyncRunningOperationState simulates finishing a sync operation on the given app. 1110 func unsetSyncRunningOperationState(t *testing.T, appServer *Server) { 1111 t.Helper() 1112 appIf := appServer.appclientset.ArgoprojV1alpha1().Applications("default") 1113 app, err := appIf.Get(t.Context(), "test", metav1.GetOptions{}) 1114 require.NoError(t, err) 1115 app.Operation = nil 1116 app.Status.OperationState = nil 1117 _, err = appIf.Update(t.Context(), app, metav1.UpdateOptions{}) 1118 require.NoError(t, err) 1119 } 1120 1121 func TestListAppsInNamespaceWithLabels(t *testing.T) { 1122 appServer := newTestAppServer(t, newTestApp(func(app *v1alpha1.Application) { 1123 app.Name = "App1" 1124 app.Namespace = "test-namespace" 1125 app.SetLabels(map[string]string{"key1": "value1", "key2": "value1"}) 1126 }), newTestApp(func(app *v1alpha1.Application) { 1127 app.Name = "App2" 1128 app.Namespace = "test-namespace" 1129 app.SetLabels(map[string]string{"key1": "value2"}) 1130 }), newTestApp(func(app *v1alpha1.Application) { 1131 app.Name = "App3" 1132 app.Namespace = "test-namespace" 1133 app.SetLabels(map[string]string{"key1": "value3"}) 1134 })) 1135 appServer.ns = "test-namespace" 1136 appQuery := application.ApplicationQuery{} 1137 namespace := "test-namespace" 1138 appQuery.AppNamespace = &namespace 1139 testListAppsWithLabels(t, appQuery, appServer) 1140 } 1141 1142 func TestListAppsInDefaultNSWithLabels(t *testing.T) { 1143 appServer := newTestAppServer(t, newTestApp(func(app *v1alpha1.Application) { 1144 app.Name = "App1" 1145 app.SetLabels(map[string]string{"key1": "value1", "key2": "value1"}) 1146 }), newTestApp(func(app *v1alpha1.Application) { 1147 app.Name = "App2" 1148 app.SetLabels(map[string]string{"key1": "value2"}) 1149 }), newTestApp(func(app *v1alpha1.Application) { 1150 app.Name = "App3" 1151 app.SetLabels(map[string]string{"key1": "value3"}) 1152 })) 1153 appQuery := application.ApplicationQuery{} 1154 testListAppsWithLabels(t, appQuery, appServer) 1155 } 1156 1157 func testListAppsWithLabels(t *testing.T, appQuery application.ApplicationQuery, appServer *Server) { 1158 t.Helper() 1159 validTests := []struct { 1160 testName string 1161 label string 1162 expectedResult []string 1163 }{ 1164 { 1165 testName: "Equality based filtering using '=' operator", 1166 label: "key1=value1", 1167 expectedResult: []string{"App1"}, 1168 }, 1169 { 1170 testName: "Equality based filtering using '==' operator", 1171 label: "key1==value1", 1172 expectedResult: []string{"App1"}, 1173 }, 1174 { 1175 testName: "Equality based filtering using '!=' operator", 1176 label: "key1!=value1", 1177 expectedResult: []string{"App2", "App3"}, 1178 }, 1179 { 1180 testName: "Set based filtering using 'in' operator", 1181 label: "key1 in (value1, value3)", 1182 expectedResult: []string{"App1", "App3"}, 1183 }, 1184 { 1185 testName: "Set based filtering using 'notin' operator", 1186 label: "key1 notin (value1, value3)", 1187 expectedResult: []string{"App2"}, 1188 }, 1189 { 1190 testName: "Set based filtering using 'exists' operator", 1191 label: "key1", 1192 expectedResult: []string{"App1", "App2", "App3"}, 1193 }, 1194 { 1195 testName: "Set based filtering using 'not exists' operator", 1196 label: "!key2", 1197 expectedResult: []string{"App2", "App3"}, 1198 }, 1199 } 1200 // test valid scenarios 1201 for _, validTest := range validTests { 1202 t.Run(validTest.testName, func(t *testing.T) { 1203 appQuery.Selector = &validTest.label 1204 res, err := appServer.List(t.Context(), &appQuery) 1205 require.NoError(t, err) 1206 apps := []string{} 1207 for i := range res.Items { 1208 apps = append(apps, res.Items[i].Name) 1209 } 1210 assert.Equal(t, validTest.expectedResult, apps) 1211 }) 1212 } 1213 1214 invalidTests := []struct { 1215 testName string 1216 label string 1217 errorMesage string 1218 }{ 1219 { 1220 testName: "Set based filtering using '>' operator", 1221 label: "key1>value1", 1222 errorMesage: "error parsing the selector", 1223 }, 1224 { 1225 testName: "Set based filtering using '<' operator", 1226 label: "key1<value1", 1227 errorMesage: "error parsing the selector", 1228 }, 1229 } 1230 // test invalid scenarios 1231 for _, invalidTest := range invalidTests { 1232 t.Run(invalidTest.testName, func(t *testing.T) { 1233 appQuery.Selector = &invalidTest.label 1234 _, err := appServer.List(t.Context(), &appQuery) 1235 assert.ErrorContains(t, err, invalidTest.errorMesage) 1236 }) 1237 } 1238 } 1239 1240 func TestListAppWithProjects(t *testing.T) { 1241 appServer := newTestAppServer(t, newTestApp(func(app *v1alpha1.Application) { 1242 app.Name = "App1" 1243 app.Spec.Project = "test-project1" 1244 }), newTestApp(func(app *v1alpha1.Application) { 1245 app.Name = "App2" 1246 app.Spec.Project = "test-project2" 1247 }), newTestApp(func(app *v1alpha1.Application) { 1248 app.Name = "App3" 1249 app.Spec.Project = "test-project3" 1250 })) 1251 1252 t.Run("List all apps", func(t *testing.T) { 1253 appQuery := application.ApplicationQuery{} 1254 appList, err := appServer.List(t.Context(), &appQuery) 1255 require.NoError(t, err) 1256 assert.Len(t, appList.Items, 3) 1257 }) 1258 1259 t.Run("List apps with projects filter set", func(t *testing.T) { 1260 appQuery := application.ApplicationQuery{Projects: []string{"test-project1"}} 1261 appList, err := appServer.List(t.Context(), &appQuery) 1262 require.NoError(t, err) 1263 assert.Len(t, appList.Items, 1) 1264 for _, app := range appList.Items { 1265 assert.Equal(t, "test-project1", app.Spec.Project) 1266 } 1267 }) 1268 1269 t.Run("List apps with project filter set (legacy field)", func(t *testing.T) { 1270 appQuery := application.ApplicationQuery{Project: []string{"test-project1"}} 1271 appList, err := appServer.List(t.Context(), &appQuery) 1272 require.NoError(t, err) 1273 assert.Len(t, appList.Items, 1) 1274 for _, app := range appList.Items { 1275 assert.Equal(t, "test-project1", app.Spec.Project) 1276 } 1277 }) 1278 1279 t.Run("List apps with both projects and project filter set", func(t *testing.T) { 1280 // If the older field is present, we should use it instead of the newer field. 1281 appQuery := application.ApplicationQuery{Project: []string{"test-project1"}, Projects: []string{"test-project2"}} 1282 appList, err := appServer.List(t.Context(), &appQuery) 1283 require.NoError(t, err) 1284 assert.Len(t, appList.Items, 1) 1285 for _, app := range appList.Items { 1286 assert.Equal(t, "test-project1", app.Spec.Project) 1287 } 1288 }) 1289 } 1290 1291 func TestListApps(t *testing.T) { 1292 appServer := newTestAppServer(t, newTestApp(func(app *v1alpha1.Application) { 1293 app.Name = "bcd" 1294 }), newTestApp(func(app *v1alpha1.Application) { 1295 app.Name = "abc" 1296 }), newTestApp(func(app *v1alpha1.Application) { 1297 app.Name = "def" 1298 })) 1299 1300 res, err := appServer.List(t.Context(), &application.ApplicationQuery{}) 1301 require.NoError(t, err) 1302 var names []string 1303 for i := range res.Items { 1304 names = append(names, res.Items[i].Name) 1305 } 1306 assert.Equal(t, []string{"abc", "bcd", "def"}, names) 1307 } 1308 1309 func TestCoupleAppsListApps(t *testing.T) { 1310 var objects []runtime.Object 1311 ctx := t.Context() 1312 1313 var groups []string 1314 for i := 0; i < 50; i++ { 1315 groups = append(groups, fmt.Sprintf("group-%d", i)) 1316 } 1317 //nolint:staticcheck 1318 ctx = context.WithValue(ctx, "claims", &jwt.MapClaims{"groups": groups}) 1319 for projectId := 0; projectId < 100; projectId++ { 1320 projectName := fmt.Sprintf("proj-%d", projectId) 1321 for appId := 0; appId < 100; appId++ { 1322 objects = append(objects, newTestApp(func(app *v1alpha1.Application) { 1323 app.Name = fmt.Sprintf("app-%d-%d", projectId, appId) 1324 app.Spec.Project = projectName 1325 })) 1326 } 1327 } 1328 1329 f := func(enf *rbac.Enforcer) { 1330 policy := ` 1331 p, role:test, applications, *, proj-10/*, allow 1332 g, group-45, role:test 1333 p, role:test2, applications, *, proj-15/*, allow 1334 g, group-47, role:test2 1335 p, role:test3, applications, *, proj-20/*, allow 1336 g, group-49, role:test3 1337 ` 1338 _ = enf.SetUserPolicy(policy) 1339 } 1340 appServer := newTestAppServerWithEnforcerConfigure(t, f, map[string]string{}, objects...) 1341 1342 res, err := appServer.List(ctx, &application.ApplicationQuery{}) 1343 1344 require.NoError(t, err) 1345 var names []string 1346 for i := range res.Items { 1347 names = append(names, res.Items[i].Name) 1348 } 1349 assert.Len(t, names, 300) 1350 } 1351 1352 func generateTestApp(num int) []*v1alpha1.Application { 1353 apps := []*v1alpha1.Application{} 1354 for i := 0; i < num; i++ { 1355 apps = append(apps, newTestApp(func(app *v1alpha1.Application) { 1356 app.Name = fmt.Sprintf("test-app%.6d", i) 1357 })) 1358 } 1359 1360 return apps 1361 } 1362 1363 func BenchmarkListMuchApps(b *testing.B) { 1364 // 10000 apps 1365 apps := generateTestApp(10000) 1366 obj := make([]runtime.Object, len(apps)) 1367 for i, v := range apps { 1368 obj[i] = v 1369 } 1370 appServer := newTestAppServerWithBenchmark(b, obj...) 1371 1372 b.ResetTimer() 1373 for n := 0; n < b.N; n++ { 1374 _, err := appServer.List(b.Context(), &application.ApplicationQuery{}) 1375 if err != nil { 1376 break 1377 } 1378 } 1379 } 1380 1381 func BenchmarkListSomeApps(b *testing.B) { 1382 // 500 apps 1383 apps := generateTestApp(500) 1384 obj := make([]runtime.Object, len(apps)) 1385 for i, v := range apps { 1386 obj[i] = v 1387 } 1388 appServer := newTestAppServerWithBenchmark(b, obj...) 1389 1390 b.ResetTimer() 1391 for n := 0; n < b.N; n++ { 1392 _, err := appServer.List(b.Context(), &application.ApplicationQuery{}) 1393 if err != nil { 1394 break 1395 } 1396 } 1397 } 1398 1399 func BenchmarkListFewApps(b *testing.B) { 1400 // 10 apps 1401 apps := generateTestApp(10) 1402 obj := make([]runtime.Object, len(apps)) 1403 for i, v := range apps { 1404 obj[i] = v 1405 } 1406 appServer := newTestAppServerWithBenchmark(b, obj...) 1407 1408 b.ResetTimer() 1409 for n := 0; n < b.N; n++ { 1410 _, err := appServer.List(b.Context(), &application.ApplicationQuery{}) 1411 if err != nil { 1412 break 1413 } 1414 } 1415 } 1416 1417 func strToPtr(v string) *string { 1418 return &v 1419 } 1420 1421 func BenchmarkListMuchAppsWithName(b *testing.B) { 1422 // 10000 apps 1423 appsMuch := generateTestApp(10000) 1424 obj := make([]runtime.Object, len(appsMuch)) 1425 for i, v := range appsMuch { 1426 obj[i] = v 1427 } 1428 appServer := newTestAppServerWithBenchmark(b, obj...) 1429 1430 b.ResetTimer() 1431 for n := 0; n < b.N; n++ { 1432 app := &application.ApplicationQuery{Name: strToPtr("test-app000099")} 1433 _, err := appServer.List(b.Context(), app) 1434 if err != nil { 1435 break 1436 } 1437 } 1438 } 1439 1440 func BenchmarkListMuchAppsWithProjects(b *testing.B) { 1441 // 10000 apps 1442 appsMuch := generateTestApp(10000) 1443 appsMuch[999].Spec.Project = "test-project1" 1444 appsMuch[1999].Spec.Project = "test-project2" 1445 obj := make([]runtime.Object, len(appsMuch)) 1446 for i, v := range appsMuch { 1447 obj[i] = v 1448 } 1449 appServer := newTestAppServerWithBenchmark(b, obj...) 1450 1451 b.ResetTimer() 1452 for n := 0; n < b.N; n++ { 1453 app := &application.ApplicationQuery{Project: []string{"test-project1", "test-project2"}} 1454 _, err := appServer.List(b.Context(), app) 1455 if err != nil { 1456 break 1457 } 1458 } 1459 } 1460 1461 func BenchmarkListMuchAppsWithRepo(b *testing.B) { 1462 // 10000 apps 1463 appsMuch := generateTestApp(10000) 1464 appsMuch[999].Spec.Source.RepoURL = "https://some-fake-source" 1465 obj := make([]runtime.Object, len(appsMuch)) 1466 for i, v := range appsMuch { 1467 obj[i] = v 1468 } 1469 appServer := newTestAppServerWithBenchmark(b, obj...) 1470 1471 b.ResetTimer() 1472 for n := 0; n < b.N; n++ { 1473 app := &application.ApplicationQuery{Repo: strToPtr("https://some-fake-source")} 1474 _, err := appServer.List(b.Context(), app) 1475 if err != nil { 1476 break 1477 } 1478 } 1479 } 1480 1481 func TestCreateApp(t *testing.T) { 1482 testApp := newTestApp() 1483 appServer := newTestAppServer(t) 1484 testApp.Spec.Project = "" 1485 createReq := application.ApplicationCreateRequest{ 1486 Application: testApp, 1487 } 1488 app, err := appServer.Create(t.Context(), &createReq) 1489 require.NoError(t, err) 1490 assert.NotNil(t, app) 1491 assert.NotNil(t, app.Spec) 1492 assert.Equal(t, "default", app.Spec.Project) 1493 } 1494 1495 func TestCreateAppWithDestName(t *testing.T) { 1496 appServer := newTestAppServer(t) 1497 testApp := newTestAppWithDestName() 1498 createReq := application.ApplicationCreateRequest{ 1499 Application: testApp, 1500 } 1501 app, err := appServer.Create(t.Context(), &createReq) 1502 require.NoError(t, err) 1503 assert.NotNil(t, app) 1504 } 1505 1506 // TestCreateAppWithOperation tests that an application created with an operation is created with the operation removed. 1507 // Avoids regressions of https://github.com/argoproj/argo-cd/security/advisories/GHSA-g623-jcgg-mhmm 1508 func TestCreateAppWithOperation(t *testing.T) { 1509 appServer := newTestAppServer(t) 1510 testApp := newTestAppWithDestName() 1511 testApp.Operation = &v1alpha1.Operation{ 1512 Sync: &v1alpha1.SyncOperation{ 1513 Manifests: []string{ 1514 "test", 1515 }, 1516 }, 1517 } 1518 createReq := application.ApplicationCreateRequest{ 1519 Application: testApp, 1520 } 1521 app, err := appServer.Create(t.Context(), &createReq) 1522 require.NoError(t, err) 1523 require.NotNil(t, app) 1524 assert.Nil(t, app.Operation) 1525 } 1526 1527 func TestCreateAppUpsert(t *testing.T) { 1528 t.Parallel() 1529 t.Run("No error when spec equals", func(t *testing.T) { 1530 t.Parallel() 1531 appServer := newTestAppServer(t) 1532 testApp := newTestApp() 1533 1534 createReq := application.ApplicationCreateRequest{ 1535 Application: testApp, 1536 } 1537 // Call Create() instead of adding the object to the tesst server to make sure the app is correctly normalized. 1538 _, err := appServer.Create(t.Context(), &createReq) 1539 require.NoError(t, err) 1540 1541 app, err := appServer.Create(t.Context(), &createReq) 1542 require.NoError(t, err) 1543 require.NotNil(t, app) 1544 }) 1545 t.Run("Error on update without upsert", func(t *testing.T) { 1546 t.Parallel() 1547 appServer := newTestAppServer(t) 1548 testApp := newTestApp() 1549 1550 // Call Create() instead of adding the object to the tesst server to make sure the app is correctly normalized. 1551 _, err := appServer.Create(t.Context(), &application.ApplicationCreateRequest{ 1552 Application: testApp, 1553 }) 1554 require.NoError(t, err) 1555 1556 newApp := newTestApp() 1557 newApp.Spec.Source.Name = "updated" 1558 createReq := application.ApplicationCreateRequest{ 1559 Application: newApp, 1560 } 1561 _, err = appServer.Create(t.Context(), &createReq) 1562 require.EqualError(t, err, "rpc error: code = InvalidArgument desc = existing application spec is different, use upsert flag to force update") 1563 }) 1564 t.Run("Invalid existing app can be updated", func(t *testing.T) { 1565 t.Parallel() 1566 testApp := newTestApp() 1567 testApp.Spec.Destination.Server = "https://invalid-cluster" 1568 appServer := newTestAppServer(t, testApp) 1569 1570 newApp := newTestAppWithDestName() 1571 newApp.TypeMeta = testApp.TypeMeta 1572 newApp.Spec.Source.Name = "updated" 1573 createReq := application.ApplicationCreateRequest{ 1574 Application: newApp, 1575 Upsert: ptr.To(true), 1576 } 1577 app, err := appServer.Create(t.Context(), &createReq) 1578 require.NoError(t, err) 1579 require.NotNil(t, app) 1580 assert.Equal(t, "updated", app.Spec.Source.Name) 1581 }) 1582 t.Run("Can update application project", func(t *testing.T) { 1583 t.Parallel() 1584 testApp := newTestApp() 1585 appServer := newTestAppServer(t, testApp) 1586 1587 newApp := newTestAppWithDestName() 1588 newApp.TypeMeta = testApp.TypeMeta 1589 newApp.Spec.Project = "my-proj" 1590 createReq := application.ApplicationCreateRequest{ 1591 Application: newApp, 1592 Upsert: ptr.To(true), 1593 } 1594 app, err := appServer.Create(t.Context(), &createReq) 1595 require.NoError(t, err) 1596 require.NotNil(t, app) 1597 assert.Equal(t, "my-proj", app.Spec.Project) 1598 }) 1599 t.Run("Existing label and annotations are preserved", func(t *testing.T) { 1600 t.Parallel() 1601 testApp := newTestApp() 1602 testApp.Annotations = map[string]string{"test": "test-value", "update": "old"} 1603 testApp.Labels = map[string]string{"test": "test-value", "update": "old"} 1604 appServer := newTestAppServer(t, testApp) 1605 1606 newApp := newTestAppWithDestName() 1607 newApp.TypeMeta = testApp.TypeMeta 1608 newApp.Annotations = map[string]string{"update": "new"} 1609 newApp.Labels = map[string]string{"update": "new"} 1610 createReq := application.ApplicationCreateRequest{ 1611 Application: newApp, 1612 Upsert: ptr.To(true), 1613 } 1614 app, err := appServer.Create(t.Context(), &createReq) 1615 require.NoError(t, err) 1616 require.NotNil(t, app) 1617 assert.Len(t, app.Annotations, 2) 1618 assert.Equal(t, "new", app.GetAnnotations()["update"]) 1619 assert.Len(t, app.Labels, 2) 1620 assert.Equal(t, "new", app.GetLabels()["update"]) 1621 }) 1622 } 1623 1624 func TestUpdateApp(t *testing.T) { 1625 t.Parallel() 1626 t.Run("Same spec", func(t *testing.T) { 1627 t.Parallel() 1628 testApp := newTestApp() 1629 appServer := newTestAppServer(t, testApp) 1630 testApp.Spec.Project = "" 1631 app, err := appServer.Update(t.Context(), &application.ApplicationUpdateRequest{ 1632 Application: testApp, 1633 }) 1634 require.NoError(t, err) 1635 assert.Equal(t, "default", app.Spec.Project) 1636 }) 1637 t.Run("Invalid existing app can be updated", func(t *testing.T) { 1638 t.Parallel() 1639 testApp := newTestApp() 1640 testApp.Spec.Destination.Server = "https://invalid-cluster" 1641 appServer := newTestAppServer(t, testApp) 1642 1643 updateApp := newTestAppWithDestName() 1644 updateApp.TypeMeta = testApp.TypeMeta 1645 updateApp.Spec.Source.Name = "updated" 1646 app, err := appServer.Update(t.Context(), &application.ApplicationUpdateRequest{ 1647 Application: updateApp, 1648 }) 1649 require.NoError(t, err) 1650 require.NotNil(t, app) 1651 assert.Equal(t, "updated", app.Spec.Source.Name) 1652 }) 1653 t.Run("Can update application project from invalid", func(t *testing.T) { 1654 t.Parallel() 1655 testApp := newTestApp() 1656 restrictedProj := &v1alpha1.AppProject{ 1657 ObjectMeta: metav1.ObjectMeta{Name: "restricted-proj", Namespace: "default"}, 1658 Spec: v1alpha1.AppProjectSpec{ 1659 SourceRepos: []string{"not-your-repo"}, 1660 Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "not-your-namespace"}}, 1661 }, 1662 } 1663 testApp.Spec.Project = restrictedProj.Name 1664 appServer := newTestAppServer(t, testApp, restrictedProj) 1665 1666 updateApp := newTestAppWithDestName() 1667 updateApp.TypeMeta = testApp.TypeMeta 1668 updateApp.Spec.Project = "my-proj" 1669 app, err := appServer.Update(t.Context(), &application.ApplicationUpdateRequest{ 1670 Application: updateApp, 1671 }) 1672 require.NoError(t, err) 1673 require.NotNil(t, app) 1674 assert.Equal(t, "my-proj", app.Spec.Project) 1675 }) 1676 t.Run("Cannot update application project to invalid", func(t *testing.T) { 1677 t.Parallel() 1678 testApp := newTestApp() 1679 restrictedProj := &v1alpha1.AppProject{ 1680 ObjectMeta: metav1.ObjectMeta{Name: "restricted-proj", Namespace: "default"}, 1681 Spec: v1alpha1.AppProjectSpec{ 1682 SourceRepos: []string{"not-your-repo"}, 1683 Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "not-your-namespace"}}, 1684 }, 1685 } 1686 appServer := newTestAppServer(t, testApp, restrictedProj) 1687 1688 updateApp := newTestAppWithDestName() 1689 updateApp.TypeMeta = testApp.TypeMeta 1690 updateApp.Spec.Project = restrictedProj.Name 1691 _, err := appServer.Update(t.Context(), &application.ApplicationUpdateRequest{ 1692 Application: updateApp, 1693 }) 1694 require.Error(t, err) 1695 require.ErrorContains(t, err, "application repo https://github.com/argoproj/argocd-example-apps.git is not permitted in project 'restricted-proj'") 1696 require.ErrorContains(t, err, "application destination server 'fake-cluster' and namespace 'fake-dest-ns' do not match any of the allowed destinations in project 'restricted-proj'") 1697 }) 1698 t.Run("Cannot update application project to inexisting", func(t *testing.T) { 1699 t.Parallel() 1700 testApp := newTestApp() 1701 appServer := newTestAppServer(t, testApp) 1702 1703 updateApp := newTestAppWithDestName() 1704 updateApp.TypeMeta = testApp.TypeMeta 1705 updateApp.Spec.Project = "i-do-not-exist" 1706 _, err := appServer.Update(t.Context(), &application.ApplicationUpdateRequest{ 1707 Application: updateApp, 1708 }) 1709 require.Error(t, err) 1710 require.ErrorContains(t, err, "app is not allowed in project \"i-do-not-exist\", or the project does not exist") 1711 }) 1712 t.Run("Can update application project with project argument", func(t *testing.T) { 1713 t.Parallel() 1714 testApp := newTestApp() 1715 appServer := newTestAppServer(t, testApp) 1716 1717 updateApp := newTestAppWithDestName() 1718 updateApp.TypeMeta = testApp.TypeMeta 1719 updateApp.Spec.Project = "my-proj" 1720 app, err := appServer.Update(t.Context(), &application.ApplicationUpdateRequest{ 1721 Application: updateApp, 1722 Project: ptr.To("default"), 1723 }) 1724 require.NoError(t, err) 1725 require.NotNil(t, app) 1726 assert.Equal(t, "my-proj", app.Spec.Project) 1727 }) 1728 t.Run("Existing label and annotations are replaced", func(t *testing.T) { 1729 t.Parallel() 1730 testApp := newTestApp() 1731 testApp.Annotations = map[string]string{"test": "test-value", "update": "old"} 1732 testApp.Labels = map[string]string{"test": "test-value", "update": "old"} 1733 appServer := newTestAppServer(t, testApp) 1734 1735 updateApp := newTestAppWithDestName() 1736 updateApp.TypeMeta = testApp.TypeMeta 1737 updateApp.Annotations = map[string]string{"update": "new"} 1738 updateApp.Labels = map[string]string{"update": "new"} 1739 app, err := appServer.Update(t.Context(), &application.ApplicationUpdateRequest{ 1740 Application: updateApp, 1741 }) 1742 require.NoError(t, err) 1743 require.NotNil(t, app) 1744 assert.Len(t, app.Annotations, 1) 1745 assert.Equal(t, "new", app.GetAnnotations()["update"]) 1746 assert.Len(t, app.Labels, 1) 1747 assert.Equal(t, "new", app.GetLabels()["update"]) 1748 }) 1749 } 1750 1751 func TestUpdateAppSpec(t *testing.T) { 1752 testApp := newTestApp() 1753 appServer := newTestAppServer(t, testApp) 1754 testApp.Spec.Project = "" 1755 spec, err := appServer.UpdateSpec(t.Context(), &application.ApplicationUpdateSpecRequest{ 1756 Name: &testApp.Name, 1757 Spec: &testApp.Spec, 1758 }) 1759 require.NoError(t, err) 1760 assert.Equal(t, "default", spec.Project) 1761 app, err := appServer.Get(t.Context(), &application.ApplicationQuery{Name: &testApp.Name}) 1762 require.NoError(t, err) 1763 assert.Equal(t, "default", app.Spec.Project) 1764 } 1765 1766 func TestDeleteApp(t *testing.T) { 1767 ctx := t.Context() 1768 appServer := newTestAppServer(t) 1769 createReq := application.ApplicationCreateRequest{ 1770 Application: newTestApp(), 1771 } 1772 app, err := appServer.Create(ctx, &createReq) 1773 require.NoError(t, err) 1774 1775 app, err = appServer.Get(ctx, &application.ApplicationQuery{Name: &app.Name}) 1776 require.NoError(t, err) 1777 assert.NotNil(t, app) 1778 1779 fakeAppCs := appServer.appclientset.(*deepCopyAppClientset).GetUnderlyingClientSet().(*apps.Clientset) 1780 // this removes the default */* reactor so we can set our own patch/delete reactor 1781 fakeAppCs.ReactionChain = nil 1782 patched := false 1783 deleted := false 1784 fakeAppCs.AddReactor("patch", "applications", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) { 1785 patched = true 1786 return true, nil, nil 1787 }) 1788 fakeAppCs.AddReactor("delete", "applications", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) { 1789 deleted = true 1790 return true, nil, nil 1791 }) 1792 fakeAppCs.AddReactor("get", "applications", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) { 1793 return true, &v1alpha1.Application{Spec: v1alpha1.ApplicationSpec{Source: &v1alpha1.ApplicationSource{}}}, nil 1794 }) 1795 appServer.appclientset = fakeAppCs 1796 1797 trueVar := true 1798 _, err = appServer.Delete(ctx, &application.ApplicationDeleteRequest{Name: &app.Name, Cascade: &trueVar}) 1799 require.NoError(t, err) 1800 assert.True(t, patched) 1801 assert.True(t, deleted) 1802 1803 // now call delete with cascade=false. patch should not be called 1804 falseVar := false 1805 patched = false 1806 deleted = false 1807 _, err = appServer.Delete(ctx, &application.ApplicationDeleteRequest{Name: &app.Name, Cascade: &falseVar}) 1808 require.NoError(t, err) 1809 assert.False(t, patched) 1810 assert.True(t, deleted) 1811 1812 patched = false 1813 deleted = false 1814 revertValues := func() { 1815 patched = false 1816 deleted = false 1817 } 1818 1819 t.Run("Delete with background propagation policy", func(t *testing.T) { 1820 policy := backgroundPropagationPolicy 1821 _, err = appServer.Delete(ctx, &application.ApplicationDeleteRequest{Name: &app.Name, PropagationPolicy: &policy}) 1822 require.NoError(t, err) 1823 assert.True(t, patched) 1824 assert.True(t, deleted) 1825 t.Cleanup(revertValues) 1826 }) 1827 1828 t.Run("Delete with cascade disabled and background propagation policy", func(t *testing.T) { 1829 policy := backgroundPropagationPolicy 1830 _, err = appServer.Delete(ctx, &application.ApplicationDeleteRequest{Name: &app.Name, Cascade: &falseVar, PropagationPolicy: &policy}) 1831 require.EqualError(t, err, "rpc error: code = InvalidArgument desc = cannot set propagation policy when cascading is disabled") 1832 assert.False(t, patched) 1833 assert.False(t, deleted) 1834 t.Cleanup(revertValues) 1835 }) 1836 1837 t.Run("Delete with invalid propagation policy", func(t *testing.T) { 1838 invalidPolicy := "invalid" 1839 _, err = appServer.Delete(ctx, &application.ApplicationDeleteRequest{Name: &app.Name, Cascade: &trueVar, PropagationPolicy: &invalidPolicy}) 1840 require.EqualError(t, err, "rpc error: code = InvalidArgument desc = invalid propagation policy: invalid") 1841 assert.False(t, patched) 1842 assert.False(t, deleted) 1843 t.Cleanup(revertValues) 1844 }) 1845 1846 t.Run("Delete with foreground propagation policy", func(t *testing.T) { 1847 policy := foregroundPropagationPolicy 1848 _, err = appServer.Delete(ctx, &application.ApplicationDeleteRequest{Name: &app.Name, Cascade: &trueVar, PropagationPolicy: &policy}) 1849 require.NoError(t, err) 1850 assert.True(t, patched) 1851 assert.True(t, deleted) 1852 t.Cleanup(revertValues) 1853 }) 1854 } 1855 1856 func TestDeleteResourcesRBAC(t *testing.T) { 1857 ctx := t.Context() 1858 //nolint:staticcheck 1859 ctx = context.WithValue(ctx, "claims", &jwt.RegisteredClaims{Subject: "test-user"}) 1860 testApp := newTestApp() 1861 appServer := newTestAppServer(t, testApp) 1862 appServer.enf.SetDefaultRole("") 1863 1864 argoCM := map[string]string{"server.rbac.disableApplicationFineGrainedRBACInheritance": "false"} 1865 appServerWithRBACInheritance := newTestAppServerWithEnforcerConfigure(t, func(_ *rbac.Enforcer) {}, argoCM, testApp) 1866 appServerWithRBACInheritance.enf.SetDefaultRole("") 1867 1868 req := application.ApplicationResourceDeleteRequest{ 1869 Name: &testApp.Name, 1870 AppNamespace: &testApp.Namespace, 1871 Group: strToPtr("fake.io"), 1872 Kind: strToPtr("PodTest"), 1873 Namespace: strToPtr("fake-ns"), 1874 ResourceName: strToPtr("my-pod-test"), 1875 } 1876 1877 expectedErrorWhenDeleteAllowed := "rpc error: code = InvalidArgument desc = PodTest fake.io my-pod-test not found as part of application test-app" 1878 1879 t.Run("delete with application permission", func(t *testing.T) { 1880 _ = appServer.enf.SetBuiltinPolicy(` 1881 p, test-user, applications, delete, default/test-app, allow 1882 `) 1883 _, err := appServer.DeleteResource(ctx, &req) 1884 assert.Equal(t, codes.PermissionDenied.String(), status.Code(err).String()) 1885 }) 1886 1887 t.Run("delete with application permission with inheritance", func(t *testing.T) { 1888 _ = appServerWithRBACInheritance.enf.SetBuiltinPolicy(` 1889 p, test-user, applications, delete, default/test-app, allow 1890 `) 1891 _, err := appServerWithRBACInheritance.DeleteResource(ctx, &req) 1892 assert.EqualError(t, err, expectedErrorWhenDeleteAllowed) 1893 }) 1894 1895 t.Run("delete with application permission but deny subresource", func(t *testing.T) { 1896 _ = appServer.enf.SetBuiltinPolicy(` 1897 p, test-user, applications, delete, default/test-app, allow 1898 p, test-user, applications, delete/*, default/test-app, deny 1899 `) 1900 _, err := appServer.DeleteResource(ctx, &req) 1901 assert.Equal(t, codes.PermissionDenied.String(), status.Code(err).String()) 1902 }) 1903 1904 t.Run("delete with application permission but deny subresource with inheritance", func(t *testing.T) { 1905 _ = appServerWithRBACInheritance.enf.SetBuiltinPolicy(` 1906 p, test-user, applications, delete, default/test-app, allow 1907 p, test-user, applications, delete/*, default/test-app, deny 1908 `) 1909 _, err := appServerWithRBACInheritance.DeleteResource(ctx, &req) 1910 assert.EqualError(t, err, expectedErrorWhenDeleteAllowed) 1911 }) 1912 1913 t.Run("delete with subresource", func(t *testing.T) { 1914 _ = appServer.enf.SetBuiltinPolicy(` 1915 p, test-user, applications, delete/*, default/test-app, allow 1916 `) 1917 _, err := appServer.DeleteResource(ctx, &req) 1918 assert.EqualError(t, err, expectedErrorWhenDeleteAllowed) 1919 }) 1920 1921 t.Run("delete with subresource but deny applications", func(t *testing.T) { 1922 _ = appServer.enf.SetBuiltinPolicy(` 1923 p, test-user, applications, delete, default/test-app, deny 1924 p, test-user, applications, delete/*, default/test-app, allow 1925 `) 1926 _, err := appServer.DeleteResource(ctx, &req) 1927 assert.EqualError(t, err, expectedErrorWhenDeleteAllowed) 1928 }) 1929 1930 t.Run("delete with subresource but deny applications with inheritance", func(t *testing.T) { 1931 _ = appServerWithRBACInheritance.enf.SetBuiltinPolicy(` 1932 p, test-user, applications, delete, default/test-app, deny 1933 p, test-user, applications, delete/*, default/test-app, allow 1934 `) 1935 _, err := appServerWithRBACInheritance.DeleteResource(ctx, &req) 1936 assert.EqualError(t, err, expectedErrorWhenDeleteAllowed) 1937 }) 1938 1939 t.Run("delete with specific subresource denied", func(t *testing.T) { 1940 _ = appServer.enf.SetBuiltinPolicy(` 1941 p, test-user, applications, delete/*, default/test-app, allow 1942 p, test-user, applications, delete/fake.io/PodTest/*, default/test-app, deny 1943 `) 1944 _, err := appServer.DeleteResource(ctx, &req) 1945 assert.Equal(t, codes.PermissionDenied.String(), status.Code(err).String()) 1946 }) 1947 } 1948 1949 func TestPatchResourcesRBAC(t *testing.T) { 1950 ctx := t.Context() 1951 //nolint:staticcheck 1952 ctx = context.WithValue(ctx, "claims", &jwt.RegisteredClaims{Subject: "test-user"}) 1953 testApp := newTestApp() 1954 appServer := newTestAppServer(t, testApp) 1955 appServer.enf.SetDefaultRole("") 1956 1957 argoCM := map[string]string{"server.rbac.disableApplicationFineGrainedRBACInheritance": "false"} 1958 appServerWithRBACInheritance := newTestAppServerWithEnforcerConfigure(t, func(_ *rbac.Enforcer) {}, argoCM, testApp) 1959 appServerWithRBACInheritance.enf.SetDefaultRole("") 1960 1961 req := application.ApplicationResourcePatchRequest{ 1962 Name: &testApp.Name, 1963 AppNamespace: &testApp.Namespace, 1964 Group: strToPtr("fake.io"), 1965 Kind: strToPtr("PodTest"), 1966 Namespace: strToPtr("fake-ns"), 1967 ResourceName: strToPtr("my-pod-test"), 1968 } 1969 1970 expectedErrorWhenUpdateAllowed := "rpc error: code = InvalidArgument desc = PodTest fake.io my-pod-test not found as part of application test-app" 1971 1972 t.Run("patch with application permission", func(t *testing.T) { 1973 _ = appServer.enf.SetBuiltinPolicy(` 1974 p, test-user, applications, update, default/test-app, allow 1975 `) 1976 _, err := appServer.PatchResource(ctx, &req) 1977 assert.Equal(t, codes.PermissionDenied.String(), status.Code(err).String()) 1978 }) 1979 1980 t.Run("patch with application permission with inheritance", func(t *testing.T) { 1981 _ = appServerWithRBACInheritance.enf.SetBuiltinPolicy(` 1982 p, test-user, applications, update, default/test-app, allow 1983 `) 1984 _, err := appServerWithRBACInheritance.PatchResource(ctx, &req) 1985 assert.EqualError(t, err, expectedErrorWhenUpdateAllowed) 1986 }) 1987 1988 t.Run("patch with application permission but deny subresource", func(t *testing.T) { 1989 _ = appServer.enf.SetBuiltinPolicy(` 1990 p, test-user, applications, update, default/test-app, allow 1991 p, test-user, applications, update/*, default/test-app, deny 1992 `) 1993 _, err := appServer.PatchResource(ctx, &req) 1994 assert.Equal(t, codes.PermissionDenied.String(), status.Code(err).String()) 1995 }) 1996 1997 t.Run("patch with application permission but deny subresource with inheritance", func(t *testing.T) { 1998 _ = appServerWithRBACInheritance.enf.SetBuiltinPolicy(` 1999 p, test-user, applications, update, default/test-app, allow 2000 p, test-user, applications, update/*, default/test-app, deny 2001 `) 2002 _, err := appServerWithRBACInheritance.PatchResource(ctx, &req) 2003 assert.EqualError(t, err, expectedErrorWhenUpdateAllowed) 2004 }) 2005 2006 t.Run("patch with subresource", func(t *testing.T) { 2007 _ = appServer.enf.SetBuiltinPolicy(` 2008 p, test-user, applications, update/*, default/test-app, allow 2009 `) 2010 _, err := appServer.PatchResource(ctx, &req) 2011 assert.EqualError(t, err, expectedErrorWhenUpdateAllowed) 2012 }) 2013 2014 t.Run("patch with subresource but deny applications", func(t *testing.T) { 2015 _ = appServer.enf.SetBuiltinPolicy(` 2016 p, test-user, applications, update, default/test-app, deny 2017 p, test-user, applications, update/*, default/test-app, allow 2018 `) 2019 _, err := appServer.PatchResource(ctx, &req) 2020 assert.EqualError(t, err, expectedErrorWhenUpdateAllowed) 2021 }) 2022 2023 t.Run("patch with subresource but deny applications with inheritance", func(t *testing.T) { 2024 _ = appServerWithRBACInheritance.enf.SetBuiltinPolicy(` 2025 p, test-user, applications, update, default/test-app, deny 2026 p, test-user, applications, update/*, default/test-app, allow 2027 `) 2028 _, err := appServerWithRBACInheritance.PatchResource(ctx, &req) 2029 assert.EqualError(t, err, expectedErrorWhenUpdateAllowed) 2030 }) 2031 2032 t.Run("patch with specific subresource denied", func(t *testing.T) { 2033 _ = appServer.enf.SetBuiltinPolicy(` 2034 p, test-user, applications, update/*, default/test-app, allow 2035 p, test-user, applications, update/fake.io/PodTest/*, default/test-app, deny 2036 `) 2037 _, err := appServer.PatchResource(ctx, &req) 2038 assert.Equal(t, codes.PermissionDenied.String(), status.Code(err).String()) 2039 }) 2040 } 2041 2042 func TestSyncAndTerminate(t *testing.T) { 2043 ctx := t.Context() 2044 appServer := newTestAppServer(t) 2045 testApp := newTestApp() 2046 testApp.Spec.Source.RepoURL = "https://github.com/argoproj/argo-cd.git" 2047 createReq := application.ApplicationCreateRequest{ 2048 Application: testApp, 2049 } 2050 app, err := appServer.Create(ctx, &createReq) 2051 require.NoError(t, err) 2052 app, err = appServer.Sync(ctx, &application.ApplicationSyncRequest{Name: &app.Name}) 2053 require.NoError(t, err) 2054 assert.NotNil(t, app) 2055 assert.NotNil(t, app.Operation) 2056 assert.Equal(t, testApp.Spec.GetSource(), *app.Operation.Sync.Source) 2057 2058 events, err := appServer.kubeclientset.CoreV1().Events(appServer.ns).List(t.Context(), metav1.ListOptions{}) 2059 require.NoError(t, err) 2060 event := events.Items[1] 2061 2062 assert.Regexp(t, ".*initiated sync to HEAD \\([0-9A-Fa-f]{40}\\).*", event.Message) 2063 2064 // set status.operationState to pretend that an operation has started by controller 2065 app.Status.OperationState = &v1alpha1.OperationState{ 2066 Operation: *app.Operation, 2067 Phase: synccommon.OperationRunning, 2068 StartedAt: metav1.NewTime(time.Now()), 2069 } 2070 _, err = appServer.appclientset.ArgoprojV1alpha1().Applications(appServer.ns).Update(t.Context(), app, metav1.UpdateOptions{}) 2071 require.NoError(t, err) 2072 2073 resp, err := appServer.TerminateOperation(ctx, &application.OperationTerminateRequest{Name: &app.Name}) 2074 require.NoError(t, err) 2075 assert.NotNil(t, resp) 2076 2077 app, err = appServer.Get(ctx, &application.ApplicationQuery{Name: &app.Name}) 2078 require.NoError(t, err) 2079 assert.NotNil(t, app) 2080 assert.Equal(t, synccommon.OperationTerminating, app.Status.OperationState.Phase) 2081 } 2082 2083 func TestSyncHelm(t *testing.T) { 2084 ctx := t.Context() 2085 appServer := newTestAppServer(t) 2086 testApp := newTestApp() 2087 testApp.Spec.Source.RepoURL = "https://argoproj.github.io/argo-helm" 2088 testApp.Spec.Source.Path = "" 2089 testApp.Spec.Source.Chart = "argo-cd" 2090 testApp.Spec.Source.TargetRevision = "0.7.*" 2091 2092 appServer.repoClientset = &mocks.Clientset{RepoServerServiceClient: fakeRepoServerClient(true)} 2093 2094 app, err := appServer.Create(ctx, &application.ApplicationCreateRequest{Application: testApp}) 2095 require.NoError(t, err) 2096 2097 app, err = appServer.Sync(ctx, &application.ApplicationSyncRequest{Name: &app.Name}) 2098 require.NoError(t, err) 2099 assert.NotNil(t, app) 2100 assert.NotNil(t, app.Operation) 2101 2102 events, err := appServer.kubeclientset.CoreV1().Events(appServer.ns).List(t.Context(), metav1.ListOptions{}) 2103 require.NoError(t, err) 2104 assert.Equal(t, "Unknown user initiated sync to 0.7.* (0.7.2)", events.Items[1].Message) 2105 } 2106 2107 func TestSyncGit(t *testing.T) { 2108 ctx := t.Context() 2109 appServer := newTestAppServer(t) 2110 testApp := newTestApp() 2111 testApp.Spec.Source.RepoURL = "https://github.com/org/test" 2112 testApp.Spec.Source.Path = "deploy" 2113 testApp.Spec.Source.TargetRevision = "0.7.*" 2114 app, err := appServer.Create(ctx, &application.ApplicationCreateRequest{Application: testApp}) 2115 require.NoError(t, err) 2116 syncReq := &application.ApplicationSyncRequest{ 2117 Name: &app.Name, 2118 Manifests: []string{ 2119 `apiVersion: v1 2120 kind: ServiceAccount 2121 metadata: 2122 name: test 2123 namespace: test`, 2124 }, 2125 } 2126 app, err = appServer.Sync(ctx, syncReq) 2127 require.NoError(t, err) 2128 assert.NotNil(t, app) 2129 assert.NotNil(t, app.Operation) 2130 events, err := appServer.kubeclientset.CoreV1().Events(appServer.ns).List(t.Context(), metav1.ListOptions{}) 2131 require.NoError(t, err) 2132 assert.Equal(t, "Unknown user initiated sync locally", events.Items[1].Message) 2133 } 2134 2135 func TestSync_WithRefresh(t *testing.T) { 2136 ctx := t.Context() 2137 appServer := newTestAppServer(t) 2138 testApp := newTestApp() 2139 testApp.Spec.SyncPolicy = &v1alpha1.SyncPolicy{ 2140 Retry: &v1alpha1.RetryStrategy{ 2141 Refresh: true, 2142 }, 2143 } 2144 testApp.Spec.Source.RepoURL = "https://github.com/argoproj/argo-cd.git" 2145 createReq := application.ApplicationCreateRequest{ 2146 Application: testApp, 2147 } 2148 app, err := appServer.Create(ctx, &createReq) 2149 require.NoError(t, err) 2150 app, err = appServer.Sync(ctx, &application.ApplicationSyncRequest{Name: &app.Name}) 2151 require.NoError(t, err) 2152 assert.NotNil(t, app) 2153 assert.NotNil(t, app.Operation) 2154 assert.True(t, app.Operation.Retry.Refresh) 2155 } 2156 2157 func TestGetManifests_WithNoCache(t *testing.T) { 2158 testApp := newTestApp() 2159 appServer := newTestAppServer(t, testApp) 2160 2161 mockRepoServiceClient := mocks.RepoServerServiceClient{} 2162 mockRepoServiceClient.On("GenerateManifest", mock.Anything, mock.MatchedBy(func(mr *apiclient.ManifestRequest) bool { 2163 // expected to be true because given NoCache in the ApplicationManifestQuery 2164 return mr.NoCache 2165 })).Return(&apiclient.ManifestResponse{}, nil) 2166 2167 appServer.repoClientset = &mocks.Clientset{RepoServerServiceClient: &mockRepoServiceClient} 2168 2169 _, err := appServer.GetManifests(t.Context(), &application.ApplicationManifestQuery{ 2170 Name: &testApp.Name, 2171 NoCache: ptr.To(true), 2172 }) 2173 require.NoError(t, err) 2174 mockRepoServiceClient.AssertExpectations(t) 2175 } 2176 2177 func TestRollbackApp(t *testing.T) { 2178 testApp := newTestApp() 2179 testApp.Status.History = []v1alpha1.RevisionHistory{{ 2180 ID: 1, 2181 Revision: "abc", 2182 Revisions: []string{"abc"}, 2183 Source: *testApp.Spec.Source.DeepCopy(), 2184 Sources: []v1alpha1.ApplicationSource{*testApp.Spec.Source.DeepCopy()}, 2185 }} 2186 appServer := newTestAppServer(t, testApp) 2187 2188 updatedApp, err := appServer.Rollback(t.Context(), &application.ApplicationRollbackRequest{ 2189 Name: &testApp.Name, 2190 Id: ptr.To(int64(1)), 2191 }) 2192 2193 require.NoError(t, err) 2194 2195 assert.NotNil(t, updatedApp.Operation) 2196 assert.NotNil(t, updatedApp.Operation.Sync) 2197 assert.NotNil(t, updatedApp.Operation.Sync.Source) 2198 assert.Equal(t, testApp.Status.History[0].Source, *updatedApp.Operation.Sync.Source) 2199 assert.Equal(t, testApp.Status.History[0].Sources, updatedApp.Operation.Sync.Sources) 2200 assert.Equal(t, testApp.Status.History[0].Revision, updatedApp.Operation.Sync.Revision) 2201 assert.Equal(t, testApp.Status.History[0].Revisions, updatedApp.Operation.Sync.Revisions) 2202 } 2203 2204 func TestRollbackApp_WithRefresh(t *testing.T) { 2205 testApp := newTestApp() 2206 testApp.Spec.SyncPolicy = &v1alpha1.SyncPolicy{ 2207 Retry: &v1alpha1.RetryStrategy{ 2208 Refresh: true, 2209 }, 2210 } 2211 2212 testApp.Status.History = []v1alpha1.RevisionHistory{{ 2213 ID: 1, 2214 Revision: "abc", 2215 Source: *testApp.Spec.Source.DeepCopy(), 2216 }} 2217 appServer := newTestAppServer(t, testApp) 2218 2219 updatedApp, err := appServer.Rollback(t.Context(), &application.ApplicationRollbackRequest{ 2220 Name: &testApp.Name, 2221 Id: ptr.To(int64(1)), 2222 }) 2223 2224 require.NoError(t, err) 2225 2226 assert.NotNil(t, updatedApp.Operation) 2227 assert.NotNil(t, updatedApp.Operation.Retry) 2228 assert.False(t, updatedApp.Operation.Retry.Refresh, "refresh should never be set on rollback") 2229 } 2230 2231 func TestUpdateAppProject(t *testing.T) { 2232 testApp := newTestApp() 2233 ctx := t.Context() 2234 //nolint:staticcheck 2235 ctx = context.WithValue(ctx, "claims", &jwt.RegisteredClaims{Subject: "admin"}) 2236 appServer := newTestAppServer(t, testApp) 2237 appServer.enf.SetDefaultRole("") 2238 2239 t.Run("update without changing project", func(t *testing.T) { 2240 _ = appServer.enf.SetBuiltinPolicy(`p, admin, applications, update, default/test-app, allow`) 2241 _, err := appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp}) 2242 require.NoError(t, err) 2243 }) 2244 2245 t.Run("cannot update to another project", func(t *testing.T) { 2246 testApp.Spec.Project = "my-proj" 2247 _, err := appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp}) 2248 assert.Equal(t, codes.PermissionDenied, status.Code(err)) 2249 }) 2250 2251 t.Run("cannot change projects without create privileges", func(t *testing.T) { 2252 _ = appServer.enf.SetBuiltinPolicy(` 2253 p, admin, applications, update, default/test-app, allow 2254 p, admin, applications, update, my-proj/test-app, allow 2255 `) 2256 _, err := appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp}) 2257 statusErr := grpc.UnwrapGRPCStatus(err) 2258 assert.NotNil(t, statusErr) 2259 assert.Equal(t, codes.PermissionDenied, statusErr.Code()) 2260 }) 2261 2262 t.Run("cannot change projects without update privileges in new project", func(t *testing.T) { 2263 _ = appServer.enf.SetBuiltinPolicy(` 2264 p, admin, applications, update, default/test-app, allow 2265 p, admin, applications, create, my-proj/test-app, allow 2266 `) 2267 _, err := appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp}) 2268 assert.Equal(t, codes.PermissionDenied, status.Code(err)) 2269 }) 2270 2271 t.Run("cannot change projects without update privileges in old project", func(t *testing.T) { 2272 _ = appServer.enf.SetBuiltinPolicy(` 2273 p, admin, applications, create, my-proj/test-app, allow 2274 p, admin, applications, update, my-proj/test-app, allow 2275 `) 2276 _, err := appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp}) 2277 statusErr := grpc.UnwrapGRPCStatus(err) 2278 assert.NotNil(t, statusErr) 2279 assert.Equal(t, codes.PermissionDenied, statusErr.Code()) 2280 }) 2281 2282 t.Run("can update project with proper permissions", func(t *testing.T) { 2283 // Verify can update project with proper permissions 2284 _ = appServer.enf.SetBuiltinPolicy(` 2285 p, admin, applications, update, default/test-app, allow 2286 p, admin, applications, create, my-proj/test-app, allow 2287 p, admin, applications, update, my-proj/test-app, allow 2288 `) 2289 updatedApp, err := appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp}) 2290 require.NoError(t, err) 2291 assert.Equal(t, "my-proj", updatedApp.Spec.Project) 2292 }) 2293 } 2294 2295 func TestAppJsonPatch(t *testing.T) { 2296 testApp := newTestAppWithAnnotations() 2297 ctx := t.Context() 2298 //nolint:staticcheck 2299 ctx = context.WithValue(ctx, "claims", &jwt.RegisteredClaims{Subject: "admin"}) 2300 appServer := newTestAppServer(t, testApp) 2301 appServer.enf.SetDefaultRole("") 2302 2303 app, err := appServer.Patch(ctx, &application.ApplicationPatchRequest{Name: &testApp.Name, Patch: ptr.To("garbage")}) 2304 require.Error(t, err) 2305 assert.Nil(t, app) 2306 2307 app, err = appServer.Patch(ctx, &application.ApplicationPatchRequest{Name: &testApp.Name, Patch: ptr.To("[]")}) 2308 require.NoError(t, err) 2309 assert.NotNil(t, app) 2310 2311 app, err = appServer.Patch(ctx, &application.ApplicationPatchRequest{Name: &testApp.Name, Patch: ptr.To(`[{"op": "replace", "path": "/spec/source/path", "value": "foo"}]`)}) 2312 require.NoError(t, err) 2313 assert.Equal(t, "foo", app.Spec.Source.Path) 2314 2315 app, err = appServer.Patch(ctx, &application.ApplicationPatchRequest{Name: &testApp.Name, Patch: ptr.To(`[{"op": "remove", "path": "/metadata/annotations/test.annotation"}]`)}) 2316 require.NoError(t, err) 2317 assert.NotContains(t, app.Annotations, "test.annotation") 2318 } 2319 2320 func TestAppMergePatch(t *testing.T) { 2321 testApp := newTestApp() 2322 ctx := t.Context() 2323 //nolint:staticcheck 2324 ctx = context.WithValue(ctx, "claims", &jwt.RegisteredClaims{Subject: "admin"}) 2325 appServer := newTestAppServer(t, testApp) 2326 appServer.enf.SetDefaultRole("") 2327 2328 app, err := appServer.Patch(ctx, &application.ApplicationPatchRequest{ 2329 Name: &testApp.Name, Patch: ptr.To(`{"spec": { "source": { "path": "foo" } }}`), PatchType: ptr.To("merge"), 2330 }) 2331 require.NoError(t, err) 2332 assert.Equal(t, "foo", app.Spec.Source.Path) 2333 } 2334 2335 func TestServer_GetApplicationSyncWindowsState(t *testing.T) { 2336 t.Run("Active", func(t *testing.T) { 2337 testApp := newTestApp() 2338 testApp.Spec.Project = "proj-maint" 2339 appServer := newTestAppServer(t, testApp) 2340 2341 active, err := appServer.GetApplicationSyncWindows(t.Context(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name}) 2342 require.NoError(t, err) 2343 assert.Len(t, active.ActiveWindows, 1) 2344 }) 2345 t.Run("Inactive", func(t *testing.T) { 2346 testApp := newTestApp() 2347 testApp.Spec.Project = "default" 2348 appServer := newTestAppServer(t, testApp) 2349 2350 active, err := appServer.GetApplicationSyncWindows(t.Context(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name}) 2351 require.NoError(t, err) 2352 assert.Empty(t, active.ActiveWindows) 2353 }) 2354 t.Run("ProjectDoesNotExist", func(t *testing.T) { 2355 testApp := newTestApp() 2356 testApp.Spec.Project = "none" 2357 appServer := newTestAppServer(t, testApp) 2358 2359 active, err := appServer.GetApplicationSyncWindows(t.Context(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name}) 2360 require.ErrorContains(t, err, "not exist") 2361 assert.Nil(t, active) 2362 }) 2363 } 2364 2365 func TestGetCachedAppState(t *testing.T) { 2366 testApp := newTestApp() 2367 testApp.ResourceVersion = "1" 2368 testApp.Spec.Project = "test-proj" 2369 testProj := &v1alpha1.AppProject{ 2370 ObjectMeta: metav1.ObjectMeta{ 2371 Name: "test-proj", 2372 Namespace: testNamespace, 2373 }, 2374 } 2375 appServer := newTestAppServer(t, testApp, testProj) 2376 fakeClientSet := appServer.appclientset.(*deepCopyAppClientset).GetUnderlyingClientSet().(*apps.Clientset) 2377 fakeClientSet.AddReactor("get", "applications", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) { 2378 return true, &v1alpha1.Application{Spec: v1alpha1.ApplicationSpec{Source: &v1alpha1.ApplicationSource{}}}, nil 2379 }) 2380 t.Run("NoError", func(t *testing.T) { 2381 err := appServer.getCachedAppState(t.Context(), testApp, func() error { 2382 return nil 2383 }) 2384 require.NoError(t, err) 2385 }) 2386 t.Run("CacheMissErrorTriggersRefresh", func(t *testing.T) { 2387 retryCount := 0 2388 patched := false 2389 watcher := watch.NewFakeWithChanSize(1, true) 2390 2391 // Configure fakeClientSet within lock, before requesting cached app state, to avoid data race 2392 fakeClientSet.Lock() 2393 fakeClientSet.ReactionChain = nil 2394 fakeClientSet.WatchReactionChain = nil 2395 fakeClientSet.AddReactor("patch", "applications", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) { 2396 patched = true 2397 updated := testApp.DeepCopy() 2398 updated.ResourceVersion = "2" 2399 appServer.appBroadcaster.OnUpdate(testApp, updated) 2400 return true, testApp, nil 2401 }) 2402 fakeClientSet.AddReactor("get", "applications", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) { 2403 return true, &v1alpha1.Application{Spec: v1alpha1.ApplicationSpec{Source: &v1alpha1.ApplicationSource{}}}, nil 2404 }) 2405 fakeClientSet.Unlock() 2406 fakeClientSet.AddWatchReactor("applications", func(_ kubetesting.Action) (handled bool, ret watch.Interface, err error) { 2407 return true, watcher, nil 2408 }) 2409 2410 err := appServer.getCachedAppState(t.Context(), testApp, func() error { 2411 res := cache.ErrCacheMiss 2412 if retryCount == 1 { 2413 res = nil 2414 } 2415 retryCount++ 2416 return res 2417 }) 2418 require.NoError(t, err) 2419 assert.Equal(t, 2, retryCount) 2420 assert.True(t, patched) 2421 }) 2422 2423 t.Run("NonCacheErrorDoesNotTriggerRefresh", func(t *testing.T) { 2424 randomError := stderrors.New("random error") 2425 err := appServer.getCachedAppState(t.Context(), testApp, func() error { 2426 return randomError 2427 }) 2428 assert.Equal(t, randomError, err) 2429 }) 2430 } 2431 2432 func TestSplitStatusPatch(t *testing.T) { 2433 specPatch := `{"spec":{"aaa":"bbb"}}` 2434 statusPatch := `{"status":{"ccc":"ddd"}}` 2435 { 2436 nonStatus, status, err := splitStatusPatch([]byte(specPatch)) 2437 require.NoError(t, err) 2438 assert.Equal(t, specPatch, string(nonStatus)) 2439 assert.Nil(t, status) 2440 } 2441 { 2442 nonStatus, status, err := splitStatusPatch([]byte(statusPatch)) 2443 require.NoError(t, err) 2444 assert.Nil(t, nonStatus) 2445 assert.Equal(t, statusPatch, string(status)) 2446 } 2447 { 2448 bothPatch := `{"spec":{"aaa":"bbb"},"status":{"ccc":"ddd"}}` 2449 nonStatus, status, err := splitStatusPatch([]byte(bothPatch)) 2450 require.NoError(t, err) 2451 assert.Equal(t, specPatch, string(nonStatus)) 2452 assert.Equal(t, statusPatch, string(status)) 2453 } 2454 { 2455 otherFields := `{"operation":{"eee":"fff"},"spec":{"aaa":"bbb"},"status":{"ccc":"ddd"}}` 2456 nonStatus, status, err := splitStatusPatch([]byte(otherFields)) 2457 require.NoError(t, err) 2458 assert.JSONEq(t, `{"operation":{"eee":"fff"},"spec":{"aaa":"bbb"}}`, string(nonStatus)) 2459 assert.Equal(t, statusPatch, string(status)) 2460 } 2461 } 2462 2463 func TestLogsGetSelectedPod(t *testing.T) { 2464 deployment := v1alpha1.ResourceRef{Group: "", Version: "v1", Kind: "Deployment", Name: "deployment", UID: "1"} 2465 rs := v1alpha1.ResourceRef{Group: "", Version: "v1", Kind: "ReplicaSet", Name: "rs", UID: "2"} 2466 podRS := v1alpha1.ResourceRef{Group: "", Version: "v1", Kind: "Pod", Name: "podrs", UID: "3"} 2467 pod := v1alpha1.ResourceRef{Group: "", Version: "v1", Kind: "Pod", Name: "pod", UID: "4"} 2468 treeNodes := []v1alpha1.ResourceNode{ 2469 {ResourceRef: deployment, ParentRefs: nil}, 2470 {ResourceRef: rs, ParentRefs: []v1alpha1.ResourceRef{deployment}}, 2471 {ResourceRef: podRS, ParentRefs: []v1alpha1.ResourceRef{rs}}, 2472 {ResourceRef: pod, ParentRefs: nil}, 2473 } 2474 appName := "appName" 2475 2476 t.Run("GetAllPods", func(t *testing.T) { 2477 podQuery := application.ApplicationPodLogsQuery{ 2478 Name: &appName, 2479 } 2480 pods := getSelectedPods(treeNodes, &podQuery) 2481 assert.Len(t, pods, 2) 2482 }) 2483 2484 t.Run("GetRSPods", func(t *testing.T) { 2485 group := "" 2486 kind := "ReplicaSet" 2487 name := "rs" 2488 podQuery := application.ApplicationPodLogsQuery{ 2489 Name: &appName, 2490 Group: &group, 2491 Kind: &kind, 2492 ResourceName: &name, 2493 } 2494 pods := getSelectedPods(treeNodes, &podQuery) 2495 assert.Len(t, pods, 1) 2496 }) 2497 2498 t.Run("GetDeploymentPods", func(t *testing.T) { 2499 group := "" 2500 kind := "Deployment" 2501 name := "deployment" 2502 podQuery := application.ApplicationPodLogsQuery{ 2503 Name: &appName, 2504 Group: &group, 2505 Kind: &kind, 2506 ResourceName: &name, 2507 } 2508 pods := getSelectedPods(treeNodes, &podQuery) 2509 assert.Len(t, pods, 1) 2510 }) 2511 2512 t.Run("NoMatchingPods", func(t *testing.T) { 2513 group := "" 2514 kind := "Service" 2515 name := "service" 2516 podQuery := application.ApplicationPodLogsQuery{ 2517 Name: &appName, 2518 Group: &group, 2519 Kind: &kind, 2520 ResourceName: &name, 2521 } 2522 pods := getSelectedPods(treeNodes, &podQuery) 2523 assert.Empty(t, pods) 2524 }) 2525 } 2526 2527 func TestMaxPodLogsRender(t *testing.T) { 2528 defaultMaxPodLogsToRender, _ := newTestAppServer(t).settingsMgr.GetMaxPodLogsToRender() 2529 2530 // Case: number of pods to view logs is less than defaultMaxPodLogsToRender 2531 podNumber := int(defaultMaxPodLogsToRender - 1) 2532 appServer, adminCtx := createAppServerWithMaxLodLogs(t, podNumber) 2533 2534 t.Run("PodLogs", func(t *testing.T) { 2535 err := appServer.PodLogs(&application.ApplicationPodLogsQuery{Name: ptr.To("test")}, &TestPodLogsServer{ctx: adminCtx}) 2536 statusCode, _ := status.FromError(err) 2537 assert.Equal(t, codes.OK, statusCode.Code()) 2538 }) 2539 2540 // Case: number of pods higher than defaultMaxPodLogsToRender 2541 podNumber = int(defaultMaxPodLogsToRender + 1) 2542 appServer, adminCtx = createAppServerWithMaxLodLogs(t, podNumber) 2543 2544 t.Run("PodLogs", func(t *testing.T) { 2545 err := appServer.PodLogs(&application.ApplicationPodLogsQuery{Name: ptr.To("test")}, &TestPodLogsServer{ctx: adminCtx}) 2546 require.Error(t, err) 2547 statusCode, _ := status.FromError(err) 2548 assert.Equal(t, codes.InvalidArgument, statusCode.Code()) 2549 assert.EqualError(t, err, "rpc error: code = InvalidArgument desc = max pods to view logs are reached. Please provide more granular query") 2550 }) 2551 2552 // Case: number of pods to view logs is less than customMaxPodLogsToRender 2553 customMaxPodLogsToRender := int64(15) 2554 podNumber = int(customMaxPodLogsToRender - 1) 2555 appServer, adminCtx = createAppServerWithMaxLodLogs(t, podNumber, customMaxPodLogsToRender) 2556 2557 t.Run("PodLogs", func(t *testing.T) { 2558 err := appServer.PodLogs(&application.ApplicationPodLogsQuery{Name: ptr.To("test")}, &TestPodLogsServer{ctx: adminCtx}) 2559 statusCode, _ := status.FromError(err) 2560 assert.Equal(t, codes.OK, statusCode.Code()) 2561 }) 2562 2563 // Case: number of pods higher than customMaxPodLogsToRender 2564 customMaxPodLogsToRender = int64(15) 2565 podNumber = int(customMaxPodLogsToRender + 1) 2566 appServer, adminCtx = createAppServerWithMaxLodLogs(t, podNumber, customMaxPodLogsToRender) 2567 2568 t.Run("PodLogs", func(t *testing.T) { 2569 err := appServer.PodLogs(&application.ApplicationPodLogsQuery{Name: ptr.To("test")}, &TestPodLogsServer{ctx: adminCtx}) 2570 require.Error(t, err) 2571 statusCode, _ := status.FromError(err) 2572 assert.Equal(t, codes.InvalidArgument, statusCode.Code()) 2573 assert.EqualError(t, err, "rpc error: code = InvalidArgument desc = max pods to view logs are reached. Please provide more granular query") 2574 }) 2575 } 2576 2577 // createAppServerWithMaxLodLogs creates a new app server with given number of pods and resources 2578 func createAppServerWithMaxLodLogs(t *testing.T, podNumber int, maxPodLogsToRender ...int64) (*Server, context.Context) { 2579 t.Helper() 2580 runtimeObjects := make([]runtime.Object, podNumber+1) 2581 resources := make([]v1alpha1.ResourceStatus, podNumber) 2582 2583 for i := 0; i < podNumber; i++ { 2584 pod := corev1.Pod{ 2585 TypeMeta: metav1.TypeMeta{ 2586 APIVersion: "v1", 2587 Kind: "Pod", 2588 }, 2589 ObjectMeta: metav1.ObjectMeta{ 2590 Name: fmt.Sprintf("pod-%d", i), 2591 Namespace: "test", 2592 }, 2593 } 2594 resources[i] = v1alpha1.ResourceStatus{ 2595 Group: pod.GroupVersionKind().Group, 2596 Kind: pod.GroupVersionKind().Kind, 2597 Version: pod.GroupVersionKind().Version, 2598 Name: pod.Name, 2599 Namespace: pod.Namespace, 2600 Status: "Synced", 2601 } 2602 runtimeObjects[i] = kube.MustToUnstructured(&pod) 2603 } 2604 2605 testApp := newTestApp(func(app *v1alpha1.Application) { 2606 app.Name = "test" 2607 app.Status.Resources = resources 2608 }) 2609 runtimeObjects[podNumber] = testApp 2610 2611 noRoleCtx := t.Context() 2612 //nolint:staticcheck 2613 adminCtx := context.WithValue(noRoleCtx, "claims", &jwt.MapClaims{"groups": []string{"admin"}}) 2614 2615 if len(maxPodLogsToRender) > 0 { 2616 f := func(enf *rbac.Enforcer) { 2617 _ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV) 2618 enf.SetDefaultRole("role:admin") 2619 } 2620 formatInt := strconv.FormatInt(maxPodLogsToRender[0], 10) 2621 appServer := newTestAppServerWithEnforcerConfigure(t, f, map[string]string{"server.maxPodLogsToRender": formatInt}, runtimeObjects...) 2622 return appServer, adminCtx 2623 } 2624 appServer := newTestAppServer(t, runtimeObjects...) 2625 return appServer, adminCtx 2626 } 2627 2628 // refreshAnnotationRemover runs an infinite loop until it detects and removes refresh annotation or given context is done 2629 func refreshAnnotationRemover(t *testing.T, ctx context.Context, patched *int32, appServer *Server, appName string, ch chan string) { 2630 t.Helper() 2631 for ctx.Err() == nil { 2632 aName, appNs := argo.ParseFromQualifiedName(appName, appServer.ns) 2633 a, err := appServer.appLister.Applications(appNs).Get(aName) 2634 require.NoError(t, err) 2635 if a.GetAnnotations() != nil && a.GetAnnotations()[v1alpha1.AnnotationKeyRefresh] != "" { 2636 a.SetAnnotations(map[string]string{}) 2637 a.SetResourceVersion("999") 2638 _, err = appServer.appclientset.ArgoprojV1alpha1().Applications(a.Namespace).Update( 2639 t.Context(), a, metav1.UpdateOptions{}) 2640 require.NoError(t, err) 2641 atomic.AddInt32(patched, 1) 2642 ch <- "" 2643 } 2644 time.Sleep(100 * time.Millisecond) 2645 } 2646 } 2647 2648 func TestGetAppRefresh_NormalRefresh(t *testing.T) { 2649 ctx, cancel := context.WithCancel(t.Context()) 2650 defer cancel() 2651 testApp := newTestApp() 2652 testApp.ResourceVersion = "1" 2653 appServer := newTestAppServer(t, testApp) 2654 2655 var patched int32 2656 2657 ch := make(chan string, 1) 2658 2659 go refreshAnnotationRemover(t, ctx, &patched, appServer, testApp.Name, ch) 2660 2661 _, err := appServer.Get(t.Context(), &application.ApplicationQuery{ 2662 Name: &testApp.Name, 2663 Refresh: ptr.To(string(v1alpha1.RefreshTypeNormal)), 2664 }) 2665 require.NoError(t, err) 2666 2667 select { 2668 case <-ch: 2669 assert.Equal(t, int32(1), atomic.LoadInt32(&patched)) 2670 case <-time.After(10 * time.Second): 2671 assert.Fail(t, "Out of time ( 10 seconds )") 2672 } 2673 } 2674 2675 func TestGetAppRefresh_HardRefresh(t *testing.T) { 2676 ctx, cancel := context.WithCancel(t.Context()) 2677 defer cancel() 2678 testApp := newTestApp() 2679 testApp.ResourceVersion = "1" 2680 appServer := newTestAppServer(t, testApp) 2681 2682 var getAppDetailsQuery *apiclient.RepoServerAppDetailsQuery 2683 mockRepoServiceClient := mocks.RepoServerServiceClient{} 2684 mockRepoServiceClient.On("GetAppDetails", mock.Anything, mock.MatchedBy(func(q *apiclient.RepoServerAppDetailsQuery) bool { 2685 getAppDetailsQuery = q 2686 return true 2687 })).Return(&apiclient.RepoAppDetailsResponse{}, nil) 2688 appServer.repoClientset = &mocks.Clientset{RepoServerServiceClient: &mockRepoServiceClient} 2689 2690 var patched int32 2691 2692 ch := make(chan string, 1) 2693 2694 go refreshAnnotationRemover(t, ctx, &patched, appServer, testApp.Name, ch) 2695 2696 _, err := appServer.Get(t.Context(), &application.ApplicationQuery{ 2697 Name: &testApp.Name, 2698 Refresh: ptr.To(string(v1alpha1.RefreshTypeHard)), 2699 }) 2700 require.NoError(t, err) 2701 require.NotNil(t, getAppDetailsQuery) 2702 assert.True(t, getAppDetailsQuery.NoCache) 2703 assert.Equal(t, testApp.Spec.Source, getAppDetailsQuery.Source) 2704 2705 require.NoError(t, err) 2706 select { 2707 case <-ch: 2708 assert.Equal(t, int32(1), atomic.LoadInt32(&patched)) 2709 case <-time.After(10 * time.Second): 2710 assert.Fail(t, "Out of time ( 10 seconds )") 2711 } 2712 } 2713 2714 func TestGetApp_HealthStatusPropagation(t *testing.T) { 2715 newServerWithTree := func(t *testing.T) (*Server, *v1alpha1.Application) { 2716 t.Helper() 2717 cacheClient := cache.NewCache(cache.NewInMemoryCache(1 * time.Hour)) 2718 2719 testApp := newTestApp() 2720 testApp.Status.ResourceHealthSource = v1alpha1.ResourceHealthLocationAppTree 2721 testApp.Status.Resources = []v1alpha1.ResourceStatus{ 2722 { 2723 Group: "apps", 2724 Kind: "Deployment", 2725 Name: "guestbook", 2726 Namespace: "default", 2727 }, 2728 } 2729 2730 appServer := newTestAppServer(t, testApp) 2731 2732 appStateCache := appstate.NewCache(cacheClient, time.Minute) 2733 appInstanceName := testApp.InstanceName(appServer.appNamespaceOrDefault(testApp.Namespace)) 2734 err := appStateCache.SetAppResourcesTree(appInstanceName, &v1alpha1.ApplicationTree{ 2735 Nodes: []v1alpha1.ResourceNode{{ 2736 ResourceRef: v1alpha1.ResourceRef{ 2737 Group: "apps", 2738 Kind: "Deployment", 2739 Name: "guestbook", 2740 Namespace: "default", 2741 }, 2742 Health: &v1alpha1.HealthStatus{Status: health.HealthStatusDegraded}, 2743 }}, 2744 }) 2745 require.NoError(t, err) 2746 appServer.cache = servercache.NewCache(appStateCache, time.Minute, time.Minute) 2747 2748 return appServer, testApp 2749 } 2750 2751 t.Run("propagated health status on get with no refresh", func(t *testing.T) { 2752 appServer, testApp := newServerWithTree(t) 2753 fetchedApp, err := appServer.Get(t.Context(), &application.ApplicationQuery{ 2754 Name: &testApp.Name, 2755 }) 2756 require.NoError(t, err) 2757 assert.Equal(t, health.HealthStatusDegraded, fetchedApp.Status.Resources[0].Health.Status) 2758 }) 2759 2760 t.Run("propagated health status on normal refresh", func(t *testing.T) { 2761 appServer, testApp := newServerWithTree(t) 2762 var patched int32 2763 ch := make(chan string, 1) 2764 ctx, cancel := context.WithCancel(t.Context()) 2765 defer cancel() 2766 go refreshAnnotationRemover(t, ctx, &patched, appServer, testApp.Name, ch) 2767 2768 fetchedApp, err := appServer.Get(t.Context(), &application.ApplicationQuery{ 2769 Name: &testApp.Name, 2770 Refresh: ptr.To(string(v1alpha1.RefreshTypeNormal)), 2771 }) 2772 require.NoError(t, err) 2773 2774 select { 2775 case <-ch: 2776 assert.Equal(t, int32(1), atomic.LoadInt32(&patched)) 2777 case <-time.After(10 * time.Second): 2778 assert.Fail(t, "Out of time ( 10 seconds )") 2779 } 2780 assert.Equal(t, health.HealthStatusDegraded, fetchedApp.Status.Resources[0].Health.Status) 2781 }) 2782 2783 t.Run("propagated health status on hard refresh", func(t *testing.T) { 2784 appServer, testApp := newServerWithTree(t) 2785 var patched int32 2786 ch := make(chan string, 1) 2787 ctx, cancel := context.WithCancel(t.Context()) 2788 defer cancel() 2789 go refreshAnnotationRemover(t, ctx, &patched, appServer, testApp.Name, ch) 2790 2791 fetchedApp, err := appServer.Get(t.Context(), &application.ApplicationQuery{ 2792 Name: &testApp.Name, 2793 Refresh: ptr.To(string(v1alpha1.RefreshTypeHard)), 2794 }) 2795 require.NoError(t, err) 2796 2797 select { 2798 case <-ch: 2799 assert.Equal(t, int32(1), atomic.LoadInt32(&patched)) 2800 case <-time.After(10 * time.Second): 2801 assert.Fail(t, "Out of time ( 10 seconds )") 2802 } 2803 assert.Equal(t, health.HealthStatusDegraded, fetchedApp.Status.Resources[0].Health.Status) 2804 }) 2805 } 2806 2807 func TestInferResourcesStatusHealth(t *testing.T) { 2808 cacheClient := cache.NewCache(cache.NewInMemoryCache(1 * time.Hour)) 2809 2810 testApp := newTestApp() 2811 testApp.Status.ResourceHealthSource = v1alpha1.ResourceHealthLocationAppTree 2812 testApp.Status.Resources = []v1alpha1.ResourceStatus{{ 2813 Group: "apps", 2814 Kind: "Deployment", 2815 Name: "guestbook", 2816 Namespace: "default", 2817 }, { 2818 Group: "apps", 2819 Kind: "StatefulSet", 2820 Name: "guestbook-stateful", 2821 Namespace: "default", 2822 }} 2823 appServer := newTestAppServer(t, testApp) 2824 appStateCache := appstate.NewCache(cacheClient, time.Minute) 2825 err := appStateCache.SetAppResourcesTree(testApp.Name, &v1alpha1.ApplicationTree{Nodes: []v1alpha1.ResourceNode{{ 2826 ResourceRef: v1alpha1.ResourceRef{ 2827 Group: "apps", 2828 Kind: "Deployment", 2829 Name: "guestbook", 2830 Namespace: "default", 2831 }, 2832 Health: &v1alpha1.HealthStatus{ 2833 Status: health.HealthStatusDegraded, 2834 }, 2835 }}}) 2836 2837 require.NoError(t, err) 2838 2839 appServer.cache = servercache.NewCache(appStateCache, time.Minute, time.Minute) 2840 2841 appServer.inferResourcesStatusHealth(testApp) 2842 2843 assert.Equal(t, health.HealthStatusDegraded, testApp.Status.Resources[0].Health.Status) 2844 assert.Nil(t, testApp.Status.Resources[1].Health) 2845 } 2846 2847 func TestInferResourcesStatusHealthWithAppInAnyNamespace(t *testing.T) { 2848 cacheClient := cache.NewCache(cache.NewInMemoryCache(1 * time.Hour)) 2849 2850 testApp := newTestApp() 2851 testApp.Namespace = "otherNamespace" 2852 testApp.Status.ResourceHealthSource = v1alpha1.ResourceHealthLocationAppTree 2853 testApp.Status.Resources = []v1alpha1.ResourceStatus{{ 2854 Group: "apps", 2855 Kind: "Deployment", 2856 Name: "guestbook", 2857 Namespace: "otherNamespace", 2858 }, { 2859 Group: "apps", 2860 Kind: "StatefulSet", 2861 Name: "guestbook-stateful", 2862 Namespace: "otherNamespace", 2863 }} 2864 appServer := newTestAppServer(t, testApp) 2865 appStateCache := appstate.NewCache(cacheClient, time.Minute) 2866 err := appStateCache.SetAppResourcesTree("otherNamespace"+"_"+testApp.Name, &v1alpha1.ApplicationTree{Nodes: []v1alpha1.ResourceNode{{ 2867 ResourceRef: v1alpha1.ResourceRef{ 2868 Group: "apps", 2869 Kind: "Deployment", 2870 Name: "guestbook", 2871 Namespace: "otherNamespace", 2872 }, 2873 Health: &v1alpha1.HealthStatus{ 2874 Status: health.HealthStatusDegraded, 2875 }, 2876 }}}) 2877 2878 require.NoError(t, err) 2879 2880 appServer.cache = servercache.NewCache(appStateCache, time.Minute, time.Minute) 2881 2882 appServer.inferResourcesStatusHealth(testApp) 2883 2884 assert.Equal(t, health.HealthStatusDegraded, testApp.Status.Resources[0].Health.Status) 2885 assert.Nil(t, testApp.Status.Resources[1].Health) 2886 } 2887 2888 func TestRunNewStyleResourceAction(t *testing.T) { 2889 cacheClient := cache.NewCache(cache.NewInMemoryCache(1 * time.Hour)) 2890 2891 group := "batch" 2892 kind := "CronJob" 2893 version := "v1" 2894 resourceName := "my-cron-job" 2895 namespace := testNamespace 2896 action := "create-job" 2897 uid := "1" 2898 2899 resources := []v1alpha1.ResourceStatus{{ 2900 Group: group, 2901 Kind: kind, 2902 Name: resourceName, 2903 Namespace: testNamespace, 2904 Version: version, 2905 }} 2906 2907 appStateCache := appstate.NewCache(cacheClient, time.Minute) 2908 2909 nodes := []v1alpha1.ResourceNode{{ 2910 ResourceRef: v1alpha1.ResourceRef{ 2911 Group: group, 2912 Kind: kind, 2913 Version: version, 2914 Name: resourceName, 2915 Namespace: testNamespace, 2916 UID: uid, 2917 }, 2918 }} 2919 2920 createJobDenyingProj := &v1alpha1.AppProject{ 2921 ObjectMeta: metav1.ObjectMeta{Name: "createJobDenyingProj", Namespace: "default"}, 2922 Spec: v1alpha1.AppProjectSpec{ 2923 SourceRepos: []string{"*"}, 2924 Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 2925 NamespaceResourceWhitelist: []metav1.GroupKind{{Group: "never", Kind: "mind"}}, 2926 }, 2927 } 2928 2929 cronJob := k8sbatchv1.CronJob{ 2930 TypeMeta: metav1.TypeMeta{ 2931 APIVersion: "batch/v1", 2932 Kind: "CronJob", 2933 }, 2934 ObjectMeta: metav1.ObjectMeta{ 2935 Name: "my-cron-job", 2936 Namespace: testNamespace, 2937 Labels: map[string]string{ 2938 "some": "label", 2939 }, 2940 }, 2941 Spec: k8sbatchv1.CronJobSpec{ 2942 Schedule: "* * * * *", 2943 JobTemplate: k8sbatchv1.JobTemplateSpec{ 2944 Spec: k8sbatchv1.JobSpec{ 2945 Template: corev1.PodTemplateSpec{ 2946 Spec: corev1.PodSpec{ 2947 Containers: []corev1.Container{ 2948 { 2949 Name: "hello", 2950 Image: "busybox:1.28", 2951 ImagePullPolicy: "IfNotPresent", 2952 Command: []string{"/bin/sh", "-c", "date; echo Hello from the Kubernetes cluster"}, 2953 }, 2954 }, 2955 RestartPolicy: corev1.RestartPolicyOnFailure, 2956 }, 2957 }, 2958 }, 2959 }, 2960 }, 2961 } 2962 2963 t.Run("CreateOperationNotPermitted", func(t *testing.T) { 2964 testApp := newTestApp() 2965 testApp.Spec.Project = "createJobDenyingProj" 2966 testApp.Status.ResourceHealthSource = v1alpha1.ResourceHealthLocationAppTree 2967 testApp.Status.Resources = resources 2968 2969 appServer := newTestAppServer(t, testApp, createJobDenyingProj, kube.MustToUnstructured(&cronJob)) 2970 appServer.cache = servercache.NewCache(appStateCache, time.Minute, time.Minute) 2971 2972 err := appStateCache.SetAppResourcesTree(testApp.Name, &v1alpha1.ApplicationTree{Nodes: nodes}) 2973 require.NoError(t, err) 2974 2975 appResponse, runErr := appServer.RunResourceActionV2(t.Context(), &application.ResourceActionRunRequestV2{ 2976 Name: &testApp.Name, 2977 Namespace: &namespace, 2978 Action: &action, 2979 AppNamespace: &testApp.Namespace, 2980 ResourceName: &resourceName, 2981 Version: &version, 2982 Group: &group, 2983 Kind: &kind, 2984 }) 2985 2986 require.ErrorContains(t, runErr, "is not permitted to manage") 2987 assert.Nil(t, appResponse) 2988 }) 2989 2990 t.Run("CreateOperationPermitted", func(t *testing.T) { 2991 testApp := newTestApp() 2992 testApp.Status.ResourceHealthSource = v1alpha1.ResourceHealthLocationAppTree 2993 testApp.Status.Resources = resources 2994 2995 appServer := newTestAppServer(t, testApp, kube.MustToUnstructured(&cronJob)) 2996 appServer.cache = servercache.NewCache(appStateCache, time.Minute, time.Minute) 2997 2998 err := appStateCache.SetAppResourcesTree(testApp.Name, &v1alpha1.ApplicationTree{Nodes: nodes}) 2999 require.NoError(t, err) 3000 3001 appResponse, runErr := appServer.RunResourceActionV2(t.Context(), &application.ResourceActionRunRequestV2{ 3002 Name: &testApp.Name, 3003 Namespace: &namespace, 3004 Action: &action, 3005 AppNamespace: &testApp.Namespace, 3006 ResourceName: &resourceName, 3007 Version: &version, 3008 Group: &group, 3009 Kind: &kind, 3010 }) 3011 3012 require.NoError(t, runErr) 3013 assert.NotNil(t, appResponse) 3014 }) 3015 } 3016 3017 func TestRunOldStyleResourceAction(t *testing.T) { 3018 cacheClient := cache.NewCache(cache.NewInMemoryCache(1 * time.Hour)) 3019 3020 group := "apps" 3021 kind := "Deployment" 3022 version := "v1" 3023 resourceName := "nginx-deploy" 3024 namespace := testNamespace 3025 action := "pause" 3026 uid := "2" 3027 3028 resources := []v1alpha1.ResourceStatus{{ 3029 Group: group, 3030 Kind: kind, 3031 Name: resourceName, 3032 Namespace: testNamespace, 3033 Version: version, 3034 }} 3035 3036 appStateCache := appstate.NewCache(cacheClient, time.Minute) 3037 3038 nodes := []v1alpha1.ResourceNode{{ 3039 ResourceRef: v1alpha1.ResourceRef{ 3040 Group: group, 3041 Kind: kind, 3042 Version: version, 3043 Name: resourceName, 3044 Namespace: testNamespace, 3045 UID: uid, 3046 }, 3047 }} 3048 3049 deployment := appsv1.Deployment{ 3050 TypeMeta: metav1.TypeMeta{ 3051 APIVersion: "apps/v1", 3052 Kind: "Deployment", 3053 }, 3054 ObjectMeta: metav1.ObjectMeta{ 3055 Name: "nginx-deploy", 3056 Namespace: testNamespace, 3057 }, 3058 } 3059 3060 t.Run("DefaultPatchOperation", func(t *testing.T) { 3061 testApp := newTestApp() 3062 testApp.Status.ResourceHealthSource = v1alpha1.ResourceHealthLocationAppTree 3063 testApp.Status.Resources = resources 3064 3065 // appServer := newTestAppServer(t, testApp, returnDeployment()) 3066 appServer := newTestAppServer(t, testApp, kube.MustToUnstructured(&deployment)) 3067 appServer.cache = servercache.NewCache(appStateCache, time.Minute, time.Minute) 3068 3069 err := appStateCache.SetAppResourcesTree(testApp.Name, &v1alpha1.ApplicationTree{Nodes: nodes}) 3070 require.NoError(t, err) 3071 3072 appResponse, runErr := appServer.RunResourceActionV2(t.Context(), &application.ResourceActionRunRequestV2{ 3073 Name: &testApp.Name, 3074 Namespace: &namespace, 3075 Action: &action, 3076 AppNamespace: &testApp.Namespace, 3077 ResourceName: &resourceName, 3078 Version: &version, 3079 Group: &group, 3080 Kind: &kind, 3081 }) 3082 3083 require.NoError(t, runErr) 3084 assert.NotNil(t, appResponse) 3085 }) 3086 } 3087 3088 func TestIsApplicationPermitted(t *testing.T) { 3089 t.Run("Incorrect project", func(t *testing.T) { 3090 testApp := newTestApp() 3091 appServer := newTestAppServer(t, testApp) 3092 projects := map[string]bool{"test-app": false} 3093 permitted := appServer.isApplicationPermitted(labels.Everything(), 0, nil, "test", "default", projects, *testApp) 3094 assert.False(t, permitted) 3095 }) 3096 3097 t.Run("Version is incorrect", func(t *testing.T) { 3098 testApp := newTestApp() 3099 appServer := newTestAppServer(t, testApp) 3100 minVersion := 100000 3101 testApp.ResourceVersion = strconv.Itoa(minVersion - 1) 3102 permitted := appServer.isApplicationPermitted(labels.Everything(), minVersion, nil, "test", "default", nil, *testApp) 3103 assert.False(t, permitted) 3104 }) 3105 3106 t.Run("Application name is incorrect", func(t *testing.T) { 3107 testApp := newTestApp() 3108 appServer := newTestAppServer(t, testApp) 3109 appName := "test" 3110 permitted := appServer.isApplicationPermitted(labels.Everything(), 0, nil, appName, "default", nil, *testApp) 3111 assert.False(t, permitted) 3112 }) 3113 3114 t.Run("Application namespace is incorrect", func(t *testing.T) { 3115 testApp := newTestApp() 3116 appServer := newTestAppServer(t, testApp) 3117 permitted := appServer.isApplicationPermitted(labels.Everything(), 0, nil, testApp.Name, "demo", nil, *testApp) 3118 assert.False(t, permitted) 3119 }) 3120 3121 t.Run("Application is not part of enabled namespace", func(t *testing.T) { 3122 testApp := newTestApp() 3123 appServer := newTestAppServer(t, testApp) 3124 appServer.ns = "server-ns" 3125 appServer.enabledNamespaces = []string{"demo"} 3126 permitted := appServer.isApplicationPermitted(labels.Everything(), 0, nil, testApp.Name, testApp.Namespace, nil, *testApp) 3127 assert.False(t, permitted) 3128 }) 3129 3130 t.Run("Application is part of enabled namespace", func(t *testing.T) { 3131 testApp := newTestApp() 3132 appServer := newTestAppServer(t, testApp) 3133 appServer.ns = "server-ns" 3134 appServer.enabledNamespaces = []string{testApp.Namespace} 3135 permitted := appServer.isApplicationPermitted(labels.Everything(), 0, nil, testApp.Name, testApp.Namespace, nil, *testApp) 3136 assert.True(t, permitted) 3137 }) 3138 } 3139 3140 func TestAppNamespaceRestrictions(t *testing.T) { 3141 t.Parallel() 3142 3143 t.Run("List applications in controller namespace", func(t *testing.T) { 3144 t.Parallel() 3145 testApp := newTestApp() 3146 appServer := newTestAppServer(t, testApp) 3147 apps, err := appServer.List(t.Context(), &application.ApplicationQuery{}) 3148 require.NoError(t, err) 3149 require.Len(t, apps.Items, 1) 3150 }) 3151 3152 t.Run("List applications with non-allowed apps existing", func(t *testing.T) { 3153 t.Parallel() 3154 testApp1 := newTestApp() 3155 testApp1.Namespace = "argocd-1" 3156 appServer := newTestAppServer(t, testApp1) 3157 apps, err := appServer.List(t.Context(), &application.ApplicationQuery{}) 3158 require.NoError(t, err) 3159 require.Empty(t, apps.Items) 3160 }) 3161 3162 t.Run("List applications with non-allowed apps existing and explicit ns request", func(t *testing.T) { 3163 t.Parallel() 3164 testApp1 := newTestApp() 3165 testApp2 := newTestApp() 3166 testApp2.Namespace = "argocd-1" 3167 appServer := newTestAppServer(t, testApp1, testApp2) 3168 apps, err := appServer.List(t.Context(), &application.ApplicationQuery{AppNamespace: ptr.To("argocd-1")}) 3169 require.NoError(t, err) 3170 require.Empty(t, apps.Items) 3171 }) 3172 3173 t.Run("List applications with allowed apps in other namespaces", func(t *testing.T) { 3174 t.Parallel() 3175 testApp1 := newTestApp() 3176 testApp1.Namespace = "argocd-1" 3177 appServer := newTestAppServer(t, testApp1) 3178 appServer.enabledNamespaces = []string{"argocd-1"} 3179 apps, err := appServer.List(t.Context(), &application.ApplicationQuery{}) 3180 require.NoError(t, err) 3181 require.Len(t, apps.Items, 1) 3182 }) 3183 3184 t.Run("Get application in control plane namespace", func(t *testing.T) { 3185 t.Parallel() 3186 testApp := newTestApp() 3187 appServer := newTestAppServer(t, testApp) 3188 app, err := appServer.Get(t.Context(), &application.ApplicationQuery{ 3189 Name: ptr.To("test-app"), 3190 }) 3191 require.NoError(t, err) 3192 assert.Equal(t, "test-app", app.GetName()) 3193 }) 3194 t.Run("Get application in other namespace when forbidden", func(t *testing.T) { 3195 t.Parallel() 3196 testApp := newTestApp() 3197 testApp.Namespace = "argocd-1" 3198 appServer := newTestAppServer(t, testApp) 3199 app, err := appServer.Get(t.Context(), &application.ApplicationQuery{ 3200 Name: ptr.To("test-app"), 3201 AppNamespace: ptr.To("argocd-1"), 3202 }) 3203 require.ErrorContains(t, err, "permission denied") 3204 require.Nil(t, app) 3205 }) 3206 t.Run("Get application in other namespace when allowed", func(t *testing.T) { 3207 t.Parallel() 3208 testApp := newTestApp() 3209 testApp.Namespace = "argocd-1" 3210 testApp.Spec.Project = "other-ns" 3211 otherNsProj := &v1alpha1.AppProject{ 3212 ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, 3213 Spec: v1alpha1.AppProjectSpec{ 3214 SourceRepos: []string{"*"}, 3215 Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 3216 SourceNamespaces: []string{"argocd-1"}, 3217 }, 3218 } 3219 appServer := newTestAppServer(t, testApp, otherNsProj) 3220 appServer.enabledNamespaces = []string{"argocd-1"} 3221 app, err := appServer.Get(t.Context(), &application.ApplicationQuery{ 3222 Name: ptr.To("test-app"), 3223 AppNamespace: ptr.To("argocd-1"), 3224 }) 3225 require.NoError(t, err) 3226 require.NotNil(t, app) 3227 require.Equal(t, "argocd-1", app.Namespace) 3228 require.Equal(t, "test-app", app.Name) 3229 }) 3230 t.Run("Get application in other namespace when project is not allowed", func(t *testing.T) { 3231 t.Parallel() 3232 testApp := newTestApp() 3233 testApp.Namespace = "argocd-1" 3234 testApp.Spec.Project = "other-ns" 3235 otherNsProj := &v1alpha1.AppProject{ 3236 ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, 3237 Spec: v1alpha1.AppProjectSpec{ 3238 SourceRepos: []string{"*"}, 3239 Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 3240 SourceNamespaces: []string{"argocd-2"}, 3241 }, 3242 } 3243 appServer := newTestAppServer(t, testApp, otherNsProj) 3244 appServer.enabledNamespaces = []string{"argocd-1"} 3245 app, err := appServer.Get(t.Context(), &application.ApplicationQuery{ 3246 Name: ptr.To("test-app"), 3247 AppNamespace: ptr.To("argocd-1"), 3248 }) 3249 require.Error(t, err) 3250 require.Nil(t, app) 3251 require.ErrorContains(t, err, "app is not allowed in project") 3252 }) 3253 t.Run("Create application in other namespace when allowed", func(t *testing.T) { 3254 t.Parallel() 3255 testApp := newTestApp() 3256 testApp.Namespace = "argocd-1" 3257 testApp.Spec.Project = "other-ns" 3258 otherNsProj := &v1alpha1.AppProject{ 3259 ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, 3260 Spec: v1alpha1.AppProjectSpec{ 3261 SourceRepos: []string{"*"}, 3262 Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 3263 SourceNamespaces: []string{"argocd-1"}, 3264 }, 3265 } 3266 appServer := newTestAppServer(t, otherNsProj) 3267 appServer.enabledNamespaces = []string{"argocd-1"} 3268 app, err := appServer.Create(t.Context(), &application.ApplicationCreateRequest{ 3269 Application: testApp, 3270 }) 3271 require.NoError(t, err) 3272 require.NotNil(t, app) 3273 assert.Equal(t, "test-app", app.Name) 3274 assert.Equal(t, "argocd-1", app.Namespace) 3275 }) 3276 3277 t.Run("Create application in other namespace when not allowed by project", func(t *testing.T) { 3278 t.Parallel() 3279 testApp := newTestApp() 3280 testApp.Namespace = "argocd-1" 3281 testApp.Spec.Project = "other-ns" 3282 otherNsProj := &v1alpha1.AppProject{ 3283 ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, 3284 Spec: v1alpha1.AppProjectSpec{ 3285 SourceRepos: []string{"*"}, 3286 Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 3287 SourceNamespaces: []string{}, 3288 }, 3289 } 3290 appServer := newTestAppServer(t, otherNsProj) 3291 appServer.enabledNamespaces = []string{"argocd-1"} 3292 app, err := appServer.Create(t.Context(), &application.ApplicationCreateRequest{ 3293 Application: testApp, 3294 }) 3295 require.Error(t, err) 3296 require.Nil(t, app) 3297 require.ErrorContains(t, err, "app is not allowed in project") 3298 }) 3299 3300 t.Run("Create application in other namespace when not allowed by configuration", func(t *testing.T) { 3301 t.Parallel() 3302 testApp := newTestApp() 3303 testApp.Namespace = "argocd-1" 3304 testApp.Spec.Project = "other-ns" 3305 otherNsProj := &v1alpha1.AppProject{ 3306 ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, 3307 Spec: v1alpha1.AppProjectSpec{ 3308 SourceRepos: []string{"*"}, 3309 Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 3310 SourceNamespaces: []string{"argocd-1"}, 3311 }, 3312 } 3313 appServer := newTestAppServer(t, otherNsProj) 3314 appServer.enabledNamespaces = []string{"argocd-2"} 3315 app, err := appServer.Create(t.Context(), &application.ApplicationCreateRequest{ 3316 Application: testApp, 3317 }) 3318 require.Error(t, err) 3319 require.Nil(t, app) 3320 require.ErrorContains(t, err, "namespace 'argocd-1' is not permitted") 3321 }) 3322 t.Run("Get application sync window in other namespace when project is allowed", func(t *testing.T) { 3323 t.Parallel() 3324 testApp := newTestApp() 3325 testApp.Namespace = "argocd-1" 3326 testApp.Spec.Project = "other-ns" 3327 otherNsProj := &v1alpha1.AppProject{ 3328 ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, 3329 Spec: v1alpha1.AppProjectSpec{ 3330 SourceRepos: []string{"*"}, 3331 Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 3332 SourceNamespaces: []string{"argocd-1"}, 3333 }, 3334 } 3335 appServer := newTestAppServer(t, testApp, otherNsProj) 3336 appServer.enabledNamespaces = []string{"argocd-1"} 3337 active, err := appServer.GetApplicationSyncWindows(t.Context(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name, AppNamespace: &testApp.Namespace}) 3338 require.NoError(t, err) 3339 assert.Empty(t, active.ActiveWindows) 3340 }) 3341 t.Run("Get application sync window in other namespace when project is not allowed", func(t *testing.T) { 3342 t.Parallel() 3343 testApp := newTestApp() 3344 testApp.Namespace = "argocd-1" 3345 testApp.Spec.Project = "other-ns" 3346 otherNsProj := &v1alpha1.AppProject{ 3347 ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, 3348 Spec: v1alpha1.AppProjectSpec{ 3349 SourceRepos: []string{"*"}, 3350 Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 3351 SourceNamespaces: []string{"argocd-2"}, 3352 }, 3353 } 3354 appServer := newTestAppServer(t, testApp, otherNsProj) 3355 appServer.enabledNamespaces = []string{"argocd-1"} 3356 active, err := appServer.GetApplicationSyncWindows(t.Context(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name, AppNamespace: &testApp.Namespace}) 3357 require.Error(t, err) 3358 require.Nil(t, active) 3359 require.ErrorContains(t, err, "app is not allowed in project") 3360 }) 3361 t.Run("Get list of links in other namespace when project is not allowed", func(t *testing.T) { 3362 t.Parallel() 3363 testApp := newTestApp() 3364 testApp.Namespace = "argocd-1" 3365 testApp.Spec.Project = "other-ns" 3366 otherNsProj := &v1alpha1.AppProject{ 3367 ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, 3368 Spec: v1alpha1.AppProjectSpec{ 3369 SourceRepos: []string{"*"}, 3370 Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 3371 SourceNamespaces: []string{"argocd-2"}, 3372 }, 3373 } 3374 appServer := newTestAppServer(t, testApp, otherNsProj) 3375 appServer.enabledNamespaces = []string{"argocd-1"} 3376 links, err := appServer.ListLinks(t.Context(), &application.ListAppLinksRequest{ 3377 Name: ptr.To("test-app"), 3378 Namespace: ptr.To("argocd-1"), 3379 }) 3380 require.Error(t, err) 3381 require.Nil(t, links) 3382 require.ErrorContains(t, err, "app is not allowed in project") 3383 }) 3384 t.Run("Get list of links in other namespace when project is allowed", func(t *testing.T) { 3385 t.Parallel() 3386 testApp := newTestApp() 3387 testApp.Namespace = "argocd-1" 3388 testApp.Spec.Project = "other-ns" 3389 otherNsProj := &v1alpha1.AppProject{ 3390 ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, 3391 Spec: v1alpha1.AppProjectSpec{ 3392 SourceRepos: []string{"*"}, 3393 Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 3394 SourceNamespaces: []string{"argocd-1"}, 3395 }, 3396 } 3397 appServer := newTestAppServer(t, testApp, otherNsProj) 3398 appServer.enabledNamespaces = []string{"argocd-1"} 3399 links, err := appServer.ListLinks(t.Context(), &application.ListAppLinksRequest{ 3400 Name: ptr.To("test-app"), 3401 Namespace: ptr.To("argocd-1"), 3402 }) 3403 require.NoError(t, err) 3404 assert.Empty(t, links.Items) 3405 }) 3406 } 3407 3408 func TestGetAmbiguousRevision_MultiSource(t *testing.T) { 3409 app := &v1alpha1.Application{ 3410 Spec: v1alpha1.ApplicationSpec{ 3411 Sources: []v1alpha1.ApplicationSource{ 3412 { 3413 TargetRevision: "revision1", 3414 }, 3415 { 3416 TargetRevision: "revision2", 3417 }, 3418 }, 3419 }, 3420 } 3421 syncReq := &application.ApplicationSyncRequest{ 3422 SourcePositions: []int64{1, 2}, 3423 Revisions: []string{"rev1", "rev2"}, 3424 } 3425 3426 sourceIndex := 0 3427 expected := "rev1" 3428 result := getAmbiguousRevision(app, syncReq, sourceIndex) 3429 assert.Equalf(t, expected, result, "Expected ambiguous revision to be %s, but got %s", expected, result) 3430 3431 sourceIndex = 1 3432 expected = "rev2" 3433 result = getAmbiguousRevision(app, syncReq, sourceIndex) 3434 assert.Equal(t, expected, result, "Expected ambiguous revision to be %s, but got %s", expected, result) 3435 3436 // Test when app.Spec.HasMultipleSources() is false 3437 app.Spec = v1alpha1.ApplicationSpec{ 3438 Source: &v1alpha1.ApplicationSource{ 3439 TargetRevision: "revision3", 3440 }, 3441 Sources: nil, 3442 } 3443 syncReq = &application.ApplicationSyncRequest{ 3444 Revision: strToPtr("revision3"), 3445 } 3446 expected = "revision3" 3447 result = getAmbiguousRevision(app, syncReq, sourceIndex) 3448 assert.Equal(t, expected, result, "Expected ambiguous revision to be %s, but got %s", expected, result) 3449 } 3450 3451 func TestGetAmbiguousRevision_SingleSource(t *testing.T) { 3452 app := &v1alpha1.Application{ 3453 Spec: v1alpha1.ApplicationSpec{ 3454 Source: &v1alpha1.ApplicationSource{ 3455 TargetRevision: "revision1", 3456 }, 3457 }, 3458 } 3459 syncReq := &application.ApplicationSyncRequest{ 3460 Revision: strToPtr("rev1"), 3461 } 3462 3463 // Test when app.Spec.HasMultipleSources() is true 3464 sourceIndex := 1 3465 expected := "rev1" 3466 result := getAmbiguousRevision(app, syncReq, sourceIndex) 3467 assert.Equalf(t, expected, result, "Expected ambiguous revision to be %s, but got %s", expected, result) 3468 } 3469 3470 func TestServer_ResolveSourceRevisions_MultiSource(t *testing.T) { 3471 s := newTestAppServer(t) 3472 3473 ctx := t.Context() 3474 a := &v1alpha1.Application{ 3475 Spec: v1alpha1.ApplicationSpec{ 3476 Sources: []v1alpha1.ApplicationSource{ 3477 { 3478 RepoURL: "https://github.com/example/repo.git", 3479 }, 3480 }, 3481 }, 3482 } 3483 3484 syncReq := &application.ApplicationSyncRequest{ 3485 SourcePositions: []int64{1}, 3486 Revisions: []string{"HEAD"}, 3487 } 3488 3489 revision, displayRevision, sourceRevisions, displayRevisions, err := s.resolveSourceRevisions(ctx, a, syncReq) 3490 3491 require.NoError(t, err) 3492 assert.Empty(t, revision) 3493 assert.Empty(t, displayRevision) 3494 assert.Equal(t, []string{fakeResolveRevisionResponse().Revision}, sourceRevisions) 3495 assert.Equal(t, []string{fakeResolveRevisionResponse().AmbiguousRevision}, displayRevisions) 3496 } 3497 3498 func TestServer_ResolveSourceRevisions_SingleSource(t *testing.T) { 3499 s := newTestAppServer(t) 3500 3501 ctx := t.Context() 3502 a := &v1alpha1.Application{ 3503 Spec: v1alpha1.ApplicationSpec{ 3504 Source: &v1alpha1.ApplicationSource{ 3505 RepoURL: "https://github.com/example/repo.git", 3506 }, 3507 }, 3508 } 3509 3510 syncReq := &application.ApplicationSyncRequest{ 3511 Revision: strToPtr("HEAD"), 3512 } 3513 3514 revision, displayRevision, sourceRevisions, displayRevisions, err := s.resolveSourceRevisions(ctx, a, syncReq) 3515 3516 require.NoError(t, err) 3517 assert.Equal(t, fakeResolveRevisionResponse().Revision, revision) 3518 assert.Equal(t, fakeResolveRevisionResponse().AmbiguousRevision, displayRevision) 3519 assert.Equal(t, []string(nil), sourceRevisions) 3520 assert.Equal(t, []string(nil), displayRevisions) 3521 } 3522 3523 func Test_RevisionMetadata(t *testing.T) { 3524 t.Parallel() 3525 3526 singleSourceApp := newTestApp() 3527 singleSourceApp.Name = "single-source-app" 3528 singleSourceApp.Spec = v1alpha1.ApplicationSpec{ 3529 Source: &v1alpha1.ApplicationSource{ 3530 RepoURL: "https://github.com/argoproj/argocd-example-apps.git", 3531 Path: "helm-guestbook", 3532 TargetRevision: "HEAD", 3533 }, 3534 } 3535 3536 multiSourceApp := newTestApp() 3537 multiSourceApp.Name = "multi-source-app" 3538 multiSourceApp.Spec = v1alpha1.ApplicationSpec{ 3539 Sources: []v1alpha1.ApplicationSource{ 3540 { 3541 RepoURL: "https://github.com/argoproj/argocd-example-apps.git", 3542 Path: "helm-guestbook", 3543 TargetRevision: "HEAD", 3544 }, 3545 { 3546 RepoURL: "https://github.com/argoproj/argocd-example-apps.git", 3547 Path: "kustomize-guestbook", 3548 TargetRevision: "HEAD", 3549 }, 3550 }, 3551 } 3552 3553 singleSourceHistory := []v1alpha1.RevisionHistory{ 3554 { 3555 ID: 1, 3556 Source: singleSourceApp.Spec.GetSource(), 3557 Revision: "a", 3558 }, 3559 } 3560 multiSourceHistory := []v1alpha1.RevisionHistory{ 3561 { 3562 ID: 1, 3563 Sources: multiSourceApp.Spec.GetSources(), 3564 Revisions: []string{"a", "b"}, 3565 }, 3566 } 3567 3568 testCases := []struct { 3569 name string 3570 multiSource bool 3571 history *struct { 3572 matchesSourceType bool 3573 } 3574 sourceIndex *int32 3575 versionId *int32 3576 expectErrorContains *string 3577 }{ 3578 { 3579 name: "single-source app without history, no source index, no version ID", 3580 multiSource: false, 3581 }, 3582 { 3583 name: "single-source app without history, no source index, missing version ID", 3584 multiSource: false, 3585 versionId: ptr.To(int32(999)), 3586 expectErrorContains: ptr.To("the app has no history"), 3587 }, 3588 { 3589 name: "single source app without history, present source index, no version ID", 3590 multiSource: false, 3591 sourceIndex: ptr.To(int32(0)), 3592 }, 3593 { 3594 name: "single source app without history, invalid source index, no version ID", 3595 multiSource: false, 3596 sourceIndex: ptr.To(int32(999)), 3597 expectErrorContains: ptr.To("source index 999 not found"), 3598 }, 3599 { 3600 name: "single source app with matching history, no source index, no version ID", 3601 multiSource: false, 3602 history: &struct{ matchesSourceType bool }{true}, 3603 }, 3604 { 3605 name: "single source app with matching history, no source index, missing version ID", 3606 multiSource: false, 3607 history: &struct{ matchesSourceType bool }{true}, 3608 versionId: ptr.To(int32(999)), 3609 expectErrorContains: ptr.To("history not found for version ID 999"), 3610 }, 3611 { 3612 name: "single source app with matching history, no source index, present version ID", 3613 multiSource: false, 3614 history: &struct{ matchesSourceType bool }{true}, 3615 versionId: ptr.To(int32(1)), 3616 }, 3617 { 3618 name: "single source app with multi-source history, no source index, no version ID", 3619 multiSource: false, 3620 history: &struct{ matchesSourceType bool }{false}, 3621 }, 3622 { 3623 name: "single source app with multi-source history, no source index, missing version ID", 3624 multiSource: false, 3625 history: &struct{ matchesSourceType bool }{false}, 3626 versionId: ptr.To(int32(999)), 3627 expectErrorContains: ptr.To("history not found for version ID 999"), 3628 }, 3629 { 3630 name: "single source app with multi-source history, no source index, present version ID", 3631 multiSource: false, 3632 history: &struct{ matchesSourceType bool }{false}, 3633 versionId: ptr.To(int32(1)), 3634 }, 3635 { 3636 name: "single-source app with multi-source history, source index 1, no version ID", 3637 multiSource: false, 3638 sourceIndex: ptr.To(int32(1)), 3639 history: &struct{ matchesSourceType bool }{false}, 3640 // Since the user requested source index 1, but no version ID, we'll get an error when looking at the live 3641 // source, because the live source is single-source. 3642 expectErrorContains: ptr.To("there is only 1 source"), 3643 }, 3644 { 3645 name: "single-source app with multi-source history, invalid source index, no version ID", 3646 multiSource: false, 3647 sourceIndex: ptr.To(int32(999)), 3648 history: &struct{ matchesSourceType bool }{false}, 3649 expectErrorContains: ptr.To("source index 999 not found"), 3650 }, 3651 { 3652 name: "single-source app with multi-source history, valid source index, present version ID", 3653 multiSource: false, 3654 sourceIndex: ptr.To(int32(1)), 3655 history: &struct{ matchesSourceType bool }{false}, 3656 versionId: ptr.To(int32(1)), 3657 }, 3658 { 3659 name: "multi-source app without history, no source index, no version ID", 3660 multiSource: true, 3661 }, 3662 { 3663 name: "multi-source app without history, no source index, missing version ID", 3664 multiSource: true, 3665 versionId: ptr.To(int32(999)), 3666 expectErrorContains: ptr.To("the app has no history"), 3667 }, 3668 { 3669 name: "multi-source app without history, present source index, no version ID", 3670 multiSource: true, 3671 sourceIndex: ptr.To(int32(1)), 3672 }, 3673 { 3674 name: "multi-source app without history, invalid source index, no version ID", 3675 multiSource: true, 3676 sourceIndex: ptr.To(int32(999)), 3677 expectErrorContains: ptr.To("source index 999 not found"), 3678 }, 3679 { 3680 name: "multi-source app with matching history, no source index, no version ID", 3681 multiSource: true, 3682 history: &struct{ matchesSourceType bool }{true}, 3683 }, 3684 { 3685 name: "multi-source app with matching history, no source index, missing version ID", 3686 multiSource: true, 3687 history: &struct{ matchesSourceType bool }{true}, 3688 versionId: ptr.To(int32(999)), 3689 expectErrorContains: ptr.To("history not found for version ID 999"), 3690 }, 3691 { 3692 name: "multi-source app with matching history, no source index, present version ID", 3693 multiSource: true, 3694 history: &struct{ matchesSourceType bool }{true}, 3695 versionId: ptr.To(int32(1)), 3696 }, 3697 { 3698 name: "multi-source app with single-source history, no source index, no version ID", 3699 multiSource: true, 3700 history: &struct{ matchesSourceType bool }{false}, 3701 }, 3702 { 3703 name: "multi-source app with single-source history, no source index, missing version ID", 3704 multiSource: true, 3705 history: &struct{ matchesSourceType bool }{false}, 3706 versionId: ptr.To(int32(999)), 3707 expectErrorContains: ptr.To("history not found for version ID 999"), 3708 }, 3709 { 3710 name: "multi-source app with single-source history, no source index, present version ID", 3711 multiSource: true, 3712 history: &struct{ matchesSourceType bool }{false}, 3713 versionId: ptr.To(int32(1)), 3714 }, 3715 { 3716 name: "multi-source app with single-source history, source index 1, no version ID", 3717 multiSource: true, 3718 sourceIndex: ptr.To(int32(1)), 3719 history: &struct{ matchesSourceType bool }{false}, 3720 }, 3721 { 3722 name: "multi-source app with single-source history, invalid source index, no version ID", 3723 multiSource: true, 3724 sourceIndex: ptr.To(int32(999)), 3725 history: &struct{ matchesSourceType bool }{false}, 3726 expectErrorContains: ptr.To("source index 999 not found"), 3727 }, 3728 { 3729 name: "multi-source app with single-source history, valid source index, present version ID", 3730 multiSource: true, 3731 sourceIndex: ptr.To(int32(0)), 3732 history: &struct{ matchesSourceType bool }{false}, 3733 versionId: ptr.To(int32(1)), 3734 }, 3735 { 3736 name: "multi-source app with single-source history, source index 1, present version ID", 3737 multiSource: true, 3738 sourceIndex: ptr.To(int32(1)), 3739 history: &struct{ matchesSourceType bool }{false}, 3740 versionId: ptr.To(int32(1)), 3741 expectErrorContains: ptr.To("source index 1 not found"), 3742 }, 3743 } 3744 3745 for _, tc := range testCases { 3746 tcc := tc 3747 t.Run(tcc.name, func(t *testing.T) { 3748 t.Parallel() 3749 3750 app := singleSourceApp.DeepCopy() 3751 if tcc.multiSource { 3752 app = multiSourceApp.DeepCopy() 3753 } 3754 if tcc.history != nil { 3755 if tcc.history.matchesSourceType { 3756 if tcc.multiSource { 3757 app.Status.History = multiSourceHistory 3758 } else { 3759 app.Status.History = singleSourceHistory 3760 } 3761 } else { 3762 if tcc.multiSource { 3763 app.Status.History = singleSourceHistory 3764 } else { 3765 app.Status.History = multiSourceHistory 3766 } 3767 } 3768 } 3769 3770 s := newTestAppServer(t, app) 3771 3772 request := &application.RevisionMetadataQuery{ 3773 Name: ptr.To(app.Name), 3774 Revision: ptr.To("HEAD"), 3775 SourceIndex: tcc.sourceIndex, 3776 VersionId: tcc.versionId, 3777 } 3778 3779 _, err := s.RevisionMetadata(t.Context(), request) 3780 if tcc.expectErrorContains != nil { 3781 require.ErrorContains(t, err, *tcc.expectErrorContains) 3782 } else { 3783 require.NoError(t, err) 3784 } 3785 }) 3786 } 3787 } 3788 3789 func Test_DeepCopyInformers(t *testing.T) { 3790 t.Parallel() 3791 3792 namespace := "test-namespace" 3793 var ro []runtime.Object 3794 appOne := newTestApp(func(app *v1alpha1.Application) { 3795 app.Name = "appOne" 3796 app.Namespace = namespace 3797 app.Spec = v1alpha1.ApplicationSpec{} 3798 }) 3799 appTwo := newTestApp(func(app *v1alpha1.Application) { 3800 app.Name = "appTwo" 3801 app.Namespace = namespace 3802 app.Spec = v1alpha1.ApplicationSpec{} 3803 }) 3804 appThree := newTestApp(func(app *v1alpha1.Application) { 3805 app.Name = "appThree" 3806 app.Namespace = namespace 3807 app.Spec = v1alpha1.ApplicationSpec{} 3808 }) 3809 ro = append(ro, appOne, appTwo, appThree) 3810 appls := []v1alpha1.Application{*appOne, *appTwo, *appThree} 3811 3812 appSetOne := &v1alpha1.ApplicationSet{ 3813 ObjectMeta: metav1.ObjectMeta{Name: "appSetOne", Namespace: namespace}, 3814 Spec: v1alpha1.ApplicationSetSpec{}, 3815 } 3816 appSetTwo := &v1alpha1.ApplicationSet{ 3817 ObjectMeta: metav1.ObjectMeta{Name: "appSetTwo", Namespace: namespace}, 3818 Spec: v1alpha1.ApplicationSetSpec{}, 3819 } 3820 appSetThree := &v1alpha1.ApplicationSet{ 3821 ObjectMeta: metav1.ObjectMeta{Name: "appSetThree", Namespace: namespace}, 3822 Spec: v1alpha1.ApplicationSetSpec{}, 3823 } 3824 ro = append(ro, appSetOne, appSetTwo, appSetThree) 3825 appSets := []v1alpha1.ApplicationSet{*appSetOne, *appSetTwo, *appSetThree} 3826 3827 appProjects := createAppProject("projOne", "projTwo", "projThree") 3828 for i := range appProjects { 3829 ro = append(ro, &appProjects[i]) 3830 } 3831 3832 s := newTestAppServer(t, ro...) 3833 3834 appList, err := s.appclientset.ArgoprojV1alpha1().Applications(namespace).List(t.Context(), metav1.ListOptions{}) 3835 require.NoError(t, err) 3836 assert.ElementsMatch(t, appls, appList.Items) 3837 sAppList := appList.Items 3838 slices.SortFunc(sAppList, func(a, b v1alpha1.Application) int { 3839 return strings.Compare(a.Name, b.Name) 3840 }) 3841 slices.SortFunc(appls, func(a, b v1alpha1.Application) int { 3842 return strings.Compare(a.Name, b.Name) 3843 }) 3844 // ensure there is a deep copy 3845 for i := range appls { 3846 assert.NotSame(t, &appls[i], &sAppList[i]) 3847 assert.NotSame(t, &appls[i].Spec, &sAppList[i].Spec) 3848 a, err := s.appclientset.ArgoprojV1alpha1().Applications(namespace).Get(t.Context(), sAppList[i].Name, metav1.GetOptions{}) 3849 require.NoError(t, err) 3850 assert.NotSame(t, a, &sAppList[i]) 3851 } 3852 3853 appSetList, err := s.appclientset.ArgoprojV1alpha1().ApplicationSets(namespace).List(t.Context(), metav1.ListOptions{}) 3854 require.NoError(t, err) 3855 assert.ElementsMatch(t, appSets, appSetList.Items) 3856 sAppSetList := appSetList.Items 3857 slices.SortFunc(sAppSetList, func(a, b v1alpha1.ApplicationSet) int { 3858 return strings.Compare(a.Name, b.Name) 3859 }) 3860 slices.SortFunc(appSets, func(a, b v1alpha1.ApplicationSet) int { 3861 return strings.Compare(a.Name, b.Name) 3862 }) 3863 for i := range appSets { 3864 assert.NotSame(t, &appSets[i], &sAppSetList[i]) 3865 assert.NotSame(t, &appSets[i].Spec, &sAppSetList[i].Spec) 3866 a, err := s.appclientset.ArgoprojV1alpha1().ApplicationSets(namespace).Get(t.Context(), 3867 sAppSetList[i].Name, metav1.GetOptions{}) 3868 require.NoError(t, err) 3869 assert.NotSame(t, a, &sAppSetList[i]) 3870 } 3871 3872 projList, err := s.appclientset.ArgoprojV1alpha1().AppProjects("deep-copy-ns").List(t.Context(), metav1.ListOptions{}) 3873 require.NoError(t, err) 3874 assert.ElementsMatch(t, appProjects, projList.Items) 3875 spList := projList.Items 3876 slices.SortFunc(spList, func(a, b v1alpha1.AppProject) int { 3877 return strings.Compare(a.Name, b.Name) 3878 }) 3879 slices.SortFunc(appProjects, func(a, b v1alpha1.AppProject) int { 3880 return strings.Compare(a.Name, b.Name) 3881 }) 3882 for i := range appProjects { 3883 assert.NotSame(t, &appProjects[i], &spList[i]) 3884 assert.NotSame(t, &appProjects[i].Spec, &spList[i].Spec) 3885 p, err := s.appclientset.ArgoprojV1alpha1().AppProjects("deep-copy-ns").Get(t.Context(), 3886 spList[i].Name, metav1.GetOptions{}) 3887 require.NoError(t, err) 3888 assert.NotSame(t, p, &spList[i]) 3889 } 3890 } 3891 3892 func TestServerSideDiff(t *testing.T) { 3893 // Create test projects (avoid "default" which is already created by newTestAppServerWithEnforcerConfigure) 3894 testProj := &v1alpha1.AppProject{ 3895 ObjectMeta: metav1.ObjectMeta{Name: "test-project", Namespace: testNamespace}, 3896 Spec: v1alpha1.AppProjectSpec{ 3897 SourceRepos: []string{"*"}, 3898 Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 3899 }, 3900 } 3901 3902 forbiddenProj := &v1alpha1.AppProject{ 3903 ObjectMeta: metav1.ObjectMeta{Name: "forbidden-project", Namespace: testNamespace}, 3904 Spec: v1alpha1.AppProjectSpec{ 3905 SourceRepos: []string{"*"}, 3906 Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 3907 }, 3908 } 3909 3910 // Create test applications that will exist in the server 3911 testApp := newTestApp(func(app *v1alpha1.Application) { 3912 app.Name = "test-app" 3913 app.Namespace = testNamespace 3914 app.Spec.Project = "test-project" 3915 }) 3916 3917 forbiddenApp := newTestApp(func(app *v1alpha1.Application) { 3918 app.Name = "forbidden-app" 3919 app.Namespace = testNamespace 3920 app.Spec.Project = "forbidden-project" 3921 }) 3922 3923 appServer := newTestAppServer(t, testProj, forbiddenProj, testApp, forbiddenApp) 3924 3925 t.Run("InputValidation", func(t *testing.T) { 3926 // Test missing application name 3927 query := &application.ApplicationServerSideDiffQuery{ 3928 AppName: ptr.To(""), // Empty name instead of nil 3929 AppNamespace: ptr.To(testNamespace), 3930 Project: ptr.To("test-project"), 3931 LiveResources: []*v1alpha1.ResourceDiff{}, 3932 TargetManifests: []string{}, 3933 } 3934 3935 _, err := appServer.ServerSideDiff(t.Context(), query) 3936 require.Error(t, err) 3937 assert.Contains(t, err.Error(), "not found") 3938 3939 // Test nil application name 3940 queryNil := &application.ApplicationServerSideDiffQuery{ 3941 AppName: nil, 3942 AppNamespace: ptr.To(testNamespace), 3943 Project: ptr.To("test-project"), 3944 LiveResources: []*v1alpha1.ResourceDiff{}, 3945 TargetManifests: []string{}, 3946 } 3947 3948 _, err = appServer.ServerSideDiff(t.Context(), queryNil) 3949 assert.Error(t, err) 3950 // Should get an error when name is nil 3951 }) 3952 3953 t.Run("InvalidManifest", func(t *testing.T) { 3954 // Test error handling for malformed JSON in target manifests 3955 query := &application.ApplicationServerSideDiffQuery{ 3956 AppName: ptr.To("test-app"), 3957 AppNamespace: ptr.To(testNamespace), 3958 Project: ptr.To("test-project"), 3959 LiveResources: []*v1alpha1.ResourceDiff{}, 3960 TargetManifests: []string{`invalid json`}, 3961 } 3962 3963 _, err := appServer.ServerSideDiff(t.Context(), query) 3964 3965 // Should return error for invalid JSON 3966 require.Error(t, err) 3967 assert.Contains(t, err.Error(), "error unmarshaling target manifest") 3968 }) 3969 3970 t.Run("InvalidLiveState", func(t *testing.T) { 3971 // Test error handling for malformed JSON in live state 3972 liveResource := &v1alpha1.ResourceDiff{ 3973 Group: "apps", 3974 Kind: "Deployment", 3975 Namespace: "default", 3976 Name: "test-deployment", 3977 LiveState: `invalid json`, 3978 TargetState: "", 3979 Modified: true, 3980 } 3981 3982 query := &application.ApplicationServerSideDiffQuery{ 3983 AppName: ptr.To("test-app"), 3984 AppNamespace: ptr.To(testNamespace), 3985 Project: ptr.To("test-project"), 3986 LiveResources: []*v1alpha1.ResourceDiff{liveResource}, 3987 TargetManifests: []string{`{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"name":"test"}}`}, 3988 } 3989 3990 _, err := appServer.ServerSideDiff(t.Context(), query) 3991 3992 // Should return error for invalid JSON in live state 3993 require.Error(t, err) 3994 assert.Contains(t, err.Error(), "error unmarshaling live state") 3995 }) 3996 3997 t.Run("EmptyRequest", func(t *testing.T) { 3998 // Test with empty resources - should succeed without errors but no diffs 3999 query := &application.ApplicationServerSideDiffQuery{ 4000 AppName: ptr.To("test-app"), 4001 AppNamespace: ptr.To(testNamespace), 4002 Project: ptr.To("test-project"), 4003 LiveResources: []*v1alpha1.ResourceDiff{}, 4004 TargetManifests: []string{}, 4005 } 4006 4007 resp, err := appServer.ServerSideDiff(t.Context(), query) 4008 4009 // Should succeed with empty response 4010 require.NoError(t, err) 4011 assert.NotNil(t, resp) 4012 assert.False(t, *resp.Modified) 4013 assert.Empty(t, resp.Items) 4014 }) 4015 4016 t.Run("MissingAppPermission", func(t *testing.T) { 4017 // Test RBAC enforcement 4018 query := &application.ApplicationServerSideDiffQuery{ 4019 AppName: ptr.To("nonexistent-app"), 4020 AppNamespace: ptr.To(testNamespace), 4021 Project: ptr.To("nonexistent-project"), 4022 LiveResources: []*v1alpha1.ResourceDiff{}, 4023 TargetManifests: []string{}, 4024 } 4025 4026 _, err := appServer.ServerSideDiff(t.Context(), query) 4027 4028 // Should fail with permission error since nonexistent-app doesn't exist 4029 require.Error(t, err) 4030 assert.Contains(t, err.Error(), "application") 4031 }) 4032 }