github.com/argoproj/argo-cd@v1.8.7/reposerver/repository/repository_test.go (about) 1 package repository 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io/ioutil" 9 "os" 10 "path/filepath" 11 "regexp" 12 "strings" 13 "testing" 14 "time" 15 16 "github.com/Masterminds/semver" 17 "github.com/ghodss/yaml" 18 "github.com/stretchr/testify/assert" 19 "github.com/stretchr/testify/mock" 20 v1 "k8s.io/api/apps/v1" 21 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 22 "k8s.io/apimachinery/pkg/runtime" 23 24 argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1" 25 "github.com/argoproj/argo-cd/reposerver/apiclient" 26 "github.com/argoproj/argo-cd/reposerver/cache" 27 "github.com/argoproj/argo-cd/reposerver/metrics" 28 fileutil "github.com/argoproj/argo-cd/test/fixture/path" 29 cacheutil "github.com/argoproj/argo-cd/util/cache" 30 "github.com/argoproj/argo-cd/util/git" 31 gitmocks "github.com/argoproj/argo-cd/util/git/mocks" 32 "github.com/argoproj/argo-cd/util/helm" 33 helmmocks "github.com/argoproj/argo-cd/util/helm/mocks" 34 "github.com/argoproj/argo-cd/util/io" 35 ) 36 37 const testSignature = `gpg: Signature made Wed Feb 26 23:22:34 2020 CET 38 gpg: using RSA key 4AEE18F83AFDEB23 39 gpg: Good signature from "GitHub (web-flow commit signing) <noreply@github.com>" [ultimate] 40 ` 41 42 type clientFunc func(*gitmocks.Client) 43 44 func newServiceWithMocks(root string, signed bool) (*Service, *gitmocks.Client) { 45 root, err := filepath.Abs(root) 46 if err != nil { 47 panic(err) 48 } 49 return newServiceWithOpt(func(gitClient *gitmocks.Client) { 50 gitClient.On("Init").Return(nil) 51 gitClient.On("Fetch").Return(nil) 52 gitClient.On("Checkout", mock.Anything).Return(nil) 53 gitClient.On("LsRemote", mock.Anything).Return(mock.Anything, nil) 54 gitClient.On("CommitSHA").Return(mock.Anything, nil) 55 gitClient.On("Root").Return(root) 56 if signed { 57 gitClient.On("VerifyCommitSignature", mock.Anything).Return(testSignature, nil) 58 } else { 59 gitClient.On("VerifyCommitSignature", mock.Anything).Return("", nil) 60 } 61 }) 62 } 63 64 func newServiceWithOpt(cf clientFunc) (*Service, *gitmocks.Client) { 65 // root, err := filepath.Abs(root) 66 // if err != nil { 67 // panic(err) 68 // } 69 helmClient := &helmmocks.Client{} 70 gitClient := &gitmocks.Client{} 71 cf(gitClient) 72 service := NewService(metrics.NewMetricsServer(), cache.NewCache( 73 cacheutil.NewCache(cacheutil.NewInMemoryCache(1*time.Minute)), 74 1*time.Minute, 75 ), RepoServerInitConstants{ParallelismLimit: 1}) 76 77 chart := "my-chart" 78 version := semver.MustParse("1.1.0") 79 helmClient.On("GetIndex").Return(&helm.Index{Entries: map[string]helm.Entries{ 80 chart: {{Version: "1.0.0"}, {Version: version.String()}}, 81 }}, nil) 82 helmClient.On("ExtractChart", chart, version).Return("./testdata/my-chart", io.NopCloser, nil) 83 helmClient.On("CleanChartCache", chart, version).Return(nil) 84 85 service.newGitClient = func(rawRepoURL string, creds git.Creds, insecure bool, enableLfs bool) (client git.Client, e error) { 86 return gitClient, nil 87 } 88 service.newHelmClient = func(repoURL string, creds helm.Creds, enableOci bool) helm.Client { 89 return helmClient 90 } 91 return service, gitClient 92 } 93 94 func newService(root string) *Service { 95 service, _ := newServiceWithMocks(root, false) 96 return service 97 } 98 99 func newServiceWithSignature(root string) *Service { 100 service, _ := newServiceWithMocks(root, true) 101 return service 102 } 103 104 func newServiceWithCommitSHA(root, revision string) *Service { 105 var revisionErr error 106 107 commitSHARegex := regexp.MustCompile("^[0-9A-Fa-f]{40}$") 108 if !commitSHARegex.MatchString(revision) { 109 revisionErr = errors.New("not a commit SHA") 110 } 111 112 service, gitClient := newServiceWithOpt(func(gitClient *gitmocks.Client) { 113 gitClient.On("Init").Return(nil) 114 gitClient.On("Fetch").Return(nil) 115 gitClient.On("Checkout", mock.Anything).Return(nil) 116 gitClient.On("LsRemote", revision).Return(revision, revisionErr) 117 gitClient.On("CommitSHA").Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil) 118 gitClient.On("Root").Return(root) 119 }) 120 121 service.newGitClient = func(rawRepoURL string, creds git.Creds, insecure bool, enableLfs bool) (client git.Client, e error) { 122 return gitClient, nil 123 } 124 125 return service 126 } 127 128 func TestGenerateYamlManifestInDir(t *testing.T) { 129 service := newService("../..") 130 131 src := argoappv1.ApplicationSource{Path: "manifests/base"} 132 q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src} 133 134 // update this value if we add/remove manifests 135 const countOfManifests = 29 136 137 res1, err := service.GenerateManifest(context.Background(), &q) 138 139 assert.NoError(t, err) 140 assert.Equal(t, countOfManifests, len(res1.Manifests)) 141 142 // this will test concatenated manifests to verify we split YAMLs correctly 143 res2, err := GenerateManifests("./testdata/concatenated", "/", "", &q, false) 144 assert.NoError(t, err) 145 assert.Equal(t, 3, len(res2.Manifests)) 146 } 147 148 // ensure we can use a semver constraint range (>= 1.0.0) and get back the correct chart (1.0.0) 149 func TestHelmManifestFromChartRepo(t *testing.T) { 150 service := newService(".") 151 source := &argoappv1.ApplicationSource{Chart: "my-chart", TargetRevision: ">= 1.0.0"} 152 request := &apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: source, NoCache: true} 153 response, err := service.GenerateManifest(context.Background(), request) 154 assert.NoError(t, err) 155 assert.NotNil(t, response) 156 assert.Equal(t, &apiclient.ManifestResponse{ 157 Manifests: []string{"{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"name\":\"my-map\"}}"}, 158 Namespace: "", 159 Server: "", 160 Revision: "1.1.0", 161 SourceType: "Helm", 162 }, response) 163 } 164 165 func TestGenerateManifestsUseExactRevision(t *testing.T) { 166 service, gitClient := newServiceWithMocks(".", false) 167 168 src := argoappv1.ApplicationSource{Path: "./testdata/recurse", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}} 169 170 q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, Revision: "abc"} 171 172 res1, err := service.GenerateManifest(context.Background(), &q) 173 assert.Nil(t, err) 174 assert.Equal(t, 2, len(res1.Manifests)) 175 assert.Equal(t, gitClient.Calls[0].Arguments[0], "abc") 176 } 177 178 func TestRecurseManifestsInDir(t *testing.T) { 179 service := newService(".") 180 181 src := argoappv1.ApplicationSource{Path: "./testdata/recurse", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}} 182 183 q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src} 184 185 res1, err := service.GenerateManifest(context.Background(), &q) 186 assert.Nil(t, err) 187 assert.Equal(t, 2, len(res1.Manifests)) 188 } 189 190 func TestInvalidManifestsInDir(t *testing.T) { 191 service := newService(".") 192 193 src := argoappv1.ApplicationSource{Path: "./testdata/invalid-manifests", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}} 194 195 q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src} 196 197 _, err := service.GenerateManifest(context.Background(), &q) 198 assert.NotNil(t, err) 199 } 200 201 func TestGenerateJsonnetManifestInDir(t *testing.T) { 202 service := newService(".") 203 204 q := apiclient.ManifestRequest{ 205 Repo: &argoappv1.Repository{}, 206 ApplicationSource: &argoappv1.ApplicationSource{ 207 Path: "./testdata/jsonnet", 208 Directory: &argoappv1.ApplicationSourceDirectory{ 209 Jsonnet: argoappv1.ApplicationSourceJsonnet{ 210 ExtVars: []argoappv1.JsonnetVar{{Name: "extVarString", Value: "extVarString"}, {Name: "extVarCode", Value: "\"extVarCode\"", Code: true}}, 211 TLAs: []argoappv1.JsonnetVar{{Name: "tlaString", Value: "tlaString"}, {Name: "tlaCode", Value: "\"tlaCode\"", Code: true}}, 212 Libs: []string{"testdata/jsonnet/vendor"}, 213 }, 214 }, 215 }, 216 } 217 res1, err := service.GenerateManifest(context.Background(), &q) 218 assert.Nil(t, err) 219 assert.Equal(t, 2, len(res1.Manifests)) 220 } 221 222 func TestGenerateKsonnetManifest(t *testing.T) { 223 service := newService("../..") 224 225 q := apiclient.ManifestRequest{ 226 Repo: &argoappv1.Repository{}, 227 ApplicationSource: &argoappv1.ApplicationSource{ 228 Path: "./test/e2e/testdata/ksonnet", 229 Ksonnet: &argoappv1.ApplicationSourceKsonnet{ 230 Environment: "dev", 231 }, 232 }, 233 } 234 res, err := service.GenerateManifest(context.Background(), &q) 235 assert.Nil(t, err) 236 assert.Equal(t, 2, len(res.Manifests)) 237 assert.Equal(t, "dev", res.Namespace) 238 assert.Equal(t, "https://kubernetes.default.svc", res.Server) 239 } 240 241 func TestGenerateHelmChartWithDependencies(t *testing.T) { 242 service := newService("../..") 243 244 cleanup := func() { 245 _ = os.Remove(filepath.Join("../../util/helm/testdata/helm2-dependency", helmDepUpMarkerFile)) 246 _ = os.RemoveAll(filepath.Join("../../util/helm/testdata/helm2-dependency", "charts")) 247 } 248 cleanup() 249 defer cleanup() 250 251 helmRepo := argoappv1.Repository{Name: "bitnami", Type: "helm", Repo: "https://charts.bitnami.com/bitnami"} 252 q := apiclient.ManifestRequest{ 253 Repo: &argoappv1.Repository{}, 254 ApplicationSource: &argoappv1.ApplicationSource{ 255 Path: "./util/helm/testdata/helm2-dependency", 256 }, 257 Repos: []*argoappv1.Repository{&helmRepo}, 258 } 259 res1, err := service.GenerateManifest(context.Background(), &q) 260 assert.Nil(t, err) 261 assert.Len(t, res1.Manifests, 10) 262 } 263 264 func TestManifestGenErrorCacheByNumRequests(t *testing.T) { 265 266 // Returns the state of the manifest generation cache, by querying the cache for the previously set result 267 getRecentCachedEntry := func(service *Service, manifestRequest *apiclient.ManifestRequest) *cache.CachedManifestResponse { 268 assert.NotNil(t, service) 269 assert.NotNil(t, manifestRequest) 270 271 cachedManifestResponse := &cache.CachedManifestResponse{} 272 err := service.cache.GetManifests(mock.Anything, manifestRequest.ApplicationSource, manifestRequest.Namespace, manifestRequest.AppLabelKey, manifestRequest.AppLabelValue, cachedManifestResponse) 273 assert.Nil(t, err) 274 return cachedManifestResponse 275 } 276 277 // Example: 278 // With repo server (test) parameters: 279 // - PauseGenerationAfterFailedGenerationAttempts: 2 280 // - PauseGenerationOnFailureForRequests: 4 281 // - TotalCacheInvocations: 10 282 // 283 // After 2 manifest generation failures in a row, the next 4 manifest generation requests should be cached, 284 // with the next 2 after that being uncached. Here's how it looks... 285 // 286 // request count) result 287 // -------------------------- 288 // 1) Attempt to generate manifest, fails. 289 // 2) Second attempt to generate manifest, fails. 290 // 3) Return cached error attempt from #2 291 // 4) Return cached error attempt from #2 292 // 5) Return cached error attempt from #2 293 // 6) Return cached error attempt from #2. Max response limit hit, so reset cache entry. 294 // 7) Attempt to generate manifest, fails. 295 // 8) Attempt to generate manifest, fails. 296 // 9) Return cached error attempt from #8 297 // 10) Return cached error attempt from #8 298 299 // The same pattern PauseGenerationAfterFailedGenerationAttempts generation attempts, followed by 300 // PauseGenerationOnFailureForRequests cached responses, should apply for various combinations of 301 // both parameters. 302 303 tests := []struct { 304 PauseGenerationAfterFailedGenerationAttempts int 305 PauseGenerationOnFailureForRequests int 306 TotalCacheInvocations int 307 }{ 308 {2, 4, 10}, 309 {3, 5, 10}, 310 {1, 2, 5}, 311 } 312 for _, tt := range tests { 313 testName := fmt.Sprintf("gen-attempts-%d-pause-%d-total-%d", tt.PauseGenerationAfterFailedGenerationAttempts, tt.PauseGenerationOnFailureForRequests, tt.TotalCacheInvocations) 314 t.Run(testName, func(t *testing.T) { 315 service := newService(".") 316 317 service.initConstants = RepoServerInitConstants{ 318 ParallelismLimit: 1, 319 PauseGenerationAfterFailedGenerationAttempts: tt.PauseGenerationAfterFailedGenerationAttempts, 320 PauseGenerationOnFailureForMinutes: 0, 321 PauseGenerationOnFailureForRequests: tt.PauseGenerationOnFailureForRequests, 322 } 323 324 totalAttempts := service.initConstants.PauseGenerationAfterFailedGenerationAttempts + service.initConstants.PauseGenerationOnFailureForRequests 325 326 for invocationCount := 0; invocationCount < tt.TotalCacheInvocations; invocationCount++ { 327 adjustedInvocation := invocationCount % totalAttempts 328 329 fmt.Printf("%d )-------------------------------------------\n", invocationCount) 330 331 manifestRequest := &apiclient.ManifestRequest{ 332 Repo: &argoappv1.Repository{}, 333 AppLabelValue: "test", 334 ApplicationSource: &argoappv1.ApplicationSource{ 335 Path: "./testdata/invalid-helm", 336 }, 337 } 338 339 res, err := service.GenerateManifest(context.Background(), manifestRequest) 340 341 // Verify invariant: res != nil xor err != nil 342 if err != nil { 343 assert.True(t, res == nil, "both err and res are non-nil res: %v err: %v", res, err) 344 } else { 345 assert.True(t, res != nil, "both err and res are nil") 346 } 347 348 cachedManifestResponse := getRecentCachedEntry(service, manifestRequest) 349 350 isCachedError := err != nil && strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix) 351 352 if adjustedInvocation < service.initConstants.PauseGenerationAfterFailedGenerationAttempts { 353 // GenerateManifest should not return cached errors for the first X responses, where X is the FailGenAttempts constants 354 assert.True(t, !isCachedError) 355 356 assert.True(t, cachedManifestResponse != nil) 357 // nolint:staticcheck 358 assert.True(t, cachedManifestResponse.ManifestResponse == nil) 359 // nolint:staticcheck 360 assert.True(t, cachedManifestResponse.FirstFailureTimestamp != 0) 361 362 // Internal cache consec failures value should increase with invocations, cached response should stay the same, 363 // nolint:staticcheck 364 assert.True(t, cachedManifestResponse.NumberOfConsecutiveFailures == adjustedInvocation+1) 365 // nolint:staticcheck 366 assert.True(t, cachedManifestResponse.NumberOfCachedResponsesReturned == 0) 367 368 } else { 369 // GenerateManifest SHOULD return cached errors for the next X responses, where X is the 370 // PauseGenerationOnFailureForRequests constant 371 assert.True(t, isCachedError) 372 assert.True(t, cachedManifestResponse != nil) 373 // nolint:staticcheck 374 assert.True(t, cachedManifestResponse.ManifestResponse == nil) 375 // nolint:staticcheck 376 assert.True(t, cachedManifestResponse.FirstFailureTimestamp != 0) 377 378 // Internal cache values should update correctly based on number of return cache entries, consecutive failures should stay the same 379 // nolint:staticcheck 380 assert.True(t, cachedManifestResponse.NumberOfConsecutiveFailures == service.initConstants.PauseGenerationAfterFailedGenerationAttempts) 381 // nolint:staticcheck 382 assert.True(t, cachedManifestResponse.NumberOfCachedResponsesReturned == (adjustedInvocation-service.initConstants.PauseGenerationAfterFailedGenerationAttempts+1)) 383 } 384 } 385 }) 386 } 387 } 388 389 func TestManifestGenErrorCacheFileContentsChange(t *testing.T) { 390 391 tmpDir, err := ioutil.TempDir("", "repository-test-") 392 assert.NoError(t, err) 393 defer os.RemoveAll(tmpDir) 394 395 service := newService(tmpDir) 396 397 service.initConstants = RepoServerInitConstants{ 398 ParallelismLimit: 1, 399 PauseGenerationAfterFailedGenerationAttempts: 2, 400 PauseGenerationOnFailureForMinutes: 0, 401 PauseGenerationOnFailureForRequests: 4, 402 } 403 404 for step := 0; step < 3; step++ { 405 406 // step 1) Attempt to generate manifests against invalid helm chart (should return uncached error) 407 // step 2) Attempt to generate manifest against valid helm chart (should succeed and return valid response) 408 // step 3) Attempt to generate manifest against invalid helm chart (should return cached value from step 2) 409 410 errorExpected := step%2 == 0 411 412 // Ensure that the target directory will succeed or fail, so we can verify the cache correctly handles it 413 err = os.RemoveAll(tmpDir) 414 assert.NoError(t, err) 415 err = os.MkdirAll(tmpDir, 0777) 416 assert.NoError(t, err) 417 if errorExpected { 418 // Copy invalid helm chart into temporary directory, ensuring manifest generation will fail 419 err = fileutil.CopyDir("./testdata/invalid-helm", tmpDir) 420 assert.NoError(t, err) 421 422 } else { 423 // Copy valid helm chart into temporary directory, ensuring generation will succeed 424 err = fileutil.CopyDir("./testdata/my-chart", tmpDir) 425 assert.NoError(t, err) 426 } 427 428 res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 429 Repo: &argoappv1.Repository{}, 430 AppLabelValue: "test", 431 ApplicationSource: &argoappv1.ApplicationSource{ 432 Path: ".", 433 }, 434 }) 435 436 fmt.Println("-", step, "-", res != nil, err != nil, errorExpected) 437 fmt.Println(" err: ", err) 438 fmt.Println(" res: ", res) 439 440 if step < 2 { 441 assert.True(t, (err != nil) == errorExpected, "error return value and error expected did not match") 442 assert.True(t, (res != nil) == !errorExpected, "GenerateManifest return value and expected value did not match") 443 } 444 445 if step == 2 { 446 assert.True(t, err == nil, "error ret val was non-nil on step 3") 447 assert.True(t, res != nil, "GenerateManifest ret val was nil on step 3") 448 } 449 } 450 } 451 452 func TestManifestGenErrorCacheByMinutesElapsed(t *testing.T) { 453 454 tests := []struct { 455 // Test with a range of pause expiration thresholds 456 PauseGenerationOnFailureForMinutes int 457 }{ 458 {1}, {2}, {10}, {24 * 60}, 459 } 460 for _, tt := range tests { 461 testName := fmt.Sprintf("pause-time-%d", tt.PauseGenerationOnFailureForMinutes) 462 t.Run(testName, func(t *testing.T) { 463 service := newService(".") 464 465 // Here we simulate the passage of time by overriding the now() function of Service 466 currentTime := time.Now() 467 service.now = func() time.Time { 468 return currentTime 469 } 470 471 service.initConstants = RepoServerInitConstants{ 472 ParallelismLimit: 1, 473 PauseGenerationAfterFailedGenerationAttempts: 1, 474 PauseGenerationOnFailureForMinutes: tt.PauseGenerationOnFailureForMinutes, 475 PauseGenerationOnFailureForRequests: 0, 476 } 477 478 // 1) Put the cache into the failure state 479 for x := 0; x < 2; x++ { 480 res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 481 Repo: &argoappv1.Repository{}, 482 AppLabelValue: "test", 483 ApplicationSource: &argoappv1.ApplicationSource{ 484 Path: "./testdata/invalid-helm", 485 }, 486 }) 487 488 assert.True(t, err != nil && res == nil) 489 490 // Ensure that the second invocation triggers the cached error state 491 if x == 1 { 492 assert.True(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix)) 493 } 494 495 } 496 497 // 2) Jump forward X-1 minutes in time, where X is the expiration boundary 498 currentTime = currentTime.Add(time.Duration(tt.PauseGenerationOnFailureForMinutes-1) * time.Minute) 499 res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 500 Repo: &argoappv1.Repository{}, 501 AppLabelValue: "test", 502 ApplicationSource: &argoappv1.ApplicationSource{ 503 Path: "./testdata/invalid-helm", 504 }, 505 }) 506 507 // 3) Ensure that the cache still returns a cached copy of the last error 508 assert.True(t, err != nil && res == nil) 509 assert.True(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix)) 510 511 // 4) Jump forward 2 minutes in time, such that the pause generation time has elapsed and we should return to normal state 512 currentTime = currentTime.Add(2 * time.Minute) 513 514 res, err = service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 515 Repo: &argoappv1.Repository{}, 516 AppLabelValue: "test", 517 ApplicationSource: &argoappv1.ApplicationSource{ 518 Path: "./testdata/invalid-helm", 519 }, 520 }) 521 522 // 5) Ensure that the service no longer returns a cached copy of the last error 523 assert.True(t, err != nil && res == nil) 524 assert.True(t, !strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix)) 525 526 }) 527 } 528 529 } 530 531 func TestManifestGenErrorCacheRespectsNoCache(t *testing.T) { 532 533 service := newService(".") 534 535 service.initConstants = RepoServerInitConstants{ 536 ParallelismLimit: 1, 537 PauseGenerationAfterFailedGenerationAttempts: 1, 538 PauseGenerationOnFailureForMinutes: 0, 539 PauseGenerationOnFailureForRequests: 4, 540 } 541 542 // 1) Put the cache into the failure state 543 for x := 0; x < 2; x++ { 544 res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 545 Repo: &argoappv1.Repository{}, 546 AppLabelValue: "test", 547 ApplicationSource: &argoappv1.ApplicationSource{ 548 Path: "./testdata/invalid-helm", 549 }, 550 }) 551 552 assert.True(t, err != nil && res == nil) 553 554 // Ensure that the second invocation is cached 555 if x == 1 { 556 assert.True(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix)) 557 } 558 } 559 560 // 2) Call generateManifest with NoCache enabled 561 res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 562 Repo: &argoappv1.Repository{}, 563 AppLabelValue: "test", 564 ApplicationSource: &argoappv1.ApplicationSource{ 565 Path: "./testdata/invalid-helm", 566 }, 567 NoCache: true, 568 }) 569 570 // 3) Ensure that the cache returns a new generation attempt, rather than a previous cached error 571 assert.True(t, err != nil && res == nil) 572 assert.True(t, !strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix)) 573 574 // 4) Call generateManifest 575 res, err = service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 576 Repo: &argoappv1.Repository{}, 577 AppLabelValue: "test", 578 ApplicationSource: &argoappv1.ApplicationSource{ 579 Path: "./testdata/invalid-helm", 580 }, 581 }) 582 583 // 5) Ensure that the subsequent invocation, after nocache, is cached 584 assert.True(t, err != nil && res == nil) 585 assert.True(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix)) 586 587 } 588 589 func TestGenerateHelmWithValues(t *testing.T) { 590 service := newService("../..") 591 592 res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 593 Repo: &argoappv1.Repository{}, 594 AppLabelValue: "test", 595 ApplicationSource: &argoappv1.ApplicationSource{ 596 Path: "./util/helm/testdata/redis", 597 Helm: &argoappv1.ApplicationSourceHelm{ 598 ValueFiles: []string{"values-production.yaml"}, 599 Values: `cluster: {slaveCount: 2}`, 600 }, 601 }, 602 }) 603 604 assert.NoError(t, err) 605 606 replicasVerified := false 607 for _, src := range res.Manifests { 608 obj := unstructured.Unstructured{} 609 err = json.Unmarshal([]byte(src), &obj) 610 assert.NoError(t, err) 611 612 if obj.GetKind() == "Deployment" && obj.GetName() == "test-redis-slave" { 613 var dep v1.Deployment 614 err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &dep) 615 assert.NoError(t, err) 616 assert.Equal(t, int32(2), *dep.Spec.Replicas) 617 replicasVerified = true 618 } 619 } 620 assert.True(t, replicasVerified) 621 622 } 623 624 // The requested value file (`../minio/values.yaml`) is outside the app path (`./util/helm/testdata/redis`), however 625 // since the requested value is sill under the repo directory (`~/go/src/github.com/argoproj/argo-cd`), it is allowed 626 func TestGenerateHelmWithValuesDirectoryTraversal(t *testing.T) { 627 service := newService("../..") 628 _, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 629 Repo: &argoappv1.Repository{}, 630 AppLabelValue: "test", 631 ApplicationSource: &argoappv1.ApplicationSource{ 632 Path: "./util/helm/testdata/redis", 633 Helm: &argoappv1.ApplicationSourceHelm{ 634 ValueFiles: []string{"../minio/values.yaml"}, 635 Values: `cluster: {slaveCount: 2}`, 636 }, 637 }, 638 }) 639 assert.NoError(t, err) 640 641 // Test the case where the path is "." 642 service = newService("./testdata/my-chart") 643 _, err = service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 644 Repo: &argoappv1.Repository{}, 645 AppLabelValue: "test", 646 ApplicationSource: &argoappv1.ApplicationSource{ 647 Path: ".", 648 }, 649 }) 650 assert.NoError(t, err) 651 } 652 653 // This is a Helm first-class app with a values file inside the repo directory 654 // (`~/go/src/github.com/argoproj/argo-cd/reposerver/repository`), so it is allowed 655 func TestHelmManifestFromChartRepoWithValueFile(t *testing.T) { 656 service := newService(".") 657 source := &argoappv1.ApplicationSource{ 658 Chart: "my-chart", 659 TargetRevision: ">= 1.0.0", 660 Helm: &argoappv1.ApplicationSourceHelm{ 661 ValueFiles: []string{"./my-chart-values.yaml"}, 662 }, 663 } 664 request := &apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: source, NoCache: true} 665 response, err := service.GenerateManifest(context.Background(), request) 666 assert.NoError(t, err) 667 assert.NotNil(t, response) 668 assert.Equal(t, &apiclient.ManifestResponse{ 669 Manifests: []string{"{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"name\":\"my-map\"}}"}, 670 Namespace: "", 671 Server: "", 672 Revision: "1.1.0", 673 SourceType: "Helm", 674 }, response) 675 } 676 677 // This is a Helm first-class app with a values file outside the repo directory 678 // (`~/go/src/github.com/argoproj/argo-cd/reposerver/repository`), so it is not allowed 679 func TestHelmManifestFromChartRepoWithValueFileOutsideRepo(t *testing.T) { 680 service := newService(".") 681 source := &argoappv1.ApplicationSource{ 682 Chart: "my-chart", 683 TargetRevision: ">= 1.0.0", 684 Helm: &argoappv1.ApplicationSourceHelm{ 685 ValueFiles: []string{"../my-chart-2/my-chart-2-values.yaml"}, 686 }, 687 } 688 request := &apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: source, NoCache: true} 689 _, err := service.GenerateManifest(context.Background(), request) 690 assert.Error(t, err, "should be on or under current directory") 691 } 692 693 func TestGenerateHelmWithURL(t *testing.T) { 694 service := newService("../..") 695 696 _, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 697 Repo: &argoappv1.Repository{}, 698 AppLabelValue: "test", 699 ApplicationSource: &argoappv1.ApplicationSource{ 700 Path: "./util/helm/testdata/redis", 701 Helm: &argoappv1.ApplicationSourceHelm{ 702 ValueFiles: []string{"https://raw.githubusercontent.com/argoproj/argocd-example-apps/master/helm-guestbook/values.yaml"}, 703 Values: `cluster: {slaveCount: 2}`, 704 }, 705 }, 706 }) 707 assert.NoError(t, err) 708 } 709 710 // The requested value file (`../../../../../minio/values.yaml`) is outside the repo directory 711 // (`~/go/src/github.com/argoproj/argo-cd`), so it is blocked 712 func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) { 713 service := newService("../..") 714 _, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 715 Repo: &argoappv1.Repository{}, 716 AppLabelValue: "test", 717 ApplicationSource: &argoappv1.ApplicationSource{ 718 Path: "./util/helm/testdata/redis", 719 Helm: &argoappv1.ApplicationSourceHelm{ 720 ValueFiles: []string{"../../../../../minio/values.yaml"}, 721 Values: `cluster: {slaveCount: 2}`, 722 }, 723 }, 724 }) 725 assert.Error(t, err, "should be on or under current directory") 726 727 service = newService("./testdata/my-chart") 728 _, err = service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 729 Repo: &argoappv1.Repository{}, 730 AppLabelValue: "test", 731 ApplicationSource: &argoappv1.ApplicationSource{ 732 Path: ".", 733 Helm: &argoappv1.ApplicationSourceHelm{ 734 ValueFiles: []string{"../my-chart-2/values.yaml"}, 735 Values: `cluster: {slaveCount: 2}`, 736 }, 737 }, 738 }) 739 assert.Error(t, err, "should be on or under current directory") 740 } 741 742 // The requested file parameter (`/tmp/external-secret.txt`) is outside the app path 743 // (`./util/helm/testdata/redis`), and outside the repo directory. It is used as a means 744 // of providing direct content to a helm chart via a specific key. 745 func TestGenerateHelmWithAbsoluteFileParameter(t *testing.T) { 746 service := newService("../..") 747 748 file, err := ioutil.TempFile("", "external-secret.txt") 749 assert.NoError(t, err) 750 externalSecretPath := file.Name() 751 defer func() { _ = os.RemoveAll(externalSecretPath) }() 752 expectedFileContent, err := ioutil.ReadFile("../../util/helm/testdata/external/external-secret.txt") 753 assert.NoError(t, err) 754 err = ioutil.WriteFile(externalSecretPath, expectedFileContent, 0644) 755 assert.NoError(t, err) 756 757 _, err = service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 758 Repo: &argoappv1.Repository{}, 759 AppLabelValue: "test", 760 ApplicationSource: &argoappv1.ApplicationSource{ 761 Path: "./util/helm/testdata/redis", 762 Helm: &argoappv1.ApplicationSourceHelm{ 763 ValueFiles: []string{"values-production.yaml"}, 764 Values: `cluster: {slaveCount: 2}`, 765 FileParameters: []argoappv1.HelmFileParameter{ 766 argoappv1.HelmFileParameter{ 767 Name: "passwordContent", 768 Path: externalSecretPath, 769 }, 770 }, 771 }, 772 }, 773 }) 774 assert.NoError(t, err) 775 } 776 777 // The requested file parameter (`../external/external-secret.txt`) is outside the app path 778 // (`./util/helm/testdata/redis`), however since the requested value is sill under the repo 779 // directory (`~/go/src/github.com/argoproj/argo-cd`), it is allowed. It is used as a means of 780 // providing direct content to a helm chart via a specific key. 781 func TestGenerateHelmWithFileParameter(t *testing.T) { 782 service := newService("../..") 783 784 _, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 785 Repo: &argoappv1.Repository{}, 786 AppLabelValue: "test", 787 ApplicationSource: &argoappv1.ApplicationSource{ 788 Path: "./util/helm/testdata/redis", 789 Helm: &argoappv1.ApplicationSourceHelm{ 790 ValueFiles: []string{"values-production.yaml"}, 791 Values: `cluster: {slaveCount: 2}`, 792 FileParameters: []argoappv1.HelmFileParameter{ 793 argoappv1.HelmFileParameter{ 794 Name: "passwordContent", 795 Path: "../external/external-secret.txt", 796 }, 797 }, 798 }, 799 }, 800 }) 801 assert.NoError(t, err) 802 } 803 804 func TestGenerateNullList(t *testing.T) { 805 service := newService(".") 806 807 res1, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 808 Repo: &argoappv1.Repository{}, 809 ApplicationSource: &argoappv1.ApplicationSource{Path: "./testdata/null-list"}, 810 }) 811 assert.Nil(t, err) 812 assert.Equal(t, len(res1.Manifests), 1) 813 assert.Contains(t, res1.Manifests[0], "prometheus-operator-operator") 814 815 res1, err = service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 816 Repo: &argoappv1.Repository{}, 817 ApplicationSource: &argoappv1.ApplicationSource{Path: "./testdata/empty-list"}, 818 }) 819 assert.Nil(t, err) 820 assert.Equal(t, len(res1.Manifests), 1) 821 assert.Contains(t, res1.Manifests[0], "prometheus-operator-operator") 822 823 res1, err = service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 824 Repo: &argoappv1.Repository{}, 825 ApplicationSource: &argoappv1.ApplicationSource{Path: "./testdata/weird-list"}, 826 }) 827 assert.Nil(t, err) 828 assert.Equal(t, 2, len(res1.Manifests)) 829 } 830 831 func TestIdentifyAppSourceTypeByAppDirWithKustomizations(t *testing.T) { 832 sourceType, err := GetAppSourceType(&argoappv1.ApplicationSource{}, "./testdata/kustomization_yaml") 833 assert.Nil(t, err) 834 assert.Equal(t, argoappv1.ApplicationSourceTypeKustomize, sourceType) 835 836 sourceType, err = GetAppSourceType(&argoappv1.ApplicationSource{}, "./testdata/kustomization_yml") 837 assert.Nil(t, err) 838 assert.Equal(t, argoappv1.ApplicationSourceTypeKustomize, sourceType) 839 840 sourceType, err = GetAppSourceType(&argoappv1.ApplicationSource{}, "./testdata/Kustomization") 841 assert.Nil(t, err) 842 assert.Equal(t, argoappv1.ApplicationSourceTypeKustomize, sourceType) 843 } 844 845 func TestRunCustomTool(t *testing.T) { 846 service := newService(".") 847 848 res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 849 AppLabelValue: "test-app", 850 Namespace: "test-namespace", 851 ApplicationSource: &argoappv1.ApplicationSource{ 852 Plugin: &argoappv1.ApplicationSourcePlugin{ 853 Name: "test", 854 }, 855 }, 856 Plugins: []*argoappv1.ConfigManagementPlugin{{ 857 Name: "test", 858 Generate: argoappv1.Command{ 859 Command: []string{"sh", "-c"}, 860 Args: []string{`echo "{\"kind\": \"FakeObject\", \"metadata\": { \"name\": \"$ARGOCD_APP_NAME\", \"namespace\": \"$ARGOCD_APP_NAMESPACE\", \"annotations\": {\"GIT_ASKPASS\": \"$GIT_ASKPASS\", \"GIT_USERNAME\": \"$GIT_USERNAME\", \"GIT_PASSWORD\": \"$GIT_PASSWORD\"}}}"`}, 861 }, 862 }}, 863 Repo: &argoappv1.Repository{ 864 Username: "foo", Password: "bar", 865 }, 866 }) 867 868 assert.Nil(t, err) 869 assert.Equal(t, 1, len(res.Manifests)) 870 871 obj := &unstructured.Unstructured{} 872 assert.Nil(t, json.Unmarshal([]byte(res.Manifests[0]), obj)) 873 874 assert.Equal(t, obj.GetName(), "test-app") 875 assert.Equal(t, obj.GetNamespace(), "test-namespace") 876 assert.Equal(t, "git-ask-pass.sh", obj.GetAnnotations()["GIT_ASKPASS"]) 877 assert.Equal(t, "foo", obj.GetAnnotations()["GIT_USERNAME"]) 878 assert.Equal(t, "bar", obj.GetAnnotations()["GIT_PASSWORD"]) 879 } 880 881 func TestGenerateFromUTF16(t *testing.T) { 882 q := apiclient.ManifestRequest{ 883 Repo: &argoappv1.Repository{}, 884 ApplicationSource: &argoappv1.ApplicationSource{}, 885 } 886 res1, err := GenerateManifests("./testdata/utf-16", "/", "", &q, false) 887 assert.Nil(t, err) 888 assert.Equal(t, 2, len(res1.Manifests)) 889 } 890 891 func TestListApps(t *testing.T) { 892 service := newService("./testdata") 893 894 res, err := service.ListApps(context.Background(), &apiclient.ListAppsRequest{Repo: &argoappv1.Repository{}}) 895 assert.NoError(t, err) 896 897 expectedApps := map[string]string{ 898 "Kustomization": "Kustomize", 899 "app-parameters": "Kustomize", 900 "invalid-helm": "Helm", 901 "invalid-kustomize": "Kustomize", 902 "kustomization_yaml": "Kustomize", 903 "kustomization_yml": "Kustomize", 904 "my-chart": "Helm", 905 "my-chart-2": "Helm", 906 } 907 assert.Equal(t, expectedApps, res.Apps) 908 } 909 910 func TestGetAppDetailsHelm(t *testing.T) { 911 service := newService("../..") 912 913 res, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{ 914 Repo: &argoappv1.Repository{}, 915 Source: &argoappv1.ApplicationSource{ 916 Path: "./util/helm/testdata/helm2-dependency", 917 }, 918 }) 919 920 assert.NoError(t, err) 921 assert.NotNil(t, res.Helm) 922 923 assert.Equal(t, "Helm", res.Type) 924 assert.EqualValues(t, []string{"values-production.yaml", "values.yaml"}, res.Helm.ValueFiles) 925 } 926 927 func TestGetAppDetailsKustomize(t *testing.T) { 928 service := newService("../..") 929 930 res, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{ 931 Repo: &argoappv1.Repository{}, 932 Source: &argoappv1.ApplicationSource{ 933 Path: "./util/kustomize/testdata/kustomization_yaml", 934 }, 935 }) 936 937 assert.NoError(t, err) 938 939 assert.Equal(t, "Kustomize", res.Type) 940 assert.NotNil(t, res.Kustomize) 941 assert.EqualValues(t, []string{"nginx:1.15.4", "k8s.gcr.io/nginx-slim:0.8"}, res.Kustomize.Images) 942 } 943 944 func TestGetAppDetailsKsonnet(t *testing.T) { 945 service := newService("../..") 946 947 res, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{ 948 Repo: &argoappv1.Repository{}, 949 Source: &argoappv1.ApplicationSource{ 950 Path: "./test/e2e/testdata/ksonnet", 951 }, 952 }) 953 954 assert.NoError(t, err) 955 956 assert.Equal(t, "Ksonnet", res.Type) 957 assert.NotNil(t, res.Ksonnet) 958 assert.Equal(t, "guestbook", res.Ksonnet.Name) 959 assert.Len(t, res.Ksonnet.Environments, 3) 960 } 961 962 func TestGetHelmCharts(t *testing.T) { 963 service := newService("../..") 964 res, err := service.GetHelmCharts(context.Background(), &apiclient.HelmChartsRequest{Repo: &argoappv1.Repository{}}) 965 assert.NoError(t, err) 966 assert.Len(t, res.Items, 1) 967 968 item := res.Items[0] 969 assert.Equal(t, "my-chart", item.Name) 970 assert.EqualValues(t, []string{"1.0.0", "1.1.0"}, item.Versions) 971 } 972 973 func TestGetRevisionMetadata(t *testing.T) { 974 service, gitClient := newServiceWithMocks("../..", false) 975 now := time.Now() 976 977 gitClient.On("RevisionMetadata", mock.Anything).Return(&git.RevisionMetadata{ 978 Message: strings.Repeat("a", 100) + "\n" + "second line", 979 Author: "author", 980 Date: now, 981 Tags: []string{"tag1", "tag2"}, 982 }, nil) 983 984 res, err := service.GetRevisionMetadata(context.Background(), &apiclient.RepoServerRevisionMetadataRequest{ 985 Repo: &argoappv1.Repository{}, 986 Revision: "c0b400fc458875d925171398f9ba9eabd5529923", 987 CheckSignature: true, 988 }) 989 990 assert.NoError(t, err) 991 assert.Equal(t, strings.Repeat("a", 61)+"...", res.Message) 992 assert.Equal(t, now, res.Date.Time) 993 assert.Equal(t, "author", res.Author) 994 assert.EqualValues(t, []string{"tag1", "tag2"}, res.Tags) 995 assert.NotEmpty(t, res.SignatureInfo) 996 997 // Cache hit - signature info should not be in result 998 res, err = service.GetRevisionMetadata(context.Background(), &apiclient.RepoServerRevisionMetadataRequest{ 999 Repo: &argoappv1.Repository{}, 1000 Revision: "c0b400fc458875d925171398f9ba9eabd5529923", 1001 CheckSignature: false, 1002 }) 1003 assert.NoError(t, err) 1004 assert.Empty(t, res.SignatureInfo) 1005 1006 // Enforce cache miss - signature info should not be in result 1007 res, err = service.GetRevisionMetadata(context.Background(), &apiclient.RepoServerRevisionMetadataRequest{ 1008 Repo: &argoappv1.Repository{}, 1009 Revision: "c0b400fc458875d925171398f9ba9eabd5529924", 1010 CheckSignature: false, 1011 }) 1012 assert.NoError(t, err) 1013 assert.Empty(t, res.SignatureInfo) 1014 1015 // Cache hit on previous entry that did not have signature info 1016 res, err = service.GetRevisionMetadata(context.Background(), &apiclient.RepoServerRevisionMetadataRequest{ 1017 Repo: &argoappv1.Repository{}, 1018 Revision: "c0b400fc458875d925171398f9ba9eabd5529924", 1019 CheckSignature: true, 1020 }) 1021 assert.NoError(t, err) 1022 assert.NotEmpty(t, res.SignatureInfo) 1023 } 1024 1025 func TestGetSignatureVerificationResult(t *testing.T) { 1026 // Commit with signature and verification requested 1027 { 1028 service := newServiceWithSignature("../..") 1029 1030 src := argoappv1.ApplicationSource{Path: "manifests/base"} 1031 q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, VerifySignature: true} 1032 1033 res, err := service.GenerateManifest(context.Background(), &q) 1034 assert.NoError(t, err) 1035 assert.Equal(t, testSignature, res.VerifyResult) 1036 } 1037 // Commit with signature and verification not requested 1038 { 1039 service := newServiceWithSignature("../..") 1040 1041 src := argoappv1.ApplicationSource{Path: "manifests/base"} 1042 q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src} 1043 1044 res, err := service.GenerateManifest(context.Background(), &q) 1045 assert.NoError(t, err) 1046 assert.Empty(t, res.VerifyResult) 1047 } 1048 // Commit without signature and verification requested 1049 { 1050 service := newService("../..") 1051 1052 src := argoappv1.ApplicationSource{Path: "manifests/base"} 1053 q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, VerifySignature: true} 1054 1055 res, err := service.GenerateManifest(context.Background(), &q) 1056 assert.NoError(t, err) 1057 assert.Empty(t, res.VerifyResult) 1058 } 1059 // Commit without signature and verification not requested 1060 { 1061 service := newService("../..") 1062 1063 src := argoappv1.ApplicationSource{Path: "manifests/base"} 1064 q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, VerifySignature: true} 1065 1066 res, err := service.GenerateManifest(context.Background(), &q) 1067 assert.NoError(t, err) 1068 assert.Empty(t, res.VerifyResult) 1069 } 1070 } 1071 1072 func Test_newEnv(t *testing.T) { 1073 assert.Equal(t, &argoappv1.Env{ 1074 &argoappv1.EnvEntry{Name: "ARGOCD_APP_NAME", Value: "my-app-name"}, 1075 &argoappv1.EnvEntry{Name: "ARGOCD_APP_NAMESPACE", Value: "my-namespace"}, 1076 &argoappv1.EnvEntry{Name: "ARGOCD_APP_REVISION", Value: "my-revision"}, 1077 &argoappv1.EnvEntry{Name: "ARGOCD_APP_SOURCE_REPO_URL", Value: "https://github.com/my-org/my-repo"}, 1078 &argoappv1.EnvEntry{Name: "ARGOCD_APP_SOURCE_PATH", Value: "my-path"}, 1079 &argoappv1.EnvEntry{Name: "ARGOCD_APP_SOURCE_TARGET_REVISION", Value: "my-target-revision"}, 1080 }, newEnv(&apiclient.ManifestRequest{ 1081 AppLabelValue: "my-app-name", 1082 Namespace: "my-namespace", 1083 Repo: &argoappv1.Repository{Repo: "https://github.com/my-org/my-repo"}, 1084 ApplicationSource: &argoappv1.ApplicationSource{ 1085 Path: "my-path", 1086 TargetRevision: "my-target-revision", 1087 }, 1088 }, "my-revision")) 1089 } 1090 1091 func TestService_newHelmClientResolveRevision(t *testing.T) { 1092 service := newService(".") 1093 1094 t.Run("EmptyRevision", func(t *testing.T) { 1095 _, _, err := service.newHelmClientResolveRevision(&argoappv1.Repository{}, "", "") 1096 assert.EqualError(t, err, "invalid revision '': improper constraint: ") 1097 }) 1098 t.Run("InvalidRevision", func(t *testing.T) { 1099 _, _, err := service.newHelmClientResolveRevision(&argoappv1.Repository{}, "???", "") 1100 assert.EqualError(t, err, "invalid revision '???': improper constraint: ???") 1101 }) 1102 } 1103 1104 func TestGetAppDetailsWithAppParameterFile(t *testing.T) { 1105 service := newService(".") 1106 details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{ 1107 Repo: &argoappv1.Repository{}, 1108 Source: &argoappv1.ApplicationSource{ 1109 Path: "./testdata/app-parameters", 1110 }, 1111 }) 1112 if !assert.NoError(t, err) { 1113 return 1114 } 1115 assert.EqualValues(t, []string{"gcr.io/heptio-images/ks-guestbook-demo:0.2"}, details.Kustomize.Images) 1116 } 1117 1118 func TestGenerateManifestsWithAppParameterFile(t *testing.T) { 1119 service := newService(".") 1120 manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 1121 Repo: &argoappv1.Repository{}, 1122 ApplicationSource: &argoappv1.ApplicationSource{ 1123 Path: "./testdata/app-parameters", 1124 }, 1125 }) 1126 if !assert.NoError(t, err) { 1127 return 1128 } 1129 resourceByKindName := make(map[string]*unstructured.Unstructured) 1130 for _, manifest := range manifests.Manifests { 1131 var un unstructured.Unstructured 1132 err := yaml.Unmarshal([]byte(manifest), &un) 1133 if !assert.NoError(t, err) { 1134 return 1135 } 1136 resourceByKindName[fmt.Sprintf("%s/%s", un.GetKind(), un.GetName())] = &un 1137 } 1138 deployment, ok := resourceByKindName["Deployment/guestbook-ui"] 1139 if !assert.True(t, ok) { 1140 return 1141 } 1142 containers, ok, _ := unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "containers") 1143 if !assert.True(t, ok) { 1144 return 1145 } 1146 image, ok, _ := unstructured.NestedString(containers[0].(map[string]interface{}), "image") 1147 if !assert.True(t, ok) { 1148 return 1149 } 1150 assert.Equal(t, "gcr.io/heptio-images/ks-guestbook-demo:0.2", image) 1151 } 1152 1153 func TestGenerateManifestWithAnnotatedAndRegularGitTagHashes(t *testing.T) { 1154 regularGitTagHash := "632039659e542ed7de0c170a4fcc1c571b288fc0" 1155 annotatedGitTaghash := "95249be61b028d566c29d47b19e65c5603388a41" 1156 invalidGitTaghash := "invalid-tag" 1157 actualCommitSHA := "632039659e542ed7de0c170a4fcc1c571b288fc0" 1158 1159 tests := []struct { 1160 name string 1161 ctx context.Context 1162 manifestRequest *apiclient.ManifestRequest 1163 wantError bool 1164 service *Service 1165 }{ 1166 { 1167 name: "Case: Git tag hash matches latest commit SHA (regular tag)", 1168 ctx: context.Background(), 1169 manifestRequest: &apiclient.ManifestRequest{ 1170 Repo: &argoappv1.Repository{}, 1171 ApplicationSource: &argoappv1.ApplicationSource{ 1172 TargetRevision: regularGitTagHash, 1173 }, 1174 NoCache: true, 1175 }, 1176 wantError: false, 1177 service: newServiceWithCommitSHA(".", regularGitTagHash), 1178 }, 1179 1180 { 1181 name: "Case: Git tag hash does not match latest commit SHA (annotated tag)", 1182 ctx: context.Background(), 1183 manifestRequest: &apiclient.ManifestRequest{ 1184 Repo: &argoappv1.Repository{}, 1185 ApplicationSource: &argoappv1.ApplicationSource{ 1186 TargetRevision: annotatedGitTaghash, 1187 }, 1188 NoCache: true, 1189 }, 1190 wantError: false, 1191 service: newServiceWithCommitSHA(".", annotatedGitTaghash), 1192 }, 1193 1194 { 1195 name: "Case: Git tag hash is invalid", 1196 ctx: context.Background(), 1197 manifestRequest: &apiclient.ManifestRequest{ 1198 Repo: &argoappv1.Repository{}, 1199 ApplicationSource: &argoappv1.ApplicationSource{ 1200 TargetRevision: invalidGitTaghash, 1201 }, 1202 NoCache: true, 1203 }, 1204 wantError: true, 1205 service: newServiceWithCommitSHA(".", invalidGitTaghash), 1206 }, 1207 } 1208 for _, tt := range tests { 1209 t.Run(tt.name, func(t *testing.T) { 1210 manifestResponse, err := tt.service.GenerateManifest(tt.ctx, tt.manifestRequest) 1211 if !tt.wantError { 1212 if err == nil { 1213 assert.Equal(t, manifestResponse.Revision, actualCommitSHA) 1214 } else { 1215 t.Errorf("unexpected error") 1216 } 1217 } else { 1218 if err == nil { 1219 t.Errorf("expected an error but did not throw one") 1220 } 1221 } 1222 1223 }) 1224 } 1225 1226 }