github.com/argoproj/argo-cd/v2@v2.10.9/server/application/application_test.go (about) 1 package application 2 3 import ( 4 "context" 5 coreerrors "errors" 6 "fmt" 7 "io" 8 "strconv" 9 "sync/atomic" 10 "testing" 11 "time" 12 13 "k8s.io/apimachinery/pkg/labels" 14 15 "github.com/argoproj/gitops-engine/pkg/health" 16 synccommon "github.com/argoproj/gitops-engine/pkg/sync/common" 17 "github.com/argoproj/gitops-engine/pkg/utils/kube" 18 "github.com/argoproj/gitops-engine/pkg/utils/kube/kubetest" 19 "github.com/argoproj/pkg/sync" 20 "github.com/golang-jwt/jwt/v4" 21 "github.com/stretchr/testify/assert" 22 "github.com/stretchr/testify/mock" 23 "github.com/stretchr/testify/require" 24 "google.golang.org/grpc/codes" 25 "google.golang.org/grpc/metadata" 26 "google.golang.org/grpc/status" 27 k8sappsv1 "k8s.io/api/apps/v1" 28 k8sbatchv1 "k8s.io/api/batch/v1" 29 corev1 "k8s.io/api/core/v1" 30 v1 "k8s.io/api/core/v1" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 33 "k8s.io/apimachinery/pkg/runtime" 34 "k8s.io/apimachinery/pkg/runtime/schema" 35 "k8s.io/apimachinery/pkg/watch" 36 "k8s.io/client-go/kubernetes/fake" 37 "k8s.io/client-go/rest" 38 kubetesting "k8s.io/client-go/testing" 39 k8scache "k8s.io/client-go/tools/cache" 40 "k8s.io/utils/pointer" 41 "sigs.k8s.io/yaml" 42 43 "github.com/argoproj/argo-cd/v2/common" 44 "github.com/argoproj/argo-cd/v2/pkg/apiclient/application" 45 appsv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 46 apps "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned/fake" 47 appinformer "github.com/argoproj/argo-cd/v2/pkg/client/informers/externalversions" 48 "github.com/argoproj/argo-cd/v2/reposerver/apiclient" 49 "github.com/argoproj/argo-cd/v2/reposerver/apiclient/mocks" 50 appmocks "github.com/argoproj/argo-cd/v2/server/application/mocks" 51 servercache "github.com/argoproj/argo-cd/v2/server/cache" 52 "github.com/argoproj/argo-cd/v2/server/rbacpolicy" 53 "github.com/argoproj/argo-cd/v2/test" 54 "github.com/argoproj/argo-cd/v2/util/argo" 55 "github.com/argoproj/argo-cd/v2/util/assets" 56 "github.com/argoproj/argo-cd/v2/util/cache" 57 cacheutil "github.com/argoproj/argo-cd/v2/util/cache" 58 "github.com/argoproj/argo-cd/v2/util/cache/appstate" 59 "github.com/argoproj/argo-cd/v2/util/db" 60 "github.com/argoproj/argo-cd/v2/util/errors" 61 "github.com/argoproj/argo-cd/v2/util/grpc" 62 "github.com/argoproj/argo-cd/v2/util/rbac" 63 "github.com/argoproj/argo-cd/v2/util/settings" 64 ) 65 66 const ( 67 testNamespace = "default" 68 fakeRepoURL = "https://git.com/repo.git" 69 ) 70 71 func fakeRepo() *appsv1.Repository { 72 return &appsv1.Repository{ 73 Repo: fakeRepoURL, 74 } 75 } 76 77 func fakeCluster() *appsv1.Cluster { 78 return &appsv1.Cluster{ 79 Server: "https://cluster-api.example.com", 80 Name: "fake-cluster", 81 Config: appsv1.ClusterConfig{}, 82 } 83 } 84 85 func fakeAppList() *apiclient.AppList { 86 return &apiclient.AppList{ 87 Apps: map[string]string{ 88 "some/path": "Ksonnet", 89 }, 90 } 91 } 92 93 func fakeResolveRevisionResponse() *apiclient.ResolveRevisionResponse { 94 return &apiclient.ResolveRevisionResponse{ 95 Revision: "f9ba9e98119bf8c1176fbd65dbae26a71d044add", 96 AmbiguousRevision: "HEAD (f9ba9e98119bf8c1176fbd65dbae26a71d044add)", 97 } 98 } 99 100 func fakeResolveRevisionResponseHelm() *apiclient.ResolveRevisionResponse { 101 return &apiclient.ResolveRevisionResponse{ 102 Revision: "0.7.*", 103 AmbiguousRevision: "0.7.* (0.7.2)", 104 } 105 } 106 107 func fakeRepoServerClient(isHelm bool) *mocks.RepoServerServiceClient { 108 mockRepoServiceClient := mocks.RepoServerServiceClient{} 109 mockRepoServiceClient.On("ListApps", mock.Anything, mock.Anything).Return(fakeAppList(), nil) 110 mockRepoServiceClient.On("GenerateManifest", mock.Anything, mock.Anything).Return(&apiclient.ManifestResponse{}, nil) 111 mockRepoServiceClient.On("GetAppDetails", mock.Anything, mock.Anything).Return(&apiclient.RepoAppDetailsResponse{}, nil) 112 mockRepoServiceClient.On("TestRepository", mock.Anything, mock.Anything).Return(&apiclient.TestRepositoryResponse{}, nil) 113 mockRepoServiceClient.On("GetRevisionMetadata", mock.Anything, mock.Anything).Return(&appsv1.RevisionMetadata{}, nil) 114 mockWithFilesClient := &mocks.RepoServerService_GenerateManifestWithFilesClient{} 115 mockWithFilesClient.On("Send", mock.Anything).Return(nil) 116 mockWithFilesClient.On("CloseAndRecv").Return(&apiclient.ManifestResponse{}, nil) 117 mockRepoServiceClient.On("GenerateManifestWithFiles", mock.Anything, mock.Anything).Return(mockWithFilesClient, nil) 118 mockRepoServiceClient.On("GetRevisionChartDetails", mock.Anything, mock.Anything).Return(&appsv1.ChartDetails{}, nil) 119 120 if isHelm { 121 mockRepoServiceClient.On("ResolveRevision", mock.Anything, mock.Anything).Return(fakeResolveRevisionResponseHelm(), nil) 122 } else { 123 mockRepoServiceClient.On("ResolveRevision", mock.Anything, mock.Anything).Return(fakeResolveRevisionResponse(), nil) 124 } 125 126 return &mockRepoServiceClient 127 } 128 129 // return an ApplicationServiceServer which returns fake data 130 func newTestAppServer(t *testing.T, objects ...runtime.Object) *Server { 131 f := func(enf *rbac.Enforcer) { 132 _ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV) 133 enf.SetDefaultRole("role:admin") 134 } 135 return newTestAppServerWithEnforcerConfigure(f, t, objects...) 136 } 137 138 func newTestAppServerWithEnforcerConfigure(f func(*rbac.Enforcer), t *testing.T, objects ...runtime.Object) *Server { 139 kubeclientset := fake.NewSimpleClientset(&v1.ConfigMap{ 140 ObjectMeta: metav1.ObjectMeta{ 141 Namespace: testNamespace, 142 Name: "argocd-cm", 143 Labels: map[string]string{ 144 "app.kubernetes.io/part-of": "argocd", 145 }, 146 }, 147 }, &v1.Secret{ 148 ObjectMeta: metav1.ObjectMeta{ 149 Name: "argocd-secret", 150 Namespace: testNamespace, 151 }, 152 Data: map[string][]byte{ 153 "admin.password": []byte("test"), 154 "server.secretkey": []byte("test"), 155 }, 156 }) 157 ctx := context.Background() 158 db := db.NewDB(testNamespace, settings.NewSettingsManager(ctx, kubeclientset, testNamespace), kubeclientset) 159 _, err := db.CreateRepository(ctx, fakeRepo()) 160 errors.CheckError(err) 161 _, err = db.CreateCluster(ctx, fakeCluster()) 162 errors.CheckError(err) 163 164 mockRepoClient := &mocks.Clientset{RepoServerServiceClient: fakeRepoServerClient(false)} 165 166 defaultProj := &appsv1.AppProject{ 167 ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "default"}, 168 Spec: appsv1.AppProjectSpec{ 169 SourceRepos: []string{"*"}, 170 Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 171 }, 172 } 173 174 myProj := &appsv1.AppProject{ 175 ObjectMeta: metav1.ObjectMeta{Name: "my-proj", Namespace: "default"}, 176 Spec: appsv1.AppProjectSpec{ 177 SourceRepos: []string{"*"}, 178 Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 179 }, 180 } 181 projWithSyncWindows := &appsv1.AppProject{ 182 ObjectMeta: metav1.ObjectMeta{Name: "proj-maint", Namespace: "default"}, 183 Spec: appsv1.AppProjectSpec{ 184 SourceRepos: []string{"*"}, 185 Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 186 SyncWindows: appsv1.SyncWindows{}, 187 }, 188 } 189 matchingWindow := &appsv1.SyncWindow{ 190 Kind: "allow", 191 Schedule: "* * * * *", 192 Duration: "1h", 193 Applications: []string{"test-app"}, 194 } 195 projWithSyncWindows.Spec.SyncWindows = append(projWithSyncWindows.Spec.SyncWindows, matchingWindow) 196 197 objects = append(objects, defaultProj, myProj, projWithSyncWindows) 198 199 fakeAppsClientset := apps.NewSimpleClientset(objects...) 200 factory := appinformer.NewSharedInformerFactoryWithOptions(fakeAppsClientset, 0, appinformer.WithNamespace(""), appinformer.WithTweakListOptions(func(options *metav1.ListOptions) {})) 201 fakeProjLister := factory.Argoproj().V1alpha1().AppProjects().Lister().AppProjects(testNamespace) 202 203 enforcer := rbac.NewEnforcer(kubeclientset, testNamespace, common.ArgoCDRBACConfigMapName, nil) 204 f(enforcer) 205 enforcer.SetClaimsEnforcerFunc(rbacpolicy.NewRBACPolicyEnforcer(enforcer, fakeProjLister).EnforceClaims) 206 207 settingsMgr := settings.NewSettingsManager(ctx, kubeclientset, testNamespace) 208 209 // populate the app informer with the fake objects 210 appInformer := factory.Argoproj().V1alpha1().Applications().Informer() 211 // TODO(jessesuen): probably should return cancel function so tests can stop background informer 212 // ctx, cancel := context.WithCancel(context.Background()) 213 go appInformer.Run(ctx.Done()) 214 if !k8scache.WaitForCacheSync(ctx.Done(), appInformer.HasSynced) { 215 panic("Timed out waiting for caches to sync") 216 } 217 218 projInformer := factory.Argoproj().V1alpha1().AppProjects().Informer() 219 go projInformer.Run(ctx.Done()) 220 if !k8scache.WaitForCacheSync(ctx.Done(), projInformer.HasSynced) { 221 panic("Timed out waiting for caches to sync") 222 } 223 224 broadcaster := new(appmocks.Broadcaster) 225 broadcaster.On("Subscribe", mock.Anything, mock.Anything).Return(func() {}).Run(func(args mock.Arguments) { 226 // Simulate the broadcaster notifying the subscriber of an application update. 227 // The second parameter to Subscribe is filters. For the purposes of tests, we ignore the filters. Future tests 228 // might require implementing those. 229 go func() { 230 events := args.Get(0).(chan *appsv1.ApplicationWatchEvent) 231 for _, obj := range objects { 232 app, ok := obj.(*appsv1.Application) 233 if ok { 234 oldVersion, err := strconv.Atoi(app.ResourceVersion) 235 if err != nil { 236 oldVersion = 0 237 } 238 clonedApp := app.DeepCopy() 239 clonedApp.ResourceVersion = fmt.Sprintf("%d", oldVersion+1) 240 events <- &appsv1.ApplicationWatchEvent{Type: watch.Added, Application: *clonedApp} 241 } 242 } 243 }() 244 }) 245 broadcaster.On("OnAdd", mock.Anything).Return() 246 broadcaster.On("OnUpdate", mock.Anything, mock.Anything).Return() 247 broadcaster.On("OnDelete", mock.Anything).Return() 248 249 appStateCache := appstate.NewCache(cache.NewCache(cache.NewInMemoryCache(time.Hour)), time.Hour) 250 // pre-populate the app cache 251 for _, obj := range objects { 252 app, ok := obj.(*appsv1.Application) 253 if ok { 254 err := appStateCache.SetAppManagedResources(app.Name, []*appsv1.ResourceDiff{}) 255 require.NoError(t, err) 256 257 // Pre-populate the resource tree based on the app's resources. 258 nodes := make([]appsv1.ResourceNode, len(app.Status.Resources)) 259 for i, res := range app.Status.Resources { 260 nodes[i] = appsv1.ResourceNode{ 261 ResourceRef: appsv1.ResourceRef{ 262 Group: res.Group, 263 Kind: res.Kind, 264 Version: res.Version, 265 Name: res.Name, 266 Namespace: res.Namespace, 267 UID: "fake", 268 }, 269 } 270 } 271 err = appStateCache.SetAppResourcesTree(app.Name, &appsv1.ApplicationTree{ 272 Nodes: nodes, 273 }) 274 require.NoError(t, err) 275 } 276 } 277 appCache := servercache.NewCache(appStateCache, time.Hour, time.Hour, time.Hour) 278 279 kubectl := &kubetest.MockKubectlCmd{} 280 kubectl = kubectl.WithGetResourceFunc(func(_ context.Context, _ *rest.Config, gvk schema.GroupVersionKind, name string, namespace string) (*unstructured.Unstructured, error) { 281 for _, obj := range objects { 282 if obj.GetObjectKind().GroupVersionKind().GroupKind() == gvk.GroupKind() { 283 if obj, ok := obj.(*unstructured.Unstructured); ok && obj.GetName() == name && obj.GetNamespace() == namespace { 284 return obj, nil 285 } 286 } 287 } 288 return nil, nil 289 }) 290 291 server, _ := NewServer( 292 testNamespace, 293 kubeclientset, 294 fakeAppsClientset, 295 factory.Argoproj().V1alpha1().Applications().Lister(), 296 appInformer, 297 broadcaster, 298 mockRepoClient, 299 appCache, 300 kubectl, 301 db, 302 enforcer, 303 sync.NewKeyLock(), 304 settingsMgr, 305 projInformer, 306 []string{}, 307 ) 308 return server.(*Server) 309 } 310 311 // return an ApplicationServiceServer which returns fake data 312 func newTestAppServerWithBenchmark(b *testing.B, objects ...runtime.Object) *Server { 313 f := func(enf *rbac.Enforcer) { 314 _ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV) 315 enf.SetDefaultRole("role:admin") 316 } 317 return newTestAppServerWithEnforcerConfigureWithBenchmark(f, b, objects...) 318 } 319 320 func newTestAppServerWithEnforcerConfigureWithBenchmark(f func(*rbac.Enforcer), b *testing.B, objects ...runtime.Object) *Server { 321 kubeclientset := fake.NewSimpleClientset(&v1.ConfigMap{ 322 ObjectMeta: metav1.ObjectMeta{ 323 Namespace: testNamespace, 324 Name: "argocd-cm", 325 Labels: map[string]string{ 326 "app.kubernetes.io/part-of": "argocd", 327 }, 328 }, 329 }, &v1.Secret{ 330 ObjectMeta: metav1.ObjectMeta{ 331 Name: "argocd-secret", 332 Namespace: testNamespace, 333 }, 334 Data: map[string][]byte{ 335 "admin.password": []byte("test"), 336 "server.secretkey": []byte("test"), 337 }, 338 }) 339 ctx := context.Background() 340 db := db.NewDB(testNamespace, settings.NewSettingsManager(ctx, kubeclientset, testNamespace), kubeclientset) 341 _, err := db.CreateRepository(ctx, fakeRepo()) 342 require.NoError(b, err) 343 _, err = db.CreateCluster(ctx, fakeCluster()) 344 require.NoError(b, err) 345 346 mockRepoClient := &mocks.Clientset{RepoServerServiceClient: fakeRepoServerClient(false)} 347 348 defaultProj := &appsv1.AppProject{ 349 ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "default"}, 350 Spec: appsv1.AppProjectSpec{ 351 SourceRepos: []string{"*"}, 352 Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 353 }, 354 } 355 myProj := &appsv1.AppProject{ 356 ObjectMeta: metav1.ObjectMeta{Name: "my-proj", Namespace: "default"}, 357 Spec: appsv1.AppProjectSpec{ 358 SourceRepos: []string{"*"}, 359 Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 360 }, 361 } 362 projWithSyncWindows := &appsv1.AppProject{ 363 ObjectMeta: metav1.ObjectMeta{Name: "proj-maint", Namespace: "default"}, 364 Spec: appsv1.AppProjectSpec{ 365 SourceRepos: []string{"*"}, 366 Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 367 SyncWindows: appsv1.SyncWindows{}, 368 }, 369 } 370 matchingWindow := &appsv1.SyncWindow{ 371 Kind: "allow", 372 Schedule: "* * * * *", 373 Duration: "1h", 374 Applications: []string{"test-app"}, 375 } 376 projWithSyncWindows.Spec.SyncWindows = append(projWithSyncWindows.Spec.SyncWindows, matchingWindow) 377 378 objects = append(objects, defaultProj, myProj, projWithSyncWindows) 379 380 fakeAppsClientset := apps.NewSimpleClientset(objects...) 381 factory := appinformer.NewSharedInformerFactoryWithOptions(fakeAppsClientset, 0, appinformer.WithNamespace(""), appinformer.WithTweakListOptions(func(options *metav1.ListOptions) {})) 382 fakeProjLister := factory.Argoproj().V1alpha1().AppProjects().Lister().AppProjects(testNamespace) 383 384 enforcer := rbac.NewEnforcer(kubeclientset, testNamespace, common.ArgoCDRBACConfigMapName, nil) 385 f(enforcer) 386 enforcer.SetClaimsEnforcerFunc(rbacpolicy.NewRBACPolicyEnforcer(enforcer, fakeProjLister).EnforceClaims) 387 388 settingsMgr := settings.NewSettingsManager(ctx, kubeclientset, testNamespace) 389 390 // populate the app informer with the fake objects 391 appInformer := factory.Argoproj().V1alpha1().Applications().Informer() 392 393 go appInformer.Run(ctx.Done()) 394 if !k8scache.WaitForCacheSync(ctx.Done(), appInformer.HasSynced) { 395 panic("Timed out waiting for caches to sync") 396 } 397 398 projInformer := factory.Argoproj().V1alpha1().AppProjects().Informer() 399 go projInformer.Run(ctx.Done()) 400 if !k8scache.WaitForCacheSync(ctx.Done(), projInformer.HasSynced) { 401 panic("Timed out waiting for caches to sync") 402 } 403 404 broadcaster := new(appmocks.Broadcaster) 405 broadcaster.On("Subscribe", mock.Anything, mock.Anything).Return(func() {}).Run(func(args mock.Arguments) { 406 // Simulate the broadcaster notifying the subscriber of an application update. 407 // The second parameter to Subscribe is filters. For the purposes of tests, we ignore the filters. Future tests 408 // might require implementing those. 409 go func() { 410 events := args.Get(0).(chan *appsv1.ApplicationWatchEvent) 411 for _, obj := range objects { 412 app, ok := obj.(*appsv1.Application) 413 if ok { 414 oldVersion, err := strconv.Atoi(app.ResourceVersion) 415 if err != nil { 416 oldVersion = 0 417 } 418 clonedApp := app.DeepCopy() 419 clonedApp.ResourceVersion = fmt.Sprintf("%d", oldVersion+1) 420 events <- &appsv1.ApplicationWatchEvent{Type: watch.Added, Application: *clonedApp} 421 } 422 } 423 }() 424 }) 425 broadcaster.On("OnAdd", mock.Anything).Return() 426 broadcaster.On("OnUpdate", mock.Anything, mock.Anything).Return() 427 broadcaster.On("OnDelete", mock.Anything).Return() 428 429 appStateCache := appstate.NewCache(cache.NewCache(cache.NewInMemoryCache(time.Hour)), time.Hour) 430 // pre-populate the app cache 431 for _, obj := range objects { 432 app, ok := obj.(*appsv1.Application) 433 if ok { 434 err := appStateCache.SetAppManagedResources(app.Name, []*appsv1.ResourceDiff{}) 435 require.NoError(b, err) 436 437 // Pre-populate the resource tree based on the app's resources. 438 nodes := make([]appsv1.ResourceNode, len(app.Status.Resources)) 439 for i, res := range app.Status.Resources { 440 nodes[i] = appsv1.ResourceNode{ 441 ResourceRef: appsv1.ResourceRef{ 442 Group: res.Group, 443 Kind: res.Kind, 444 Version: res.Version, 445 Name: res.Name, 446 Namespace: res.Namespace, 447 UID: "fake", 448 }, 449 } 450 } 451 err = appStateCache.SetAppResourcesTree(app.Name, &appsv1.ApplicationTree{ 452 Nodes: nodes, 453 }) 454 require.NoError(b, err) 455 } 456 } 457 appCache := servercache.NewCache(appStateCache, time.Hour, time.Hour, time.Hour) 458 459 kubectl := &kubetest.MockKubectlCmd{} 460 kubectl = kubectl.WithGetResourceFunc(func(_ context.Context, _ *rest.Config, gvk schema.GroupVersionKind, name string, namespace string) (*unstructured.Unstructured, error) { 461 for _, obj := range objects { 462 if obj.GetObjectKind().GroupVersionKind().GroupKind() == gvk.GroupKind() { 463 if obj, ok := obj.(*unstructured.Unstructured); ok && obj.GetName() == name && obj.GetNamespace() == namespace { 464 return obj, nil 465 } 466 } 467 } 468 return nil, nil 469 }) 470 471 server, _ := NewServer( 472 testNamespace, 473 kubeclientset, 474 fakeAppsClientset, 475 factory.Argoproj().V1alpha1().Applications().Lister(), 476 appInformer, 477 broadcaster, 478 mockRepoClient, 479 appCache, 480 kubectl, 481 db, 482 enforcer, 483 sync.NewKeyLock(), 484 settingsMgr, 485 projInformer, 486 []string{}, 487 ) 488 return server.(*Server) 489 } 490 491 const fakeApp = ` 492 apiVersion: argoproj.io/v1alpha1 493 kind: Application 494 metadata: 495 name: test-app 496 namespace: default 497 spec: 498 source: 499 path: some/path 500 repoURL: https://github.com/argoproj/argocd-example-apps.git 501 targetRevision: HEAD 502 ksonnet: 503 environment: default 504 destination: 505 namespace: ` + test.FakeDestNamespace + ` 506 server: https://cluster-api.example.com 507 ` 508 509 const fakeAppWithDestName = ` 510 apiVersion: argoproj.io/v1alpha1 511 kind: Application 512 metadata: 513 name: test-app 514 namespace: default 515 spec: 516 source: 517 path: some/path 518 repoURL: https://github.com/argoproj/argocd-example-apps.git 519 targetRevision: HEAD 520 ksonnet: 521 environment: default 522 destination: 523 namespace: ` + test.FakeDestNamespace + ` 524 name: fake-cluster 525 ` 526 527 const fakeAppWithAnnotations = ` 528 apiVersion: argoproj.io/v1alpha1 529 kind: Application 530 metadata: 531 name: test-app 532 namespace: default 533 annotations: 534 test.annotation: test 535 spec: 536 source: 537 path: some/path 538 repoURL: https://github.com/argoproj/argocd-example-apps.git 539 targetRevision: HEAD 540 ksonnet: 541 environment: default 542 destination: 543 namespace: ` + test.FakeDestNamespace + ` 544 server: https://cluster-api.example.com 545 ` 546 547 func newTestAppWithDestName(opts ...func(app *appsv1.Application)) *appsv1.Application { 548 return createTestApp(fakeAppWithDestName, opts...) 549 } 550 551 func newTestApp(opts ...func(app *appsv1.Application)) *appsv1.Application { 552 return createTestApp(fakeApp, opts...) 553 } 554 555 func newTestAppWithAnnotations(opts ...func(app *appsv1.Application)) *appsv1.Application { 556 return createTestApp(fakeAppWithAnnotations, opts...) 557 } 558 559 func createTestApp(testApp string, opts ...func(app *appsv1.Application)) *appsv1.Application { 560 var app appsv1.Application 561 err := yaml.Unmarshal([]byte(testApp), &app) 562 if err != nil { 563 panic(err) 564 } 565 for i := range opts { 566 opts[i](&app) 567 } 568 return &app 569 } 570 571 type TestServerStream struct { 572 ctx context.Context 573 appName string 574 headerSent bool 575 project string 576 } 577 578 func (t *TestServerStream) SetHeader(metadata.MD) error { 579 return nil 580 } 581 582 func (t *TestServerStream) SendHeader(metadata.MD) error { 583 return nil 584 } 585 586 func (t *TestServerStream) SetTrailer(metadata.MD) {} 587 588 func (t *TestServerStream) Context() context.Context { 589 return t.ctx 590 } 591 592 func (t *TestServerStream) SendMsg(m interface{}) error { 593 return nil 594 } 595 596 func (t *TestServerStream) RecvMsg(m interface{}) error { 597 return nil 598 } 599 600 func (t *TestServerStream) SendAndClose(r *apiclient.ManifestResponse) error { 601 return nil 602 } 603 604 func (t *TestServerStream) Recv() (*application.ApplicationManifestQueryWithFilesWrapper, error) { 605 if !t.headerSent { 606 t.headerSent = true 607 return &application.ApplicationManifestQueryWithFilesWrapper{Part: &application.ApplicationManifestQueryWithFilesWrapper_Query{ 608 Query: &application.ApplicationManifestQueryWithFiles{ 609 Name: pointer.String(t.appName), 610 Project: pointer.String(t.project), 611 Checksum: pointer.String(""), 612 }, 613 }}, nil 614 } 615 return nil, io.EOF 616 } 617 618 func (t *TestServerStream) ServerStream() TestServerStream { 619 return TestServerStream{} 620 } 621 622 type TestResourceTreeServer struct { 623 ctx context.Context 624 } 625 626 func (t *TestResourceTreeServer) Send(tree *appsv1.ApplicationTree) error { 627 return nil 628 } 629 630 func (t *TestResourceTreeServer) SetHeader(metadata.MD) error { 631 return nil 632 } 633 634 func (t *TestResourceTreeServer) SendHeader(metadata.MD) error { 635 return nil 636 } 637 638 func (t *TestResourceTreeServer) SetTrailer(metadata.MD) {} 639 640 func (t *TestResourceTreeServer) Context() context.Context { 641 return t.ctx 642 } 643 644 func (t *TestResourceTreeServer) SendMsg(m interface{}) error { 645 return nil 646 } 647 648 func (t *TestResourceTreeServer) RecvMsg(m interface{}) error { 649 return nil 650 } 651 652 type TestPodLogsServer struct { 653 ctx context.Context 654 } 655 656 func (t *TestPodLogsServer) Send(log *application.LogEntry) error { 657 return nil 658 } 659 660 func (t *TestPodLogsServer) SetHeader(metadata.MD) error { 661 return nil 662 } 663 664 func (t *TestPodLogsServer) SendHeader(metadata.MD) error { 665 return nil 666 } 667 668 func (t *TestPodLogsServer) SetTrailer(metadata.MD) {} 669 670 func (t *TestPodLogsServer) Context() context.Context { 671 return t.ctx 672 } 673 674 func (t *TestPodLogsServer) SendMsg(m interface{}) error { 675 return nil 676 } 677 678 func (t *TestPodLogsServer) RecvMsg(m interface{}) error { 679 return nil 680 } 681 682 func TestNoAppEnumeration(t *testing.T) { 683 // This test ensures that malicious users can't infer the existence or non-existence of Applications by inspecting 684 // error messages. The errors for "app does not exist" must be the same as errors for "you aren't allowed to 685 // interact with this app." 686 687 // These tests are only important on API calls where the full app RBAC name (project, namespace, and name) is _not_ 688 // known based on the query parameters. For example, the Create call cannot leak existence of Applications, because 689 // the Application's project, namespace, and name are all specified in the API call. The call can be rejected 690 // immediately if the user does not have access. But the Delete endpoint may be called with just the Application 691 // name. So we cannot return a different error message for "does not exist" and "you don't have delete permissions," 692 // because the user could infer that the Application exists if they do not get the "does not exist" message. For 693 // endpoints that do not require the full RBAC name, we must return a generic "permission denied" for both "does not 694 // exist" and "no access." 695 696 f := func(enf *rbac.Enforcer) { 697 _ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV) 698 enf.SetDefaultRole("role:none") 699 } 700 deployment := k8sappsv1.Deployment{ 701 TypeMeta: metav1.TypeMeta{ 702 APIVersion: "apps/v1", 703 Kind: "Deployment", 704 }, 705 ObjectMeta: metav1.ObjectMeta{ 706 Name: "test", 707 Namespace: "test", 708 }, 709 } 710 testApp := newTestApp(func(app *appsv1.Application) { 711 app.Name = "test" 712 app.Status.Resources = []appsv1.ResourceStatus{ 713 { 714 Group: deployment.GroupVersionKind().Group, 715 Kind: deployment.GroupVersionKind().Kind, 716 Version: deployment.GroupVersionKind().Version, 717 Name: deployment.Name, 718 Namespace: deployment.Namespace, 719 Status: "Synced", 720 }, 721 } 722 app.Status.History = []appsv1.RevisionHistory{ 723 { 724 ID: 0, 725 Source: appsv1.ApplicationSource{ 726 TargetRevision: "something-old", 727 }, 728 }, 729 } 730 }) 731 testHelmApp := newTestApp(func(app *appsv1.Application) { 732 app.Name = "test-helm" 733 app.Spec.Source.Path = "" 734 app.Spec.Source.Chart = "test" 735 app.Status.Resources = []appsv1.ResourceStatus{ 736 { 737 Group: deployment.GroupVersionKind().Group, 738 Kind: deployment.GroupVersionKind().Kind, 739 Version: deployment.GroupVersionKind().Version, 740 Name: deployment.Name, 741 Namespace: deployment.Namespace, 742 Status: "Synced", 743 }, 744 } 745 app.Status.History = []appsv1.RevisionHistory{ 746 { 747 ID: 0, 748 Source: appsv1.ApplicationSource{ 749 TargetRevision: "something-old", 750 }, 751 }, 752 } 753 }) 754 testDeployment := kube.MustToUnstructured(&deployment) 755 appServer := newTestAppServerWithEnforcerConfigure(f, t, testApp, testHelmApp, testDeployment) 756 757 noRoleCtx := context.Background() 758 // nolint:staticcheck 759 adminCtx := context.WithValue(noRoleCtx, "claims", &jwt.MapClaims{"groups": []string{"admin"}}) 760 761 t.Run("Get", func(t *testing.T) { 762 // nolint:staticcheck 763 _, err := appServer.Get(adminCtx, &application.ApplicationQuery{Name: pointer.String("test")}) 764 assert.NoError(t, err) 765 // nolint:staticcheck 766 _, err = appServer.Get(noRoleCtx, &application.ApplicationQuery{Name: pointer.String("test")}) 767 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 768 // nolint:staticcheck 769 _, err = appServer.Get(adminCtx, &application.ApplicationQuery{Name: pointer.String("doest-not-exist")}) 770 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 771 // nolint:staticcheck 772 _, err = appServer.Get(adminCtx, &application.ApplicationQuery{Name: pointer.String("doest-not-exist"), Project: []string{"test"}}) 773 assert.Equal(t, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", err.Error(), "when the request specifies a project, we can return the standard k8s error message") 774 }) 775 776 t.Run("GetManifests", func(t *testing.T) { 777 _, err := appServer.GetManifests(adminCtx, &application.ApplicationManifestQuery{Name: pointer.String("test")}) 778 assert.NoError(t, err) 779 _, err = appServer.GetManifests(noRoleCtx, &application.ApplicationManifestQuery{Name: pointer.String("test")}) 780 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 781 _, err = appServer.GetManifests(adminCtx, &application.ApplicationManifestQuery{Name: pointer.String("doest-not-exist")}) 782 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 783 _, err = appServer.GetManifests(adminCtx, &application.ApplicationManifestQuery{Name: pointer.String("doest-not-exist"), Project: pointer.String("test")}) 784 assert.Equal(t, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", err.Error(), "when the request specifies a project, we can return the standard k8s error message") 785 }) 786 787 t.Run("ListResourceEvents", func(t *testing.T) { 788 _, err := appServer.ListResourceEvents(adminCtx, &application.ApplicationResourceEventsQuery{Name: pointer.String("test")}) 789 assert.NoError(t, err) 790 _, err = appServer.ListResourceEvents(noRoleCtx, &application.ApplicationResourceEventsQuery{Name: pointer.String("test")}) 791 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 792 _, err = appServer.ListResourceEvents(adminCtx, &application.ApplicationResourceEventsQuery{Name: pointer.String("doest-not-exist")}) 793 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 794 _, err = appServer.ListResourceEvents(adminCtx, &application.ApplicationResourceEventsQuery{Name: pointer.String("doest-not-exist"), Project: pointer.String("test")}) 795 assert.Equal(t, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", err.Error(), "when the request specifies a project, we can return the standard k8s error message") 796 }) 797 798 t.Run("UpdateSpec", func(t *testing.T) { 799 _, err := appServer.UpdateSpec(adminCtx, &application.ApplicationUpdateSpecRequest{Name: pointer.String("test"), Spec: &appsv1.ApplicationSpec{ 800 Destination: appsv1.ApplicationDestination{Namespace: "default", Server: "https://cluster-api.example.com"}, 801 Source: &appsv1.ApplicationSource{RepoURL: "https://some-fake-source", Path: "."}, 802 }}) 803 assert.NoError(t, err) 804 _, err = appServer.UpdateSpec(noRoleCtx, &application.ApplicationUpdateSpecRequest{Name: pointer.String("test"), Spec: &appsv1.ApplicationSpec{ 805 Destination: appsv1.ApplicationDestination{Namespace: "default", Server: "https://cluster-api.example.com"}, 806 Source: &appsv1.ApplicationSource{RepoURL: "https://some-fake-source", Path: "."}, 807 }}) 808 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 809 _, err = appServer.UpdateSpec(adminCtx, &application.ApplicationUpdateSpecRequest{Name: pointer.String("doest-not-exist"), Spec: &appsv1.ApplicationSpec{ 810 Destination: appsv1.ApplicationDestination{Namespace: "default", Server: "https://cluster-api.example.com"}, 811 Source: &appsv1.ApplicationSource{RepoURL: "https://some-fake-source", Path: "."}, 812 }}) 813 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 814 _, err = appServer.UpdateSpec(adminCtx, &application.ApplicationUpdateSpecRequest{Name: pointer.String("doest-not-exist"), Project: pointer.String("test"), Spec: &appsv1.ApplicationSpec{ 815 Destination: appsv1.ApplicationDestination{Namespace: "default", Server: "https://cluster-api.example.com"}, 816 Source: &appsv1.ApplicationSource{RepoURL: "https://some-fake-source", Path: "."}, 817 }}) 818 assert.Equal(t, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", err.Error(), "when the request specifies a project, we can return the standard k8s error message") 819 }) 820 821 t.Run("Patch", func(t *testing.T) { 822 _, err := appServer.Patch(adminCtx, &application.ApplicationPatchRequest{Name: pointer.String("test"), Patch: pointer.String(`[{"op": "replace", "path": "/spec/source/path", "value": "foo"}]`)}) 823 assert.NoError(t, err) 824 _, err = appServer.Patch(noRoleCtx, &application.ApplicationPatchRequest{Name: pointer.String("test"), Patch: pointer.String(`[{"op": "replace", "path": "/spec/source/path", "value": "foo"}]`)}) 825 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 826 _, err = appServer.Patch(adminCtx, &application.ApplicationPatchRequest{Name: pointer.String("doest-not-exist")}) 827 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 828 _, err = appServer.Patch(adminCtx, &application.ApplicationPatchRequest{Name: pointer.String("doest-not-exist"), Project: pointer.String("test")}) 829 assert.Equal(t, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", err.Error(), "when the request specifies a project, we can return the standard k8s error message") 830 }) 831 832 t.Run("GetResource", func(t *testing.T) { 833 _, err := appServer.GetResource(adminCtx, &application.ApplicationResourceRequest{Name: pointer.String("test"), ResourceName: pointer.String("test"), Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Namespace: pointer.String("test")}) 834 assert.NoError(t, err) 835 _, err = appServer.GetResource(noRoleCtx, &application.ApplicationResourceRequest{Name: pointer.String("test"), ResourceName: pointer.String("test"), Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Namespace: pointer.String("test")}) 836 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 837 _, err = appServer.GetResource(adminCtx, &application.ApplicationResourceRequest{Name: pointer.String("doest-not-exist"), ResourceName: pointer.String("test"), Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Namespace: pointer.String("test")}) 838 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 839 _, err = appServer.GetResource(adminCtx, &application.ApplicationResourceRequest{Name: pointer.String("doest-not-exist"), Project: pointer.String("test"), ResourceName: pointer.String("test"), Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Namespace: pointer.String("test")}) 840 assert.Equal(t, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", err.Error(), "when the request specifies a project, we can return the standard k8s error message") 841 }) 842 843 t.Run("PatchResource", func(t *testing.T) { 844 _, err := appServer.PatchResource(adminCtx, &application.ApplicationResourcePatchRequest{Name: pointer.String("test"), ResourceName: pointer.String("test"), Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Namespace: pointer.String("test"), Patch: pointer.String(`[{"op": "replace", "path": "/spec/replicas", "value": 3}]`)}) 845 // This will always throw an error, because the kubectl mock for PatchResource is hard-coded to return nil. 846 // The best we can do is to confirm we get past the permission check. 847 assert.NotEqual(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 848 _, err = appServer.PatchResource(noRoleCtx, &application.ApplicationResourcePatchRequest{Name: pointer.String("test"), ResourceName: pointer.String("test"), Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Namespace: pointer.String("test"), Patch: pointer.String(`[{"op": "replace", "path": "/spec/replicas", "value": 3}]`)}) 849 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 850 _, err = appServer.PatchResource(adminCtx, &application.ApplicationResourcePatchRequest{Name: pointer.String("doest-not-exist"), ResourceName: pointer.String("test"), Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Namespace: pointer.String("test"), Patch: pointer.String(`[{"op": "replace", "path": "/spec/replicas", "value": 3}]`)}) 851 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 852 _, err = appServer.PatchResource(adminCtx, &application.ApplicationResourcePatchRequest{Name: pointer.String("doest-not-exist"), Project: pointer.String("test"), ResourceName: pointer.String("test"), Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Namespace: pointer.String("test"), Patch: pointer.String(`[{"op": "replace", "path": "/spec/replicas", "value": 3}]`)}) 853 assert.Equal(t, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", err.Error(), "when the request specifies a project, we can return the standard k8s error message") 854 }) 855 856 t.Run("DeleteResource", func(t *testing.T) { 857 _, err := appServer.DeleteResource(adminCtx, &application.ApplicationResourceDeleteRequest{Name: pointer.String("test"), ResourceName: pointer.String("test"), Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Namespace: pointer.String("test")}) 858 assert.NoError(t, err) 859 _, err = appServer.DeleteResource(noRoleCtx, &application.ApplicationResourceDeleteRequest{Name: pointer.String("test"), ResourceName: pointer.String("test"), Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Namespace: pointer.String("test")}) 860 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 861 _, err = appServer.DeleteResource(adminCtx, &application.ApplicationResourceDeleteRequest{Name: pointer.String("doest-not-exist"), ResourceName: pointer.String("test"), Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Namespace: pointer.String("test")}) 862 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 863 _, err = appServer.DeleteResource(adminCtx, &application.ApplicationResourceDeleteRequest{Name: pointer.String("doest-not-exist"), Project: pointer.String("test"), ResourceName: pointer.String("test"), Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Namespace: pointer.String("test")}) 864 assert.Equal(t, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", err.Error(), "when the request specifies a project, we can return the standard k8s error message") 865 }) 866 867 t.Run("ResourceTree", func(t *testing.T) { 868 _, err := appServer.ResourceTree(adminCtx, &application.ResourcesQuery{ApplicationName: pointer.String("test")}) 869 assert.NoError(t, err) 870 _, err = appServer.ResourceTree(noRoleCtx, &application.ResourcesQuery{ApplicationName: pointer.String("test")}) 871 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 872 _, err = appServer.ResourceTree(adminCtx, &application.ResourcesQuery{ApplicationName: pointer.String("doest-not-exist")}) 873 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 874 _, err = appServer.ResourceTree(adminCtx, &application.ResourcesQuery{ApplicationName: pointer.String("doest-not-exist"), Project: pointer.String("test")}) 875 assert.Equal(t, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", err.Error(), "when the request specifies a project, we can return the standard k8s error message") 876 }) 877 878 t.Run("RevisionMetadata", func(t *testing.T) { 879 _, err := appServer.RevisionMetadata(adminCtx, &application.RevisionMetadataQuery{Name: pointer.String("test")}) 880 assert.NoError(t, err) 881 _, err = appServer.RevisionMetadata(noRoleCtx, &application.RevisionMetadataQuery{Name: pointer.String("test")}) 882 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 883 _, err = appServer.RevisionMetadata(adminCtx, &application.RevisionMetadataQuery{Name: pointer.String("doest-not-exist")}) 884 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 885 _, err = appServer.RevisionMetadata(adminCtx, &application.RevisionMetadataQuery{Name: pointer.String("doest-not-exist"), Project: pointer.String("test")}) 886 assert.Equal(t, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", err.Error(), "when the request specifies a project, we can return the standard k8s error message") 887 }) 888 889 t.Run("RevisionChartDetails", func(t *testing.T) { 890 _, err := appServer.RevisionChartDetails(adminCtx, &application.RevisionMetadataQuery{Name: pointer.String("test-helm")}) 891 assert.NoError(t, err) 892 _, err = appServer.RevisionChartDetails(noRoleCtx, &application.RevisionMetadataQuery{Name: pointer.String("test-helm")}) 893 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 894 _, err = appServer.RevisionChartDetails(adminCtx, &application.RevisionMetadataQuery{Name: pointer.String("doest-not-exist")}) 895 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 896 _, err = appServer.RevisionChartDetails(adminCtx, &application.RevisionMetadataQuery{Name: pointer.String("doest-not-exist"), Project: pointer.String("test")}) 897 assert.Equal(t, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", err.Error(), "when the request specifies a project, we can return the standard k8s error message") 898 }) 899 900 t.Run("ManagedResources", func(t *testing.T) { 901 _, err := appServer.ManagedResources(adminCtx, &application.ResourcesQuery{ApplicationName: pointer.String("test")}) 902 assert.NoError(t, err) 903 _, err = appServer.ManagedResources(noRoleCtx, &application.ResourcesQuery{ApplicationName: pointer.String("test")}) 904 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 905 _, err = appServer.ManagedResources(adminCtx, &application.ResourcesQuery{ApplicationName: pointer.String("doest-not-exist")}) 906 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 907 _, err = appServer.ManagedResources(adminCtx, &application.ResourcesQuery{ApplicationName: pointer.String("doest-not-exist"), Project: pointer.String("test")}) 908 assert.Equal(t, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", err.Error(), "when the request specifies a project, we can return the standard k8s error message") 909 }) 910 911 t.Run("Sync", func(t *testing.T) { 912 _, err := appServer.Sync(adminCtx, &application.ApplicationSyncRequest{Name: pointer.String("test")}) 913 assert.NoError(t, err) 914 _, err = appServer.Sync(noRoleCtx, &application.ApplicationSyncRequest{Name: pointer.String("test")}) 915 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 916 _, err = appServer.Sync(adminCtx, &application.ApplicationSyncRequest{Name: pointer.String("doest-not-exist")}) 917 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 918 _, err = appServer.Sync(adminCtx, &application.ApplicationSyncRequest{Name: pointer.String("doest-not-exist"), Project: pointer.String("test")}) 919 assert.Equal(t, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", err.Error(), "when the request specifies a project, we can return the standard k8s error message") 920 }) 921 922 t.Run("TerminateOperation", func(t *testing.T) { 923 // The sync operation is already started from the previous test. We just need to set the field that the 924 // controller would set if this were an actual Argo CD environment. 925 setSyncRunningOperationState(t, appServer) 926 _, err := appServer.TerminateOperation(adminCtx, &application.OperationTerminateRequest{Name: pointer.String("test")}) 927 assert.NoError(t, err) 928 _, err = appServer.TerminateOperation(noRoleCtx, &application.OperationTerminateRequest{Name: pointer.String("test")}) 929 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 930 _, err = appServer.TerminateOperation(adminCtx, &application.OperationTerminateRequest{Name: pointer.String("doest-not-exist")}) 931 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 932 _, err = appServer.TerminateOperation(adminCtx, &application.OperationTerminateRequest{Name: pointer.String("doest-not-exist"), Project: pointer.String("test")}) 933 assert.Equal(t, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", err.Error(), "when the request specifies a project, we can return the standard k8s error message") 934 }) 935 936 t.Run("Rollback", func(t *testing.T) { 937 unsetSyncRunningOperationState(t, appServer) 938 _, err := appServer.Rollback(adminCtx, &application.ApplicationRollbackRequest{Name: pointer.String("test")}) 939 assert.NoError(t, err) 940 _, err = appServer.Rollback(noRoleCtx, &application.ApplicationRollbackRequest{Name: pointer.String("test")}) 941 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 942 _, err = appServer.Rollback(adminCtx, &application.ApplicationRollbackRequest{Name: pointer.String("doest-not-exist")}) 943 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 944 _, err = appServer.Rollback(adminCtx, &application.ApplicationRollbackRequest{Name: pointer.String("doest-not-exist"), Project: pointer.String("test")}) 945 assert.Equal(t, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", err.Error(), "when the request specifies a project, we can return the standard k8s error message") 946 }) 947 948 t.Run("ListResourceActions", func(t *testing.T) { 949 _, err := appServer.ListResourceActions(adminCtx, &application.ApplicationResourceRequest{Name: pointer.String("test"), ResourceName: pointer.String("test"), Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Namespace: pointer.String("test")}) 950 assert.NoError(t, err) 951 _, err = appServer.ListResourceActions(noRoleCtx, &application.ApplicationResourceRequest{Name: pointer.String("test")}) 952 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 953 _, err = appServer.ListResourceActions(noRoleCtx, &application.ApplicationResourceRequest{Group: pointer.String("argoproj.io"), Kind: pointer.String("Application"), Name: pointer.String("test")}) 954 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 955 _, err = appServer.ListResourceActions(adminCtx, &application.ApplicationResourceRequest{Name: pointer.String("doest-not-exist")}) 956 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 957 _, err = appServer.ListResourceActions(adminCtx, &application.ApplicationResourceRequest{Name: pointer.String("doest-not-exist"), Project: pointer.String("test")}) 958 assert.Equal(t, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", err.Error(), "when the request specifies a project, we can return the standard k8s error message") 959 }) 960 961 t.Run("RunResourceAction", func(t *testing.T) { 962 _, err := appServer.RunResourceAction(adminCtx, &application.ResourceActionRunRequest{Name: pointer.String("test"), ResourceName: pointer.String("test"), Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Namespace: pointer.String("test"), Action: pointer.String("restart")}) 963 assert.NoError(t, err) 964 _, err = appServer.RunResourceAction(noRoleCtx, &application.ResourceActionRunRequest{Name: pointer.String("test")}) 965 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 966 _, err = appServer.RunResourceAction(noRoleCtx, &application.ResourceActionRunRequest{Group: pointer.String("argoproj.io"), Kind: pointer.String("Application"), Name: pointer.String("test")}) 967 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 968 _, err = appServer.RunResourceAction(adminCtx, &application.ResourceActionRunRequest{Name: pointer.String("doest-not-exist")}) 969 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 970 _, err = appServer.RunResourceAction(adminCtx, &application.ResourceActionRunRequest{Name: pointer.String("doest-not-exist"), Project: pointer.String("test")}) 971 assert.Equal(t, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", err.Error(), "when the request specifies a project, we can return the standard k8s error message") 972 }) 973 974 t.Run("GetApplicationSyncWindows", func(t *testing.T) { 975 _, err := appServer.GetApplicationSyncWindows(adminCtx, &application.ApplicationSyncWindowsQuery{Name: pointer.String("test")}) 976 assert.NoError(t, err) 977 _, err = appServer.GetApplicationSyncWindows(noRoleCtx, &application.ApplicationSyncWindowsQuery{Name: pointer.String("test")}) 978 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 979 _, err = appServer.GetApplicationSyncWindows(adminCtx, &application.ApplicationSyncWindowsQuery{Name: pointer.String("doest-not-exist")}) 980 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 981 _, err = appServer.GetApplicationSyncWindows(adminCtx, &application.ApplicationSyncWindowsQuery{Name: pointer.String("doest-not-exist"), Project: pointer.String("test")}) 982 assert.Equal(t, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", err.Error(), "when the request specifies a project, we can return the standard k8s error message") 983 }) 984 985 t.Run("GetManifestsWithFiles", func(t *testing.T) { 986 err := appServer.GetManifestsWithFiles(&TestServerStream{ctx: adminCtx, appName: "test"}) 987 assert.NoError(t, err) 988 err = appServer.GetManifestsWithFiles(&TestServerStream{ctx: noRoleCtx, appName: "test"}) 989 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 990 err = appServer.GetManifestsWithFiles(&TestServerStream{ctx: adminCtx, appName: "does-not-exist"}) 991 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 992 err = appServer.GetManifestsWithFiles(&TestServerStream{ctx: adminCtx, appName: "does-not-exist", project: "test"}) 993 assert.Equal(t, "rpc error: code = NotFound desc = applications.argoproj.io \"does-not-exist\" not found", err.Error(), "when the request specifies a project, we can return the standard k8s error message") 994 }) 995 996 t.Run("WatchResourceTree", func(t *testing.T) { 997 err := appServer.WatchResourceTree(&application.ResourcesQuery{ApplicationName: pointer.String("test")}, &TestResourceTreeServer{ctx: adminCtx}) 998 assert.NoError(t, err) 999 err = appServer.WatchResourceTree(&application.ResourcesQuery{ApplicationName: pointer.String("test")}, &TestResourceTreeServer{ctx: noRoleCtx}) 1000 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 1001 err = appServer.WatchResourceTree(&application.ResourcesQuery{ApplicationName: pointer.String("does-not-exist")}, &TestResourceTreeServer{ctx: adminCtx}) 1002 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 1003 err = appServer.WatchResourceTree(&application.ResourcesQuery{ApplicationName: pointer.String("does-not-exist"), Project: pointer.String("test")}, &TestResourceTreeServer{ctx: adminCtx}) 1004 assert.Equal(t, "rpc error: code = NotFound desc = applications.argoproj.io \"does-not-exist\" not found", err.Error(), "when the request specifies a project, we can return the standard k8s error message") 1005 }) 1006 1007 t.Run("PodLogs", func(t *testing.T) { 1008 err := appServer.PodLogs(&application.ApplicationPodLogsQuery{Name: pointer.String("test")}, &TestPodLogsServer{ctx: adminCtx}) 1009 assert.NoError(t, err) 1010 err = appServer.PodLogs(&application.ApplicationPodLogsQuery{Name: pointer.String("test")}, &TestPodLogsServer{ctx: noRoleCtx}) 1011 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 1012 err = appServer.PodLogs(&application.ApplicationPodLogsQuery{Name: pointer.String("does-not-exist")}, &TestPodLogsServer{ctx: adminCtx}) 1013 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 1014 err = appServer.PodLogs(&application.ApplicationPodLogsQuery{Name: pointer.String("does-not-exist"), Project: pointer.String("test")}, &TestPodLogsServer{ctx: adminCtx}) 1015 assert.Equal(t, "rpc error: code = NotFound desc = applications.argoproj.io \"does-not-exist\" not found", err.Error(), "when the request specifies a project, we can return the standard k8s error message") 1016 }) 1017 1018 t.Run("ListLinks", func(t *testing.T) { 1019 _, err := appServer.ListLinks(adminCtx, &application.ListAppLinksRequest{Name: pointer.String("test")}) 1020 assert.NoError(t, err) 1021 _, err = appServer.ListLinks(noRoleCtx, &application.ListAppLinksRequest{Name: pointer.String("test")}) 1022 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 1023 _, err = appServer.ListLinks(adminCtx, &application.ListAppLinksRequest{Name: pointer.String("does-not-exist")}) 1024 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 1025 _, err = appServer.ListLinks(adminCtx, &application.ListAppLinksRequest{Name: pointer.String("does-not-exist"), Project: pointer.String("test")}) 1026 assert.Equal(t, "rpc error: code = NotFound desc = applications.argoproj.io \"does-not-exist\" not found", err.Error(), "when the request specifies a project, we can return the standard k8s error message") 1027 }) 1028 1029 t.Run("ListResourceLinks", func(t *testing.T) { 1030 _, err := appServer.ListResourceLinks(adminCtx, &application.ApplicationResourceRequest{Name: pointer.String("test"), ResourceName: pointer.String("test"), Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Namespace: pointer.String("test")}) 1031 assert.NoError(t, err) 1032 _, err = appServer.ListResourceLinks(noRoleCtx, &application.ApplicationResourceRequest{Name: pointer.String("test"), ResourceName: pointer.String("test"), Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Namespace: pointer.String("test")}) 1033 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 1034 _, err = appServer.ListResourceLinks(adminCtx, &application.ApplicationResourceRequest{Name: pointer.String("does-not-exist"), ResourceName: pointer.String("test"), Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Namespace: pointer.String("test")}) 1035 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 1036 _, err = appServer.ListResourceLinks(adminCtx, &application.ApplicationResourceRequest{Name: pointer.String("does-not-exist"), ResourceName: pointer.String("test"), Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Namespace: pointer.String("test"), Project: pointer.String("test")}) 1037 assert.Equal(t, "rpc error: code = NotFound desc = applications.argoproj.io \"does-not-exist\" not found", err.Error(), "when the request specifies a project, we can return the standard k8s error message") 1038 }) 1039 1040 // Do this last so other stuff doesn't fail. 1041 t.Run("Delete", func(t *testing.T) { 1042 _, err := appServer.Delete(adminCtx, &application.ApplicationDeleteRequest{Name: pointer.String("test")}) 1043 assert.NoError(t, err) 1044 _, err = appServer.Delete(noRoleCtx, &application.ApplicationDeleteRequest{Name: pointer.String("test")}) 1045 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 1046 _, err = appServer.Delete(adminCtx, &application.ApplicationDeleteRequest{Name: pointer.String("doest-not-exist")}) 1047 assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") 1048 _, err = appServer.Delete(adminCtx, &application.ApplicationDeleteRequest{Name: pointer.String("doest-not-exist"), Project: pointer.String("test")}) 1049 assert.Equal(t, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", err.Error(), "when the request specifies a project, we can return the standard k8s error message") 1050 }) 1051 } 1052 1053 // setSyncRunningOperationState simulates starting a sync operation on the given app. 1054 func setSyncRunningOperationState(t *testing.T, appServer *Server) { 1055 appIf := appServer.appclientset.ArgoprojV1alpha1().Applications("default") 1056 app, err := appIf.Get(context.Background(), "test", metav1.GetOptions{}) 1057 require.NoError(t, err) 1058 // This sets the status that would be set by the controller usually. 1059 app.Status.OperationState = &appsv1.OperationState{Phase: synccommon.OperationRunning, Operation: appsv1.Operation{Sync: &appsv1.SyncOperation{}}} 1060 _, err = appIf.Update(context.Background(), app, metav1.UpdateOptions{}) 1061 require.NoError(t, err) 1062 } 1063 1064 // unsetSyncRunningOperationState simulates finishing a sync operation on the given app. 1065 func unsetSyncRunningOperationState(t *testing.T, appServer *Server) { 1066 appIf := appServer.appclientset.ArgoprojV1alpha1().Applications("default") 1067 app, err := appIf.Get(context.Background(), "test", metav1.GetOptions{}) 1068 require.NoError(t, err) 1069 app.Operation = nil 1070 app.Status.OperationState = nil 1071 _, err = appIf.Update(context.Background(), app, metav1.UpdateOptions{}) 1072 require.NoError(t, err) 1073 } 1074 1075 func TestListAppsInNamespaceWithLabels(t *testing.T) { 1076 appServer := newTestAppServer(t, newTestApp(func(app *appsv1.Application) { 1077 app.Name = "App1" 1078 app.ObjectMeta.Namespace = "test-namespace" 1079 app.SetLabels(map[string]string{"key1": "value1", "key2": "value1"}) 1080 }), newTestApp(func(app *appsv1.Application) { 1081 app.Name = "App2" 1082 app.ObjectMeta.Namespace = "test-namespace" 1083 app.SetLabels(map[string]string{"key1": "value2"}) 1084 }), newTestApp(func(app *appsv1.Application) { 1085 app.Name = "App3" 1086 app.ObjectMeta.Namespace = "test-namespace" 1087 app.SetLabels(map[string]string{"key1": "value3"}) 1088 })) 1089 appServer.ns = "test-namespace" 1090 appQuery := application.ApplicationQuery{} 1091 namespace := "test-namespace" 1092 appQuery.AppNamespace = &namespace 1093 testListAppsWithLabels(t, appQuery, appServer) 1094 } 1095 1096 func TestListAppsInDefaultNSWithLabels(t *testing.T) { 1097 appServer := newTestAppServer(t, newTestApp(func(app *appsv1.Application) { 1098 app.Name = "App1" 1099 app.SetLabels(map[string]string{"key1": "value1", "key2": "value1"}) 1100 }), newTestApp(func(app *appsv1.Application) { 1101 app.Name = "App2" 1102 app.SetLabels(map[string]string{"key1": "value2"}) 1103 }), newTestApp(func(app *appsv1.Application) { 1104 app.Name = "App3" 1105 app.SetLabels(map[string]string{"key1": "value3"}) 1106 })) 1107 appQuery := application.ApplicationQuery{} 1108 testListAppsWithLabels(t, appQuery, appServer) 1109 } 1110 1111 func testListAppsWithLabels(t *testing.T, appQuery application.ApplicationQuery, appServer *Server) { 1112 validTests := []struct { 1113 testName string 1114 label string 1115 expectedResult []string 1116 }{ 1117 {testName: "Equality based filtering using '=' operator", 1118 label: "key1=value1", 1119 expectedResult: []string{"App1"}}, 1120 {testName: "Equality based filtering using '==' operator", 1121 label: "key1==value1", 1122 expectedResult: []string{"App1"}}, 1123 {testName: "Equality based filtering using '!=' operator", 1124 label: "key1!=value1", 1125 expectedResult: []string{"App2", "App3"}}, 1126 {testName: "Set based filtering using 'in' operator", 1127 label: "key1 in (value1, value3)", 1128 expectedResult: []string{"App1", "App3"}}, 1129 {testName: "Set based filtering using 'notin' operator", 1130 label: "key1 notin (value1, value3)", 1131 expectedResult: []string{"App2"}}, 1132 {testName: "Set based filtering using 'exists' operator", 1133 label: "key1", 1134 expectedResult: []string{"App1", "App2", "App3"}}, 1135 {testName: "Set based filtering using 'not exists' operator", 1136 label: "!key2", 1137 expectedResult: []string{"App2", "App3"}}, 1138 } 1139 // test valid scenarios 1140 for _, validTest := range validTests { 1141 t.Run(validTest.testName, func(t *testing.T) { 1142 appQuery.Selector = &validTest.label 1143 res, err := appServer.List(context.Background(), &appQuery) 1144 assert.NoError(t, err) 1145 apps := []string{} 1146 for i := range res.Items { 1147 apps = append(apps, res.Items[i].Name) 1148 } 1149 assert.Equal(t, validTest.expectedResult, apps) 1150 }) 1151 } 1152 1153 invalidTests := []struct { 1154 testName string 1155 label string 1156 errorMesage string 1157 }{ 1158 {testName: "Set based filtering using '>' operator", 1159 label: "key1>value1", 1160 errorMesage: "error parsing the selector"}, 1161 {testName: "Set based filtering using '<' operator", 1162 label: "key1<value1", 1163 errorMesage: "error parsing the selector"}, 1164 } 1165 // test invalid scenarios 1166 for _, invalidTest := range invalidTests { 1167 t.Run(invalidTest.testName, func(t *testing.T) { 1168 appQuery.Selector = &invalidTest.label 1169 _, err := appServer.List(context.Background(), &appQuery) 1170 assert.ErrorContains(t, err, invalidTest.errorMesage) 1171 }) 1172 } 1173 } 1174 1175 func TestListAppWithProjects(t *testing.T) { 1176 appServer := newTestAppServer(t, newTestApp(func(app *appsv1.Application) { 1177 app.Name = "App1" 1178 app.Spec.Project = "test-project1" 1179 }), newTestApp(func(app *appsv1.Application) { 1180 app.Name = "App2" 1181 app.Spec.Project = "test-project2" 1182 }), newTestApp(func(app *appsv1.Application) { 1183 app.Name = "App3" 1184 app.Spec.Project = "test-project3" 1185 })) 1186 1187 t.Run("List all apps", func(t *testing.T) { 1188 appQuery := application.ApplicationQuery{} 1189 appList, err := appServer.List(context.Background(), &appQuery) 1190 assert.NoError(t, err) 1191 assert.Len(t, appList.Items, 3) 1192 }) 1193 1194 t.Run("List apps with projects filter set", func(t *testing.T) { 1195 appQuery := application.ApplicationQuery{Projects: []string{"test-project1"}} 1196 appList, err := appServer.List(context.Background(), &appQuery) 1197 assert.NoError(t, err) 1198 assert.Len(t, appList.Items, 1) 1199 for _, app := range appList.Items { 1200 assert.Equal(t, "test-project1", app.Spec.Project) 1201 } 1202 }) 1203 1204 t.Run("List apps with project filter set (legacy field)", func(t *testing.T) { 1205 appQuery := application.ApplicationQuery{Project: []string{"test-project1"}} 1206 appList, err := appServer.List(context.Background(), &appQuery) 1207 assert.NoError(t, err) 1208 assert.Len(t, appList.Items, 1) 1209 for _, app := range appList.Items { 1210 assert.Equal(t, "test-project1", app.Spec.Project) 1211 } 1212 }) 1213 1214 t.Run("List apps with both projects and project filter set", func(t *testing.T) { 1215 // If the older field is present, we should use it instead of the newer field. 1216 appQuery := application.ApplicationQuery{Project: []string{"test-project1"}, Projects: []string{"test-project2"}} 1217 appList, err := appServer.List(context.Background(), &appQuery) 1218 assert.NoError(t, err) 1219 assert.Len(t, appList.Items, 1) 1220 for _, app := range appList.Items { 1221 assert.Equal(t, "test-project1", app.Spec.Project) 1222 } 1223 }) 1224 } 1225 1226 func TestListApps(t *testing.T) { 1227 appServer := newTestAppServer(t, newTestApp(func(app *appsv1.Application) { 1228 app.Name = "bcd" 1229 }), newTestApp(func(app *appsv1.Application) { 1230 app.Name = "abc" 1231 }), newTestApp(func(app *appsv1.Application) { 1232 app.Name = "def" 1233 })) 1234 1235 res, err := appServer.List(context.Background(), &application.ApplicationQuery{}) 1236 assert.NoError(t, err) 1237 var names []string 1238 for i := range res.Items { 1239 names = append(names, res.Items[i].Name) 1240 } 1241 assert.Equal(t, []string{"abc", "bcd", "def"}, names) 1242 } 1243 1244 func TestCoupleAppsListApps(t *testing.T) { 1245 var objects []runtime.Object 1246 ctx := context.Background() 1247 1248 var groups []string 1249 for i := 0; i < 50; i++ { 1250 groups = append(groups, fmt.Sprintf("group-%d", i)) 1251 } 1252 // nolint:staticcheck 1253 ctx = context.WithValue(ctx, "claims", &jwt.MapClaims{"groups": groups}) 1254 for projectId := 0; projectId < 100; projectId++ { 1255 projectName := fmt.Sprintf("proj-%d", projectId) 1256 for appId := 0; appId < 100; appId++ { 1257 objects = append(objects, newTestApp(func(app *appsv1.Application) { 1258 app.Name = fmt.Sprintf("app-%d-%d", projectId, appId) 1259 app.Spec.Project = projectName 1260 })) 1261 } 1262 } 1263 1264 f := func(enf *rbac.Enforcer) { 1265 policy := ` 1266 p, role:test, applications, *, proj-10/*, allow 1267 g, group-45, role:test 1268 p, role:test2, applications, *, proj-15/*, allow 1269 g, group-47, role:test2 1270 p, role:test3, applications, *, proj-20/*, allow 1271 g, group-49, role:test3 1272 ` 1273 _ = enf.SetUserPolicy(policy) 1274 } 1275 appServer := newTestAppServerWithEnforcerConfigure(f, t, objects...) 1276 1277 res, err := appServer.List(ctx, &application.ApplicationQuery{}) 1278 1279 assert.NoError(t, err) 1280 var names []string 1281 for i := range res.Items { 1282 names = append(names, res.Items[i].Name) 1283 } 1284 assert.Equal(t, 300, len(names)) 1285 } 1286 1287 func generateTestApp(num int) []*appsv1.Application { 1288 apps := []*appsv1.Application{} 1289 for i := 0; i < num; i++ { 1290 apps = append(apps, newTestApp(func(app *appsv1.Application) { 1291 app.Name = fmt.Sprintf("test-app%.6d", i) 1292 })) 1293 } 1294 1295 return apps 1296 } 1297 1298 func BenchmarkListMuchApps(b *testing.B) { 1299 // 10000 apps 1300 apps := generateTestApp(10000) 1301 obj := make([]runtime.Object, len(apps)) 1302 for i, v := range apps { 1303 obj[i] = v 1304 } 1305 appServer := newTestAppServerWithBenchmark(b, obj...) 1306 1307 b.ResetTimer() 1308 for n := 0; n < b.N; n++ { 1309 _, err := appServer.List(context.Background(), &application.ApplicationQuery{}) 1310 if err != nil { 1311 break 1312 } 1313 } 1314 } 1315 1316 func BenchmarkListSomeApps(b *testing.B) { 1317 // 500 apps 1318 apps := generateTestApp(500) 1319 obj := make([]runtime.Object, len(apps)) 1320 for i, v := range apps { 1321 obj[i] = v 1322 } 1323 appServer := newTestAppServerWithBenchmark(b, obj...) 1324 1325 b.ResetTimer() 1326 for n := 0; n < b.N; n++ { 1327 _, err := appServer.List(context.Background(), &application.ApplicationQuery{}) 1328 if err != nil { 1329 break 1330 } 1331 } 1332 } 1333 1334 func BenchmarkListFewApps(b *testing.B) { 1335 // 10 apps 1336 apps := generateTestApp(10) 1337 obj := make([]runtime.Object, len(apps)) 1338 for i, v := range apps { 1339 obj[i] = v 1340 } 1341 appServer := newTestAppServerWithBenchmark(b, obj...) 1342 1343 b.ResetTimer() 1344 for n := 0; n < b.N; n++ { 1345 _, err := appServer.List(context.Background(), &application.ApplicationQuery{}) 1346 if err != nil { 1347 break 1348 } 1349 } 1350 } 1351 1352 func strToPtr(v string) *string { 1353 return &v 1354 } 1355 1356 func BenchmarkListMuchAppsWithName(b *testing.B) { 1357 // 10000 apps 1358 appsMuch := generateTestApp(10000) 1359 obj := make([]runtime.Object, len(appsMuch)) 1360 for i, v := range appsMuch { 1361 obj[i] = v 1362 } 1363 appServer := newTestAppServerWithBenchmark(b, obj...) 1364 1365 b.ResetTimer() 1366 for n := 0; n < b.N; n++ { 1367 app := &application.ApplicationQuery{Name: strToPtr("test-app000099")} 1368 _, err := appServer.List(context.Background(), app) 1369 if err != nil { 1370 break 1371 } 1372 } 1373 } 1374 1375 func BenchmarkListMuchAppsWithProjects(b *testing.B) { 1376 // 10000 apps 1377 appsMuch := generateTestApp(10000) 1378 appsMuch[999].Spec.Project = "test-project1" 1379 appsMuch[1999].Spec.Project = "test-project2" 1380 obj := make([]runtime.Object, len(appsMuch)) 1381 for i, v := range appsMuch { 1382 obj[i] = v 1383 } 1384 appServer := newTestAppServerWithBenchmark(b, obj...) 1385 1386 b.ResetTimer() 1387 for n := 0; n < b.N; n++ { 1388 app := &application.ApplicationQuery{Project: []string{"test-project1", "test-project2"}} 1389 _, err := appServer.List(context.Background(), app) 1390 if err != nil { 1391 break 1392 } 1393 } 1394 } 1395 1396 func BenchmarkListMuchAppsWithRepo(b *testing.B) { 1397 // 10000 apps 1398 appsMuch := generateTestApp(10000) 1399 appsMuch[999].Spec.Source.RepoURL = "https://some-fake-source" 1400 obj := make([]runtime.Object, len(appsMuch)) 1401 for i, v := range appsMuch { 1402 obj[i] = v 1403 } 1404 appServer := newTestAppServerWithBenchmark(b, obj...) 1405 1406 b.ResetTimer() 1407 for n := 0; n < b.N; n++ { 1408 app := &application.ApplicationQuery{Repo: strToPtr("https://some-fake-source")} 1409 _, err := appServer.List(context.Background(), app) 1410 if err != nil { 1411 break 1412 } 1413 } 1414 } 1415 1416 func TestCreateApp(t *testing.T) { 1417 testApp := newTestApp() 1418 appServer := newTestAppServer(t) 1419 testApp.Spec.Project = "" 1420 createReq := application.ApplicationCreateRequest{ 1421 Application: testApp, 1422 } 1423 app, err := appServer.Create(context.Background(), &createReq) 1424 assert.NoError(t, err) 1425 assert.NotNil(t, app) 1426 assert.NotNil(t, app.Spec) 1427 assert.Equal(t, app.Spec.Project, "default") 1428 } 1429 1430 func TestCreateAppWithDestName(t *testing.T) { 1431 appServer := newTestAppServer(t) 1432 testApp := newTestAppWithDestName() 1433 createReq := application.ApplicationCreateRequest{ 1434 Application: testApp, 1435 } 1436 app, err := appServer.Create(context.Background(), &createReq) 1437 assert.NoError(t, err) 1438 assert.NotNil(t, app) 1439 assert.Equal(t, app.Spec.Destination.Server, "https://cluster-api.example.com") 1440 } 1441 1442 // TestCreateAppWithOperation tests that an application created with an operation is created with the operation removed. 1443 // Avoids regressions of https://github.com/argoproj/argo-cd/security/advisories/GHSA-g623-jcgg-mhmm 1444 func TestCreateAppWithOperation(t *testing.T) { 1445 appServer := newTestAppServer(t) 1446 testApp := newTestAppWithDestName() 1447 testApp.Operation = &appsv1.Operation{ 1448 Sync: &appsv1.SyncOperation{ 1449 Manifests: []string{ 1450 "test", 1451 }, 1452 }, 1453 } 1454 createReq := application.ApplicationCreateRequest{ 1455 Application: testApp, 1456 } 1457 app, err := appServer.Create(context.Background(), &createReq) 1458 require.NoError(t, err) 1459 require.NotNil(t, app) 1460 assert.Nil(t, app.Operation) 1461 } 1462 1463 func TestUpdateApp(t *testing.T) { 1464 testApp := newTestApp() 1465 appServer := newTestAppServer(t, testApp) 1466 testApp.Spec.Project = "" 1467 app, err := appServer.Update(context.Background(), &application.ApplicationUpdateRequest{ 1468 Application: testApp, 1469 }) 1470 assert.Nil(t, err) 1471 assert.Equal(t, app.Spec.Project, "default") 1472 } 1473 1474 func TestUpdateAppSpec(t *testing.T) { 1475 testApp := newTestApp() 1476 appServer := newTestAppServer(t, testApp) 1477 testApp.Spec.Project = "" 1478 spec, err := appServer.UpdateSpec(context.Background(), &application.ApplicationUpdateSpecRequest{ 1479 Name: &testApp.Name, 1480 Spec: &testApp.Spec, 1481 }) 1482 assert.NoError(t, err) 1483 assert.Equal(t, "default", spec.Project) 1484 app, err := appServer.Get(context.Background(), &application.ApplicationQuery{Name: &testApp.Name}) 1485 assert.NoError(t, err) 1486 assert.Equal(t, "default", app.Spec.Project) 1487 } 1488 1489 func TestDeleteApp(t *testing.T) { 1490 ctx := context.Background() 1491 appServer := newTestAppServer(t) 1492 createReq := application.ApplicationCreateRequest{ 1493 Application: newTestApp(), 1494 } 1495 app, err := appServer.Create(ctx, &createReq) 1496 assert.Nil(t, err) 1497 1498 app, err = appServer.Get(ctx, &application.ApplicationQuery{Name: &app.Name}) 1499 assert.Nil(t, err) 1500 assert.NotNil(t, app) 1501 1502 fakeAppCs := appServer.appclientset.(*apps.Clientset) 1503 // this removes the default */* reactor so we can set our own patch/delete reactor 1504 fakeAppCs.ReactionChain = nil 1505 patched := false 1506 deleted := false 1507 fakeAppCs.AddReactor("patch", "applications", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { 1508 patched = true 1509 return true, nil, nil 1510 }) 1511 fakeAppCs.AddReactor("delete", "applications", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { 1512 deleted = true 1513 return true, nil, nil 1514 }) 1515 fakeAppCs.AddReactor("get", "applications", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { 1516 return true, &appsv1.Application{Spec: appsv1.ApplicationSpec{Source: &appsv1.ApplicationSource{}}}, nil 1517 }) 1518 appServer.appclientset = fakeAppCs 1519 1520 trueVar := true 1521 _, err = appServer.Delete(ctx, &application.ApplicationDeleteRequest{Name: &app.Name, Cascade: &trueVar}) 1522 assert.Nil(t, err) 1523 assert.True(t, patched) 1524 assert.True(t, deleted) 1525 1526 // now call delete with cascade=false. patch should not be called 1527 falseVar := false 1528 patched = false 1529 deleted = false 1530 _, err = appServer.Delete(ctx, &application.ApplicationDeleteRequest{Name: &app.Name, Cascade: &falseVar}) 1531 assert.Nil(t, err) 1532 assert.False(t, patched) 1533 assert.True(t, deleted) 1534 1535 patched = false 1536 deleted = false 1537 revertValues := func() { 1538 patched = false 1539 deleted = false 1540 } 1541 1542 t.Run("Delete with background propagation policy", func(t *testing.T) { 1543 policy := backgroundPropagationPolicy 1544 _, err = appServer.Delete(ctx, &application.ApplicationDeleteRequest{Name: &app.Name, PropagationPolicy: &policy}) 1545 assert.Nil(t, err) 1546 assert.True(t, patched) 1547 assert.True(t, deleted) 1548 t.Cleanup(revertValues) 1549 }) 1550 1551 t.Run("Delete with cascade disabled and background propagation policy", func(t *testing.T) { 1552 policy := backgroundPropagationPolicy 1553 _, err = appServer.Delete(ctx, &application.ApplicationDeleteRequest{Name: &app.Name, Cascade: &falseVar, PropagationPolicy: &policy}) 1554 assert.EqualError(t, err, "rpc error: code = InvalidArgument desc = cannot set propagation policy when cascading is disabled") 1555 assert.False(t, patched) 1556 assert.False(t, deleted) 1557 t.Cleanup(revertValues) 1558 }) 1559 1560 t.Run("Delete with invalid propagation policy", func(t *testing.T) { 1561 invalidPolicy := "invalid" 1562 _, err = appServer.Delete(ctx, &application.ApplicationDeleteRequest{Name: &app.Name, Cascade: &trueVar, PropagationPolicy: &invalidPolicy}) 1563 assert.EqualError(t, err, "rpc error: code = InvalidArgument desc = invalid propagation policy: invalid") 1564 assert.False(t, patched) 1565 assert.False(t, deleted) 1566 t.Cleanup(revertValues) 1567 }) 1568 1569 t.Run("Delete with foreground propagation policy", func(t *testing.T) { 1570 policy := foregroundPropagationPolicy 1571 _, err = appServer.Delete(ctx, &application.ApplicationDeleteRequest{Name: &app.Name, Cascade: &trueVar, PropagationPolicy: &policy}) 1572 assert.Nil(t, err) 1573 assert.True(t, patched) 1574 assert.True(t, deleted) 1575 t.Cleanup(revertValues) 1576 }) 1577 } 1578 1579 func TestSyncAndTerminate(t *testing.T) { 1580 ctx := context.Background() 1581 appServer := newTestAppServer(t) 1582 testApp := newTestApp() 1583 testApp.Spec.Source.RepoURL = "https://github.com/argoproj/argo-cd.git" 1584 createReq := application.ApplicationCreateRequest{ 1585 Application: testApp, 1586 } 1587 app, err := appServer.Create(ctx, &createReq) 1588 assert.Nil(t, err) 1589 app, err = appServer.Sync(ctx, &application.ApplicationSyncRequest{Name: &app.Name}) 1590 assert.Nil(t, err) 1591 assert.NotNil(t, app) 1592 assert.NotNil(t, app.Operation) 1593 1594 events, err := appServer.kubeclientset.CoreV1().Events(appServer.ns).List(context.Background(), metav1.ListOptions{}) 1595 assert.Nil(t, err) 1596 event := events.Items[1] 1597 1598 assert.Regexp(t, ".*initiated sync to HEAD \\([0-9A-Fa-f]{40}\\).*", event.Message) 1599 1600 // set status.operationState to pretend that an operation has started by controller 1601 app.Status.OperationState = &appsv1.OperationState{ 1602 Operation: *app.Operation, 1603 Phase: synccommon.OperationRunning, 1604 StartedAt: metav1.NewTime(time.Now()), 1605 } 1606 _, err = appServer.appclientset.ArgoprojV1alpha1().Applications(appServer.ns).Update(context.Background(), app, metav1.UpdateOptions{}) 1607 assert.Nil(t, err) 1608 1609 resp, err := appServer.TerminateOperation(ctx, &application.OperationTerminateRequest{Name: &app.Name}) 1610 assert.Nil(t, err) 1611 assert.NotNil(t, resp) 1612 1613 app, err = appServer.Get(ctx, &application.ApplicationQuery{Name: &app.Name}) 1614 assert.Nil(t, err) 1615 assert.NotNil(t, app) 1616 assert.Equal(t, synccommon.OperationTerminating, app.Status.OperationState.Phase) 1617 } 1618 1619 func TestSyncHelm(t *testing.T) { 1620 ctx := context.Background() 1621 appServer := newTestAppServer(t) 1622 testApp := newTestApp() 1623 testApp.Spec.Source.RepoURL = "https://argoproj.github.io/argo-helm" 1624 testApp.Spec.Source.Path = "" 1625 testApp.Spec.Source.Chart = "argo-cd" 1626 testApp.Spec.Source.TargetRevision = "0.7.*" 1627 1628 appServer.repoClientset = &mocks.Clientset{RepoServerServiceClient: fakeRepoServerClient(true)} 1629 1630 app, err := appServer.Create(ctx, &application.ApplicationCreateRequest{Application: testApp}) 1631 assert.NoError(t, err) 1632 1633 app, err = appServer.Sync(ctx, &application.ApplicationSyncRequest{Name: &app.Name}) 1634 assert.NoError(t, err) 1635 assert.NotNil(t, app) 1636 assert.NotNil(t, app.Operation) 1637 1638 events, err := appServer.kubeclientset.CoreV1().Events(appServer.ns).List(context.Background(), metav1.ListOptions{}) 1639 assert.NoError(t, err) 1640 assert.Equal(t, "Unknown user initiated sync to 0.7.* (0.7.2)", events.Items[1].Message) 1641 } 1642 1643 func TestSyncGit(t *testing.T) { 1644 ctx := context.Background() 1645 appServer := newTestAppServer(t) 1646 testApp := newTestApp() 1647 testApp.Spec.Source.RepoURL = "https://github.com/org/test" 1648 testApp.Spec.Source.Path = "deploy" 1649 testApp.Spec.Source.TargetRevision = "0.7.*" 1650 app, err := appServer.Create(ctx, &application.ApplicationCreateRequest{Application: testApp}) 1651 assert.NoError(t, err) 1652 syncReq := &application.ApplicationSyncRequest{ 1653 Name: &app.Name, 1654 Manifests: []string{ 1655 `apiVersion: v1 1656 kind: ServiceAccount 1657 metadata: 1658 name: test 1659 namespace: test`, 1660 }, 1661 } 1662 app, err = appServer.Sync(ctx, syncReq) 1663 assert.NoError(t, err) 1664 assert.NotNil(t, app) 1665 assert.NotNil(t, app.Operation) 1666 events, err := appServer.kubeclientset.CoreV1().Events(appServer.ns).List(context.Background(), metav1.ListOptions{}) 1667 assert.NoError(t, err) 1668 assert.Equal(t, "Unknown user initiated sync locally", events.Items[1].Message) 1669 } 1670 1671 func TestRollbackApp(t *testing.T) { 1672 testApp := newTestApp() 1673 testApp.Status.History = []appsv1.RevisionHistory{{ 1674 ID: 1, 1675 Revision: "abc", 1676 Source: *testApp.Spec.Source.DeepCopy(), 1677 }} 1678 appServer := newTestAppServer(t, testApp) 1679 1680 updatedApp, err := appServer.Rollback(context.Background(), &application.ApplicationRollbackRequest{ 1681 Name: &testApp.Name, 1682 Id: pointer.Int64(1), 1683 }) 1684 1685 assert.Nil(t, err) 1686 1687 assert.NotNil(t, updatedApp.Operation) 1688 assert.NotNil(t, updatedApp.Operation.Sync) 1689 assert.NotNil(t, updatedApp.Operation.Sync.Source) 1690 assert.Equal(t, "abc", updatedApp.Operation.Sync.Revision) 1691 } 1692 1693 func TestUpdateAppProject(t *testing.T) { 1694 testApp := newTestApp() 1695 ctx := context.Background() 1696 // nolint:staticcheck 1697 ctx = context.WithValue(ctx, "claims", &jwt.StandardClaims{Subject: "admin"}) 1698 appServer := newTestAppServer(t, testApp) 1699 appServer.enf.SetDefaultRole("") 1700 1701 t.Run("update without changing project", func(t *testing.T) { 1702 _ = appServer.enf.SetBuiltinPolicy(`p, admin, applications, update, default/test-app, allow`) 1703 _, err := appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp}) 1704 assert.NoError(t, err) 1705 }) 1706 1707 t.Run("cannot update to another project", func(t *testing.T) { 1708 testApp.Spec.Project = "my-proj" 1709 _, err := appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp}) 1710 assert.Equal(t, status.Code(err), codes.PermissionDenied) 1711 }) 1712 1713 t.Run("cannot change projects without create privileges", func(t *testing.T) { 1714 _ = appServer.enf.SetBuiltinPolicy(` 1715 p, admin, applications, update, default/test-app, allow 1716 p, admin, applications, update, my-proj/test-app, allow 1717 `) 1718 _, err := appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp}) 1719 statusErr := grpc.UnwrapGRPCStatus(err) 1720 assert.NotNil(t, statusErr) 1721 assert.Equal(t, codes.PermissionDenied, statusErr.Code()) 1722 }) 1723 1724 t.Run("cannot change projects without update privileges in new project", func(t *testing.T) { 1725 _ = appServer.enf.SetBuiltinPolicy(` 1726 p, admin, applications, update, default/test-app, allow 1727 p, admin, applications, create, my-proj/test-app, allow 1728 `) 1729 _, err := appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp}) 1730 assert.Equal(t, codes.PermissionDenied, status.Code(err)) 1731 }) 1732 1733 t.Run("cannot change projects without update privileges in old project", func(t *testing.T) { 1734 _ = appServer.enf.SetBuiltinPolicy(` 1735 p, admin, applications, create, my-proj/test-app, allow 1736 p, admin, applications, update, my-proj/test-app, allow 1737 `) 1738 _, err := appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp}) 1739 statusErr := grpc.UnwrapGRPCStatus(err) 1740 assert.NotNil(t, statusErr) 1741 assert.Equal(t, codes.PermissionDenied, statusErr.Code()) 1742 }) 1743 1744 t.Run("can update project with proper permissions", func(t *testing.T) { 1745 // Verify can update project with proper permissions 1746 _ = appServer.enf.SetBuiltinPolicy(` 1747 p, admin, applications, update, default/test-app, allow 1748 p, admin, applications, create, my-proj/test-app, allow 1749 p, admin, applications, update, my-proj/test-app, allow 1750 `) 1751 updatedApp, err := appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp}) 1752 assert.NoError(t, err) 1753 assert.Equal(t, "my-proj", updatedApp.Spec.Project) 1754 }) 1755 } 1756 1757 func TestAppJsonPatch(t *testing.T) { 1758 testApp := newTestAppWithAnnotations() 1759 ctx := context.Background() 1760 // nolint:staticcheck 1761 ctx = context.WithValue(ctx, "claims", &jwt.StandardClaims{Subject: "admin"}) 1762 appServer := newTestAppServer(t, testApp) 1763 appServer.enf.SetDefaultRole("") 1764 1765 app, err := appServer.Patch(ctx, &application.ApplicationPatchRequest{Name: &testApp.Name, Patch: pointer.String("garbage")}) 1766 assert.Error(t, err) 1767 assert.Nil(t, app) 1768 1769 app, err = appServer.Patch(ctx, &application.ApplicationPatchRequest{Name: &testApp.Name, Patch: pointer.String("[]")}) 1770 assert.NoError(t, err) 1771 assert.NotNil(t, app) 1772 1773 app, err = appServer.Patch(ctx, &application.ApplicationPatchRequest{Name: &testApp.Name, Patch: pointer.String(`[{"op": "replace", "path": "/spec/source/path", "value": "foo"}]`)}) 1774 assert.NoError(t, err) 1775 assert.Equal(t, "foo", app.Spec.Source.Path) 1776 1777 app, err = appServer.Patch(ctx, &application.ApplicationPatchRequest{Name: &testApp.Name, Patch: pointer.String(`[{"op": "remove", "path": "/metadata/annotations/test.annotation"}]`)}) 1778 assert.NoError(t, err) 1779 assert.NotContains(t, app.Annotations, "test.annotation") 1780 } 1781 1782 func TestAppMergePatch(t *testing.T) { 1783 testApp := newTestApp() 1784 ctx := context.Background() 1785 // nolint:staticcheck 1786 ctx = context.WithValue(ctx, "claims", &jwt.StandardClaims{Subject: "admin"}) 1787 appServer := newTestAppServer(t, testApp) 1788 appServer.enf.SetDefaultRole("") 1789 1790 app, err := appServer.Patch(ctx, &application.ApplicationPatchRequest{ 1791 Name: &testApp.Name, Patch: pointer.String(`{"spec": { "source": { "path": "foo" } }}`), PatchType: pointer.String("merge")}) 1792 assert.NoError(t, err) 1793 assert.Equal(t, "foo", app.Spec.Source.Path) 1794 } 1795 1796 func TestServer_GetApplicationSyncWindowsState(t *testing.T) { 1797 t.Run("Active", func(t *testing.T) { 1798 testApp := newTestApp() 1799 testApp.Spec.Project = "proj-maint" 1800 appServer := newTestAppServer(t, testApp) 1801 1802 active, err := appServer.GetApplicationSyncWindows(context.Background(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name}) 1803 assert.NoError(t, err) 1804 assert.Equal(t, 1, len(active.ActiveWindows)) 1805 }) 1806 t.Run("Inactive", func(t *testing.T) { 1807 testApp := newTestApp() 1808 testApp.Spec.Project = "default" 1809 appServer := newTestAppServer(t, testApp) 1810 1811 active, err := appServer.GetApplicationSyncWindows(context.Background(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name}) 1812 assert.NoError(t, err) 1813 assert.Equal(t, 0, len(active.ActiveWindows)) 1814 }) 1815 t.Run("ProjectDoesNotExist", func(t *testing.T) { 1816 testApp := newTestApp() 1817 testApp.Spec.Project = "none" 1818 appServer := newTestAppServer(t, testApp) 1819 1820 active, err := appServer.GetApplicationSyncWindows(context.Background(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name}) 1821 assert.Contains(t, err.Error(), "not exist") 1822 assert.Nil(t, active) 1823 }) 1824 } 1825 1826 func TestGetCachedAppState(t *testing.T) { 1827 testApp := newTestApp() 1828 testApp.ObjectMeta.ResourceVersion = "1" 1829 testApp.Spec.Project = "test-proj" 1830 testProj := &appsv1.AppProject{ 1831 ObjectMeta: metav1.ObjectMeta{ 1832 Name: "test-proj", 1833 Namespace: testNamespace, 1834 }, 1835 } 1836 appServer := newTestAppServer(t, testApp, testProj) 1837 fakeClientSet := appServer.appclientset.(*apps.Clientset) 1838 fakeClientSet.AddReactor("get", "applications", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { 1839 return true, &appsv1.Application{Spec: appsv1.ApplicationSpec{Source: &appsv1.ApplicationSource{}}}, nil 1840 }) 1841 t.Run("NoError", func(t *testing.T) { 1842 err := appServer.getCachedAppState(context.Background(), testApp, func() error { 1843 return nil 1844 }) 1845 assert.NoError(t, err) 1846 }) 1847 t.Run("CacheMissErrorTriggersRefresh", func(t *testing.T) { 1848 retryCount := 0 1849 patched := false 1850 watcher := watch.NewFakeWithChanSize(1, true) 1851 1852 // Configure fakeClientSet within lock, before requesting cached app state, to avoid data race 1853 { 1854 fakeClientSet.Lock() 1855 fakeClientSet.ReactionChain = nil 1856 fakeClientSet.WatchReactionChain = nil 1857 fakeClientSet.AddReactor("patch", "applications", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { 1858 patched = true 1859 updated := testApp.DeepCopy() 1860 updated.ResourceVersion = "2" 1861 appServer.appBroadcaster.OnUpdate(testApp, updated) 1862 return true, testApp, nil 1863 }) 1864 fakeClientSet.AddReactor("get", "applications", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { 1865 return true, &appsv1.Application{Spec: appsv1.ApplicationSpec{Source: &appsv1.ApplicationSource{}}}, nil 1866 }) 1867 fakeClientSet.Unlock() 1868 fakeClientSet.AddWatchReactor("applications", func(action kubetesting.Action) (handled bool, ret watch.Interface, err error) { 1869 return true, watcher, nil 1870 }) 1871 } 1872 1873 err := appServer.getCachedAppState(context.Background(), testApp, func() error { 1874 res := cache.ErrCacheMiss 1875 if retryCount == 1 { 1876 res = nil 1877 } 1878 retryCount++ 1879 return res 1880 }) 1881 assert.Equal(t, nil, err) 1882 assert.Equal(t, 2, retryCount) 1883 assert.True(t, patched) 1884 }) 1885 1886 t.Run("NonCacheErrorDoesNotTriggerRefresh", func(t *testing.T) { 1887 randomError := coreerrors.New("random error") 1888 err := appServer.getCachedAppState(context.Background(), testApp, func() error { 1889 return randomError 1890 }) 1891 assert.Equal(t, randomError, err) 1892 }) 1893 } 1894 1895 func TestSplitStatusPatch(t *testing.T) { 1896 specPatch := `{"spec":{"aaa":"bbb"}}` 1897 statusPatch := `{"status":{"ccc":"ddd"}}` 1898 { 1899 nonStatus, status, err := splitStatusPatch([]byte(specPatch)) 1900 assert.NoError(t, err) 1901 assert.Equal(t, specPatch, string(nonStatus)) 1902 assert.Nil(t, status) 1903 } 1904 { 1905 nonStatus, status, err := splitStatusPatch([]byte(statusPatch)) 1906 assert.NoError(t, err) 1907 assert.Nil(t, nonStatus) 1908 assert.Equal(t, statusPatch, string(status)) 1909 } 1910 { 1911 bothPatch := `{"spec":{"aaa":"bbb"},"status":{"ccc":"ddd"}}` 1912 nonStatus, status, err := splitStatusPatch([]byte(bothPatch)) 1913 assert.NoError(t, err) 1914 assert.Equal(t, specPatch, string(nonStatus)) 1915 assert.Equal(t, statusPatch, string(status)) 1916 } 1917 { 1918 otherFields := `{"operation":{"eee":"fff"},"spec":{"aaa":"bbb"},"status":{"ccc":"ddd"}}` 1919 nonStatus, status, err := splitStatusPatch([]byte(otherFields)) 1920 assert.NoError(t, err) 1921 assert.Equal(t, `{"operation":{"eee":"fff"},"spec":{"aaa":"bbb"}}`, string(nonStatus)) 1922 assert.Equal(t, statusPatch, string(status)) 1923 } 1924 } 1925 1926 func TestLogsGetSelectedPod(t *testing.T) { 1927 deployment := appsv1.ResourceRef{Group: "", Version: "v1", Kind: "Deployment", Name: "deployment", UID: "1"} 1928 rs := appsv1.ResourceRef{Group: "", Version: "v1", Kind: "ReplicaSet", Name: "rs", UID: "2"} 1929 podRS := appsv1.ResourceRef{Group: "", Version: "v1", Kind: "Pod", Name: "podrs", UID: "3"} 1930 pod := appsv1.ResourceRef{Group: "", Version: "v1", Kind: "Pod", Name: "pod", UID: "4"} 1931 treeNodes := []appsv1.ResourceNode{ 1932 {ResourceRef: deployment, ParentRefs: nil}, 1933 {ResourceRef: rs, ParentRefs: []appsv1.ResourceRef{deployment}}, 1934 {ResourceRef: podRS, ParentRefs: []appsv1.ResourceRef{rs}}, 1935 {ResourceRef: pod, ParentRefs: nil}, 1936 } 1937 appName := "appName" 1938 1939 t.Run("GetAllPods", func(t *testing.T) { 1940 podQuery := application.ApplicationPodLogsQuery{ 1941 Name: &appName, 1942 } 1943 pods := getSelectedPods(treeNodes, &podQuery) 1944 assert.Equal(t, 2, len(pods)) 1945 }) 1946 1947 t.Run("GetRSPods", func(t *testing.T) { 1948 group := "" 1949 kind := "ReplicaSet" 1950 name := "rs" 1951 podQuery := application.ApplicationPodLogsQuery{ 1952 Name: &appName, 1953 Group: &group, 1954 Kind: &kind, 1955 ResourceName: &name, 1956 } 1957 pods := getSelectedPods(treeNodes, &podQuery) 1958 assert.Equal(t, 1, len(pods)) 1959 }) 1960 1961 t.Run("GetDeploymentPods", func(t *testing.T) { 1962 group := "" 1963 kind := "Deployment" 1964 name := "deployment" 1965 podQuery := application.ApplicationPodLogsQuery{ 1966 Name: &appName, 1967 Group: &group, 1968 Kind: &kind, 1969 ResourceName: &name, 1970 } 1971 pods := getSelectedPods(treeNodes, &podQuery) 1972 assert.Equal(t, 1, len(pods)) 1973 }) 1974 1975 t.Run("NoMatchingPods", func(t *testing.T) { 1976 group := "" 1977 kind := "Service" 1978 name := "service" 1979 podQuery := application.ApplicationPodLogsQuery{ 1980 Name: &appName, 1981 Group: &group, 1982 Kind: &kind, 1983 ResourceName: &name, 1984 } 1985 pods := getSelectedPods(treeNodes, &podQuery) 1986 assert.Equal(t, 0, len(pods)) 1987 }) 1988 } 1989 1990 // refreshAnnotationRemover runs an infinite loop until it detects and removes refresh annotation or given context is done 1991 func refreshAnnotationRemover(t *testing.T, ctx context.Context, patched *int32, appServer *Server, appName string, ch chan string) { 1992 for ctx.Err() == nil { 1993 aName, appNs := argo.ParseFromQualifiedName(appName, appServer.ns) 1994 a, err := appServer.appLister.Applications(appNs).Get(aName) 1995 require.NoError(t, err) 1996 a = a.DeepCopy() 1997 if a.GetAnnotations() != nil && a.GetAnnotations()[appsv1.AnnotationKeyRefresh] != "" { 1998 a.SetAnnotations(map[string]string{}) 1999 a.SetResourceVersion("999") 2000 _, err = appServer.appclientset.ArgoprojV1alpha1().Applications(a.Namespace).Update( 2001 context.Background(), a, metav1.UpdateOptions{}) 2002 require.NoError(t, err) 2003 atomic.AddInt32(patched, 1) 2004 ch <- "" 2005 } 2006 time.Sleep(100 * time.Millisecond) 2007 } 2008 } 2009 2010 func TestGetAppRefresh_NormalRefresh(t *testing.T) { 2011 ctx, cancel := context.WithCancel(context.Background()) 2012 defer cancel() 2013 testApp := newTestApp() 2014 testApp.ObjectMeta.ResourceVersion = "1" 2015 appServer := newTestAppServer(t, testApp) 2016 2017 var patched int32 2018 2019 ch := make(chan string, 1) 2020 2021 go refreshAnnotationRemover(t, ctx, &patched, appServer, testApp.Name, ch) 2022 2023 _, err := appServer.Get(context.Background(), &application.ApplicationQuery{ 2024 Name: &testApp.Name, 2025 Refresh: pointer.String(string(appsv1.RefreshTypeNormal)), 2026 }) 2027 assert.NoError(t, err) 2028 2029 select { 2030 case <-ch: 2031 assert.Equal(t, atomic.LoadInt32(&patched), int32(1)) 2032 case <-time.After(10 * time.Second): 2033 assert.Fail(t, "Out of time ( 10 seconds )") 2034 } 2035 2036 } 2037 2038 func TestGetAppRefresh_HardRefresh(t *testing.T) { 2039 ctx, cancel := context.WithCancel(context.Background()) 2040 defer cancel() 2041 testApp := newTestApp() 2042 testApp.ObjectMeta.ResourceVersion = "1" 2043 appServer := newTestAppServer(t, testApp) 2044 2045 var getAppDetailsQuery *apiclient.RepoServerAppDetailsQuery 2046 mockRepoServiceClient := mocks.RepoServerServiceClient{} 2047 mockRepoServiceClient.On("GetAppDetails", mock.Anything, mock.MatchedBy(func(q *apiclient.RepoServerAppDetailsQuery) bool { 2048 getAppDetailsQuery = q 2049 return true 2050 })).Return(&apiclient.RepoAppDetailsResponse{}, nil) 2051 appServer.repoClientset = &mocks.Clientset{RepoServerServiceClient: &mockRepoServiceClient} 2052 2053 var patched int32 2054 2055 ch := make(chan string, 1) 2056 2057 go refreshAnnotationRemover(t, ctx, &patched, appServer, testApp.Name, ch) 2058 2059 _, err := appServer.Get(context.Background(), &application.ApplicationQuery{ 2060 Name: &testApp.Name, 2061 Refresh: pointer.String(string(appsv1.RefreshTypeHard)), 2062 }) 2063 assert.NoError(t, err) 2064 require.NotNil(t, getAppDetailsQuery) 2065 assert.True(t, getAppDetailsQuery.NoCache) 2066 assert.Equal(t, testApp.Spec.Source, getAppDetailsQuery.Source) 2067 2068 assert.NoError(t, err) 2069 select { 2070 case <-ch: 2071 assert.Equal(t, atomic.LoadInt32(&patched), int32(1)) 2072 case <-time.After(10 * time.Second): 2073 assert.Fail(t, "Out of time ( 10 seconds )") 2074 } 2075 } 2076 2077 func TestInferResourcesStatusHealth(t *testing.T) { 2078 cacheClient := cacheutil.NewCache(cacheutil.NewInMemoryCache(1 * time.Hour)) 2079 2080 testApp := newTestApp() 2081 testApp.Status.ResourceHealthSource = appsv1.ResourceHealthLocationAppTree 2082 testApp.Status.Resources = []appsv1.ResourceStatus{{ 2083 Group: "apps", 2084 Kind: "Deployment", 2085 Name: "guestbook", 2086 Namespace: "default", 2087 }, { 2088 Group: "apps", 2089 Kind: "StatefulSet", 2090 Name: "guestbook-stateful", 2091 Namespace: "default", 2092 }} 2093 appServer := newTestAppServer(t, testApp) 2094 appStateCache := appstate.NewCache(cacheClient, time.Minute) 2095 err := appStateCache.SetAppResourcesTree(testApp.Name, &appsv1.ApplicationTree{Nodes: []appsv1.ResourceNode{{ 2096 ResourceRef: appsv1.ResourceRef{ 2097 Group: "apps", 2098 Kind: "Deployment", 2099 Name: "guestbook", 2100 Namespace: "default", 2101 }, 2102 Health: &appsv1.HealthStatus{ 2103 Status: health.HealthStatusDegraded, 2104 }, 2105 }}}) 2106 2107 require.NoError(t, err) 2108 2109 appServer.cache = servercache.NewCache(appStateCache, time.Minute, time.Minute, time.Minute) 2110 2111 appServer.inferResourcesStatusHealth(testApp) 2112 2113 assert.Equal(t, health.HealthStatusDegraded, testApp.Status.Resources[0].Health.Status) 2114 assert.Nil(t, testApp.Status.Resources[1].Health) 2115 } 2116 2117 func TestRunNewStyleResourceAction(t *testing.T) { 2118 cacheClient := cacheutil.NewCache(cacheutil.NewInMemoryCache(1 * time.Hour)) 2119 2120 group := "batch" 2121 kind := "CronJob" 2122 version := "v1" 2123 resourceName := "my-cron-job" 2124 namespace := testNamespace 2125 action := "create-job" 2126 uid := "1" 2127 2128 resources := []appsv1.ResourceStatus{{ 2129 Group: group, 2130 Kind: kind, 2131 Name: resourceName, 2132 Namespace: testNamespace, 2133 Version: version, 2134 }} 2135 2136 appStateCache := appstate.NewCache(cacheClient, time.Minute) 2137 2138 nodes := []appsv1.ResourceNode{{ 2139 ResourceRef: appsv1.ResourceRef{ 2140 Group: group, 2141 Kind: kind, 2142 Version: version, 2143 Name: resourceName, 2144 Namespace: testNamespace, 2145 UID: uid, 2146 }, 2147 }} 2148 2149 createJobDenyingProj := &appsv1.AppProject{ 2150 ObjectMeta: metav1.ObjectMeta{Name: "createJobDenyingProj", Namespace: "default"}, 2151 Spec: appsv1.AppProjectSpec{ 2152 SourceRepos: []string{"*"}, 2153 Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 2154 NamespaceResourceWhitelist: []metav1.GroupKind{{Group: "never", Kind: "mind"}}, 2155 }, 2156 } 2157 2158 cronJob := k8sbatchv1.CronJob{ 2159 TypeMeta: metav1.TypeMeta{ 2160 APIVersion: "batch/v1", 2161 Kind: "CronJob", 2162 }, 2163 ObjectMeta: metav1.ObjectMeta{ 2164 Name: "my-cron-job", 2165 Namespace: testNamespace, 2166 Labels: map[string]string{ 2167 "some": "label", 2168 }, 2169 }, 2170 Spec: k8sbatchv1.CronJobSpec{ 2171 Schedule: "* * * * *", 2172 JobTemplate: k8sbatchv1.JobTemplateSpec{ 2173 Spec: k8sbatchv1.JobSpec{ 2174 Template: corev1.PodTemplateSpec{ 2175 Spec: corev1.PodSpec{ 2176 Containers: []corev1.Container{ 2177 { 2178 Name: "hello", 2179 Image: "busybox:1.28", 2180 ImagePullPolicy: "IfNotPresent", 2181 Command: []string{"/bin/sh", "-c", "date; echo Hello from the Kubernetes cluster"}, 2182 }, 2183 }, 2184 RestartPolicy: corev1.RestartPolicyOnFailure, 2185 }, 2186 }, 2187 }, 2188 }, 2189 }, 2190 } 2191 2192 t.Run("CreateOperationNotPermitted", func(t *testing.T) { 2193 testApp := newTestApp() 2194 testApp.Spec.Project = "createJobDenyingProj" 2195 testApp.Status.ResourceHealthSource = appsv1.ResourceHealthLocationAppTree 2196 testApp.Status.Resources = resources 2197 2198 appServer := newTestAppServer(t, testApp, createJobDenyingProj, kube.MustToUnstructured(&cronJob)) 2199 appServer.cache = servercache.NewCache(appStateCache, time.Minute, time.Minute, time.Minute) 2200 2201 err := appStateCache.SetAppResourcesTree(testApp.Name, &appsv1.ApplicationTree{Nodes: nodes}) 2202 require.NoError(t, err) 2203 2204 appResponse, runErr := appServer.RunResourceAction(context.Background(), &application.ResourceActionRunRequest{ 2205 Name: &testApp.Name, 2206 Namespace: &namespace, 2207 Action: &action, 2208 AppNamespace: &testApp.Namespace, 2209 ResourceName: &resourceName, 2210 Version: &version, 2211 Group: &group, 2212 Kind: &kind, 2213 }) 2214 2215 assert.Contains(t, runErr.Error(), "is not permitted to manage") 2216 assert.Nil(t, appResponse) 2217 }) 2218 2219 t.Run("CreateOperationPermitted", func(t *testing.T) { 2220 testApp := newTestApp() 2221 testApp.Status.ResourceHealthSource = appsv1.ResourceHealthLocationAppTree 2222 testApp.Status.Resources = resources 2223 2224 appServer := newTestAppServer(t, testApp, kube.MustToUnstructured(&cronJob)) 2225 appServer.cache = servercache.NewCache(appStateCache, time.Minute, time.Minute, time.Minute) 2226 2227 err := appStateCache.SetAppResourcesTree(testApp.Name, &appsv1.ApplicationTree{Nodes: nodes}) 2228 require.NoError(t, err) 2229 2230 appResponse, runErr := appServer.RunResourceAction(context.Background(), &application.ResourceActionRunRequest{ 2231 Name: &testApp.Name, 2232 Namespace: &namespace, 2233 Action: &action, 2234 AppNamespace: &testApp.Namespace, 2235 ResourceName: &resourceName, 2236 Version: &version, 2237 Group: &group, 2238 Kind: &kind, 2239 }) 2240 2241 require.NoError(t, runErr) 2242 assert.NotNil(t, appResponse) 2243 }) 2244 } 2245 2246 func TestRunOldStyleResourceAction(t *testing.T) { 2247 cacheClient := cacheutil.NewCache(cacheutil.NewInMemoryCache(1 * time.Hour)) 2248 2249 group := "apps" 2250 kind := "Deployment" 2251 version := "v1" 2252 resourceName := "nginx-deploy" 2253 namespace := testNamespace 2254 action := "pause" 2255 uid := "2" 2256 2257 resources := []appsv1.ResourceStatus{{ 2258 Group: group, 2259 Kind: kind, 2260 Name: resourceName, 2261 Namespace: testNamespace, 2262 Version: version, 2263 }} 2264 2265 appStateCache := appstate.NewCache(cacheClient, time.Minute) 2266 2267 nodes := []appsv1.ResourceNode{{ 2268 ResourceRef: appsv1.ResourceRef{ 2269 Group: group, 2270 Kind: kind, 2271 Version: version, 2272 Name: resourceName, 2273 Namespace: testNamespace, 2274 UID: uid, 2275 }, 2276 }} 2277 2278 deployment := k8sappsv1.Deployment{ 2279 TypeMeta: metav1.TypeMeta{ 2280 APIVersion: "apps/v1", 2281 Kind: "Deployment", 2282 }, 2283 ObjectMeta: metav1.ObjectMeta{ 2284 Name: "nginx-deploy", 2285 Namespace: testNamespace, 2286 }, 2287 } 2288 2289 t.Run("DefaultPatchOperation", func(t *testing.T) { 2290 testApp := newTestApp() 2291 testApp.Status.ResourceHealthSource = appsv1.ResourceHealthLocationAppTree 2292 testApp.Status.Resources = resources 2293 2294 // appServer := newTestAppServer(t, testApp, returnDeployment()) 2295 appServer := newTestAppServer(t, testApp, kube.MustToUnstructured(&deployment)) 2296 appServer.cache = servercache.NewCache(appStateCache, time.Minute, time.Minute, time.Minute) 2297 2298 err := appStateCache.SetAppResourcesTree(testApp.Name, &appsv1.ApplicationTree{Nodes: nodes}) 2299 require.NoError(t, err) 2300 2301 appResponse, runErr := appServer.RunResourceAction(context.Background(), &application.ResourceActionRunRequest{ 2302 Name: &testApp.Name, 2303 Namespace: &namespace, 2304 Action: &action, 2305 AppNamespace: &testApp.Namespace, 2306 ResourceName: &resourceName, 2307 Version: &version, 2308 Group: &group, 2309 Kind: &kind, 2310 }) 2311 2312 require.NoError(t, runErr) 2313 assert.NotNil(t, appResponse) 2314 }) 2315 } 2316 2317 func TestIsApplicationPermitted(t *testing.T) { 2318 t.Run("Incorrect project", func(t *testing.T) { 2319 testApp := newTestApp() 2320 appServer := newTestAppServer(t, testApp) 2321 projects := map[string]bool{"test-app": false} 2322 permitted := appServer.isApplicationPermitted(labels.Everything(), 0, nil, "test", "default", projects, *testApp) 2323 assert.False(t, permitted) 2324 }) 2325 2326 t.Run("Version is incorrect", func(t *testing.T) { 2327 testApp := newTestApp() 2328 appServer := newTestAppServer(t, testApp) 2329 minVersion := 100000 2330 testApp.ResourceVersion = strconv.Itoa(minVersion - 1) 2331 permitted := appServer.isApplicationPermitted(labels.Everything(), minVersion, nil, "test", "default", nil, *testApp) 2332 assert.False(t, permitted) 2333 }) 2334 2335 t.Run("Application name is incorrect", func(t *testing.T) { 2336 testApp := newTestApp() 2337 appServer := newTestAppServer(t, testApp) 2338 appName := "test" 2339 permitted := appServer.isApplicationPermitted(labels.Everything(), 0, nil, appName, "default", nil, *testApp) 2340 assert.False(t, permitted) 2341 }) 2342 2343 t.Run("Application namespace is incorrect", func(t *testing.T) { 2344 testApp := newTestApp() 2345 appServer := newTestAppServer(t, testApp) 2346 permitted := appServer.isApplicationPermitted(labels.Everything(), 0, nil, testApp.Name, "demo", nil, *testApp) 2347 assert.False(t, permitted) 2348 }) 2349 2350 t.Run("Application is not part of enabled namespace", func(t *testing.T) { 2351 testApp := newTestApp() 2352 appServer := newTestAppServer(t, testApp) 2353 appServer.ns = "server-ns" 2354 appServer.enabledNamespaces = []string{"demo"} 2355 permitted := appServer.isApplicationPermitted(labels.Everything(), 0, nil, testApp.Name, testApp.Namespace, nil, *testApp) 2356 assert.False(t, permitted) 2357 }) 2358 2359 t.Run("Application is part of enabled namespace", func(t *testing.T) { 2360 testApp := newTestApp() 2361 appServer := newTestAppServer(t, testApp) 2362 appServer.ns = "server-ns" 2363 appServer.enabledNamespaces = []string{testApp.Namespace} 2364 permitted := appServer.isApplicationPermitted(labels.Everything(), 0, nil, testApp.Name, testApp.Namespace, nil, *testApp) 2365 assert.True(t, permitted) 2366 }) 2367 } 2368 2369 func TestAppNamespaceRestrictions(t *testing.T) { 2370 t.Run("List applications in controller namespace", func(t *testing.T) { 2371 testApp := newTestApp() 2372 appServer := newTestAppServer(t, testApp) 2373 apps, err := appServer.List(context.TODO(), &application.ApplicationQuery{}) 2374 require.NoError(t, err) 2375 require.Len(t, apps.Items, 1) 2376 }) 2377 2378 t.Run("List applications with non-allowed apps existing", func(t *testing.T) { 2379 testApp1 := newTestApp() 2380 testApp1.Namespace = "argocd-1" 2381 appServer := newTestAppServer(t, testApp1) 2382 apps, err := appServer.List(context.TODO(), &application.ApplicationQuery{}) 2383 require.NoError(t, err) 2384 require.Len(t, apps.Items, 0) 2385 }) 2386 2387 t.Run("List applications with non-allowed apps existing and explicit ns request", func(t *testing.T) { 2388 testApp1 := newTestApp() 2389 testApp2 := newTestApp() 2390 testApp2.Namespace = "argocd-1" 2391 appServer := newTestAppServer(t, testApp1, testApp2) 2392 apps, err := appServer.List(context.TODO(), &application.ApplicationQuery{AppNamespace: pointer.String("argocd-1")}) 2393 require.NoError(t, err) 2394 require.Len(t, apps.Items, 0) 2395 }) 2396 2397 t.Run("List applications with allowed apps in other namespaces", func(t *testing.T) { 2398 testApp1 := newTestApp() 2399 testApp1.Namespace = "argocd-1" 2400 appServer := newTestAppServer(t, testApp1) 2401 appServer.enabledNamespaces = []string{"argocd-1"} 2402 apps, err := appServer.List(context.TODO(), &application.ApplicationQuery{}) 2403 require.NoError(t, err) 2404 require.Len(t, apps.Items, 1) 2405 }) 2406 2407 t.Run("Get application in control plane namespace", func(t *testing.T) { 2408 testApp := newTestApp() 2409 appServer := newTestAppServer(t, testApp) 2410 app, err := appServer.Get(context.TODO(), &application.ApplicationQuery{ 2411 Name: pointer.String("test-app"), 2412 }) 2413 require.NoError(t, err) 2414 assert.Equal(t, "test-app", app.GetName()) 2415 }) 2416 t.Run("Get application in other namespace when forbidden", func(t *testing.T) { 2417 testApp := newTestApp() 2418 testApp.Namespace = "argocd-1" 2419 appServer := newTestAppServer(t, testApp) 2420 app, err := appServer.Get(context.TODO(), &application.ApplicationQuery{ 2421 Name: pointer.String("test-app"), 2422 AppNamespace: pointer.String("argocd-1"), 2423 }) 2424 require.Error(t, err) 2425 require.ErrorContains(t, err, "permission denied") 2426 require.Nil(t, app) 2427 }) 2428 t.Run("Get application in other namespace when allowed", func(t *testing.T) { 2429 testApp := newTestApp() 2430 testApp.Namespace = "argocd-1" 2431 testApp.Spec.Project = "other-ns" 2432 otherNsProj := &appsv1.AppProject{ 2433 ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, 2434 Spec: appsv1.AppProjectSpec{ 2435 SourceRepos: []string{"*"}, 2436 Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 2437 SourceNamespaces: []string{"argocd-1"}, 2438 }, 2439 } 2440 appServer := newTestAppServer(t, testApp, otherNsProj) 2441 appServer.enabledNamespaces = []string{"argocd-1"} 2442 app, err := appServer.Get(context.TODO(), &application.ApplicationQuery{ 2443 Name: pointer.String("test-app"), 2444 AppNamespace: pointer.String("argocd-1"), 2445 }) 2446 require.NoError(t, err) 2447 require.NotNil(t, app) 2448 require.Equal(t, "argocd-1", app.Namespace) 2449 require.Equal(t, "test-app", app.Name) 2450 }) 2451 t.Run("Get application in other namespace when project is not allowed", func(t *testing.T) { 2452 testApp := newTestApp() 2453 testApp.Namespace = "argocd-1" 2454 testApp.Spec.Project = "other-ns" 2455 otherNsProj := &appsv1.AppProject{ 2456 ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, 2457 Spec: appsv1.AppProjectSpec{ 2458 SourceRepos: []string{"*"}, 2459 Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 2460 SourceNamespaces: []string{"argocd-2"}, 2461 }, 2462 } 2463 appServer := newTestAppServer(t, testApp, otherNsProj) 2464 appServer.enabledNamespaces = []string{"argocd-1"} 2465 app, err := appServer.Get(context.TODO(), &application.ApplicationQuery{ 2466 Name: pointer.String("test-app"), 2467 AppNamespace: pointer.String("argocd-1"), 2468 }) 2469 require.Error(t, err) 2470 require.Nil(t, app) 2471 require.ErrorContains(t, err, "app is not allowed in project") 2472 }) 2473 t.Run("Create application in other namespace when allowed", func(t *testing.T) { 2474 testApp := newTestApp() 2475 testApp.Namespace = "argocd-1" 2476 testApp.Spec.Project = "other-ns" 2477 otherNsProj := &appsv1.AppProject{ 2478 ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, 2479 Spec: appsv1.AppProjectSpec{ 2480 SourceRepos: []string{"*"}, 2481 Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 2482 SourceNamespaces: []string{"argocd-1"}, 2483 }, 2484 } 2485 appServer := newTestAppServer(t, otherNsProj) 2486 appServer.enabledNamespaces = []string{"argocd-1"} 2487 app, err := appServer.Create(context.TODO(), &application.ApplicationCreateRequest{ 2488 Application: testApp, 2489 }) 2490 require.NoError(t, err) 2491 require.NotNil(t, app) 2492 assert.Equal(t, "test-app", app.Name) 2493 assert.Equal(t, "argocd-1", app.Namespace) 2494 }) 2495 2496 t.Run("Create application in other namespace when not allowed by project", func(t *testing.T) { 2497 testApp := newTestApp() 2498 testApp.Namespace = "argocd-1" 2499 testApp.Spec.Project = "other-ns" 2500 otherNsProj := &appsv1.AppProject{ 2501 ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, 2502 Spec: appsv1.AppProjectSpec{ 2503 SourceRepos: []string{"*"}, 2504 Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 2505 SourceNamespaces: []string{}, 2506 }, 2507 } 2508 appServer := newTestAppServer(t, otherNsProj) 2509 appServer.enabledNamespaces = []string{"argocd-1"} 2510 app, err := appServer.Create(context.TODO(), &application.ApplicationCreateRequest{ 2511 Application: testApp, 2512 }) 2513 require.Error(t, err) 2514 require.Nil(t, app) 2515 require.ErrorContains(t, err, "app is not allowed in project") 2516 }) 2517 2518 t.Run("Create application in other namespace when not allowed by configuration", func(t *testing.T) { 2519 testApp := newTestApp() 2520 testApp.Namespace = "argocd-1" 2521 testApp.Spec.Project = "other-ns" 2522 otherNsProj := &appsv1.AppProject{ 2523 ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, 2524 Spec: appsv1.AppProjectSpec{ 2525 SourceRepos: []string{"*"}, 2526 Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 2527 SourceNamespaces: []string{"argocd-1"}, 2528 }, 2529 } 2530 appServer := newTestAppServer(t, otherNsProj) 2531 appServer.enabledNamespaces = []string{"argocd-2"} 2532 app, err := appServer.Create(context.TODO(), &application.ApplicationCreateRequest{ 2533 Application: testApp, 2534 }) 2535 require.Error(t, err) 2536 require.Nil(t, app) 2537 require.ErrorContains(t, err, "namespace 'argocd-1' is not permitted") 2538 }) 2539 t.Run("Get application sync window in other namespace when project is allowed", func(t *testing.T) { 2540 testApp := newTestApp() 2541 testApp.Namespace = "argocd-1" 2542 testApp.Spec.Project = "other-ns" 2543 otherNsProj := &appsv1.AppProject{ 2544 ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, 2545 Spec: appsv1.AppProjectSpec{ 2546 SourceRepos: []string{"*"}, 2547 Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 2548 SourceNamespaces: []string{"argocd-1"}, 2549 }, 2550 } 2551 appServer := newTestAppServer(t, testApp, otherNsProj) 2552 appServer.enabledNamespaces = []string{"argocd-1"} 2553 active, err := appServer.GetApplicationSyncWindows(context.TODO(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name, AppNamespace: &testApp.Namespace}) 2554 assert.NoError(t, err) 2555 assert.Equal(t, 0, len(active.ActiveWindows)) 2556 }) 2557 t.Run("Get application sync window in other namespace when project is not allowed", func(t *testing.T) { 2558 testApp := newTestApp() 2559 testApp.Namespace = "argocd-1" 2560 testApp.Spec.Project = "other-ns" 2561 otherNsProj := &appsv1.AppProject{ 2562 ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, 2563 Spec: appsv1.AppProjectSpec{ 2564 SourceRepos: []string{"*"}, 2565 Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 2566 SourceNamespaces: []string{"argocd-2"}, 2567 }, 2568 } 2569 appServer := newTestAppServer(t, testApp, otherNsProj) 2570 appServer.enabledNamespaces = []string{"argocd-1"} 2571 active, err := appServer.GetApplicationSyncWindows(context.TODO(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name, AppNamespace: &testApp.Namespace}) 2572 require.Error(t, err) 2573 require.Nil(t, active) 2574 require.ErrorContains(t, err, "app is not allowed in project") 2575 }) 2576 t.Run("Get list of links in other namespace when project is not allowed", func(t *testing.T) { 2577 testApp := newTestApp() 2578 testApp.Namespace = "argocd-1" 2579 testApp.Spec.Project = "other-ns" 2580 otherNsProj := &appsv1.AppProject{ 2581 ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, 2582 Spec: appsv1.AppProjectSpec{ 2583 SourceRepos: []string{"*"}, 2584 Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 2585 SourceNamespaces: []string{"argocd-2"}, 2586 }, 2587 } 2588 appServer := newTestAppServer(t, testApp, otherNsProj) 2589 appServer.enabledNamespaces = []string{"argocd-1"} 2590 links, err := appServer.ListLinks(context.TODO(), &application.ListAppLinksRequest{ 2591 Name: pointer.String("test-app"), 2592 Namespace: pointer.String("argocd-1"), 2593 }) 2594 require.Error(t, err) 2595 require.Nil(t, links) 2596 require.ErrorContains(t, err, "app is not allowed in project") 2597 }) 2598 t.Run("Get list of links in other namespace when project is allowed", func(t *testing.T) { 2599 testApp := newTestApp() 2600 testApp.Namespace = "argocd-1" 2601 testApp.Spec.Project = "other-ns" 2602 otherNsProj := &appsv1.AppProject{ 2603 ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"}, 2604 Spec: appsv1.AppProjectSpec{ 2605 SourceRepos: []string{"*"}, 2606 Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 2607 SourceNamespaces: []string{"argocd-1"}, 2608 }, 2609 } 2610 appServer := newTestAppServer(t, testApp, otherNsProj) 2611 appServer.enabledNamespaces = []string{"argocd-1"} 2612 links, err := appServer.ListLinks(context.TODO(), &application.ListAppLinksRequest{ 2613 Name: pointer.String("test-app"), 2614 Namespace: pointer.String("argocd-1"), 2615 }) 2616 require.NoError(t, err) 2617 assert.Equal(t, 0, len(links.Items)) 2618 }) 2619 }