github.com/argoproj/argo-cd/v3@v3.2.1/util/argo/argo_test.go (about) 1 package argo 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "path/filepath" 8 "testing" 9 10 "github.com/argoproj/gitops-engine/pkg/utils/kube" 11 "github.com/argoproj/gitops-engine/pkg/utils/kube/kubetest" 12 "github.com/stretchr/testify/assert" 13 "github.com/stretchr/testify/mock" 14 "github.com/stretchr/testify/require" 15 "google.golang.org/grpc/codes" 16 "google.golang.org/grpc/status" 17 corev1 "k8s.io/api/core/v1" 18 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 19 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 20 "k8s.io/apimachinery/pkg/runtime/schema" 21 "k8s.io/client-go/kubernetes/fake" 22 "k8s.io/client-go/tools/cache" 23 24 "github.com/argoproj/gitops-engine/pkg/sync/common" 25 26 argoappv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" 27 appclientset "github.com/argoproj/argo-cd/v3/pkg/client/clientset/versioned/fake" 28 "github.com/argoproj/argo-cd/v3/pkg/client/informers/externalversions/application/v1alpha1" 29 applisters "github.com/argoproj/argo-cd/v3/pkg/client/listers/application/v1alpha1" 30 "github.com/argoproj/argo-cd/v3/reposerver/apiclient" 31 "github.com/argoproj/argo-cd/v3/reposerver/apiclient/mocks" 32 "github.com/argoproj/argo-cd/v3/test" 33 "github.com/argoproj/argo-cd/v3/util/db" 34 dbmocks "github.com/argoproj/argo-cd/v3/util/db/mocks" 35 "github.com/argoproj/argo-cd/v3/util/settings" 36 ) 37 38 func TestRefreshApp(t *testing.T) { 39 var testApp argoappv1.Application 40 testApp.Name = "test-app" 41 testApp.Namespace = "default" 42 appClientset := appclientset.NewSimpleClientset(&testApp) 43 appIf := appClientset.ArgoprojV1alpha1().Applications("default") 44 _, err := RefreshApp(appIf, "test-app", argoappv1.RefreshTypeNormal, true) 45 require.NoError(t, err) 46 // For some reason, the fake Application interface doesn't reflect the patch status after Patch(), 47 // so can't verify it was set in unit tests. 48 // _, ok := newApp.Annotations[common.AnnotationKeyRefresh] 49 // assert.True(t, ok) 50 } 51 52 func TestGetAppProjectWithNoProjDefined(t *testing.T) { 53 projName := "default" 54 namespace := "default" 55 56 cm := corev1.ConfigMap{ 57 ObjectMeta: metav1.ObjectMeta{ 58 Name: "argocd-cm", 59 Namespace: test.FakeArgoCDNamespace, 60 Labels: map[string]string{ 61 "app.kubernetes.io/part-of": "argocd", 62 }, 63 }, 64 } 65 66 testProj := &argoappv1.AppProject{ 67 ObjectMeta: metav1.ObjectMeta{Name: projName, Namespace: namespace}, 68 } 69 70 var testApp argoappv1.Application 71 testApp.Name = "test-app" 72 testApp.Namespace = namespace 73 appClientset := appclientset.NewSimpleClientset(testProj) 74 ctx, cancel := context.WithCancel(t.Context()) 75 defer cancel() 76 indexers := cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc} 77 informer := v1alpha1.NewAppProjectInformer(appClientset, namespace, 0, indexers) 78 go informer.Run(ctx.Done()) 79 cache.WaitForCacheSync(ctx.Done(), informer.HasSynced) 80 81 kubeClient := fake.NewClientset(&cm) 82 settingsMgr := settings.NewSettingsManager(t.Context(), kubeClient, test.FakeArgoCDNamespace) 83 argoDB := db.NewDB("default", settingsMgr, kubeClient) 84 proj, err := GetAppProject(ctx, &testApp, applisters.NewAppProjectLister(informer.GetIndexer()), namespace, settingsMgr, argoDB) 85 require.NoError(t, err) 86 assert.Equal(t, proj.Name, projName) 87 } 88 89 func TestIncludeResource(t *testing.T) { 90 // Resource filters format - GROUP:KIND:NAMESPACE/NAME or GROUP:KIND:NAME 91 var ( 92 blankValues = argoappv1.SyncOperationResource{Group: "", Kind: "", Name: "", Namespace: "", Exclude: false} 93 // *:*:* 94 includeAllResources = argoappv1.SyncOperationResource{Group: "*", Kind: "*", Name: "*", Namespace: "", Exclude: false} 95 // !*:*:* 96 excludeAllResources = argoappv1.SyncOperationResource{Group: "*", Kind: "*", Name: "*", Namespace: "", Exclude: true} 97 // *:Service:* 98 includeAllServiceResources = argoappv1.SyncOperationResource{Group: "*", Kind: "Service", Name: "*", Namespace: "", Exclude: false} 99 // !*:Service:* 100 excludeAllServiceResources = argoappv1.SyncOperationResource{Group: "*", Kind: "Service", Name: "*", Namespace: "", Exclude: true} 101 // apps:ReplicaSet:backend 102 includeAllReplicaSetResource = argoappv1.SyncOperationResource{Group: "apps", Kind: "ReplicaSet", Name: "*", Namespace: "", Exclude: false} 103 // apps:ReplicaSet:backend 104 includeReplicaSetResource = argoappv1.SyncOperationResource{Group: "apps", Kind: "ReplicaSet", Name: "backend", Namespace: "", Exclude: false} 105 // !apps:ReplicaSet:backend 106 excludeReplicaSetResource = argoappv1.SyncOperationResource{Group: "apps", Kind: "ReplicaSet", Name: "backend", Namespace: "", Exclude: true} 107 ) 108 tests := []struct { 109 testName string 110 name string 111 namespace string 112 gvk schema.GroupVersionKind 113 syncOperationResource []*argoappv1.SyncOperationResource 114 expectedResult bool 115 }{ 116 //--resource apps:ReplicaSet:backend --resource *:Service:* 117 { 118 testName: "Include ReplicaSet backend resource and all service resources", 119 name: "backend", 120 namespace: "default", 121 gvk: schema.GroupVersionKind{Group: "apps", Kind: "ReplicaSet"}, 122 syncOperationResource: []*argoappv1.SyncOperationResource{&includeAllServiceResources, &includeReplicaSetResource}, 123 expectedResult: true, 124 }, 125 //--resource apps:ReplicaSet:backend --resource *:Service:* 126 { 127 testName: "Include ReplicaSet backend resource and all service resources", 128 name: "main-page-down", 129 namespace: "default", 130 gvk: schema.GroupVersionKind{Group: "batch", Kind: "Job"}, 131 syncOperationResource: []*argoappv1.SyncOperationResource{&includeAllServiceResources, &includeReplicaSetResource}, 132 expectedResult: false, 133 }, 134 //--resource apps:ReplicaSet:backend --resource !*:Service:* 135 { 136 testName: "Include ReplicaSet backend resource and exclude all service resources", 137 name: "main-page-down", 138 namespace: "default", 139 gvk: schema.GroupVersionKind{Group: "batch", Kind: "Job"}, 140 syncOperationResource: []*argoappv1.SyncOperationResource{&excludeAllServiceResources, &includeReplicaSetResource}, 141 expectedResult: false, 142 }, 143 // --resource !apps:ReplicaSet:backend --resource !*:Service:* 144 { 145 testName: "Exclude ReplicaSet backend resource and all service resources", 146 name: "main-page-down", 147 namespace: "default", 148 gvk: schema.GroupVersionKind{Group: "batch", Kind: "Job"}, 149 syncOperationResource: []*argoappv1.SyncOperationResource{&excludeReplicaSetResource, &excludeAllServiceResources}, 150 expectedResult: true, 151 }, 152 // --resource !apps:ReplicaSet:backend 153 { 154 testName: "Exclude ReplicaSet backend resource", 155 name: "backend", 156 namespace: "default", 157 gvk: schema.GroupVersionKind{Group: "apps", Kind: "ReplicaSet"}, 158 syncOperationResource: []*argoappv1.SyncOperationResource{&excludeReplicaSetResource}, 159 expectedResult: false, 160 }, 161 // --resource !apps:ReplicaSet:backend --resource !*:Service:* 162 { 163 testName: "Exclude ReplicaSet backend resource and all service resources(dummy condition)", 164 name: "backend", 165 namespace: "default", 166 gvk: schema.GroupVersionKind{Group: "apps", Kind: "ReplicaSet"}, 167 syncOperationResource: []*argoappv1.SyncOperationResource{&excludeReplicaSetResource, &excludeAllServiceResources}, 168 expectedResult: false, 169 }, 170 // --resource apps:ReplicaSet:backend 171 { 172 testName: "Include ReplicaSet backend resource", 173 name: "backend", 174 namespace: "default", 175 gvk: schema.GroupVersionKind{Group: "apps", Kind: "ReplicaSet"}, 176 syncOperationResource: []*argoappv1.SyncOperationResource{&includeReplicaSetResource}, 177 expectedResult: true, 178 }, 179 // --resource !*:Service:* 180 { 181 testName: "Exclude Service resources", 182 name: "backend", 183 namespace: "default", 184 gvk: schema.GroupVersionKind{Group: "", Kind: "Service"}, 185 syncOperationResource: []*argoappv1.SyncOperationResource{&excludeAllServiceResources}, 186 expectedResult: false, 187 }, 188 // --resource *:Service:* 189 { 190 testName: "Include Service resources", 191 name: "backend", 192 namespace: "default", 193 gvk: schema.GroupVersionKind{Group: "", Kind: "Service"}, 194 syncOperationResource: []*argoappv1.SyncOperationResource{&includeAllServiceResources}, 195 expectedResult: true, 196 }, 197 // --resource apps:ReplicaSet:* --resource !apps:ReplicaSet:backend 198 { 199 testName: "Include & Exclude ReplicaSet resources", 200 name: "backend", 201 namespace: "default", 202 gvk: schema.GroupVersionKind{Group: "apps", Kind: "ReplicaSet"}, 203 syncOperationResource: []*argoappv1.SyncOperationResource{&includeAllReplicaSetResource, &excludeReplicaSetResource}, 204 expectedResult: false, 205 }, 206 // --resource !*:*:* 207 { 208 testName: "Exclude all resources", 209 name: "backend", 210 namespace: "default", 211 gvk: schema.GroupVersionKind{Group: "", Kind: "Service"}, 212 syncOperationResource: []*argoappv1.SyncOperationResource{&excludeAllResources}, 213 expectedResult: false, 214 }, 215 // --resource *:*:* 216 { 217 testName: "Include all resources", 218 name: "backend", 219 namespace: "default", 220 gvk: schema.GroupVersionKind{Group: "", Kind: "Service"}, 221 syncOperationResource: []*argoappv1.SyncOperationResource{&includeAllResources}, 222 expectedResult: true, 223 }, 224 { 225 testName: "No Filters", 226 name: "backend", 227 namespace: "default", 228 gvk: schema.GroupVersionKind{Group: "", Kind: "Service"}, 229 syncOperationResource: []*argoappv1.SyncOperationResource{&blankValues}, 230 expectedResult: false, 231 }, 232 { 233 testName: "Default values", 234 expectedResult: true, 235 }, 236 } 237 238 for _, test := range tests { 239 t.Run(test.testName, func(t *testing.T) { 240 isResourceIncluded := IncludeResource(test.name, test.namespace, test.gvk, test.syncOperationResource) 241 assert.Equal(t, test.expectedResult, isResourceIncluded) 242 }) 243 } 244 } 245 246 func TestContainsSyncResource(t *testing.T) { 247 var ( 248 blankUnstructured unstructured.Unstructured 249 blankResource argoappv1.SyncOperationResource 250 helloResource = argoappv1.SyncOperationResource{Name: "hello"} 251 ) 252 tables := []struct { 253 u *unstructured.Unstructured 254 rr []argoappv1.SyncOperationResource 255 expected bool 256 }{ 257 {&blankUnstructured, []argoappv1.SyncOperationResource{}, false}, 258 {&blankUnstructured, []argoappv1.SyncOperationResource{blankResource}, true}, 259 {&blankUnstructured, []argoappv1.SyncOperationResource{helloResource}, false}, 260 } 261 262 for _, table := range tables { 263 out := ContainsSyncResource(table.u.GetName(), table.u.GetNamespace(), table.u.GroupVersionKind(), table.rr) 264 assert.Equal(t, table.expected, out, "Expected %t for slice %+v contains resource %+v; instead got %t", table.expected, table.rr, table.u, out) 265 } 266 } 267 268 // TestNilOutZerValueAppSources verifies we will nil out app source specs when they are their zero-value 269 func TestNilOutZerValueAppSources(t *testing.T) { 270 var spec *argoappv1.ApplicationSpec 271 spec = NormalizeApplicationSpec(&argoappv1.ApplicationSpec{Source: &argoappv1.ApplicationSource{Kustomize: &argoappv1.ApplicationSourceKustomize{NamePrefix: "foo"}}}) 272 assert.NotNil(t, spec.GetSource().Kustomize) 273 spec = NormalizeApplicationSpec(&argoappv1.ApplicationSpec{Source: &argoappv1.ApplicationSource{Kustomize: &argoappv1.ApplicationSourceKustomize{NamePrefix: ""}}}) 274 source := spec.GetSource() 275 assert.Nil(t, source.Kustomize) 276 277 spec = NormalizeApplicationSpec(&argoappv1.ApplicationSpec{Source: &argoappv1.ApplicationSource{Kustomize: &argoappv1.ApplicationSourceKustomize{NameSuffix: "foo"}}}) 278 assert.NotNil(t, spec.GetSource().Kustomize) 279 spec = NormalizeApplicationSpec(&argoappv1.ApplicationSpec{Source: &argoappv1.ApplicationSource{Kustomize: &argoappv1.ApplicationSourceKustomize{NameSuffix: ""}}}) 280 source = spec.GetSource() 281 assert.Nil(t, source.Kustomize) 282 283 spec = NormalizeApplicationSpec(&argoappv1.ApplicationSpec{Source: &argoappv1.ApplicationSource{Helm: &argoappv1.ApplicationSourceHelm{ValueFiles: []string{"values.yaml"}}}}) 284 assert.NotNil(t, spec.GetSource().Helm) 285 spec = NormalizeApplicationSpec(&argoappv1.ApplicationSpec{Source: &argoappv1.ApplicationSource{Helm: &argoappv1.ApplicationSourceHelm{ValueFiles: []string{}}}}) 286 assert.Nil(t, spec.GetSource().Helm) 287 288 spec = NormalizeApplicationSpec(&argoappv1.ApplicationSpec{Source: &argoappv1.ApplicationSource{Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}}}) 289 assert.NotNil(t, spec.GetSource().Directory) 290 spec = NormalizeApplicationSpec(&argoappv1.ApplicationSpec{Source: &argoappv1.ApplicationSource{Directory: &argoappv1.ApplicationSourceDirectory{Recurse: false}}}) 291 assert.Nil(t, spec.GetSource().Directory) 292 } 293 294 func TestValidatePermissionsEmptyDestination(t *testing.T) { 295 conditions, err := ValidatePermissions(t.Context(), &argoappv1.ApplicationSpec{ 296 Source: &argoappv1.ApplicationSource{RepoURL: "https://github.com/argoproj/argo-cd", Path: "."}, 297 }, &argoappv1.AppProject{ 298 Spec: argoappv1.AppProjectSpec{ 299 SourceRepos: []string{"*"}, 300 Destinations: []argoappv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 301 }, 302 }, nil) 303 require.NoError(t, err) 304 assert.ElementsMatch(t, conditions, []argoappv1.ApplicationCondition{{Type: argoappv1.ApplicationConditionInvalidSpecError, Message: "Destination server missing from app spec"}}) 305 } 306 307 func TestValidateChartWithoutRevision(t *testing.T) { 308 appSpec := &argoappv1.ApplicationSpec{ 309 Source: &argoappv1.ApplicationSource{RepoURL: "https://charts.helm.sh/incubator/", Chart: "myChart", TargetRevision: ""}, 310 Destination: argoappv1.ApplicationDestination{ 311 Server: "https://kubernetes.default.svc", Namespace: "default", 312 }, 313 } 314 cluster := &argoappv1.Cluster{Server: "https://kubernetes.default.svc"} 315 db := &dbmocks.ArgoDB{} 316 ctx := t.Context() 317 db.On("GetCluster", ctx, appSpec.Destination.Server).Return(cluster, nil) 318 319 conditions, err := ValidatePermissions(ctx, appSpec, &argoappv1.AppProject{ 320 Spec: argoappv1.AppProjectSpec{ 321 SourceRepos: []string{"*"}, 322 Destinations: []argoappv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 323 }, 324 }, db) 325 require.NoError(t, err) 326 assert.Len(t, conditions, 1) 327 assert.Equal(t, argoappv1.ApplicationConditionInvalidSpecError, conditions[0].Type) 328 assert.Equal(t, "spec.source.targetRevision is required if the manifest source is a helm chart", conditions[0].Message) 329 } 330 331 func TestAPIResourcesToStrings(t *testing.T) { 332 resources := []kube.APIResourceInfo{{ 333 GroupVersionResource: schema.GroupVersionResource{Group: "apps", Version: "v1beta1"}, 334 GroupKind: schema.GroupKind{Kind: "Deployment"}, 335 }, { 336 GroupVersionResource: schema.GroupVersionResource{Group: "apps", Version: "v1beta2"}, 337 GroupKind: schema.GroupKind{Kind: "Deployment"}, 338 }, { 339 GroupVersionResource: schema.GroupVersionResource{Group: "extensions", Version: "v1beta1"}, 340 GroupKind: schema.GroupKind{Kind: "Deployment"}, 341 }} 342 343 assert.ElementsMatch(t, []string{"apps/v1beta1", "apps/v1beta2", "extensions/v1beta1"}, APIResourcesToStrings(resources, false)) 344 assert.ElementsMatch(t, []string{ 345 "apps/v1beta1", "apps/v1beta1/Deployment", "apps/v1beta2", "apps/v1beta2/Deployment", "extensions/v1beta1", "extensions/v1beta1/Deployment", 346 }, 347 APIResourcesToStrings(resources, true)) 348 } 349 350 func TestValidateRepo(t *testing.T) { 351 repoPath, err := filepath.Abs("./../..") 352 require.NoError(t, err) 353 354 apiResources := []kube.APIResourceInfo{{ 355 GroupVersionResource: schema.GroupVersionResource{Group: "apps", Version: "v1beta1"}, 356 GroupKind: schema.GroupKind{Kind: "Deployment"}, 357 }, { 358 GroupVersionResource: schema.GroupVersionResource{Group: "apps", Version: "v1beta2"}, 359 GroupKind: schema.GroupKind{Kind: "Deployment"}, 360 }} 361 kubeVersion := "v1.16" 362 kustomizeOptions := &argoappv1.KustomizeOptions{BuildOptions: ""} 363 repo := &argoappv1.Repository{Repo: "file://" + repoPath} 364 cluster := &argoappv1.Cluster{Server: "sample server"} 365 app := &argoappv1.Application{ 366 Spec: argoappv1.ApplicationSpec{ 367 Source: &argoappv1.ApplicationSource{ 368 RepoURL: repo.Repo, 369 }, 370 Destination: argoappv1.ApplicationDestination{ 371 Server: cluster.Server, 372 Namespace: "default", 373 }, 374 }, 375 } 376 377 proj := &argoappv1.AppProject{ 378 Spec: argoappv1.AppProjectSpec{ 379 SourceRepos: []string{"*"}, 380 }, 381 } 382 383 helmRepos := []*argoappv1.Repository{{Repo: "sample helm repo"}} 384 385 repoClient := &mocks.RepoServerServiceClient{} 386 source := app.Spec.GetSource() 387 repoClient.On("GetAppDetails", t.Context(), &apiclient.RepoServerAppDetailsQuery{ 388 Repo: repo, 389 Source: &source, 390 Repos: helmRepos, 391 KustomizeOptions: kustomizeOptions, 392 HelmOptions: &argoappv1.HelmOptions{ValuesFileSchemes: []string{"https", "http"}}, 393 NoRevisionCache: true, 394 }).Return(&apiclient.RepoAppDetailsResponse{}, nil) 395 396 repo.Type = "git" 397 repoClient.On("TestRepository", t.Context(), &apiclient.TestRepositoryRequest{ 398 Repo: repo, 399 }).Return(&apiclient.TestRepositoryResponse{ 400 VerifiedRepository: true, 401 }, nil) 402 403 repoClientSet := &mocks.Clientset{RepoServerServiceClient: repoClient} 404 405 db := &dbmocks.ArgoDB{} 406 407 db.On("GetRepository", t.Context(), app.Spec.Source.RepoURL, "").Return(repo, nil) 408 db.On("ListHelmRepositories", t.Context()).Return(helmRepos, nil) 409 db.On("ListOCIRepositories", t.Context()).Return([]*argoappv1.Repository{}, nil) 410 db.On("GetCluster", t.Context(), app.Spec.Destination.Server).Return(cluster, nil) 411 db.On("GetAllHelmRepositoryCredentials", t.Context()).Return(nil, nil) 412 db.On("GetAllOCIRepositoryCredentials", t.Context()).Return([]*argoappv1.RepoCreds{}, nil) 413 414 var receivedRequest *apiclient.ManifestRequest 415 416 repoClient.On("GenerateManifest", t.Context(), mock.MatchedBy(func(req *apiclient.ManifestRequest) bool { 417 receivedRequest = req 418 return true 419 })).Return(nil, nil) 420 421 cm := corev1.ConfigMap{ 422 ObjectMeta: metav1.ObjectMeta{ 423 Name: "argocd-cm", 424 Namespace: test.FakeArgoCDNamespace, 425 Labels: map[string]string{ 426 "app.kubernetes.io/part-of": "argocd", 427 }, 428 }, 429 Data: map[string]string{ 430 "globalProjects": ` 431 - projectName: default-x 432 labelSelector: 433 matchExpressions: 434 - key: is-x 435 operator: Exists 436 - projectName: default-non-x 437 labelSelector: 438 matchExpressions: 439 - key: is-x 440 operator: DoesNotExist 441 `, 442 }, 443 } 444 445 kubeClient := fake.NewClientset(&cm) 446 settingsMgr := settings.NewSettingsManager(t.Context(), kubeClient, test.FakeArgoCDNamespace) 447 448 conditions, err := ValidateRepo(t.Context(), app, repoClientSet, db, &kubetest.MockKubectlCmd{Version: kubeVersion, APIResources: apiResources}, proj, settingsMgr) 449 450 require.NoError(t, err) 451 assert.Empty(t, conditions) 452 assert.ElementsMatch(t, []string{"apps/v1beta1", "apps/v1beta1/Deployment", "apps/v1beta2", "apps/v1beta2/Deployment"}, receivedRequest.ApiVersions) 453 assert.Equal(t, kubeVersion, receivedRequest.KubeVersion) 454 assert.Equal(t, app.Spec.Destination.Namespace, receivedRequest.Namespace) 455 assert.Equal(t, &source, receivedRequest.ApplicationSource) 456 assert.Equal(t, kustomizeOptions, receivedRequest.KustomizeOptions) 457 } 458 459 func TestFormatAppConditions(t *testing.T) { 460 conditions := []argoappv1.ApplicationCondition{ 461 { 462 Type: EventReasonOperationCompleted, 463 Message: "Foo", 464 }, 465 { 466 Type: EventReasonResourceCreated, 467 Message: "Bar", 468 }, 469 } 470 471 t.Run("Single Condition", func(t *testing.T) { 472 res := FormatAppConditions(conditions[0:1]) 473 assert.NotEmpty(t, res) 474 assert.Equal(t, EventReasonOperationCompleted+": Foo", res) 475 }) 476 477 t.Run("Multiple Conditions", func(t *testing.T) { 478 res := FormatAppConditions(conditions) 479 assert.NotEmpty(t, res) 480 assert.Equal(t, fmt.Sprintf("%s: Foo;%s: Bar", EventReasonOperationCompleted, EventReasonResourceCreated), res) 481 }) 482 483 t.Run("Empty Conditions", func(t *testing.T) { 484 res := FormatAppConditions([]argoappv1.ApplicationCondition{}) 485 assert.Empty(t, res) 486 }) 487 } 488 489 func TestFilterByProjects(t *testing.T) { 490 apps := []argoappv1.Application{ 491 { 492 Spec: argoappv1.ApplicationSpec{ 493 Project: "fooproj", 494 }, 495 }, 496 { 497 Spec: argoappv1.ApplicationSpec{ 498 Project: "barproj", 499 }, 500 }, 501 } 502 503 t.Run("No apps in single project", func(t *testing.T) { 504 res := FilterByProjects(apps, []string{"foobarproj"}) 505 assert.Empty(t, res) 506 }) 507 508 t.Run("Single app in single project", func(t *testing.T) { 509 res := FilterByProjects(apps, []string{"fooproj"}) 510 assert.Len(t, res, 1) 511 }) 512 513 t.Run("Single app in multiple project", func(t *testing.T) { 514 res := FilterByProjects(apps, []string{"fooproj", "foobarproj"}) 515 assert.Len(t, res, 1) 516 }) 517 518 t.Run("Multiple apps in multiple project", func(t *testing.T) { 519 res := FilterByProjects(apps, []string{"fooproj", "barproj"}) 520 assert.Len(t, res, 2) 521 }) 522 } 523 524 func TestFilterByProjectsP(t *testing.T) { 525 apps := []*argoappv1.Application{ 526 { 527 Spec: argoappv1.ApplicationSpec{ 528 Project: "fooproj", 529 }, 530 }, 531 { 532 Spec: argoappv1.ApplicationSpec{ 533 Project: "barproj", 534 }, 535 }, 536 } 537 538 t.Run("No apps in single project", func(t *testing.T) { 539 res := FilterByProjectsP(apps, []string{"foobarproj"}) 540 assert.Empty(t, res) 541 }) 542 543 t.Run("Single app in single project", func(t *testing.T) { 544 res := FilterByProjectsP(apps, []string{"fooproj"}) 545 assert.Len(t, res, 1) 546 }) 547 548 t.Run("Single app in multiple project", func(t *testing.T) { 549 res := FilterByProjectsP(apps, []string{"fooproj", "foobarproj"}) 550 assert.Len(t, res, 1) 551 }) 552 553 t.Run("Multiple apps in multiple project", func(t *testing.T) { 554 res := FilterByProjectsP(apps, []string{"fooproj", "barproj"}) 555 assert.Len(t, res, 2) 556 }) 557 } 558 559 func TestFilterByRepo(t *testing.T) { 560 apps := []argoappv1.Application{ 561 { 562 Spec: argoappv1.ApplicationSpec{ 563 Source: &argoappv1.ApplicationSource{ 564 RepoURL: "git@github.com:owner/repo.git", 565 }, 566 }, 567 }, 568 { 569 Spec: argoappv1.ApplicationSpec{ 570 Source: &argoappv1.ApplicationSource{ 571 RepoURL: "git@github.com:owner/otherrepo.git", 572 }, 573 }, 574 }, 575 } 576 577 t.Run("Empty filter", func(t *testing.T) { 578 res := FilterByRepo(apps, "") 579 assert.Len(t, res, 2) 580 }) 581 582 t.Run("Match", func(t *testing.T) { 583 res := FilterByRepo(apps, "git@github.com:owner/repo.git") 584 assert.Len(t, res, 1) 585 }) 586 587 t.Run("No match", func(t *testing.T) { 588 res := FilterByRepo(apps, "git@github.com:owner/willnotmatch.git") 589 assert.Empty(t, res) 590 }) 591 } 592 593 func TestFilterByRepoP(t *testing.T) { 594 apps := []*argoappv1.Application{ 595 { 596 Spec: argoappv1.ApplicationSpec{ 597 Source: &argoappv1.ApplicationSource{ 598 RepoURL: "git@github.com:owner/repo.git", 599 }, 600 }, 601 }, 602 { 603 Spec: argoappv1.ApplicationSpec{ 604 Source: &argoappv1.ApplicationSource{ 605 RepoURL: "git@github.com:owner/otherrepo.git", 606 }, 607 }, 608 }, 609 } 610 611 t.Run("Empty filter", func(t *testing.T) { 612 res := FilterByRepoP(apps, "") 613 assert.Len(t, res, 2) 614 }) 615 616 t.Run("Match", func(t *testing.T) { 617 res := FilterByRepoP(apps, "git@github.com:owner/repo.git") 618 assert.Len(t, res, 1) 619 }) 620 621 t.Run("No match", func(t *testing.T) { 622 res := FilterByRepoP(apps, "git@github.com:owner/willnotmatch.git") 623 assert.Empty(t, res) 624 }) 625 } 626 627 func TestValidatePermissions(t *testing.T) { 628 t.Run("Empty Repo URL result in condition", func(t *testing.T) { 629 spec := argoappv1.ApplicationSpec{ 630 Source: &argoappv1.ApplicationSource{ 631 RepoURL: "", 632 }, 633 } 634 proj := argoappv1.AppProject{} 635 db := &dbmocks.ArgoDB{} 636 conditions, err := ValidatePermissions(t.Context(), &spec, &proj, db) 637 require.NoError(t, err) 638 assert.Len(t, conditions, 1) 639 assert.Equal(t, argoappv1.ApplicationConditionInvalidSpecError, conditions[0].Type) 640 assert.Contains(t, conditions[0].Message, "are required") 641 }) 642 643 t.Run("Incomplete Path/Chart combo result in condition", func(t *testing.T) { 644 spec := argoappv1.ApplicationSpec{ 645 Source: &argoappv1.ApplicationSource{ 646 RepoURL: "http://some/where", 647 Path: "", 648 Chart: "", 649 }, 650 } 651 proj := argoappv1.AppProject{} 652 db := &dbmocks.ArgoDB{} 653 conditions, err := ValidatePermissions(t.Context(), &spec, &proj, db) 654 require.NoError(t, err) 655 assert.Len(t, conditions, 1) 656 assert.Equal(t, argoappv1.ApplicationConditionInvalidSpecError, conditions[0].Type) 657 assert.Contains(t, conditions[0].Message, "are required") 658 }) 659 660 t.Run("Helm chart requires targetRevision", func(t *testing.T) { 661 spec := argoappv1.ApplicationSpec{ 662 Source: &argoappv1.ApplicationSource{ 663 RepoURL: "http://some/where", 664 Path: "", 665 Chart: "somechart", 666 }, 667 } 668 proj := argoappv1.AppProject{} 669 db := &dbmocks.ArgoDB{} 670 conditions, err := ValidatePermissions(t.Context(), &spec, &proj, db) 671 require.NoError(t, err) 672 assert.Len(t, conditions, 1) 673 assert.Equal(t, argoappv1.ApplicationConditionInvalidSpecError, conditions[0].Type) 674 assert.Contains(t, conditions[0].Message, "is required if the manifest source is a helm chart") 675 }) 676 677 t.Run("Application source is not permitted in project", func(t *testing.T) { 678 spec := argoappv1.ApplicationSpec{ 679 Source: &argoappv1.ApplicationSource{ 680 RepoURL: "http://some/where", 681 Path: "", 682 Chart: "somechart", 683 TargetRevision: "1.4.1", 684 }, 685 Destination: argoappv1.ApplicationDestination{ 686 Server: "https://127.0.0.1:6443", 687 Namespace: "testns", 688 }, 689 } 690 proj := argoappv1.AppProject{ 691 Spec: argoappv1.AppProjectSpec{ 692 Destinations: []argoappv1.ApplicationDestination{ 693 { 694 Server: "*", 695 Namespace: "*", 696 }, 697 }, 698 SourceRepos: []string{"http://some/where/else"}, 699 }, 700 } 701 cluster := &argoappv1.Cluster{Server: "https://127.0.0.1:6443", Name: "test"} 702 db := &dbmocks.ArgoDB{} 703 db.On("GetCluster", t.Context(), spec.Destination.Server).Return(cluster, nil) 704 conditions, err := ValidatePermissions(t.Context(), &spec, &proj, db) 705 require.NoError(t, err) 706 assert.Len(t, conditions, 1) 707 assert.Contains(t, conditions[0].Message, "application repo http://some/where is not permitted") 708 }) 709 710 t.Run("Application destination is not permitted in project", func(t *testing.T) { 711 spec := argoappv1.ApplicationSpec{ 712 Source: &argoappv1.ApplicationSource{ 713 RepoURL: "http://some/where", 714 Path: "", 715 Chart: "somechart", 716 TargetRevision: "1.4.1", 717 }, 718 Destination: argoappv1.ApplicationDestination{ 719 Server: "https://127.0.0.1:6443", 720 Namespace: "testns", 721 }, 722 } 723 proj := argoappv1.AppProject{ 724 Spec: argoappv1.AppProjectSpec{ 725 Destinations: []argoappv1.ApplicationDestination{ 726 { 727 Server: "*", 728 Namespace: "default", 729 }, 730 }, 731 SourceRepos: []string{"http://some/where"}, 732 }, 733 } 734 cluster := &argoappv1.Cluster{Server: "https://127.0.0.1:6443", Name: "test"} 735 db := &dbmocks.ArgoDB{} 736 db.On("GetCluster", t.Context(), spec.Destination.Server).Return(cluster, nil) 737 conditions, err := ValidatePermissions(t.Context(), &spec, &proj, db) 738 require.NoError(t, err) 739 assert.Len(t, conditions, 1) 740 assert.Contains(t, conditions[0].Message, "application destination") 741 }) 742 743 t.Run("Destination cluster does not exist", func(t *testing.T) { 744 spec := argoappv1.ApplicationSpec{ 745 Source: &argoappv1.ApplicationSource{ 746 RepoURL: "http://some/where", 747 Path: "", 748 Chart: "somechart", 749 TargetRevision: "1.4.1", 750 }, 751 Destination: argoappv1.ApplicationDestination{ 752 Server: "https://127.0.0.1:6443", 753 Namespace: "default", 754 }, 755 } 756 proj := argoappv1.AppProject{ 757 Spec: argoappv1.AppProjectSpec{ 758 Destinations: []argoappv1.ApplicationDestination{ 759 { 760 Server: "*", 761 Namespace: "default", 762 }, 763 }, 764 SourceRepos: []string{"http://some/where"}, 765 }, 766 } 767 db := &dbmocks.ArgoDB{} 768 db.On("GetCluster", t.Context(), spec.Destination.Server).Return(nil, status.Errorf(codes.NotFound, "Cluster does not exist")) 769 conditions, err := ValidatePermissions(t.Context(), &spec, &proj, db) 770 require.NoError(t, err) 771 assert.Len(t, conditions, 1) 772 assert.Contains(t, conditions[0].Message, "Cluster does not exist") 773 }) 774 775 t.Run("Destination cluster name does not exist", func(t *testing.T) { 776 spec := argoappv1.ApplicationSpec{ 777 Source: &argoappv1.ApplicationSource{ 778 RepoURL: "http://some/where", 779 Path: "", 780 Chart: "somechart", 781 TargetRevision: "1.4.1", 782 }, 783 Destination: argoappv1.ApplicationDestination{ 784 Name: "does-not-exist", 785 Namespace: "default", 786 }, 787 } 788 proj := argoappv1.AppProject{ 789 Spec: argoappv1.AppProjectSpec{ 790 Destinations: []argoappv1.ApplicationDestination{ 791 { 792 Server: "*", 793 Namespace: "default", 794 }, 795 }, 796 SourceRepos: []string{"http://some/where"}, 797 }, 798 } 799 db := &dbmocks.ArgoDB{} 800 db.On("GetClusterServersByName", t.Context(), "does-not-exist").Return(nil, nil) 801 conditions, err := ValidatePermissions(t.Context(), &spec, &proj, db) 802 require.NoError(t, err) 803 assert.Len(t, conditions, 1) 804 assert.Contains(t, conditions[0].Message, "there are no clusters with this name: does-not-exist") 805 }) 806 807 t.Run("Cannot get cluster info from DB", func(t *testing.T) { 808 spec := argoappv1.ApplicationSpec{ 809 Source: &argoappv1.ApplicationSource{ 810 RepoURL: "http://some/where", 811 Path: "", 812 Chart: "somechart", 813 TargetRevision: "1.4.1", 814 }, 815 Destination: argoappv1.ApplicationDestination{ 816 Server: "https://127.0.0.1:6443", 817 Namespace: "default", 818 }, 819 } 820 proj := argoappv1.AppProject{ 821 Spec: argoappv1.AppProjectSpec{ 822 Destinations: []argoappv1.ApplicationDestination{ 823 { 824 Server: "*", 825 Namespace: "default", 826 }, 827 }, 828 SourceRepos: []string{"http://some/where"}, 829 }, 830 } 831 db := &dbmocks.ArgoDB{} 832 db.On("GetCluster", t.Context(), spec.Destination.Server).Return(nil, errors.New("Unknown error occurred")) 833 conditions, err := ValidatePermissions(t.Context(), &spec, &proj, db) 834 require.NoError(t, err) 835 assert.Len(t, conditions, 1) 836 assert.Contains(t, conditions[0].Message, "Unknown error occurred") 837 }) 838 839 t.Run("Destination cluster name resolves to valid server", func(t *testing.T) { 840 spec := argoappv1.ApplicationSpec{ 841 Source: &argoappv1.ApplicationSource{ 842 RepoURL: "http://some/where", 843 Path: "", 844 Chart: "somechart", 845 TargetRevision: "1.4.1", 846 }, 847 Destination: argoappv1.ApplicationDestination{ 848 Name: "does-exist", 849 Namespace: "default", 850 }, 851 } 852 proj := argoappv1.AppProject{ 853 Spec: argoappv1.AppProjectSpec{ 854 Destinations: []argoappv1.ApplicationDestination{ 855 { 856 Server: "*", 857 Namespace: "default", 858 }, 859 }, 860 SourceRepos: []string{"http://some/where"}, 861 }, 862 } 863 db := &dbmocks.ArgoDB{} 864 cluster := argoappv1.Cluster{ 865 Name: "does-exist", 866 Server: "https://127.0.0.1:6443", 867 } 868 db.On("GetClusterServersByName", t.Context(), "does-exist").Return([]string{"https://127.0.0.1:6443"}, nil) 869 db.On("GetCluster", t.Context(), "https://127.0.0.1:6443").Return(&cluster, nil) 870 conditions, err := ValidatePermissions(t.Context(), &spec, &proj, db) 871 require.NoError(t, err) 872 assert.Empty(t, conditions) 873 }) 874 } 875 876 func TestSetAppOperations(t *testing.T) { 877 t.Run("Application not existing", func(t *testing.T) { 878 appIf := appclientset.NewSimpleClientset().ArgoprojV1alpha1().Applications("default") 879 app, err := SetAppOperation(appIf, "someapp", &argoappv1.Operation{Sync: &argoappv1.SyncOperation{Revision: "aaa"}}) 880 require.Error(t, err) 881 assert.Nil(t, app) 882 }) 883 884 t.Run("Operation already in progress", func(t *testing.T) { 885 a := argoappv1.Application{ 886 ObjectMeta: metav1.ObjectMeta{ 887 Name: "someapp", 888 Namespace: "default", 889 }, 890 Operation: &argoappv1.Operation{Sync: &argoappv1.SyncOperation{Revision: "aaa"}}, 891 } 892 appIf := appclientset.NewSimpleClientset(&a).ArgoprojV1alpha1().Applications("default") 893 app, err := SetAppOperation(appIf, "someapp", &argoappv1.Operation{Sync: &argoappv1.SyncOperation{Revision: "aaa"}}) 894 require.ErrorContains(t, err, "operation is already in progress") 895 assert.Nil(t, app) 896 }) 897 898 t.Run("Operation unspecified", func(t *testing.T) { 899 a := argoappv1.Application{ 900 ObjectMeta: metav1.ObjectMeta{ 901 Name: "someapp", 902 Namespace: "default", 903 }, 904 } 905 appIf := appclientset.NewSimpleClientset(&a).ArgoprojV1alpha1().Applications("default") 906 app, err := SetAppOperation(appIf, "someapp", &argoappv1.Operation{Sync: nil}) 907 require.ErrorContains(t, err, "Operation unspecified") 908 assert.Nil(t, app) 909 }) 910 911 t.Run("Success", func(t *testing.T) { 912 a := argoappv1.Application{ 913 ObjectMeta: metav1.ObjectMeta{ 914 Name: "someapp", 915 Namespace: "default", 916 }, 917 } 918 appIf := appclientset.NewSimpleClientset(&a).ArgoprojV1alpha1().Applications("default") 919 app, err := SetAppOperation(appIf, "someapp", &argoappv1.Operation{Sync: &argoappv1.SyncOperation{Revision: "aaa"}}) 920 require.NoError(t, err) 921 assert.NotNil(t, app) 922 }) 923 } 924 925 func TestGetDestinationCluster(t *testing.T) { 926 t.Run("Validate destination with server url", func(t *testing.T) { 927 dest := argoappv1.ApplicationDestination{ 928 Server: "https://127.0.0.1:6443", 929 Namespace: "default", 930 } 931 932 expectedCluster := &argoappv1.Cluster{Server: "https://127.0.0.1:6443"} 933 db := &dbmocks.ArgoDB{} 934 db.On("GetCluster", t.Context(), "https://127.0.0.1:6443").Return(expectedCluster, nil) 935 936 destCluster, err := GetDestinationCluster(t.Context(), dest, db) 937 require.NoError(t, err) 938 require.NotNil(t, expectedCluster) 939 assert.Equal(t, expectedCluster, destCluster) 940 }) 941 942 t.Run("Validate destination with server name", func(t *testing.T) { 943 dest := argoappv1.ApplicationDestination{ 944 Name: "minikube", 945 } 946 947 db := &dbmocks.ArgoDB{} 948 db.On("GetClusterServersByName", t.Context(), "minikube").Return([]string{"https://127.0.0.1:6443"}, nil) 949 db.On("GetCluster", t.Context(), "https://127.0.0.1:6443").Return(&argoappv1.Cluster{Server: "https://127.0.0.1:6443", Name: "minikube"}, nil) 950 951 destCluster, err := GetDestinationCluster(t.Context(), dest, db) 952 require.NoError(t, err) 953 assert.Equal(t, "https://127.0.0.1:6443", destCluster.Server) 954 }) 955 956 t.Run("Error when having both server url and name", func(t *testing.T) { 957 dest := argoappv1.ApplicationDestination{ 958 Server: "https://127.0.0.1:6443", 959 Name: "minikube", 960 Namespace: "default", 961 } 962 963 _, err := GetDestinationCluster(t.Context(), dest, nil) 964 assert.EqualError(t, err, "application destination can't have both name and server defined: minikube https://127.0.0.1:6443") 965 }) 966 967 t.Run("GetClusterServersByName fails", func(t *testing.T) { 968 dest := argoappv1.ApplicationDestination{ 969 Name: "minikube", 970 } 971 972 db := &dbmocks.ArgoDB{} 973 db.On("GetClusterServersByName", t.Context(), mock.Anything).Return(nil, errors.New("an error occurred")) 974 975 _, err := GetDestinationCluster(t.Context(), dest, db) 976 require.ErrorContains(t, err, "an error occurred") 977 }) 978 979 t.Run("Destination cluster does not exist", func(t *testing.T) { 980 dest := argoappv1.ApplicationDestination{ 981 Name: "minikube", 982 } 983 984 db := &dbmocks.ArgoDB{} 985 db.On("GetClusterServersByName", t.Context(), "minikube").Return(nil, nil) 986 987 _, err := GetDestinationCluster(t.Context(), dest, db) 988 assert.EqualError(t, err, "there are no clusters with this name: minikube") 989 }) 990 991 t.Run("Validate too many clusters with the same name", func(t *testing.T) { 992 dest := argoappv1.ApplicationDestination{ 993 Name: "dind", 994 } 995 996 db := &dbmocks.ArgoDB{} 997 db.On("GetClusterServersByName", t.Context(), "dind").Return([]string{"https://127.0.0.1:2443", "https://127.0.0.1:8443"}, nil) 998 999 _, err := GetDestinationCluster(t.Context(), dest, db) 1000 assert.EqualError(t, err, "there are 2 clusters with the same name: [https://127.0.0.1:2443 https://127.0.0.1:8443]") 1001 }) 1002 } 1003 1004 func TestFilterByName(t *testing.T) { 1005 apps := []argoappv1.Application{ 1006 { 1007 ObjectMeta: metav1.ObjectMeta{ 1008 Name: "foo", 1009 }, 1010 Spec: argoappv1.ApplicationSpec{ 1011 Project: "fooproj", 1012 }, 1013 }, 1014 { 1015 ObjectMeta: metav1.ObjectMeta{ 1016 Name: "bar", 1017 }, 1018 Spec: argoappv1.ApplicationSpec{ 1019 Project: "barproj", 1020 }, 1021 }, 1022 } 1023 1024 t.Run("Name is empty string", func(t *testing.T) { 1025 res, err := FilterByName(apps, "") 1026 require.NoError(t, err) 1027 assert.Len(t, res, 2) 1028 }) 1029 1030 t.Run("Single app by name", func(t *testing.T) { 1031 res, err := FilterByName(apps, "foo") 1032 require.NoError(t, err) 1033 assert.Len(t, res, 1) 1034 }) 1035 1036 t.Run("No such app", func(t *testing.T) { 1037 res, err := FilterByName(apps, "foobar") 1038 require.Error(t, err) 1039 assert.Empty(t, res) 1040 }) 1041 } 1042 1043 func TestFilterByNameP(t *testing.T) { 1044 apps := []*argoappv1.Application{ 1045 { 1046 ObjectMeta: metav1.ObjectMeta{ 1047 Name: "foo", 1048 }, 1049 Spec: argoappv1.ApplicationSpec{ 1050 Project: "fooproj", 1051 }, 1052 }, 1053 { 1054 ObjectMeta: metav1.ObjectMeta{ 1055 Name: "bar", 1056 }, 1057 Spec: argoappv1.ApplicationSpec{ 1058 Project: "barproj", 1059 }, 1060 }, 1061 } 1062 1063 t.Run("Name is empty string", func(t *testing.T) { 1064 res := FilterByNameP(apps, "") 1065 assert.Len(t, res, 2) 1066 }) 1067 1068 t.Run("Single app by name", func(t *testing.T) { 1069 res := FilterByNameP(apps, "foo") 1070 assert.Len(t, res, 1) 1071 }) 1072 1073 t.Run("No such app", func(t *testing.T) { 1074 res := FilterByNameP(apps, "foobar") 1075 assert.Empty(t, res) 1076 }) 1077 } 1078 1079 func TestGetGlobalProjects(t *testing.T) { 1080 t.Run("Multiple global projects", func(t *testing.T) { 1081 namespace := "default" 1082 1083 cm := corev1.ConfigMap{ 1084 ObjectMeta: metav1.ObjectMeta{ 1085 Name: "argocd-cm", 1086 Namespace: test.FakeArgoCDNamespace, 1087 Labels: map[string]string{ 1088 "app.kubernetes.io/part-of": "argocd", 1089 }, 1090 }, 1091 Data: map[string]string{ 1092 "globalProjects": ` 1093 - projectName: default-x 1094 labelSelector: 1095 matchExpressions: 1096 - key: is-x 1097 operator: Exists 1098 - projectName: default-non-x 1099 labelSelector: 1100 matchExpressions: 1101 - key: is-x 1102 operator: DoesNotExist 1103 `, 1104 }, 1105 } 1106 1107 defaultX := &argoappv1.AppProject{ 1108 ObjectMeta: metav1.ObjectMeta{Name: "default-x", Namespace: namespace}, 1109 Spec: argoappv1.AppProjectSpec{ 1110 ClusterResourceWhitelist: []metav1.GroupKind{ 1111 {Group: "*", Kind: "*"}, 1112 }, 1113 ClusterResourceBlacklist: []metav1.GroupKind{ 1114 {Kind: "Volume"}, 1115 }, 1116 }, 1117 } 1118 1119 defaultNonX := &argoappv1.AppProject{ 1120 ObjectMeta: metav1.ObjectMeta{Name: "default-non-x", Namespace: namespace}, 1121 Spec: argoappv1.AppProjectSpec{ 1122 ClusterResourceBlacklist: []metav1.GroupKind{ 1123 {Group: "*", Kind: "*"}, 1124 }, 1125 }, 1126 } 1127 1128 isX := &argoappv1.AppProject{ 1129 ObjectMeta: metav1.ObjectMeta{ 1130 Name: "is-x", 1131 Namespace: namespace, 1132 Labels: map[string]string{ 1133 "is-x": "yep", 1134 }, 1135 }, 1136 } 1137 1138 isNoX := &argoappv1.AppProject{ 1139 ObjectMeta: metav1.ObjectMeta{Name: "is-no-x", Namespace: namespace}, 1140 } 1141 1142 projClientset := appclientset.NewSimpleClientset(defaultX, defaultNonX, isX, isNoX) 1143 ctx, cancel := context.WithCancel(t.Context()) 1144 defer cancel() 1145 indexers := cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc} 1146 informer := v1alpha1.NewAppProjectInformer(projClientset, namespace, 0, indexers) 1147 go informer.Run(ctx.Done()) 1148 cache.WaitForCacheSync(ctx.Done(), informer.HasSynced) 1149 1150 kubeClient := fake.NewSimpleClientset(&cm) 1151 settingsMgr := settings.NewSettingsManager(t.Context(), kubeClient, test.FakeArgoCDNamespace) 1152 1153 projLister := applisters.NewAppProjectLister(informer.GetIndexer()) 1154 1155 xGlobalProjects := GetGlobalProjects(isX, projLister, settingsMgr) 1156 assert.Len(t, xGlobalProjects, 1) 1157 assert.Equal(t, "default-x", xGlobalProjects[0].Name) 1158 1159 nonXGlobalProjects := GetGlobalProjects(isNoX, projLister, settingsMgr) 1160 assert.Len(t, nonXGlobalProjects, 1) 1161 assert.Equal(t, "default-non-x", nonXGlobalProjects[0].Name) 1162 }) 1163 } 1164 1165 func Test_GetDifferentPathsBetweenStructs(t *testing.T) { 1166 r1 := argoappv1.Repository{} 1167 r2 := argoappv1.Repository{ 1168 Name: "SomeName", 1169 } 1170 1171 difference, _ := GetDifferentPathsBetweenStructs(r1, r2) 1172 assert.Equal(t, []string{"Name"}, difference) 1173 } 1174 1175 func Test_GenerateSpecIsDifferentErrorMessageWithNoDiff(t *testing.T) { 1176 r1 := argoappv1.Repository{} 1177 r2 := argoappv1.Repository{} 1178 1179 msg := GenerateSpecIsDifferentErrorMessage("application", r1, r2) 1180 assert.Equal(t, "existing application spec is different; use upsert flag to force update", msg) 1181 } 1182 1183 func Test_GenerateSpecIsDifferentErrorMessageWithDiff(t *testing.T) { 1184 r1 := argoappv1.Repository{} 1185 r2 := argoappv1.Repository{ 1186 Name: "test", 1187 } 1188 1189 msg := GenerateSpecIsDifferentErrorMessage("repo", r1, r2) 1190 assert.Equal(t, "existing repo spec is different; use upsert flag to force update; difference in keys \"Name\"", msg) 1191 } 1192 1193 func Test_ParseAppQualifiedName(t *testing.T) { 1194 testcases := []struct { 1195 name string 1196 input string 1197 implicitNs string 1198 appName string 1199 appNs string 1200 }{ 1201 {"Full qualified without implicit NS", "namespace/name", "", "name", "namespace"}, 1202 {"Non qualified without implicit NS", "name", "", "name", ""}, 1203 {"Full qualified with implicit NS", "namespace/name", "namespace2", "name", "namespace"}, 1204 {"Non qualified with implicit NS", "name", "namespace2", "name", "namespace2"}, 1205 {"Invalid without implicit NS", "namespace_name", "", "namespace_name", ""}, 1206 } 1207 1208 for _, tt := range testcases { 1209 t.Run(tt.name, func(t *testing.T) { 1210 appName, appNs := ParseFromQualifiedName(tt.input, tt.implicitNs) 1211 assert.Equal(t, tt.appName, appName) 1212 assert.Equal(t, tt.appNs, appNs) 1213 }) 1214 } 1215 } 1216 1217 func Test_ParseAppInstanceName(t *testing.T) { 1218 testcases := []struct { 1219 name string 1220 input string 1221 implicitNs string 1222 appName string 1223 appNs string 1224 }{ 1225 {"Full qualified without implicit NS", "namespace_name", "", "name", "namespace"}, 1226 {"Non qualified without implicit NS", "name", "", "name", ""}, 1227 {"Full qualified with implicit NS", "namespace_name", "namespace2", "name", "namespace"}, 1228 {"Non qualified with implicit NS", "name", "namespace2", "name", "namespace2"}, 1229 {"Invalid without implicit NS", "namespace/name", "", "namespace/name", ""}, 1230 } 1231 1232 for _, tt := range testcases { 1233 t.Run(tt.name, func(t *testing.T) { 1234 appName, appNs := ParseInstanceName(tt.input, tt.implicitNs) 1235 assert.Equal(t, tt.appName, appName) 1236 assert.Equal(t, tt.appNs, appNs) 1237 }) 1238 } 1239 } 1240 1241 func Test_AppInstanceName(t *testing.T) { 1242 testcases := []struct { 1243 name string 1244 appName string 1245 appNamespace string 1246 defaultNs string 1247 result string 1248 }{ 1249 {"defaultns different as appns", "appname", "appns", "defaultns", "appns_appname"}, 1250 {"defaultns same as appns", "appname", "appns", "appns", "appname"}, 1251 {"defaultns set and appns not given", "appname", "", "appns", "appname"}, 1252 {"neither defaultns nor appns set", "appname", "", "appns", "appname"}, 1253 } 1254 1255 for _, tt := range testcases { 1256 t.Run(tt.name, func(t *testing.T) { 1257 result := AppInstanceName(tt.appName, tt.appNamespace, tt.defaultNs) 1258 assert.Equal(t, tt.result, result) 1259 }) 1260 } 1261 } 1262 1263 func Test_AppInstanceNameFromQualified(t *testing.T) { 1264 testcases := []struct { 1265 name string 1266 appName string 1267 defaultNs string 1268 result string 1269 }{ 1270 {"Qualified name with namespace not being defaultns", "appns/appname", "defaultns", "appns_appname"}, 1271 {"Qualified name with namespace being defaultns", "defaultns/appname", "defaultns", "appname"}, 1272 {"Qualified name without namespace", "appname", "defaultns", "appname"}, 1273 {"Qualified name without namespace and defaultns", "appname", "", "appname"}, 1274 } 1275 1276 for _, tt := range testcases { 1277 t.Run(tt.name, func(t *testing.T) { 1278 result := InstanceNameFromQualified(tt.appName, tt.defaultNs) 1279 assert.Equal(t, tt.result, result) 1280 }) 1281 } 1282 } 1283 1284 func Test_GetRefSources(t *testing.T) { 1285 repoPath, err := filepath.Abs("./../..") 1286 require.NoError(t, err) 1287 1288 getMultiSourceAppSpec := func(sources argoappv1.ApplicationSources) *argoappv1.ApplicationSpec { 1289 return &argoappv1.ApplicationSpec{ 1290 Sources: sources, 1291 } 1292 } 1293 1294 repo := &argoappv1.Repository{Repo: "file://" + repoPath} 1295 1296 t.Run("target ref exists", func(t *testing.T) { 1297 argoSpec := getMultiSourceAppSpec(argoappv1.ApplicationSources{ 1298 {RepoURL: "file://" + repoPath, Ref: "source-1_2"}, 1299 {RepoURL: "file://" + repoPath}, 1300 }) 1301 1302 refSources, err := GetRefSources(t.Context(), argoSpec.Sources, argoSpec.Project, func(_ context.Context, _ string, _ string) (*argoappv1.Repository, error) { 1303 return repo, nil 1304 }, []string{}) 1305 1306 expectedRefSource := argoappv1.RefTargetRevisionMapping{ 1307 "$source-1_2": &argoappv1.RefTarget{ 1308 Repo: *repo, 1309 }, 1310 } 1311 require.NoError(t, err) 1312 assert.Len(t, refSources, 1) 1313 assert.Equal(t, expectedRefSource, refSources) 1314 }) 1315 1316 t.Run("target ref does not exist", func(t *testing.T) { 1317 argoSpec := getMultiSourceAppSpec(argoappv1.ApplicationSources{ 1318 {RepoURL: "file://does-not-exist", Ref: "source1"}, 1319 {RepoURL: "file://" + repoPath}, 1320 }) 1321 1322 refSources, err := GetRefSources(t.Context(), argoSpec.Sources, argoSpec.Project, func(_ context.Context, _ string, _ string) (*argoappv1.Repository, error) { 1323 return nil, errors.New("repo does not exist") 1324 }, []string{}) 1325 1326 require.Error(t, err) 1327 assert.Empty(t, refSources) 1328 }) 1329 1330 t.Run("invalid ref", func(t *testing.T) { 1331 argoSpec := getMultiSourceAppSpec(argoappv1.ApplicationSources{ 1332 {RepoURL: "file://does-not-exist", Ref: "%invalid-name%"}, 1333 {RepoURL: "file://" + repoPath}, 1334 }) 1335 1336 refSources, err := GetRefSources(t.Context(), argoSpec.Sources, argoSpec.Project, func(_ context.Context, _ string, _ string) (*argoappv1.Repository, error) { 1337 return nil, err 1338 }, []string{}) 1339 1340 require.Error(t, err) 1341 assert.Empty(t, refSources) 1342 }) 1343 1344 t.Run("duplicate ref keys", func(t *testing.T) { 1345 argoSpec := getMultiSourceAppSpec(argoappv1.ApplicationSources{ 1346 {RepoURL: "file://does-not-exist", Ref: "source1"}, 1347 {RepoURL: "file://does-not-exist", Ref: "source1"}, 1348 }) 1349 1350 refSources, err := GetRefSources(t.Context(), argoSpec.Sources, argoSpec.Project, func(_ context.Context, _ string, _ string) (*argoappv1.Repository, error) { 1351 return nil, err 1352 }, []string{}) 1353 1354 require.Error(t, err) 1355 assert.Empty(t, refSources) 1356 }) 1357 } 1358 1359 func TestValidatePermissionsMultipleSources(t *testing.T) { 1360 t.Run("Empty Repo URL result in condition", func(t *testing.T) { 1361 spec := argoappv1.ApplicationSpec{ 1362 Sources: argoappv1.ApplicationSources{ 1363 {RepoURL: ""}, 1364 }, 1365 } 1366 1367 proj := argoappv1.AppProject{} 1368 db := &dbmocks.ArgoDB{} 1369 conditions, err := ValidatePermissions(t.Context(), &spec, &proj, db) 1370 require.NoError(t, err) 1371 assert.Len(t, conditions, 1) 1372 assert.Equal(t, argoappv1.ApplicationConditionInvalidSpecError, conditions[0].Type) 1373 assert.Contains(t, conditions[0].Message, "are required") 1374 }) 1375 1376 t.Run("Incomplete Path/Chart/Ref combo result in condition", func(t *testing.T) { 1377 spec := argoappv1.ApplicationSpec{ 1378 Sources: argoappv1.ApplicationSources{ 1379 { 1380 RepoURL: "http://some/where", 1381 Path: "", 1382 Chart: "", 1383 Ref: "", 1384 }, 1385 }, 1386 } 1387 proj := argoappv1.AppProject{} 1388 db := &dbmocks.ArgoDB{} 1389 conditions, err := ValidatePermissions(t.Context(), &spec, &proj, db) 1390 require.NoError(t, err) 1391 assert.Len(t, conditions, 1) 1392 assert.Equal(t, argoappv1.ApplicationConditionInvalidSpecError, conditions[0].Type) 1393 assert.Contains(t, conditions[0].Message, "are required") 1394 }) 1395 1396 t.Run("One of the Application sources is not permitted in project", func(t *testing.T) { 1397 spec := argoappv1.ApplicationSpec{ 1398 Sources: argoappv1.ApplicationSources{ 1399 { 1400 RepoURL: "http://some/where", 1401 Path: "", 1402 Chart: "somechart", 1403 TargetRevision: "1.4.1", 1404 }, 1405 }, 1406 Destination: argoappv1.ApplicationDestination{ 1407 Server: "https://127.0.0.1:6443", 1408 Namespace: "testns", 1409 }, 1410 } 1411 proj := argoappv1.AppProject{ 1412 Spec: argoappv1.AppProjectSpec{ 1413 Destinations: []argoappv1.ApplicationDestination{ 1414 { 1415 Server: "*", 1416 Namespace: "*", 1417 }, 1418 }, 1419 SourceRepos: []string{"http://some/where/else"}, 1420 }, 1421 } 1422 cluster := &argoappv1.Cluster{Server: "https://127.0.0.1:6443", Name: "test"} 1423 db := &dbmocks.ArgoDB{} 1424 db.On("GetCluster", t.Context(), spec.Destination.Server).Return(cluster, nil) 1425 conditions, err := ValidatePermissions(t.Context(), &spec, &proj, db) 1426 require.NoError(t, err) 1427 assert.Len(t, conditions, 1) 1428 assert.Contains(t, conditions[0].Message, "application repo http://some/where is not permitted") 1429 }) 1430 1431 t.Run("Source with a Ref field and missing Path/Chart field", func(t *testing.T) { 1432 spec := argoappv1.ApplicationSpec{ 1433 Sources: argoappv1.ApplicationSources{ 1434 { 1435 RepoURL: "http://some/where", 1436 Path: "", 1437 Chart: "", 1438 Ref: "somechart", 1439 }, 1440 }, 1441 Destination: argoappv1.ApplicationDestination{ 1442 Name: "does-exist", 1443 Namespace: "default", 1444 }, 1445 } 1446 proj := argoappv1.AppProject{ 1447 Spec: argoappv1.AppProjectSpec{ 1448 Destinations: []argoappv1.ApplicationDestination{ 1449 { 1450 Server: "*", 1451 Namespace: "default", 1452 }, 1453 }, 1454 SourceRepos: []string{"http://some/where"}, 1455 }, 1456 } 1457 db := &dbmocks.ArgoDB{} 1458 cluster := argoappv1.Cluster{ 1459 Name: "does-exist", 1460 Server: "https://127.0.0.1:6443", 1461 } 1462 db.On("GetClusterServersByName", t.Context(), "does-exist").Return([]string{"https://127.0.0.1:6443"}, nil) 1463 db.On("GetCluster", t.Context(), "https://127.0.0.1:6443").Return(&cluster, nil) 1464 conditions, err := ValidatePermissions(t.Context(), &spec, &proj, db) 1465 require.NoError(t, err) 1466 assert.Empty(t, conditions) 1467 }) 1468 } 1469 1470 func TestAugmentSyncMsg(t *testing.T) { 1471 mockAPIResourcesFn := func() ([]kube.APIResourceInfo, error) { 1472 return []kube.APIResourceInfo{ 1473 { 1474 GroupKind: schema.GroupKind{ 1475 Group: "apps", 1476 Kind: "Deployment", 1477 }, 1478 GroupVersionResource: schema.GroupVersionResource{ 1479 Group: "apps", 1480 Version: "v1", 1481 }, 1482 }, 1483 { 1484 GroupKind: schema.GroupKind{ 1485 Group: "networking.k8s.io", 1486 Kind: "Ingress", 1487 }, 1488 GroupVersionResource: schema.GroupVersionResource{ 1489 Group: "networking.k8s.io", 1490 Version: "v1", 1491 }, 1492 }, 1493 }, nil 1494 } 1495 1496 testcases := []struct { 1497 name string 1498 msg string 1499 expectedMessage string 1500 res common.ResourceSyncResult 1501 mockFn func() ([]kube.APIResourceInfo, error) 1502 errMsg string 1503 }{ 1504 { 1505 name: "match specific k8s error", 1506 msg: "the server could not find the requested resource", 1507 res: common.ResourceSyncResult{ 1508 ResourceKey: kube.ResourceKey{ 1509 Name: "deployment-resource", 1510 Namespace: "test-namespace", 1511 Kind: "Deployment", 1512 Group: "apps", 1513 }, 1514 Version: "v1beta1", 1515 }, 1516 expectedMessage: "The Kubernetes API could not find version \"v1beta1\" of apps/Deployment for requested resource test-namespace/deployment-resource. Version \"v1\" of apps/Deployment is installed on the destination cluster.", 1517 mockFn: mockAPIResourcesFn, 1518 }, 1519 { 1520 name: "any random k8s msg", 1521 msg: "random message from k8s", 1522 res: common.ResourceSyncResult{ 1523 ResourceKey: kube.ResourceKey{ 1524 Name: "deployment-resource", 1525 Namespace: "test-namespace", 1526 Kind: "Deployment", 1527 Group: "apps", 1528 }, 1529 Version: "v1beta1", 1530 }, 1531 expectedMessage: "random message from k8s", 1532 mockFn: mockAPIResourcesFn, 1533 }, 1534 { 1535 name: "resource doesn't exist in the target cluster", 1536 res: common.ResourceSyncResult{ 1537 ResourceKey: kube.ResourceKey{ 1538 Name: "persistent-volume-resource", 1539 Namespace: "test-namespace", 1540 Kind: "PersistentVolume", 1541 Group: "", 1542 }, 1543 Version: "v1", 1544 }, 1545 msg: "the server could not find the requested resource", 1546 expectedMessage: "The Kubernetes API could not find /PersistentVolume for requested resource test-namespace/persistent-volume-resource. Make sure the \"PersistentVolume\" CRD is installed on the destination cluster.", 1547 mockFn: mockAPIResourcesFn, 1548 }, 1549 { 1550 name: "API Resource returns error", 1551 res: common.ResourceSyncResult{ 1552 ResourceKey: kube.ResourceKey{ 1553 Name: "persistent-volume-resource", 1554 Namespace: "test-namespace", 1555 Kind: "PersistentVolume", 1556 Group: "", 1557 }, 1558 Version: "v1", 1559 }, 1560 msg: "the server could not find the requested resource", 1561 expectedMessage: "the server could not find the requested resource", 1562 mockFn: func() ([]kube.APIResourceInfo, error) { 1563 return nil, errors.New("failed to fetch resource of given kind %s from the target cluster") 1564 }, 1565 errMsg: "failed to get API resource info for group \"\" and kind \"PersistentVolume\": failed to get API resource info: failed to fetch resource of given kind %s from the target cluster", 1566 }, 1567 { 1568 name: "old Ingress type returns error suggesting new Ingress type", 1569 res: common.ResourceSyncResult{ 1570 ResourceKey: kube.ResourceKey{ 1571 Name: "ingress-resource", 1572 Namespace: "test-namespace", 1573 Kind: "Ingress", 1574 Group: "extensions", 1575 }, 1576 Version: "v1beta1", 1577 }, 1578 msg: "the server could not find the requested resource", 1579 expectedMessage: "The Kubernetes API could not find version \"v1beta1\" of extensions/Ingress for requested resource test-namespace/ingress-resource. Version \"v1\" of networking.k8s.io/Ingress is installed on the destination cluster.", 1580 mockFn: mockAPIResourcesFn, 1581 }, 1582 } 1583 1584 for _, tt := range testcases { 1585 t.Run(tt.name, func(t *testing.T) { 1586 tt.res.Message = tt.msg 1587 msg, err := AugmentSyncMsg(tt.res, tt.mockFn) 1588 if tt.errMsg != "" { 1589 assert.EqualError(t, err, tt.errMsg) 1590 } else { 1591 require.NoError(t, err) 1592 assert.Equal(t, tt.expectedMessage, msg) 1593 } 1594 }) 1595 } 1596 } 1597 1598 func TestGetAppEventLabels(t *testing.T) { 1599 tests := []struct { 1600 name string 1601 cmInEventLabelKeys string 1602 cmExEventLabelKeys string 1603 appLabels map[string]string 1604 projLabels map[string]string 1605 expectedEventLabels map[string]string 1606 }{ 1607 { 1608 name: "no label keys in cm - no event labels", 1609 cmInEventLabelKeys: "", 1610 appLabels: map[string]string{"team": "A", "tier": "frontend"}, 1611 projLabels: map[string]string{"environment": "dev"}, 1612 expectedEventLabels: nil, 1613 }, 1614 { 1615 name: "label keys in cm, no labels on app & proj - no event labels", 1616 cmInEventLabelKeys: "team, environment", 1617 appLabels: nil, 1618 projLabels: nil, 1619 expectedEventLabels: nil, 1620 }, 1621 { 1622 name: "labels on app, no labels on proj - event labels matched on app only", 1623 cmInEventLabelKeys: "team, environment", 1624 appLabels: map[string]string{"team": "A", "tier": "frontend"}, 1625 projLabels: nil, 1626 expectedEventLabels: map[string]string{"team": "A"}, 1627 }, 1628 { 1629 name: "no labels on app, labels on proj - event labels matched on proj only", 1630 cmInEventLabelKeys: "team, environment", 1631 appLabels: nil, 1632 projLabels: map[string]string{"environment": "dev"}, 1633 expectedEventLabels: map[string]string{"environment": "dev"}, 1634 }, 1635 { 1636 name: "labels on app & proj with conflicts - event labels matched on both app & proj and app labels prioritized on conflict", 1637 cmInEventLabelKeys: "team, environment", 1638 appLabels: map[string]string{"team": "A", "environment": "stage", "tier": "frontend"}, 1639 projLabels: map[string]string{"environment": "dev"}, 1640 expectedEventLabels: map[string]string{"team": "A", "environment": "stage"}, 1641 }, 1642 { 1643 name: "wildcard support - matched all labels", 1644 cmInEventLabelKeys: "*", 1645 appLabels: map[string]string{"team": "A", "tier": "frontend"}, 1646 projLabels: map[string]string{"environment": "dev"}, 1647 expectedEventLabels: map[string]string{"team": "A", "tier": "frontend", "environment": "dev"}, 1648 }, 1649 { 1650 name: "exclude event labels", 1651 cmInEventLabelKeys: "example.com/team,tier,env*", 1652 cmExEventLabelKeys: "tie*", 1653 appLabels: map[string]string{"example.com/team": "A", "tier": "frontend"}, 1654 projLabels: map[string]string{"environment": "dev"}, 1655 expectedEventLabels: map[string]string{"example.com/team": "A", "environment": "dev"}, 1656 }, 1657 } 1658 for _, tt := range tests { 1659 t.Run(tt.name, func(t *testing.T) { 1660 cm := corev1.ConfigMap{ 1661 ObjectMeta: metav1.ObjectMeta{ 1662 Name: "argocd-cm", 1663 Namespace: test.FakeArgoCDNamespace, 1664 Labels: map[string]string{ 1665 "app.kubernetes.io/part-of": "argocd", 1666 }, 1667 }, 1668 Data: map[string]string{ 1669 "resource.includeEventLabelKeys": tt.cmInEventLabelKeys, 1670 "resource.excludeEventLabelKeys": tt.cmExEventLabelKeys, 1671 }, 1672 } 1673 1674 proj := &argoappv1.AppProject{ 1675 ObjectMeta: metav1.ObjectMeta{ 1676 Name: "default", 1677 Namespace: test.FakeArgoCDNamespace, 1678 Labels: tt.projLabels, 1679 }, 1680 } 1681 1682 var app argoappv1.Application 1683 app.Name = "test-app" 1684 app.Namespace = test.FakeArgoCDNamespace 1685 app.Labels = tt.appLabels 1686 appClientset := appclientset.NewSimpleClientset(proj) 1687 ctx, cancel := context.WithCancel(t.Context()) 1688 defer cancel() 1689 indexers := cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc} 1690 informer := v1alpha1.NewAppProjectInformer(appClientset, test.FakeArgoCDNamespace, 0, indexers) 1691 go informer.Run(ctx.Done()) 1692 cache.WaitForCacheSync(ctx.Done(), informer.HasSynced) 1693 1694 kubeClient := fake.NewSimpleClientset(&cm) 1695 settingsMgr := settings.NewSettingsManager(t.Context(), kubeClient, test.FakeArgoCDNamespace) 1696 argoDB := db.NewDB("default", settingsMgr, kubeClient) 1697 1698 eventLabels := GetAppEventLabels(ctx, &app, applisters.NewAppProjectLister(informer.GetIndexer()), test.FakeArgoCDNamespace, settingsMgr, argoDB) 1699 assert.Len(t, eventLabels, len(tt.expectedEventLabels)) 1700 for ek, ev := range tt.expectedEventLabels { 1701 v, found := eventLabels[ek] 1702 assert.True(t, found) 1703 assert.Equal(t, ev, v) 1704 } 1705 }) 1706 } 1707 }