github.com/argoproj/argo-cd/v3@v3.2.1/reposerver/repository/repository_test.go (about) 1 package repository 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "errors" 8 "fmt" 9 goio "io" 10 "io/fs" 11 "net/mail" 12 "os" 13 "os/exec" 14 "path" 15 "path/filepath" 16 "regexp" 17 "slices" 18 "sort" 19 "strings" 20 "sync" 21 "testing" 22 "time" 23 24 log "github.com/sirupsen/logrus" 25 "k8s.io/apimachinery/pkg/api/resource" 26 "k8s.io/apimachinery/pkg/util/intstr" 27 28 "github.com/argoproj/argo-cd/v3/util/oci" 29 30 cacheutil "github.com/argoproj/argo-cd/v3/util/cache" 31 32 "github.com/stretchr/testify/assert" 33 "github.com/stretchr/testify/mock" 34 "github.com/stretchr/testify/require" 35 appsv1 "k8s.io/api/apps/v1" 36 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 37 "k8s.io/apimachinery/pkg/runtime" 38 "sigs.k8s.io/yaml" 39 40 "github.com/argoproj/argo-cd/v3/common" 41 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" 42 "github.com/argoproj/argo-cd/v3/reposerver/apiclient" 43 "github.com/argoproj/argo-cd/v3/reposerver/cache" 44 repositorymocks "github.com/argoproj/argo-cd/v3/reposerver/cache/mocks" 45 "github.com/argoproj/argo-cd/v3/reposerver/metrics" 46 fileutil "github.com/argoproj/argo-cd/v3/test/fixture/path" 47 "github.com/argoproj/argo-cd/v3/util/argo" 48 "github.com/argoproj/argo-cd/v3/util/git" 49 gitmocks "github.com/argoproj/argo-cd/v3/util/git/mocks" 50 "github.com/argoproj/argo-cd/v3/util/helm" 51 helmmocks "github.com/argoproj/argo-cd/v3/util/helm/mocks" 52 utilio "github.com/argoproj/argo-cd/v3/util/io" 53 iomocks "github.com/argoproj/argo-cd/v3/util/io/mocks" 54 ocimocks "github.com/argoproj/argo-cd/v3/util/oci/mocks" 55 "github.com/argoproj/argo-cd/v3/util/settings" 56 ) 57 58 const testSignature = `gpg: Signature made Wed Feb 26 23:22:34 2020 CET 59 gpg: using RSA key 4AEE18F83AFDEB23 60 gpg: Good signature from "GitHub (web-flow commit signing) <noreply@github.com>" [ultimate] 61 ` 62 63 type clientFunc func(*gitmocks.Client, *helmmocks.Client, *ocimocks.Client, *iomocks.TempPaths) 64 65 type repoCacheMocks struct { 66 mock.Mock 67 cacheutilCache *cacheutil.Cache 68 cache *cache.Cache 69 mockCache *repositorymocks.MockRepoCache 70 } 71 72 type newGitRepoHelmChartOptions struct { 73 chartName string 74 // valuesFiles is a map of the values file name to the key/value pairs to be written to the file 75 valuesFiles map[string]map[string]string 76 } 77 78 type newGitRepoOptions struct { 79 path string 80 createPath bool 81 remote string 82 addEmptyCommit bool 83 helmChartOptions newGitRepoHelmChartOptions 84 } 85 86 func newCacheMocks() *repoCacheMocks { 87 return newCacheMocksWithOpts(1*time.Minute, 1*time.Minute, 10*time.Second) 88 } 89 90 func newCacheMocksWithOpts(repoCacheExpiration, revisionCacheExpiration, revisionCacheLockTimeout time.Duration) *repoCacheMocks { 91 mockRepoCache := repositorymocks.NewMockRepoCache(&repositorymocks.MockCacheOptions{ 92 RepoCacheExpiration: 1 * time.Minute, 93 RevisionCacheExpiration: 1 * time.Minute, 94 ReadDelay: 0, 95 WriteDelay: 0, 96 }) 97 cacheutilCache := cacheutil.NewCache(mockRepoCache.RedisClient) 98 return &repoCacheMocks{ 99 cacheutilCache: cacheutilCache, 100 cache: cache.NewCache(cacheutilCache, repoCacheExpiration, revisionCacheExpiration, revisionCacheLockTimeout), 101 mockCache: mockRepoCache, 102 } 103 } 104 105 func newServiceWithMocks(t *testing.T, root string, signed bool) (*Service, *gitmocks.Client, *repoCacheMocks) { 106 t.Helper() 107 root, err := filepath.Abs(root) 108 if err != nil { 109 panic(err) 110 } 111 return newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, ociClient *ocimocks.Client, paths *iomocks.TempPaths) { 112 gitClient.On("Init").Return(nil) 113 gitClient.On("IsRevisionPresent", mock.Anything).Return(false) 114 gitClient.On("Fetch", mock.Anything).Return(nil) 115 gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil) 116 gitClient.On("LsRemote", mock.Anything).Return(mock.Anything, nil) 117 gitClient.On("CommitSHA").Return(mock.Anything, nil) 118 gitClient.On("Root").Return(root) 119 gitClient.On("IsAnnotatedTag").Return(false) 120 if signed { 121 gitClient.On("VerifyCommitSignature", mock.Anything).Return(testSignature, nil) 122 } else { 123 gitClient.On("VerifyCommitSignature", mock.Anything).Return("", nil) 124 } 125 126 chart := "my-chart" 127 oobChart := "out-of-bounds-chart" 128 version := "1.1.0" 129 helmClient.On("GetIndex", mock.AnythingOfType("bool"), mock.Anything).Return(&helm.Index{Entries: map[string]helm.Entries{ 130 chart: {{Version: "1.0.0"}, {Version: version}}, 131 oobChart: {{Version: "1.0.0"}, {Version: version}}, 132 }}, nil) 133 helmClient.On("GetTags", mock.Anything, mock.Anything).Return(nil, nil) 134 helmClient.On("ExtractChart", chart, version, false, int64(0), false).Return("./testdata/my-chart", utilio.NopCloser, nil) 135 helmClient.On("ExtractChart", oobChart, version, false, int64(0), false).Return("./testdata2/out-of-bounds-chart", utilio.NopCloser, nil) 136 helmClient.On("CleanChartCache", chart, version).Return(nil) 137 helmClient.On("CleanChartCache", oobChart, version).Return(nil) 138 helmClient.On("DependencyBuild").Return(nil) 139 140 ociClient.On("GetTags", mock.Anything, mock.Anything).Return(nil) 141 ociClient.On("ResolveRevision", mock.Anything, mock.Anything, mock.Anything).Return("", nil) 142 ociClient.On("Extract", mock.Anything, mock.Anything).Return("./testdata/my-chart", utilio.NopCloser, nil) 143 144 paths.On("Add", mock.Anything, mock.Anything).Return(root, nil) 145 paths.On("GetPath", mock.Anything).Return(root, nil) 146 paths.On("GetPathIfExists", mock.Anything).Return(root, nil) 147 paths.On("GetPaths").Return(map[string]string{"fake-nonce": root}) 148 }, root) 149 } 150 151 func newServiceWithOpt(t *testing.T, cf clientFunc, root string) (*Service, *gitmocks.Client, *repoCacheMocks) { 152 t.Helper() 153 helmClient := &helmmocks.Client{} 154 gitClient := &gitmocks.Client{} 155 ociClient := &ocimocks.Client{} 156 paths := &iomocks.TempPaths{} 157 cf(gitClient, helmClient, ociClient, paths) 158 cacheMocks := newCacheMocks() 159 t.Cleanup(cacheMocks.mockCache.StopRedisCallback) 160 service := NewService(metrics.NewMetricsServer(), cacheMocks.cache, RepoServerInitConstants{ParallelismLimit: 1}, &git.NoopCredsStore{}, root) 161 162 service.newGitClient = func(_ string, _ string, _ git.Creds, _ bool, _ bool, _ string, _ string, _ ...git.ClientOpts) (client git.Client, e error) { 163 return gitClient, nil 164 } 165 service.newHelmClient = func(_ string, _ helm.Creds, _ bool, _ string, _ string, _ ...helm.ClientOpts) helm.Client { 166 return helmClient 167 } 168 service.newOCIClient = func(_ string, _ oci.Creds, _ string, _ string, _ []string, _ ...oci.ClientOpts) (oci.Client, error) { 169 return ociClient, nil 170 } 171 service.gitRepoInitializer = func(_ string) goio.Closer { 172 return utilio.NopCloser 173 } 174 service.gitRepoPaths = paths 175 return service, gitClient, cacheMocks 176 } 177 178 func newService(t *testing.T, root string) *Service { 179 t.Helper() 180 service, _, _ := newServiceWithMocks(t, root, false) 181 return service 182 } 183 184 func newServiceWithSignature(t *testing.T, root string) *Service { 185 t.Helper() 186 service, _, _ := newServiceWithMocks(t, root, true) 187 return service 188 } 189 190 func newServiceWithCommitSHA(t *testing.T, root, revision string) *Service { 191 t.Helper() 192 var revisionErr error 193 194 commitSHARegex := regexp.MustCompile("^[0-9A-Fa-f]{40}$") 195 if !commitSHARegex.MatchString(revision) { 196 revisionErr = errors.New("not a commit SHA") 197 } 198 199 service, gitClient, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) { 200 gitClient.On("Init").Return(nil) 201 gitClient.On("IsRevisionPresent", mock.Anything).Return(false) 202 gitClient.On("Fetch", mock.Anything).Return(nil) 203 gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil) 204 gitClient.On("LsRemote", revision).Return(revision, revisionErr) 205 gitClient.On("CommitSHA").Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil) 206 gitClient.On("Root").Return(root) 207 paths.On("GetPath", mock.Anything).Return(root, nil) 208 paths.On("GetPathIfExists", mock.Anything).Return(root, nil) 209 }, root) 210 211 service.newGitClient = func(_ string, _ string, _ git.Creds, _ bool, _ bool, _ string, _ string, _ ...git.ClientOpts) (client git.Client, e error) { 212 return gitClient, nil 213 } 214 215 return service 216 } 217 218 func TestGenerateYamlManifestInDir(t *testing.T) { 219 service := newService(t, "../../manifests/base") 220 221 src := v1alpha1.ApplicationSource{Path: "."} 222 q := apiclient.ManifestRequest{ 223 Repo: &v1alpha1.Repository{}, 224 ApplicationSource: &src, 225 ProjectName: "something", 226 ProjectSourceRepos: []string{"*"}, 227 } 228 229 // update this value if we add/remove manifests 230 const countOfManifests = 50 231 232 res1, err := service.GenerateManifest(t.Context(), &q) 233 234 require.NoError(t, err) 235 assert.Len(t, res1.Manifests, countOfManifests) 236 237 // this will test concatenated manifests to verify we split YAMLs correctly 238 res2, err := GenerateManifests(t.Context(), "./testdata/concatenated", "/", "", &q, false, &git.NoopCredsStore{}, resource.MustParse("0"), nil) 239 require.NoError(t, err) 240 assert.Len(t, res2.Manifests, 3) 241 } 242 243 func Test_GenerateManifest_KustomizeWithVersionOverride(t *testing.T) { 244 t.Parallel() 245 246 service := newService(t, "../../util/kustomize/testdata/kustomize-with-version-override") 247 248 src := v1alpha1.ApplicationSource{Path: "."} 249 q := apiclient.ManifestRequest{ 250 Repo: &v1alpha1.Repository{}, 251 ApplicationSource: &src, 252 ProjectName: "something", 253 ProjectSourceRepos: []string{"*"}, 254 KustomizeOptions: &v1alpha1.KustomizeOptions{ 255 Versions: []v1alpha1.KustomizeVersion{}, 256 }, 257 } 258 259 _, err := service.GenerateManifest(t.Context(), &q) 260 require.ErrorAs(t, err, &settings.KustomizeVersionNotRegisteredError{Version: "v1.2.3"}) 261 262 q.KustomizeOptions.Versions = []v1alpha1.KustomizeVersion{ 263 { 264 Name: "v1.2.3", 265 Path: "kustomize", 266 }, 267 } 268 269 res, err := service.GenerateManifest(t.Context(), &q) 270 require.NoError(t, err) 271 assert.NotNil(t, res) 272 } 273 274 func Test_GenerateManifests_NoOutOfBoundsAccess(t *testing.T) { 275 t.Parallel() 276 277 testCases := []struct { 278 name string 279 outOfBoundsFilename string 280 outOfBoundsFileContents string 281 mustNotContain string // Optional string that must not appear in error or manifest output. If empty, use outOfBoundsFileContents. 282 }{ 283 { 284 name: "out of bounds JSON file should not appear in error output", 285 outOfBoundsFilename: "test.json", 286 outOfBoundsFileContents: `{"some": "json"}`, 287 }, 288 { 289 name: "malformed JSON file contents should not appear in error output", 290 outOfBoundsFilename: "test.json", 291 outOfBoundsFileContents: "$", 292 }, 293 { 294 name: "out of bounds JSON manifest should not appear in manifest output", 295 outOfBoundsFilename: "test.json", 296 // JSON marshalling is deterministic. So if there's a leak, exactly this should appear in the manifests. 297 outOfBoundsFileContents: `{"apiVersion":"v1","kind":"Secret","metadata":{"name":"test","namespace":"default"},"type":"Opaque"}`, 298 }, 299 { 300 name: "out of bounds YAML manifest should not appear in manifest output", 301 outOfBoundsFilename: "test.yaml", 302 outOfBoundsFileContents: "apiVersion: v1\nkind: Secret\nmetadata:\n name: test\n namespace: default\ntype: Opaque", 303 mustNotContain: `{"apiVersion":"v1","kind":"Secret","metadata":{"name":"test","namespace":"default"},"type":"Opaque"}`, 304 }, 305 } 306 307 for _, testCase := range testCases { 308 testCaseCopy := testCase 309 t.Run(testCaseCopy.name, func(t *testing.T) { 310 t.Parallel() 311 312 outOfBoundsDir := t.TempDir() 313 outOfBoundsFile := path.Join(outOfBoundsDir, testCaseCopy.outOfBoundsFilename) 314 err := os.WriteFile(outOfBoundsFile, []byte(testCaseCopy.outOfBoundsFileContents), os.FileMode(0o444)) 315 require.NoError(t, err) 316 317 repoDir := t.TempDir() 318 err = os.Symlink(outOfBoundsFile, path.Join(repoDir, testCaseCopy.outOfBoundsFilename)) 319 require.NoError(t, err) 320 321 mustNotContain := testCaseCopy.outOfBoundsFileContents 322 if testCaseCopy.mustNotContain != "" { 323 mustNotContain = testCaseCopy.mustNotContain 324 } 325 326 q := apiclient.ManifestRequest{ 327 Repo: &v1alpha1.Repository{}, ApplicationSource: &v1alpha1.ApplicationSource{}, ProjectName: "something", 328 ProjectSourceRepos: []string{"*"}, 329 } 330 res, err := GenerateManifests(t.Context(), repoDir, "", "", &q, false, &git.NoopCredsStore{}, resource.MustParse("0"), nil) 331 require.Error(t, err) 332 assert.NotContains(t, err.Error(), mustNotContain) 333 require.ErrorContains(t, err, "illegal filepath") 334 assert.Nil(t, res) 335 }) 336 } 337 } 338 339 func TestGenerateManifests_MissingSymlinkDestination(t *testing.T) { 340 repoDir := t.TempDir() 341 err := os.Symlink("/obviously/does/not/exist", path.Join(repoDir, "test.yaml")) 342 require.NoError(t, err) 343 344 q := apiclient.ManifestRequest{ 345 Repo: &v1alpha1.Repository{}, ApplicationSource: &v1alpha1.ApplicationSource{}, ProjectName: "something", 346 ProjectSourceRepos: []string{"*"}, 347 } 348 _, err = GenerateManifests(t.Context(), repoDir, "", "", &q, false, &git.NoopCredsStore{}, resource.MustParse("0"), nil) 349 require.NoError(t, err) 350 } 351 352 func TestGenerateManifests_K8SAPIResetCache(t *testing.T) { 353 service := newService(t, "../../manifests/base") 354 355 src := v1alpha1.ApplicationSource{Path: "."} 356 q := apiclient.ManifestRequest{ 357 KubeVersion: "v1.16.0", 358 Repo: &v1alpha1.Repository{}, 359 ApplicationSource: &src, 360 ProjectName: "something", 361 ProjectSourceRepos: []string{"*"}, 362 } 363 364 cachedFakeResponse := &apiclient.ManifestResponse{Manifests: []string{"Fake"}, Revision: mock.Anything} 365 366 err := service.cache.SetManifests(mock.Anything, &src, q.RefSources, &q, "", "", "", "", &cache.CachedManifestResponse{ManifestResponse: cachedFakeResponse}, nil, "") 367 require.NoError(t, err) 368 369 res, err := service.GenerateManifest(t.Context(), &q) 370 require.NoError(t, err) 371 assert.Equal(t, cachedFakeResponse, res) 372 373 q.KubeVersion = "v1.17.0" 374 res, err = service.GenerateManifest(t.Context(), &q) 375 require.NoError(t, err) 376 assert.NotEqual(t, cachedFakeResponse, res) 377 assert.Greater(t, len(res.Manifests), 1) 378 } 379 380 func TestGenerateManifests_EmptyCache(t *testing.T) { 381 service, gitMocks, mockCache := newServiceWithMocks(t, "../../manifests/base", false) 382 383 src := v1alpha1.ApplicationSource{Path: "."} 384 q := apiclient.ManifestRequest{ 385 Repo: &v1alpha1.Repository{}, 386 ApplicationSource: &src, 387 ProjectName: "something", 388 ProjectSourceRepos: []string{"*"}, 389 } 390 391 err := service.cache.SetManifests(mock.Anything, &src, q.RefSources, &q, "", "", "", "", &cache.CachedManifestResponse{ManifestResponse: nil}, nil, "") 392 require.NoError(t, err) 393 394 res, err := service.GenerateManifest(t.Context(), &q) 395 require.NoError(t, err) 396 assert.NotEmpty(t, res.Manifests) 397 mockCache.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{ 398 ExternalSets: 2, 399 ExternalGets: 2, 400 ExternalDeletes: 1, 401 }) 402 gitMocks.AssertCalled(t, "LsRemote", mock.Anything) 403 gitMocks.AssertCalled(t, "Fetch", mock.Anything) 404 } 405 406 // Test that when Generate manifest is called with a source that is ref only it does not try to generate manifests or hit the manifest cache 407 // but it does resolve and cache the revision 408 func TestGenerateManifest_RefOnlyShortCircuit(t *testing.T) { 409 lsremoteCalled := false 410 dir := t.TempDir() 411 repopath := dir + "/tmprepo" 412 repoRemote := "file://" + repopath 413 cacheMocks := newCacheMocks() 414 t.Cleanup(cacheMocks.mockCache.StopRedisCallback) 415 service := NewService(metrics.NewMetricsServer(), cacheMocks.cache, RepoServerInitConstants{ParallelismLimit: 1}, &git.NoopCredsStore{}, repopath) 416 service.newGitClient = func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, proxy string, noProxy string, opts ...git.ClientOpts) (client git.Client, e error) { 417 opts = append(opts, git.WithEventHandlers(git.EventHandlers{ 418 // Primary check, we want to make sure ls-remote is not called when the item is in cache 419 OnLsRemote: func(_ string) func() { 420 return func() { 421 lsremoteCalled = true 422 } 423 }, 424 OnFetch: func(_ string) func() { 425 return func() { 426 assert.Fail(t, "Fetch should not be called from GenerateManifest when the source is ref only") 427 } 428 }, 429 })) 430 gitClient, err := git.NewClientExt(rawRepoURL, root, creds, insecure, enableLfs, proxy, noProxy, opts...) 431 return gitClient, err 432 } 433 revision := initGitRepo(t, newGitRepoOptions{ 434 path: repopath, 435 createPath: true, 436 remote: repoRemote, 437 addEmptyCommit: true, 438 }) 439 src := v1alpha1.ApplicationSource{RepoURL: repoRemote, TargetRevision: "HEAD", Ref: "test-ref"} 440 repo := &v1alpha1.Repository{ 441 Repo: repoRemote, 442 } 443 q := apiclient.ManifestRequest{ 444 Repo: repo, 445 Revision: "HEAD", 446 HasMultipleSources: true, 447 ApplicationSource: &src, 448 ProjectName: "default", 449 ProjectSourceRepos: []string{"*"}, 450 } 451 _, err := service.GenerateManifest(t.Context(), &q) 452 require.NoError(t, err) 453 cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{ 454 ExternalSets: 2, 455 ExternalGets: 2, 456 }) 457 assert.True(t, lsremoteCalled, "ls-remote should be called when the source is ref only") 458 var revisions [][2]string 459 require.NoError(t, cacheMocks.cacheutilCache.GetItem("git-refs|"+repoRemote, &revisions)) 460 assert.ElementsMatch(t, [][2]string{{"refs/heads/main", revision}, {"HEAD", "ref: refs/heads/main"}}, revisions) 461 } 462 463 // Test that calling manifest generation on source helm reference helm files that when the revision is cached it does not call ls-remote 464 func TestGenerateManifestsHelmWithRefs_CachedNoLsRemote(t *testing.T) { 465 dir := t.TempDir() 466 repopath := dir + "/tmprepo" 467 cacheMocks := newCacheMocks() 468 t.Cleanup(func() { 469 cacheMocks.mockCache.StopRedisCallback() 470 err := filepath.WalkDir(dir, 471 func(path string, _ fs.DirEntry, err error) error { 472 if err == nil { 473 return os.Chmod(path, 0o777) 474 } 475 return err 476 }) 477 require.NoError(t, err) 478 }) 479 service := NewService(metrics.NewMetricsServer(), cacheMocks.cache, RepoServerInitConstants{ParallelismLimit: 1}, &git.NoopCredsStore{}, repopath) 480 var gitClient git.Client 481 var err error 482 service.newGitClient = func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, proxy string, noProxy string, opts ...git.ClientOpts) (client git.Client, e error) { 483 opts = append(opts, git.WithEventHandlers(git.EventHandlers{ 484 // Primary check, we want to make sure ls-remote is not called when the item is in cache 485 OnLsRemote: func(_ string) func() { 486 return func() { 487 assert.Fail(t, "LsRemote should not be called when the item is in cache") 488 } 489 }, 490 })) 491 gitClient, err = git.NewClientExt(rawRepoURL, root, creds, insecure, enableLfs, proxy, noProxy, opts...) 492 return gitClient, err 493 } 494 repoRemote := "file://" + repopath 495 revision := initGitRepo(t, newGitRepoOptions{ 496 path: repopath, 497 createPath: true, 498 remote: repoRemote, 499 helmChartOptions: newGitRepoHelmChartOptions{ 500 chartName: "my-chart", 501 valuesFiles: map[string]map[string]string{"test.yaml": {"testval": "test"}}, 502 }, 503 }) 504 src := v1alpha1.ApplicationSource{RepoURL: repoRemote, Path: ".", TargetRevision: "HEAD", Helm: &v1alpha1.ApplicationSourceHelm{ 505 ValueFiles: []string{"$ref/test.yaml"}, 506 }} 507 repo := &v1alpha1.Repository{ 508 Repo: repoRemote, 509 } 510 q := apiclient.ManifestRequest{ 511 Repo: repo, 512 Revision: "HEAD", 513 HasMultipleSources: true, 514 ApplicationSource: &src, 515 ProjectName: "default", 516 ProjectSourceRepos: []string{"*"}, 517 RefSources: map[string]*v1alpha1.RefTarget{"$ref": {TargetRevision: "HEAD", Repo: *repo}}, 518 } 519 err = cacheMocks.cacheutilCache.SetItem("git-refs|"+repoRemote, [][2]string{{"HEAD", revision}}, nil) 520 require.NoError(t, err) 521 _, err = service.GenerateManifest(t.Context(), &q) 522 require.NoError(t, err) 523 cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{ 524 ExternalSets: 2, 525 ExternalGets: 5, 526 }) 527 } 528 529 // ensure we can use a semver constraint range (>= 1.0.0) and get back the correct chart (1.0.0) 530 func TestHelmManifestFromChartRepo(t *testing.T) { 531 root := t.TempDir() 532 service, gitMocks, mockCache := newServiceWithMocks(t, root, false) 533 source := &v1alpha1.ApplicationSource{Chart: "my-chart", TargetRevision: ">= 1.0.0"} 534 request := &apiclient.ManifestRequest{ 535 Repo: &v1alpha1.Repository{}, ApplicationSource: source, NoCache: true, ProjectName: "something", 536 ProjectSourceRepos: []string{"*"}, 537 } 538 response, err := service.GenerateManifest(t.Context(), request) 539 require.NoError(t, err) 540 assert.NotNil(t, response) 541 assert.Equal(t, &apiclient.ManifestResponse{ 542 Manifests: []string{"{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"name\":\"my-map\"}}"}, 543 Namespace: "", 544 Server: "", 545 Revision: "1.1.0", 546 SourceType: "Helm", 547 Commands: []string{`helm template . --name-template "" --include-crds`}, 548 }, response) 549 mockCache.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{ 550 ExternalSets: 1, 551 ExternalGets: 0, 552 }) 553 gitMocks.AssertNotCalled(t, "LsRemote", mock.Anything) 554 } 555 556 func TestHelmChartReferencingExternalValues(t *testing.T) { 557 service := newService(t, ".") 558 spec := v1alpha1.ApplicationSpec{ 559 Sources: []v1alpha1.ApplicationSource{ 560 {RepoURL: "https://helm.example.com", Chart: "my-chart", TargetRevision: ">= 1.0.0", Helm: &v1alpha1.ApplicationSourceHelm{ 561 ValueFiles: []string{"$ref/testdata/my-chart/my-chart-values.yaml"}, 562 }}, 563 {Ref: "ref", RepoURL: "https://git.example.com/test/repo"}, 564 }, 565 } 566 refSources, err := argo.GetRefSources(t.Context(), spec.Sources, spec.Project, func(_ context.Context, _ string, _ string) (*v1alpha1.Repository, error) { 567 return &v1alpha1.Repository{ 568 Repo: "https://git.example.com/test/repo", 569 }, nil 570 }, []string{}) 571 require.NoError(t, err) 572 request := &apiclient.ManifestRequest{ 573 Repo: &v1alpha1.Repository{}, ApplicationSource: &spec.Sources[0], NoCache: true, RefSources: refSources, HasMultipleSources: true, ProjectName: "something", 574 ProjectSourceRepos: []string{"*"}, 575 } 576 response, err := service.GenerateManifest(t.Context(), request) 577 require.NoError(t, err) 578 assert.NotNil(t, response) 579 assert.Equal(t, &apiclient.ManifestResponse{ 580 Manifests: []string{"{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"name\":\"my-map\"}}"}, 581 Namespace: "", 582 Server: "", 583 Revision: "1.1.0", 584 SourceType: "Helm", 585 Commands: []string{`helm template . --name-template "" --values ./testdata/my-chart/my-chart-values.yaml --include-crds`}, 586 }, response) 587 } 588 589 func TestHelmChartReferencingExternalValues_InvalidRefs(t *testing.T) { 590 spec := v1alpha1.ApplicationSpec{ 591 Sources: []v1alpha1.ApplicationSource{ 592 {RepoURL: "https://helm.example.com", Chart: "my-chart", TargetRevision: ">= 1.0.0", Helm: &v1alpha1.ApplicationSourceHelm{ 593 ValueFiles: []string{"$ref/testdata/my-chart/my-chart-values.yaml"}, 594 }}, 595 {RepoURL: "https://git.example.com/test/repo"}, 596 }, 597 } 598 599 // Empty refsource 600 service := newService(t, ".") 601 602 getRepository := func(_ context.Context, _ string, _ string) (*v1alpha1.Repository, error) { 603 return &v1alpha1.Repository{ 604 Repo: "https://git.example.com/test/repo", 605 }, nil 606 } 607 608 refSources, err := argo.GetRefSources(t.Context(), spec.Sources, spec.Project, getRepository, []string{}) 609 require.NoError(t, err) 610 611 request := &apiclient.ManifestRequest{ 612 Repo: &v1alpha1.Repository{}, ApplicationSource: &spec.Sources[0], NoCache: true, RefSources: refSources, HasMultipleSources: true, ProjectName: "something", 613 ProjectSourceRepos: []string{"*"}, 614 } 615 response, err := service.GenerateManifest(t.Context(), request) 616 require.Error(t, err) 617 assert.Nil(t, response) 618 619 // Invalid ref 620 service = newService(t, ".") 621 622 spec.Sources[1].Ref = "Invalid" 623 refSources, err = argo.GetRefSources(t.Context(), spec.Sources, spec.Project, getRepository, []string{}) 624 require.NoError(t, err) 625 626 request = &apiclient.ManifestRequest{ 627 Repo: &v1alpha1.Repository{}, ApplicationSource: &spec.Sources[0], NoCache: true, RefSources: refSources, HasMultipleSources: true, ProjectName: "something", 628 ProjectSourceRepos: []string{"*"}, 629 } 630 response, err = service.GenerateManifest(t.Context(), request) 631 require.Error(t, err) 632 assert.Nil(t, response) 633 634 // Helm chart as ref (unsupported) 635 service = newService(t, ".") 636 637 spec.Sources[1].Ref = "ref" 638 spec.Sources[1].Chart = "helm-chart" 639 refSources, err = argo.GetRefSources(t.Context(), spec.Sources, spec.Project, getRepository, []string{}) 640 require.NoError(t, err) 641 642 request = &apiclient.ManifestRequest{ 643 Repo: &v1alpha1.Repository{}, ApplicationSource: &spec.Sources[0], NoCache: true, RefSources: refSources, HasMultipleSources: true, ProjectName: "something", 644 ProjectSourceRepos: []string{"*"}, 645 } 646 response, err = service.GenerateManifest(t.Context(), request) 647 require.Error(t, err) 648 assert.Nil(t, response) 649 } 650 651 func TestHelmChartReferencingExternalValues_OutOfBounds_Symlink(t *testing.T) { 652 service := newService(t, ".") 653 err := os.Mkdir("testdata/oob-symlink", 0o755) 654 require.NoError(t, err) 655 t.Cleanup(func() { 656 err = os.RemoveAll("testdata/oob-symlink") 657 require.NoError(t, err) 658 }) 659 // Create a symlink to a file outside the repo 660 err = os.Symlink("../../../values.yaml", "./testdata/oob-symlink/oob-symlink.yaml") 661 // Create a regular file to reference from another source 662 err = os.WriteFile("./testdata/oob-symlink/values.yaml", []byte("foo: bar"), 0o644) 663 require.NoError(t, err) 664 spec := v1alpha1.ApplicationSpec{ 665 Project: "default", 666 Sources: []v1alpha1.ApplicationSource{ 667 {RepoURL: "https://helm.example.com", Chart: "my-chart", TargetRevision: ">= 1.0.0", Helm: &v1alpha1.ApplicationSourceHelm{ 668 // Reference `ref` but do not use the oob symlink. The mere existence of the link should be enough to 669 // cause an error. 670 ValueFiles: []string{"$ref/testdata/oob-symlink/values.yaml"}, 671 }}, 672 {Ref: "ref", RepoURL: "https://git.example.com/test/repo"}, 673 }, 674 } 675 refSources, err := argo.GetRefSources(t.Context(), spec.Sources, spec.Project, func(_ context.Context, _ string, _ string) (*v1alpha1.Repository, error) { 676 return &v1alpha1.Repository{ 677 Repo: "https://git.example.com/test/repo", 678 }, nil 679 }, []string{}) 680 require.NoError(t, err) 681 request := &apiclient.ManifestRequest{Repo: &v1alpha1.Repository{}, ApplicationSource: &spec.Sources[0], NoCache: true, RefSources: refSources, HasMultipleSources: true} 682 _, err = service.GenerateManifest(t.Context(), request) 683 require.Error(t, err) 684 } 685 686 func TestGenerateManifestsUseExactRevision(t *testing.T) { 687 service, gitClient, _ := newServiceWithMocks(t, ".", false) 688 689 src := v1alpha1.ApplicationSource{Path: "./testdata/recurse", Directory: &v1alpha1.ApplicationSourceDirectory{Recurse: true}} 690 691 q := apiclient.ManifestRequest{ 692 Repo: &v1alpha1.Repository{}, ApplicationSource: &src, Revision: "abc", ProjectName: "something", 693 ProjectSourceRepos: []string{"*"}, 694 } 695 696 res1, err := service.GenerateManifest(t.Context(), &q) 697 require.NoError(t, err) 698 assert.Len(t, res1.Manifests, 2) 699 assert.Equal(t, "abc", gitClient.Calls[0].Arguments[0]) 700 } 701 702 func TestRecurseManifestsInDir(t *testing.T) { 703 service := newService(t, ".") 704 705 src := v1alpha1.ApplicationSource{Path: "./testdata/recurse", Directory: &v1alpha1.ApplicationSourceDirectory{Recurse: true}} 706 707 q := apiclient.ManifestRequest{ 708 Repo: &v1alpha1.Repository{}, ApplicationSource: &src, ProjectName: "something", 709 ProjectSourceRepos: []string{"*"}, 710 } 711 712 res1, err := service.GenerateManifest(t.Context(), &q) 713 require.NoError(t, err) 714 assert.Len(t, res1.Manifests, 2) 715 } 716 717 func TestInvalidManifestsInDir(t *testing.T) { 718 service := newService(t, ".") 719 720 src := v1alpha1.ApplicationSource{Path: "./testdata/invalid-manifests", Directory: &v1alpha1.ApplicationSourceDirectory{Recurse: true}} 721 722 q := apiclient.ManifestRequest{Repo: &v1alpha1.Repository{}, ApplicationSource: &src} 723 724 _, err := service.GenerateManifest(t.Context(), &q) 725 require.Error(t, err) 726 } 727 728 func TestSkippedInvalidManifestsInDir(t *testing.T) { 729 service := newService(t, ".") 730 731 src := v1alpha1.ApplicationSource{Path: "./testdata/invalid-manifests-skipped", Directory: &v1alpha1.ApplicationSourceDirectory{Recurse: true}} 732 733 q := apiclient.ManifestRequest{Repo: &v1alpha1.Repository{}, ApplicationSource: &src} 734 735 _, err := service.GenerateManifest(t.Context(), &q) 736 require.NoError(t, err) 737 } 738 739 func TestInvalidMetadata(t *testing.T) { 740 service := newService(t, ".") 741 742 src := v1alpha1.ApplicationSource{Path: "./testdata/invalid-metadata", Directory: &v1alpha1.ApplicationSourceDirectory{Recurse: true}} 743 q := apiclient.ManifestRequest{Repo: &v1alpha1.Repository{}, ApplicationSource: &src, AppLabelKey: "test", AppName: "invalid-metadata", TrackingMethod: "annotation+label"} 744 _, err := service.GenerateManifest(t.Context(), &q) 745 assert.ErrorContains(t, err, "contains non-string value in the map under key \"invalid\"") 746 } 747 748 func TestNilMetadataAccessors(t *testing.T) { 749 service := newService(t, ".") 750 expected := "{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"annotations\":{\"argocd.argoproj.io/tracking-id\":\"nil-metadata-accessors:/ConfigMap:/my-map\"},\"labels\":{\"test\":\"nil-metadata-accessors\"},\"name\":\"my-map\"},\"stringData\":{\"foo\":\"bar\"}}" 751 752 src := v1alpha1.ApplicationSource{Path: "./testdata/nil-metadata-accessors", Directory: &v1alpha1.ApplicationSourceDirectory{Recurse: true}} 753 q := apiclient.ManifestRequest{Repo: &v1alpha1.Repository{}, ApplicationSource: &src, AppLabelKey: "test", AppName: "nil-metadata-accessors", TrackingMethod: "annotation+label"} 754 res, err := service.GenerateManifest(t.Context(), &q) 755 require.NoError(t, err) 756 assert.Len(t, res.Manifests, 1) 757 assert.Equal(t, expected, res.Manifests[0]) 758 } 759 760 func TestGenerateJsonnetManifestInDir(t *testing.T) { 761 service := newService(t, ".") 762 763 q := apiclient.ManifestRequest{ 764 Repo: &v1alpha1.Repository{}, 765 ApplicationSource: &v1alpha1.ApplicationSource{ 766 Path: "./testdata/jsonnet", 767 Directory: &v1alpha1.ApplicationSourceDirectory{ 768 Jsonnet: v1alpha1.ApplicationSourceJsonnet{ 769 ExtVars: []v1alpha1.JsonnetVar{{Name: "extVarString", Value: "extVarString"}, {Name: "extVarCode", Value: "\"extVarCode\"", Code: true}}, 770 TLAs: []v1alpha1.JsonnetVar{{Name: "tlaString", Value: "tlaString"}, {Name: "tlaCode", Value: "\"tlaCode\"", Code: true}}, 771 Libs: []string{"testdata/jsonnet/vendor"}, 772 }, 773 }, 774 }, 775 ProjectName: "something", 776 ProjectSourceRepos: []string{"*"}, 777 } 778 res1, err := service.GenerateManifest(t.Context(), &q) 779 require.NoError(t, err) 780 assert.Len(t, res1.Manifests, 2) 781 } 782 783 func TestGenerateJsonnetManifestInRootDir(t *testing.T) { 784 service := newService(t, "testdata/jsonnet-1") 785 786 q := apiclient.ManifestRequest{ 787 Repo: &v1alpha1.Repository{}, 788 ApplicationSource: &v1alpha1.ApplicationSource{ 789 Path: ".", 790 Directory: &v1alpha1.ApplicationSourceDirectory{ 791 Jsonnet: v1alpha1.ApplicationSourceJsonnet{ 792 ExtVars: []v1alpha1.JsonnetVar{{Name: "extVarString", Value: "extVarString"}, {Name: "extVarCode", Value: "\"extVarCode\"", Code: true}}, 793 TLAs: []v1alpha1.JsonnetVar{{Name: "tlaString", Value: "tlaString"}, {Name: "tlaCode", Value: "\"tlaCode\"", Code: true}}, 794 Libs: []string{"."}, 795 }, 796 }, 797 }, 798 ProjectName: "something", 799 ProjectSourceRepos: []string{"*"}, 800 } 801 res1, err := service.GenerateManifest(t.Context(), &q) 802 require.NoError(t, err) 803 assert.Len(t, res1.Manifests, 2) 804 } 805 806 func TestGenerateJsonnetLibOutside(t *testing.T) { 807 service := newService(t, ".") 808 809 q := apiclient.ManifestRequest{ 810 Repo: &v1alpha1.Repository{}, 811 ApplicationSource: &v1alpha1.ApplicationSource{ 812 Path: "./testdata/jsonnet", 813 Directory: &v1alpha1.ApplicationSourceDirectory{ 814 Jsonnet: v1alpha1.ApplicationSourceJsonnet{ 815 Libs: []string{"../../../testdata/jsonnet/vendor"}, 816 }, 817 }, 818 }, 819 ProjectName: "something", 820 ProjectSourceRepos: []string{"*"}, 821 } 822 _, err := service.GenerateManifest(t.Context(), &q) 823 require.ErrorContains(t, err, "file '../../../testdata/jsonnet/vendor' resolved to outside repository root") 824 } 825 826 func TestManifestGenErrorCacheByNumRequests(t *testing.T) { 827 // Returns the state of the manifest generation cache, by querying the cache for the previously set result 828 getRecentCachedEntry := func(service *Service, manifestRequest *apiclient.ManifestRequest) *cache.CachedManifestResponse { 829 assert.NotNil(t, service) 830 assert.NotNil(t, manifestRequest) 831 832 cachedManifestResponse := &cache.CachedManifestResponse{} 833 err := service.cache.GetManifests(mock.Anything, manifestRequest.ApplicationSource, manifestRequest.RefSources, manifestRequest, manifestRequest.Namespace, "", manifestRequest.AppLabelKey, manifestRequest.AppName, cachedManifestResponse, nil, "") 834 require.NoError(t, err) 835 return cachedManifestResponse 836 } 837 838 // Example: 839 // With repo server (test) parameters: 840 // - PauseGenerationAfterFailedGenerationAttempts: 2 841 // - PauseGenerationOnFailureForRequests: 4 842 // - TotalCacheInvocations: 10 843 // 844 // After 2 manifest generation failures in a row, the next 4 manifest generation requests should be cached, 845 // with the next 2 after that being uncached. Here's how it looks... 846 // 847 // request count) result 848 // -------------------------- 849 // 1) Attempt to generate manifest, fails. 850 // 2) Second attempt to generate manifest, fails. 851 // 3) Return cached error attempt from #2 852 // 4) Return cached error attempt from #2 853 // 5) Return cached error attempt from #2 854 // 6) Return cached error attempt from #2. Max response limit hit, so reset cache entry. 855 // 7) Attempt to generate manifest, fails. 856 // 8) Attempt to generate manifest, fails. 857 // 9) Return cached error attempt from #8 858 // 10) Return cached error attempt from #8 859 860 // The same pattern PauseGenerationAfterFailedGenerationAttempts generation attempts, followed by 861 // PauseGenerationOnFailureForRequests cached responses, should apply for various combinations of 862 // both parameters. 863 864 tests := []struct { 865 PauseGenerationAfterFailedGenerationAttempts int 866 PauseGenerationOnFailureForRequests int 867 TotalCacheInvocations int 868 }{ 869 {2, 4, 10}, 870 {3, 5, 10}, 871 {1, 2, 5}, 872 } 873 for _, tt := range tests { 874 testName := fmt.Sprintf("gen-attempts-%d-pause-%d-total-%d", tt.PauseGenerationAfterFailedGenerationAttempts, tt.PauseGenerationOnFailureForRequests, tt.TotalCacheInvocations) 875 t.Run(testName, func(t *testing.T) { 876 service := newService(t, ".") 877 878 service.initConstants = RepoServerInitConstants{ 879 ParallelismLimit: 1, 880 PauseGenerationAfterFailedGenerationAttempts: tt.PauseGenerationAfterFailedGenerationAttempts, 881 PauseGenerationOnFailureForMinutes: 0, 882 PauseGenerationOnFailureForRequests: tt.PauseGenerationOnFailureForRequests, 883 } 884 885 totalAttempts := service.initConstants.PauseGenerationAfterFailedGenerationAttempts + service.initConstants.PauseGenerationOnFailureForRequests 886 887 for invocationCount := 0; invocationCount < tt.TotalCacheInvocations; invocationCount++ { 888 adjustedInvocation := invocationCount % totalAttempts 889 890 fmt.Printf("%d )-------------------------------------------\n", invocationCount) 891 892 manifestRequest := &apiclient.ManifestRequest{ 893 Repo: &v1alpha1.Repository{}, 894 AppName: "test", 895 ApplicationSource: &v1alpha1.ApplicationSource{ 896 Path: "./testdata/invalid-helm", 897 }, 898 } 899 900 res, err := service.GenerateManifest(t.Context(), manifestRequest) 901 902 // Verify invariant: res != nil xor err != nil 903 if err != nil { 904 assert.Nil(t, res, "both err and res are non-nil res: %v err: %v", res, err) 905 } else { 906 assert.NotNil(t, res, "both err and res are nil") 907 } 908 909 cachedManifestResponse := getRecentCachedEntry(service, manifestRequest) 910 911 isCachedError := err != nil && strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix) 912 913 if adjustedInvocation < service.initConstants.PauseGenerationAfterFailedGenerationAttempts { 914 // GenerateManifest should not return cached errors for the first X responses, where X is the FailGenAttempts constants 915 require.False(t, isCachedError) 916 917 require.NotNil(t, cachedManifestResponse) 918 assert.Nil(t, cachedManifestResponse.ManifestResponse) 919 assert.NotEqual(t, 0, cachedManifestResponse.FirstFailureTimestamp) 920 921 // Internal cache consec failures value should increase with invocations, cached response should stay the same, 922 assert.Equal(t, cachedManifestResponse.NumberOfConsecutiveFailures, adjustedInvocation+1) 923 assert.Equal(t, 0, cachedManifestResponse.NumberOfCachedResponsesReturned) 924 } else { 925 // GenerateManifest SHOULD return cached errors for the next X responses, where X is the 926 // PauseGenerationOnFailureForRequests constant 927 assert.True(t, isCachedError) 928 require.NotNil(t, cachedManifestResponse) 929 assert.Nil(t, cachedManifestResponse.ManifestResponse) 930 assert.NotEqual(t, 0, cachedManifestResponse.FirstFailureTimestamp) 931 932 // Internal cache values should update correctly based on number of return cache entries, consecutive failures should stay the same 933 assert.Equal(t, cachedManifestResponse.NumberOfConsecutiveFailures, service.initConstants.PauseGenerationAfterFailedGenerationAttempts) 934 assert.Equal(t, cachedManifestResponse.NumberOfCachedResponsesReturned, (adjustedInvocation - service.initConstants.PauseGenerationAfterFailedGenerationAttempts + 1)) 935 } 936 } 937 }) 938 } 939 } 940 941 func TestManifestGenErrorCacheFileContentsChange(t *testing.T) { 942 tmpDir := t.TempDir() 943 944 service := newService(t, tmpDir) 945 946 service.initConstants = RepoServerInitConstants{ 947 ParallelismLimit: 1, 948 PauseGenerationAfterFailedGenerationAttempts: 2, 949 PauseGenerationOnFailureForMinutes: 0, 950 PauseGenerationOnFailureForRequests: 4, 951 } 952 953 for step := 0; step < 3; step++ { 954 // step 1) Attempt to generate manifests against invalid helm chart (should return uncached error) 955 // step 2) Attempt to generate manifest against valid helm chart (should succeed and return valid response) 956 // step 3) Attempt to generate manifest against invalid helm chart (should return cached value from step 2) 957 958 errorExpected := step%2 == 0 959 960 // Ensure that the target directory will succeed or fail, so we can verify the cache correctly handles it 961 err := os.RemoveAll(tmpDir) 962 require.NoError(t, err) 963 err = os.MkdirAll(tmpDir, 0o777) 964 require.NoError(t, err) 965 if errorExpected { 966 // Copy invalid helm chart into temporary directory, ensuring manifest generation will fail 967 err = fileutil.CopyDir("./testdata/invalid-helm", tmpDir) 968 require.NoError(t, err) 969 } else { 970 // Copy valid helm chart into temporary directory, ensuring generation will succeed 971 err = fileutil.CopyDir("./testdata/my-chart", tmpDir) 972 require.NoError(t, err) 973 } 974 975 res, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 976 Repo: &v1alpha1.Repository{}, 977 AppName: "test", 978 ApplicationSource: &v1alpha1.ApplicationSource{ 979 Path: ".", 980 }, 981 ProjectName: "something", 982 ProjectSourceRepos: []string{"*"}, 983 }) 984 985 fmt.Println("-", step, "-", res != nil, err != nil, errorExpected) 986 fmt.Println(" err: ", err) 987 fmt.Println(" res: ", res) 988 989 if step < 2 { 990 if errorExpected { 991 require.Error(t, err, "error return value and error expected did not match") 992 assert.Nil(t, res, "GenerateManifest return value and expected value did not match") 993 } else { 994 require.NoError(t, err, "error return value and error expected did not match") 995 assert.NotNil(t, res, "GenerateManifest return value and expected value did not match") 996 } 997 } 998 999 if step == 2 { 1000 require.NoError(t, err, "error ret val was non-nil on step 3") 1001 assert.NotNil(t, res, "GenerateManifest ret val was nil on step 3") 1002 } 1003 } 1004 } 1005 1006 func TestManifestGenErrorCacheByMinutesElapsed(t *testing.T) { 1007 tests := []struct { 1008 // Test with a range of pause expiration thresholds 1009 PauseGenerationOnFailureForMinutes int 1010 }{ 1011 {1}, {2}, {10}, {24 * 60}, 1012 } 1013 for _, tt := range tests { 1014 testName := fmt.Sprintf("pause-time-%d", tt.PauseGenerationOnFailureForMinutes) 1015 t.Run(testName, func(t *testing.T) { 1016 service := newService(t, ".") 1017 1018 // Here we simulate the passage of time by overriding the now() function of Service 1019 currentTime := time.Now() 1020 service.now = func() time.Time { 1021 return currentTime 1022 } 1023 1024 service.initConstants = RepoServerInitConstants{ 1025 ParallelismLimit: 1, 1026 PauseGenerationAfterFailedGenerationAttempts: 1, 1027 PauseGenerationOnFailureForMinutes: tt.PauseGenerationOnFailureForMinutes, 1028 PauseGenerationOnFailureForRequests: 0, 1029 } 1030 1031 // 1) Put the cache into the failure state 1032 for x := 0; x < 2; x++ { 1033 res, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 1034 Repo: &v1alpha1.Repository{}, 1035 AppName: "test", 1036 ApplicationSource: &v1alpha1.ApplicationSource{ 1037 Path: "./testdata/invalid-helm", 1038 }, 1039 }) 1040 1041 assert.True(t, err != nil && res == nil) 1042 1043 // Ensure that the second invocation triggers the cached error state 1044 if x == 1 { 1045 assert.True(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix)) 1046 } 1047 } 1048 1049 // 2) Jump forward X-1 minutes in time, where X is the expiration boundary 1050 currentTime = currentTime.Add(time.Duration(tt.PauseGenerationOnFailureForMinutes-1) * time.Minute) 1051 res, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 1052 Repo: &v1alpha1.Repository{}, 1053 AppName: "test", 1054 ApplicationSource: &v1alpha1.ApplicationSource{ 1055 Path: "./testdata/invalid-helm", 1056 }, 1057 }) 1058 1059 // 3) Ensure that the cache still returns a cached copy of the last error 1060 assert.True(t, err != nil && res == nil) 1061 assert.True(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix)) 1062 1063 // 4) Jump forward 2 minutes in time, such that the pause generation time has elapsed and we should return to normal state 1064 currentTime = currentTime.Add(2 * time.Minute) 1065 1066 res, err = service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 1067 Repo: &v1alpha1.Repository{}, 1068 AppName: "test", 1069 ApplicationSource: &v1alpha1.ApplicationSource{ 1070 Path: "./testdata/invalid-helm", 1071 }, 1072 }) 1073 1074 // 5) Ensure that the service no longer returns a cached copy of the last error 1075 assert.True(t, err != nil && res == nil) 1076 assert.False(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix)) 1077 }) 1078 } 1079 } 1080 1081 func TestManifestGenErrorCacheRespectsNoCache(t *testing.T) { 1082 service := newService(t, ".") 1083 1084 service.initConstants = RepoServerInitConstants{ 1085 ParallelismLimit: 1, 1086 PauseGenerationAfterFailedGenerationAttempts: 1, 1087 PauseGenerationOnFailureForMinutes: 0, 1088 PauseGenerationOnFailureForRequests: 4, 1089 } 1090 1091 // 1) Put the cache into the failure state 1092 for x := 0; x < 2; x++ { 1093 res, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 1094 Repo: &v1alpha1.Repository{}, 1095 AppName: "test", 1096 ApplicationSource: &v1alpha1.ApplicationSource{ 1097 Path: "./testdata/invalid-helm", 1098 }, 1099 }) 1100 1101 assert.True(t, err != nil && res == nil) 1102 1103 // Ensure that the second invocation is cached 1104 if x == 1 { 1105 assert.True(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix)) 1106 } 1107 } 1108 1109 // 2) Call generateManifest with NoCache enabled 1110 res, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 1111 Repo: &v1alpha1.Repository{}, 1112 AppName: "test", 1113 ApplicationSource: &v1alpha1.ApplicationSource{ 1114 Path: "./testdata/invalid-helm", 1115 }, 1116 NoCache: true, 1117 }) 1118 1119 // 3) Ensure that the cache returns a new generation attempt, rather than a previous cached error 1120 assert.True(t, err != nil && res == nil) 1121 assert.False(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix)) 1122 1123 // 4) Call generateManifest 1124 res, err = service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 1125 Repo: &v1alpha1.Repository{}, 1126 AppName: "test", 1127 ApplicationSource: &v1alpha1.ApplicationSource{ 1128 Path: "./testdata/invalid-helm", 1129 }, 1130 }) 1131 1132 // 5) Ensure that the subsequent invocation, after nocache, is cached 1133 assert.True(t, err != nil && res == nil) 1134 assert.True(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix)) 1135 } 1136 1137 func TestGenerateHelmKubeVersion(t *testing.T) { 1138 service := newService(t, "../../util/helm/testdata/redis") 1139 1140 res, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 1141 Repo: &v1alpha1.Repository{}, 1142 AppName: "test", 1143 ApplicationSource: &v1alpha1.ApplicationSource{ 1144 Path: ".", 1145 Helm: &v1alpha1.ApplicationSourceHelm{ 1146 KubeVersion: "1.30.11+IKS", 1147 }, 1148 }, 1149 ProjectName: "something", 1150 ProjectSourceRepos: []string{"*"}, 1151 }) 1152 1153 require.NoError(t, err) 1154 assert.Len(t, res.Commands, 1) 1155 assert.Contains(t, res.Commands[0], "--kube-version 1.30.11") 1156 } 1157 1158 func TestGenerateHelmWithValues(t *testing.T) { 1159 service := newService(t, "../../util/helm/testdata/redis") 1160 1161 res, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 1162 Repo: &v1alpha1.Repository{}, 1163 AppName: "test", 1164 ApplicationSource: &v1alpha1.ApplicationSource{ 1165 Path: ".", 1166 Helm: &v1alpha1.ApplicationSourceHelm{ 1167 ValueFiles: []string{"values-production.yaml"}, 1168 ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)}, 1169 }, 1170 }, 1171 ProjectName: "something", 1172 ProjectSourceRepos: []string{"*"}, 1173 }) 1174 1175 require.NoError(t, err) 1176 1177 replicasVerified := false 1178 for _, src := range res.Manifests { 1179 obj := unstructured.Unstructured{} 1180 err = json.Unmarshal([]byte(src), &obj) 1181 require.NoError(t, err) 1182 1183 if obj.GetKind() == "Deployment" && obj.GetName() == "test-redis-slave" { 1184 var dep appsv1.Deployment 1185 err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &dep) 1186 require.NoError(t, err) 1187 assert.Equal(t, int32(2), *dep.Spec.Replicas) 1188 replicasVerified = true 1189 } 1190 } 1191 assert.True(t, replicasVerified) 1192 } 1193 1194 func TestHelmWithMissingValueFiles(t *testing.T) { 1195 service := newService(t, "../../util/helm/testdata/redis") 1196 missingValuesFile := "values-prod-overrides.yaml" 1197 1198 req := &apiclient.ManifestRequest{ 1199 Repo: &v1alpha1.Repository{}, 1200 AppName: "test", 1201 ApplicationSource: &v1alpha1.ApplicationSource{ 1202 Path: ".", 1203 Helm: &v1alpha1.ApplicationSourceHelm{ 1204 ValueFiles: []string{"values-production.yaml", missingValuesFile}, 1205 }, 1206 }, 1207 ProjectName: "something", 1208 ProjectSourceRepos: []string{"*"}, 1209 } 1210 1211 // Should fail since we're passing a non-existent values file, and error should indicate that 1212 _, err := service.GenerateManifest(t.Context(), req) 1213 require.ErrorContains(t, err, missingValuesFile+": no such file or directory") 1214 1215 // Should template without error even if defining a non-existent values file 1216 req.ApplicationSource.Helm.IgnoreMissingValueFiles = true 1217 _, err = service.GenerateManifest(t.Context(), req) 1218 require.NoError(t, err) 1219 } 1220 1221 func TestGenerateHelmWithEnvVars(t *testing.T) { 1222 service := newService(t, "../../util/helm/testdata/redis") 1223 1224 res, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 1225 Repo: &v1alpha1.Repository{}, 1226 AppName: "production", 1227 ApplicationSource: &v1alpha1.ApplicationSource{ 1228 Path: ".", 1229 Helm: &v1alpha1.ApplicationSourceHelm{ 1230 ValueFiles: []string{"values-$ARGOCD_APP_NAME.yaml"}, 1231 }, 1232 }, 1233 ProjectName: "something", 1234 ProjectSourceRepos: []string{"*"}, 1235 }) 1236 1237 require.NoError(t, err) 1238 1239 replicasVerified := false 1240 for _, src := range res.Manifests { 1241 obj := unstructured.Unstructured{} 1242 err = json.Unmarshal([]byte(src), &obj) 1243 require.NoError(t, err) 1244 1245 if obj.GetKind() == "Deployment" && obj.GetName() == "production-redis-slave" { 1246 var dep appsv1.Deployment 1247 err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &dep) 1248 require.NoError(t, err) 1249 assert.Equal(t, int32(3), *dep.Spec.Replicas) 1250 replicasVerified = true 1251 } 1252 } 1253 assert.True(t, replicasVerified) 1254 } 1255 1256 // The requested value file (`../minio/values.yaml`) is outside the app path (`./util/helm/testdata/redis`), however 1257 // since the requested value is still under the repo directory (`~/go/src/github.com/argoproj/argo-cd`), it is allowed 1258 func TestGenerateHelmWithValuesDirectoryTraversal(t *testing.T) { 1259 service := newService(t, "../../util/helm/testdata") 1260 _, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 1261 Repo: &v1alpha1.Repository{}, 1262 AppName: "test", 1263 ApplicationSource: &v1alpha1.ApplicationSource{ 1264 Path: "./redis", 1265 Helm: &v1alpha1.ApplicationSourceHelm{ 1266 ValueFiles: []string{"../minio/values.yaml"}, 1267 ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)}, 1268 }, 1269 }, 1270 ProjectName: "something", 1271 ProjectSourceRepos: []string{"*"}, 1272 }) 1273 require.NoError(t, err) 1274 1275 // Test the case where the path is "." 1276 service = newService(t, "./testdata") 1277 _, err = service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 1278 Repo: &v1alpha1.Repository{}, 1279 AppName: "test", 1280 ApplicationSource: &v1alpha1.ApplicationSource{ 1281 Path: "./my-chart", 1282 }, 1283 ProjectName: "something", 1284 ProjectSourceRepos: []string{"*"}, 1285 }) 1286 require.NoError(t, err) 1287 } 1288 1289 func TestChartRepoWithOutOfBoundsSymlink(t *testing.T) { 1290 service := newService(t, ".") 1291 source := &v1alpha1.ApplicationSource{Chart: "out-of-bounds-chart", TargetRevision: ">= 1.0.0"} 1292 request := &apiclient.ManifestRequest{Repo: &v1alpha1.Repository{}, ApplicationSource: source, NoCache: true} 1293 _, err := service.GenerateManifest(t.Context(), request) 1294 assert.ErrorContains(t, err, "chart contains out-of-bounds symlinks") 1295 } 1296 1297 // This is a Helm first-class app with a values file inside the repo directory 1298 // (`~/go/src/github.com/argoproj/argo-cd/reposerver/repository`), so it is allowed 1299 func TestHelmManifestFromChartRepoWithValueFile(t *testing.T) { 1300 service := newService(t, ".") 1301 source := &v1alpha1.ApplicationSource{ 1302 Chart: "my-chart", 1303 TargetRevision: ">= 1.0.0", 1304 Helm: &v1alpha1.ApplicationSourceHelm{ 1305 ValueFiles: []string{"./my-chart-values.yaml"}, 1306 }, 1307 } 1308 request := &apiclient.ManifestRequest{ 1309 Repo: &v1alpha1.Repository{}, 1310 ApplicationSource: source, 1311 NoCache: true, 1312 ProjectName: "something", 1313 ProjectSourceRepos: []string{"*"}, 1314 } 1315 response, err := service.GenerateManifest(t.Context(), request) 1316 require.NoError(t, err) 1317 assert.NotNil(t, response) 1318 assert.Equal(t, &apiclient.ManifestResponse{ 1319 Manifests: []string{"{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"name\":\"my-map\"}}"}, 1320 Namespace: "", 1321 Server: "", 1322 Revision: "1.1.0", 1323 SourceType: "Helm", 1324 Commands: []string{`helm template . --name-template "" --values ./testdata/my-chart/my-chart-values.yaml --include-crds`}, 1325 }, response) 1326 } 1327 1328 // This is a Helm first-class app with a values file outside the repo directory 1329 // (`~/go/src/github.com/argoproj/argo-cd/reposerver/repository`), so it is not allowed 1330 func TestHelmManifestFromChartRepoWithValueFileOutsideRepo(t *testing.T) { 1331 service := newService(t, ".") 1332 source := &v1alpha1.ApplicationSource{ 1333 Chart: "my-chart", 1334 TargetRevision: ">= 1.0.0", 1335 Helm: &v1alpha1.ApplicationSourceHelm{ 1336 ValueFiles: []string{"../my-chart-2/my-chart-2-values.yaml"}, 1337 }, 1338 } 1339 request := &apiclient.ManifestRequest{Repo: &v1alpha1.Repository{}, ApplicationSource: source, NoCache: true} 1340 _, err := service.GenerateManifest(t.Context(), request) 1341 require.Error(t, err) 1342 } 1343 1344 func TestHelmManifestFromChartRepoWithValueFileLinks(t *testing.T) { 1345 t.Run("Valid symlink", func(t *testing.T) { 1346 service := newService(t, ".") 1347 source := &v1alpha1.ApplicationSource{ 1348 Chart: "my-chart", 1349 TargetRevision: ">= 1.0.0", 1350 Helm: &v1alpha1.ApplicationSourceHelm{ 1351 ValueFiles: []string{"my-chart-link.yaml"}, 1352 }, 1353 } 1354 request := &apiclient.ManifestRequest{ 1355 Repo: &v1alpha1.Repository{}, ApplicationSource: source, NoCache: true, ProjectName: "something", 1356 ProjectSourceRepos: []string{"*"}, 1357 } 1358 _, err := service.GenerateManifest(t.Context(), request) 1359 require.NoError(t, err) 1360 }) 1361 } 1362 1363 func TestGenerateHelmWithURL(t *testing.T) { 1364 service := newService(t, "../../util/helm/testdata/redis") 1365 1366 _, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 1367 Repo: &v1alpha1.Repository{}, 1368 AppName: "test", 1369 ApplicationSource: &v1alpha1.ApplicationSource{ 1370 Path: ".", 1371 Helm: &v1alpha1.ApplicationSourceHelm{ 1372 ValueFiles: []string{"https://raw.githubusercontent.com/argoproj/argocd-example-apps/master/helm-guestbook/values.yaml"}, 1373 ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)}, 1374 }, 1375 }, 1376 ProjectName: "something", 1377 ProjectSourceRepos: []string{"*"}, 1378 HelmOptions: &v1alpha1.HelmOptions{ValuesFileSchemes: []string{"https"}}, 1379 }) 1380 require.NoError(t, err) 1381 } 1382 1383 // The requested value file (`../minio/values.yaml`) is outside the repo directory 1384 // (`~/go/src/github.com/argoproj/argo-cd/util/helm/testdata/redis`), so it is blocked 1385 func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) { 1386 t.Run("Values file with relative path pointing outside repo root", func(t *testing.T) { 1387 service := newService(t, "../../util/helm/testdata/redis") 1388 _, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 1389 Repo: &v1alpha1.Repository{}, 1390 AppName: "test", 1391 ApplicationSource: &v1alpha1.ApplicationSource{ 1392 Path: ".", 1393 Helm: &v1alpha1.ApplicationSourceHelm{ 1394 ValueFiles: []string{"../minio/values.yaml"}, 1395 ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)}, 1396 }, 1397 }, 1398 ProjectName: "something", 1399 ProjectSourceRepos: []string{"*"}, 1400 }) 1401 assert.ErrorContains(t, err, "outside repository root") 1402 }) 1403 1404 t.Run("Values file with relative path pointing inside repo root", func(t *testing.T) { 1405 service := newService(t, "./testdata") 1406 _, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 1407 Repo: &v1alpha1.Repository{}, 1408 AppName: "test", 1409 ApplicationSource: &v1alpha1.ApplicationSource{ 1410 Path: "./my-chart", 1411 Helm: &v1alpha1.ApplicationSourceHelm{ 1412 ValueFiles: []string{"../my-chart/my-chart-values.yaml"}, 1413 ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)}, 1414 }, 1415 }, 1416 ProjectName: "something", 1417 ProjectSourceRepos: []string{"*"}, 1418 }) 1419 require.NoError(t, err) 1420 }) 1421 1422 t.Run("Values file with absolute path stays within repo root", func(t *testing.T) { 1423 service := newService(t, "./testdata") 1424 _, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 1425 Repo: &v1alpha1.Repository{}, 1426 AppName: "test", 1427 ApplicationSource: &v1alpha1.ApplicationSource{ 1428 Path: "./my-chart", 1429 Helm: &v1alpha1.ApplicationSourceHelm{ 1430 ValueFiles: []string{"/my-chart/my-chart-values.yaml"}, 1431 ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)}, 1432 }, 1433 }, 1434 ProjectName: "something", 1435 ProjectSourceRepos: []string{"*"}, 1436 }) 1437 require.NoError(t, err) 1438 }) 1439 1440 t.Run("Values file with absolute path using back-references outside repo root", func(t *testing.T) { 1441 service := newService(t, "./testdata") 1442 _, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 1443 Repo: &v1alpha1.Repository{}, 1444 AppName: "test", 1445 ApplicationSource: &v1alpha1.ApplicationSource{ 1446 Path: "./my-chart", 1447 Helm: &v1alpha1.ApplicationSourceHelm{ 1448 ValueFiles: []string{"/../../../my-chart-values.yaml"}, 1449 ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)}, 1450 }, 1451 }, 1452 ProjectName: "something", 1453 ProjectSourceRepos: []string{"*"}, 1454 }) 1455 assert.ErrorContains(t, err, "outside repository root") 1456 }) 1457 1458 t.Run("Remote values file from forbidden protocol", func(t *testing.T) { 1459 service := newService(t, "./testdata") 1460 _, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 1461 Repo: &v1alpha1.Repository{}, 1462 AppName: "test", 1463 ApplicationSource: &v1alpha1.ApplicationSource{ 1464 Path: "./my-chart", 1465 Helm: &v1alpha1.ApplicationSourceHelm{ 1466 ValueFiles: []string{"file://../../../../my-chart-values.yaml"}, 1467 ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)}, 1468 }, 1469 }, 1470 ProjectName: "something", 1471 ProjectSourceRepos: []string{"*"}, 1472 }) 1473 assert.ErrorContains(t, err, "is not allowed") 1474 }) 1475 1476 t.Run("Remote values file from custom allowed protocol", func(t *testing.T) { 1477 service := newService(t, "./testdata") 1478 _, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 1479 Repo: &v1alpha1.Repository{}, 1480 AppName: "test", 1481 ApplicationSource: &v1alpha1.ApplicationSource{ 1482 Path: "./my-chart", 1483 Helm: &v1alpha1.ApplicationSourceHelm{ 1484 ValueFiles: []string{"s3://my-bucket/my-chart-values.yaml"}, 1485 }, 1486 }, 1487 HelmOptions: &v1alpha1.HelmOptions{ValuesFileSchemes: []string{"s3"}}, 1488 ProjectName: "something", 1489 ProjectSourceRepos: []string{"*"}, 1490 }) 1491 assert.ErrorContains(t, err, "s3://my-bucket/my-chart-values.yaml: no such file or directory") 1492 }) 1493 } 1494 1495 // File parameter should not allow traversal outside of the repository root 1496 func TestGenerateHelmWithAbsoluteFileParameter(t *testing.T) { 1497 service := newService(t, "../..") 1498 1499 file, err := os.CreateTemp(t.TempDir(), "external-secret.txt") 1500 require.NoError(t, err) 1501 externalSecretPath := file.Name() 1502 defer func() { _ = os.RemoveAll(externalSecretPath) }() 1503 expectedFileContent, err := os.ReadFile("../../util/helm/testdata/external/external-secret.txt") 1504 require.NoError(t, err) 1505 err = os.WriteFile(externalSecretPath, expectedFileContent, 0o644) 1506 require.NoError(t, err) 1507 defer func() { 1508 if err = file.Close(); err != nil { 1509 panic(err) 1510 } 1511 }() 1512 1513 _, err = service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 1514 Repo: &v1alpha1.Repository{}, 1515 AppName: "test", 1516 ApplicationSource: &v1alpha1.ApplicationSource{ 1517 Path: "./util/helm/testdata/redis", 1518 Helm: &v1alpha1.ApplicationSourceHelm{ 1519 ValueFiles: []string{"values-production.yaml"}, 1520 ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)}, 1521 FileParameters: []v1alpha1.HelmFileParameter{{ 1522 Name: "passwordContent", 1523 Path: externalSecretPath, 1524 }}, 1525 }, 1526 }, 1527 ProjectName: "something", 1528 ProjectSourceRepos: []string{"*"}, 1529 }) 1530 require.Error(t, err) 1531 } 1532 1533 // The requested file parameter (`../external/external-secret.txt`) is outside the app path 1534 // (`./util/helm/testdata/redis`), however since the requested value is still under the repo 1535 // directory (`~/go/src/github.com/argoproj/argo-cd`), it is allowed. It is used as a means of 1536 // providing direct content to a helm chart via a specific key. 1537 func TestGenerateHelmWithFileParameter(t *testing.T) { 1538 service := newService(t, "../../util/helm/testdata") 1539 1540 res, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 1541 Repo: &v1alpha1.Repository{}, 1542 AppName: "test", 1543 ApplicationSource: &v1alpha1.ApplicationSource{ 1544 Path: "./redis", 1545 Helm: &v1alpha1.ApplicationSourceHelm{ 1546 ValueFiles: []string{"values-production.yaml"}, 1547 Values: `cluster: {slaveCount: 10}`, 1548 ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)}, 1549 FileParameters: []v1alpha1.HelmFileParameter{{ 1550 Name: "passwordContent", 1551 Path: "../external/external-secret.txt", 1552 }}, 1553 }, 1554 }, 1555 ProjectName: "something", 1556 ProjectSourceRepos: []string{"*"}, 1557 }) 1558 require.NoError(t, err) 1559 assert.Contains(t, res.Manifests[6], `"replicas":2`, "ValuesObject should override Values") 1560 } 1561 1562 func TestGenerateNullList(t *testing.T) { 1563 service := newService(t, ".") 1564 1565 t.Run("null list", func(t *testing.T) { 1566 res1, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 1567 Repo: &v1alpha1.Repository{}, 1568 ApplicationSource: &v1alpha1.ApplicationSource{Path: "./testdata/null-list"}, 1569 NoCache: true, 1570 ProjectName: "something", 1571 ProjectSourceRepos: []string{"*"}, 1572 }) 1573 require.NoError(t, err) 1574 assert.Len(t, res1.Manifests, 1) 1575 assert.Contains(t, res1.Manifests[0], "prometheus-operator-operator") 1576 }) 1577 1578 t.Run("empty list", func(t *testing.T) { 1579 res1, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 1580 Repo: &v1alpha1.Repository{}, 1581 ApplicationSource: &v1alpha1.ApplicationSource{Path: "./testdata/empty-list"}, 1582 NoCache: true, 1583 ProjectName: "something", 1584 ProjectSourceRepos: []string{"*"}, 1585 }) 1586 require.NoError(t, err) 1587 assert.Len(t, res1.Manifests, 1) 1588 assert.Contains(t, res1.Manifests[0], "prometheus-operator-operator") 1589 }) 1590 1591 t.Run("weird list", func(t *testing.T) { 1592 res1, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 1593 Repo: &v1alpha1.Repository{}, 1594 ApplicationSource: &v1alpha1.ApplicationSource{Path: "./testdata/weird-list"}, 1595 NoCache: true, 1596 ProjectName: "something", 1597 ProjectSourceRepos: []string{"*"}, 1598 }) 1599 require.NoError(t, err) 1600 assert.Len(t, res1.Manifests, 2) 1601 }) 1602 } 1603 1604 func TestIdentifyAppSourceTypeByAppDirWithKustomizations(t *testing.T) { 1605 sourceType, err := GetAppSourceType(t.Context(), &v1alpha1.ApplicationSource{}, "./testdata/kustomization_yaml", "./testdata", "testapp", map[string]bool{}, []string{}, []string{}) 1606 require.NoError(t, err) 1607 assert.Equal(t, v1alpha1.ApplicationSourceTypeKustomize, sourceType) 1608 1609 sourceType, err = GetAppSourceType(t.Context(), &v1alpha1.ApplicationSource{}, "./testdata/kustomization_yml", "./testdata", "testapp", map[string]bool{}, []string{}, []string{}) 1610 require.NoError(t, err) 1611 assert.Equal(t, v1alpha1.ApplicationSourceTypeKustomize, sourceType) 1612 1613 sourceType, err = GetAppSourceType(t.Context(), &v1alpha1.ApplicationSource{}, "./testdata/Kustomization", "./testdata", "testapp", map[string]bool{}, []string{}, []string{}) 1614 require.NoError(t, err) 1615 assert.Equal(t, v1alpha1.ApplicationSourceTypeKustomize, sourceType) 1616 } 1617 1618 func TestGenerateFromUTF16(t *testing.T) { 1619 q := apiclient.ManifestRequest{ 1620 Repo: &v1alpha1.Repository{}, 1621 ApplicationSource: &v1alpha1.ApplicationSource{}, 1622 ProjectName: "something", 1623 ProjectSourceRepos: []string{"*"}, 1624 } 1625 res1, err := GenerateManifests(t.Context(), "./testdata/utf-16", "/", "", &q, false, &git.NoopCredsStore{}, resource.MustParse("0"), nil) 1626 require.NoError(t, err) 1627 assert.Len(t, res1.Manifests, 2) 1628 } 1629 1630 func TestListApps(t *testing.T) { 1631 service := newService(t, "./testdata") 1632 1633 res, err := service.ListApps(t.Context(), &apiclient.ListAppsRequest{Repo: &v1alpha1.Repository{}}) 1634 require.NoError(t, err) 1635 1636 expectedApps := map[string]string{ 1637 "Kustomization": "Kustomize", 1638 "app-parameters/multi": "Kustomize", 1639 "app-parameters/single-app-only": "Kustomize", 1640 "app-parameters/single-global": "Kustomize", 1641 "app-parameters/single-global-helm": "Helm", 1642 "in-bounds-values-file-link": "Helm", 1643 "invalid-helm": "Helm", 1644 "invalid-kustomize": "Kustomize", 1645 "kustomization_yaml": "Kustomize", 1646 "kustomization_yml": "Kustomize", 1647 "my-chart": "Helm", 1648 "my-chart-2": "Helm", 1649 "oci-dependencies": "Helm", 1650 "out-of-bounds-values-file-link": "Helm", 1651 "values-files": "Helm", 1652 "helm-with-dependencies": "Helm", 1653 "helm-with-dependencies-alias": "Helm", 1654 "helm-with-local-dependency": "Helm", 1655 "simple-chart": "Helm", 1656 "broken-schema-verification": "Helm", 1657 } 1658 assert.Equal(t, expectedApps, res.Apps) 1659 } 1660 1661 func TestGetAppDetailsHelm(t *testing.T) { 1662 service := newService(t, "../../util/helm/testdata/dependency") 1663 1664 res, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{ 1665 Repo: &v1alpha1.Repository{}, 1666 Source: &v1alpha1.ApplicationSource{ 1667 Path: ".", 1668 }, 1669 }) 1670 1671 require.NoError(t, err) 1672 assert.NotNil(t, res.Helm) 1673 1674 assert.Equal(t, "Helm", res.Type) 1675 assert.Equal(t, []string{"values-production.yaml", "values.yaml"}, res.Helm.ValueFiles) 1676 } 1677 1678 func TestGetAppDetailsHelmUsesCache(t *testing.T) { 1679 service := newService(t, "../../util/helm/testdata/dependency") 1680 1681 res, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{ 1682 Repo: &v1alpha1.Repository{}, 1683 Source: &v1alpha1.ApplicationSource{ 1684 Path: ".", 1685 }, 1686 }) 1687 1688 require.NoError(t, err) 1689 assert.NotNil(t, res.Helm) 1690 1691 assert.Equal(t, "Helm", res.Type) 1692 assert.Equal(t, []string{"values-production.yaml", "values.yaml"}, res.Helm.ValueFiles) 1693 } 1694 1695 func TestGetAppDetailsHelm_WithNoValuesFile(t *testing.T) { 1696 service := newService(t, "../../util/helm/testdata/api-versions") 1697 1698 res, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{ 1699 Repo: &v1alpha1.Repository{}, 1700 Source: &v1alpha1.ApplicationSource{ 1701 Path: ".", 1702 }, 1703 }) 1704 1705 require.NoError(t, err) 1706 assert.NotNil(t, res.Helm) 1707 1708 assert.Equal(t, "Helm", res.Type) 1709 assert.Empty(t, res.Helm.ValueFiles) 1710 assert.Empty(t, res.Helm.Values) 1711 } 1712 1713 func TestGetAppDetailsKustomize(t *testing.T) { 1714 service := newService(t, "../../util/kustomize/testdata/kustomization_yaml") 1715 1716 res, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{ 1717 Repo: &v1alpha1.Repository{}, 1718 Source: &v1alpha1.ApplicationSource{ 1719 Path: ".", 1720 }, 1721 }) 1722 1723 require.NoError(t, err) 1724 1725 assert.Equal(t, "Kustomize", res.Type) 1726 assert.NotNil(t, res.Kustomize) 1727 assert.Equal(t, []string{"nginx:1.15.4", "registry.k8s.io/nginx-slim:0.8"}, res.Kustomize.Images) 1728 } 1729 1730 func TestGetAppDetailsKustomize_CustomVersion(t *testing.T) { 1731 service := newService(t, "../../util/kustomize/testdata/kustomize-with-version-override") 1732 1733 q := &apiclient.RepoServerAppDetailsQuery{ 1734 Repo: &v1alpha1.Repository{}, 1735 Source: &v1alpha1.ApplicationSource{ 1736 Path: ".", 1737 }, 1738 KustomizeOptions: &v1alpha1.KustomizeOptions{}, 1739 } 1740 1741 _, err := service.GetAppDetails(t.Context(), q) 1742 require.ErrorAs(t, err, &settings.KustomizeVersionNotRegisteredError{Version: "v1.2.3"}) 1743 1744 q.KustomizeOptions.Versions = []v1alpha1.KustomizeVersion{ 1745 { 1746 Name: "v1.2.3", 1747 Path: "kustomize", 1748 }, 1749 } 1750 1751 res, err := service.GetAppDetails(t.Context(), q) 1752 require.NoError(t, err) 1753 assert.Equal(t, "Kustomize", res.Type) 1754 } 1755 1756 func TestGetHelmCharts(t *testing.T) { 1757 service := newService(t, "../..") 1758 res, err := service.GetHelmCharts(t.Context(), &apiclient.HelmChartsRequest{Repo: &v1alpha1.Repository{}}) 1759 1760 // fix flakiness 1761 sort.Slice(res.Items, func(i, j int) bool { 1762 return res.Items[i].Name < res.Items[j].Name 1763 }) 1764 1765 require.NoError(t, err) 1766 assert.Len(t, res.Items, 2) 1767 1768 item := res.Items[0] 1769 assert.Equal(t, "my-chart", item.Name) 1770 assert.Equal(t, []string{"1.0.0", "1.1.0"}, item.Versions) 1771 1772 item2 := res.Items[1] 1773 assert.Equal(t, "out-of-bounds-chart", item2.Name) 1774 assert.Equal(t, []string{"1.0.0", "1.1.0"}, item2.Versions) 1775 } 1776 1777 func TestGetRevisionMetadata(t *testing.T) { 1778 service, gitClient, _ := newServiceWithMocks(t, "../..", false) 1779 now := time.Now() 1780 1781 gitClient.On("RevisionMetadata", mock.Anything).Return(&git.RevisionMetadata{ 1782 Message: "test", 1783 Author: "author", 1784 Date: now, 1785 Tags: []string{"tag1", "tag2"}, 1786 References: []git.RevisionReference{ 1787 { 1788 Commit: &git.CommitMetadata{ 1789 Author: mail.Address{ 1790 Name: "test-name", 1791 Address: "test-email@example.com", 1792 }, 1793 Date: now.Format(time.RFC3339), 1794 Subject: "test-subject", 1795 SHA: "test-sha", 1796 RepoURL: "test-repo-url", 1797 }, 1798 }, 1799 }, 1800 }, nil) 1801 1802 res, err := service.GetRevisionMetadata(t.Context(), &apiclient.RepoServerRevisionMetadataRequest{ 1803 Repo: &v1alpha1.Repository{}, 1804 Revision: "c0b400fc458875d925171398f9ba9eabd5529923", 1805 CheckSignature: true, 1806 }) 1807 1808 require.NoError(t, err) 1809 assert.Equal(t, "test", res.Message) 1810 assert.Equal(t, now, res.Date.Time) 1811 assert.Equal(t, "author", res.Author) 1812 assert.Equal(t, []string{"tag1", "tag2"}, res.Tags) 1813 assert.NotEmpty(t, res.SignatureInfo) 1814 require.Len(t, res.References, 1) 1815 require.NotNil(t, res.References[0].Commit) 1816 assert.Equal(t, "test-sha", res.References[0].Commit.SHA) 1817 1818 // Check for truncated revision value 1819 res, err = service.GetRevisionMetadata(t.Context(), &apiclient.RepoServerRevisionMetadataRequest{ 1820 Repo: &v1alpha1.Repository{}, 1821 Revision: "c0b400f", 1822 CheckSignature: true, 1823 }) 1824 1825 require.NoError(t, err) 1826 assert.Equal(t, "test", res.Message) 1827 assert.Equal(t, now, res.Date.Time) 1828 assert.Equal(t, "author", res.Author) 1829 assert.Equal(t, []string{"tag1", "tag2"}, res.Tags) 1830 assert.NotEmpty(t, res.SignatureInfo) 1831 1832 // Cache hit - signature info should not be in result 1833 res, err = service.GetRevisionMetadata(t.Context(), &apiclient.RepoServerRevisionMetadataRequest{ 1834 Repo: &v1alpha1.Repository{}, 1835 Revision: "c0b400fc458875d925171398f9ba9eabd5529923", 1836 CheckSignature: false, 1837 }) 1838 require.NoError(t, err) 1839 assert.Empty(t, res.SignatureInfo) 1840 1841 // Enforce cache miss - signature info should not be in result 1842 res, err = service.GetRevisionMetadata(t.Context(), &apiclient.RepoServerRevisionMetadataRequest{ 1843 Repo: &v1alpha1.Repository{}, 1844 Revision: "da52afd3b2df1ec49470603d8bbb46954dab1091", 1845 CheckSignature: false, 1846 }) 1847 require.NoError(t, err) 1848 assert.Empty(t, res.SignatureInfo) 1849 1850 // Cache hit on previous entry that did not have signature info 1851 res, err = service.GetRevisionMetadata(t.Context(), &apiclient.RepoServerRevisionMetadataRequest{ 1852 Repo: &v1alpha1.Repository{}, 1853 Revision: "da52afd3b2df1ec49470603d8bbb46954dab1091", 1854 CheckSignature: true, 1855 }) 1856 require.NoError(t, err) 1857 assert.NotEmpty(t, res.SignatureInfo) 1858 } 1859 1860 func TestGetSignatureVerificationResult(t *testing.T) { 1861 // Commit with signature and verification requested 1862 { 1863 service := newServiceWithSignature(t, "../../manifests/base") 1864 1865 src := v1alpha1.ApplicationSource{Path: "."} 1866 q := apiclient.ManifestRequest{ 1867 Repo: &v1alpha1.Repository{}, 1868 ApplicationSource: &src, 1869 VerifySignature: true, 1870 ProjectName: "something", 1871 ProjectSourceRepos: []string{"*"}, 1872 } 1873 1874 res, err := service.GenerateManifest(t.Context(), &q) 1875 require.NoError(t, err) 1876 assert.Equal(t, testSignature, res.VerifyResult) 1877 } 1878 // Commit with signature and verification not requested 1879 { 1880 service := newServiceWithSignature(t, "../../manifests/base") 1881 1882 src := v1alpha1.ApplicationSource{Path: "."} 1883 q := apiclient.ManifestRequest{ 1884 Repo: &v1alpha1.Repository{}, ApplicationSource: &src, ProjectName: "something", 1885 ProjectSourceRepos: []string{"*"}, 1886 } 1887 1888 res, err := service.GenerateManifest(t.Context(), &q) 1889 require.NoError(t, err) 1890 assert.Empty(t, res.VerifyResult) 1891 } 1892 // Commit without signature and verification requested 1893 { 1894 service := newService(t, "../../manifests/base") 1895 1896 src := v1alpha1.ApplicationSource{Path: "."} 1897 q := apiclient.ManifestRequest{ 1898 Repo: &v1alpha1.Repository{}, ApplicationSource: &src, VerifySignature: true, ProjectName: "something", 1899 ProjectSourceRepos: []string{"*"}, 1900 } 1901 1902 res, err := service.GenerateManifest(t.Context(), &q) 1903 require.NoError(t, err) 1904 assert.Empty(t, res.VerifyResult) 1905 } 1906 // Commit without signature and verification not requested 1907 { 1908 service := newService(t, "../../manifests/base") 1909 1910 src := v1alpha1.ApplicationSource{Path: "."} 1911 q := apiclient.ManifestRequest{ 1912 Repo: &v1alpha1.Repository{}, ApplicationSource: &src, VerifySignature: true, ProjectName: "something", 1913 ProjectSourceRepos: []string{"*"}, 1914 } 1915 1916 res, err := service.GenerateManifest(t.Context(), &q) 1917 require.NoError(t, err) 1918 assert.Empty(t, res.VerifyResult) 1919 } 1920 } 1921 1922 func Test_newEnv(t *testing.T) { 1923 assert.Equal(t, &v1alpha1.Env{ 1924 &v1alpha1.EnvEntry{Name: "ARGOCD_APP_NAME", Value: "my-app-name"}, 1925 &v1alpha1.EnvEntry{Name: "ARGOCD_APP_NAMESPACE", Value: "my-namespace"}, 1926 &v1alpha1.EnvEntry{Name: "ARGOCD_APP_PROJECT_NAME", Value: "my-project-name"}, 1927 &v1alpha1.EnvEntry{Name: "ARGOCD_APP_REVISION", Value: "my-revision"}, 1928 &v1alpha1.EnvEntry{Name: "ARGOCD_APP_REVISION_SHORT", Value: "my-revi"}, 1929 &v1alpha1.EnvEntry{Name: "ARGOCD_APP_REVISION_SHORT_8", Value: "my-revis"}, 1930 &v1alpha1.EnvEntry{Name: "ARGOCD_APP_SOURCE_REPO_URL", Value: "https://github.com/my-org/my-repo"}, 1931 &v1alpha1.EnvEntry{Name: "ARGOCD_APP_SOURCE_PATH", Value: "my-path"}, 1932 &v1alpha1.EnvEntry{Name: "ARGOCD_APP_SOURCE_TARGET_REVISION", Value: "my-target-revision"}, 1933 }, newEnv(&apiclient.ManifestRequest{ 1934 AppName: "my-app-name", 1935 Namespace: "my-namespace", 1936 ProjectName: "my-project-name", 1937 Repo: &v1alpha1.Repository{Repo: "https://github.com/my-org/my-repo"}, 1938 ApplicationSource: &v1alpha1.ApplicationSource{ 1939 Path: "my-path", 1940 TargetRevision: "my-target-revision", 1941 }, 1942 }, "my-revision")) 1943 } 1944 1945 func TestService_newHelmClientResolveRevision(t *testing.T) { 1946 service := newService(t, ".") 1947 1948 t.Run("EmptyRevision", func(t *testing.T) { 1949 _, _, err := service.newHelmClientResolveRevision(&v1alpha1.Repository{}, "", "my-chart", true) 1950 assert.EqualError(t, err, "invalid revision: failed to determine semver constraint: improper constraint: ") 1951 }) 1952 t.Run("InvalidRevision", func(t *testing.T) { 1953 _, _, err := service.newHelmClientResolveRevision(&v1alpha1.Repository{}, "???", "my-chart", true) 1954 assert.EqualError(t, err, "invalid revision: failed to determine semver constraint: improper constraint: ???") 1955 }) 1956 } 1957 1958 func TestGetAppDetailsWithAppParameterFile(t *testing.T) { 1959 t.Run("No app name set and app specific file exists", func(t *testing.T) { 1960 service := newService(t, ".") 1961 runWithTempTestdata(t, "multi", func(t *testing.T, path string) { 1962 t.Helper() 1963 details, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{ 1964 Repo: &v1alpha1.Repository{}, 1965 Source: &v1alpha1.ApplicationSource{ 1966 Path: path, 1967 }, 1968 }) 1969 require.NoError(t, err) 1970 assert.Equal(t, []string{"quay.io/argoprojlabs/argocd-e2e-container:0.2"}, details.Kustomize.Images) 1971 }) 1972 }) 1973 t.Run("No app specific override", func(t *testing.T) { 1974 service := newService(t, ".") 1975 runWithTempTestdata(t, "single-global", func(t *testing.T, path string) { 1976 t.Helper() 1977 details, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{ 1978 Repo: &v1alpha1.Repository{}, 1979 Source: &v1alpha1.ApplicationSource{ 1980 Path: path, 1981 }, 1982 AppName: "testapp", 1983 }) 1984 require.NoError(t, err) 1985 assert.Equal(t, []string{"quay.io/argoprojlabs/argocd-e2e-container:0.2"}, details.Kustomize.Images) 1986 }) 1987 }) 1988 t.Run("Only app specific override", func(t *testing.T) { 1989 service := newService(t, ".") 1990 runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) { 1991 t.Helper() 1992 details, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{ 1993 Repo: &v1alpha1.Repository{}, 1994 Source: &v1alpha1.ApplicationSource{ 1995 Path: path, 1996 }, 1997 AppName: "testapp", 1998 }) 1999 require.NoError(t, err) 2000 assert.Equal(t, []string{"quay.io/argoprojlabs/argocd-e2e-container:0.3"}, details.Kustomize.Images) 2001 }) 2002 }) 2003 t.Run("App specific override", func(t *testing.T) { 2004 service := newService(t, ".") 2005 runWithTempTestdata(t, "multi", func(t *testing.T, path string) { 2006 t.Helper() 2007 details, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{ 2008 Repo: &v1alpha1.Repository{}, 2009 Source: &v1alpha1.ApplicationSource{ 2010 Path: path, 2011 }, 2012 AppName: "testapp", 2013 }) 2014 require.NoError(t, err) 2015 assert.Equal(t, []string{"quay.io/argoprojlabs/argocd-e2e-container:0.3"}, details.Kustomize.Images) 2016 }) 2017 }) 2018 t.Run("App specific overrides containing non-mergeable field", func(t *testing.T) { 2019 service := newService(t, ".") 2020 runWithTempTestdata(t, "multi", func(t *testing.T, path string) { 2021 t.Helper() 2022 details, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{ 2023 Repo: &v1alpha1.Repository{}, 2024 Source: &v1alpha1.ApplicationSource{ 2025 Path: path, 2026 }, 2027 AppName: "unmergeable", 2028 }) 2029 require.NoError(t, err) 2030 assert.Equal(t, []string{"quay.io/argoprojlabs/argocd-e2e-container:0.3"}, details.Kustomize.Images) 2031 }) 2032 }) 2033 t.Run("Broken app-specific overrides", func(t *testing.T) { 2034 service := newService(t, ".") 2035 runWithTempTestdata(t, "multi", func(t *testing.T, path string) { 2036 t.Helper() 2037 _, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{ 2038 Repo: &v1alpha1.Repository{}, 2039 Source: &v1alpha1.ApplicationSource{ 2040 Path: path, 2041 }, 2042 AppName: "broken", 2043 }) 2044 require.Error(t, err) 2045 }) 2046 }) 2047 } 2048 2049 // There are unit test that will use kustomize set and by that modify the 2050 // kustomization.yaml. For proper testing, we need to copy the testdata to a 2051 // temporary path, run the tests, and then throw the copy away again. 2052 func mkTempParameters(source string) string { 2053 tempDir, err := os.MkdirTemp("./testdata", "app-parameters") 2054 if err != nil { 2055 panic(err) 2056 } 2057 cmd := exec.Command("cp", "-R", source, tempDir) 2058 err = cmd.Run() 2059 if err != nil { 2060 os.RemoveAll(tempDir) 2061 panic(err) 2062 } 2063 return tempDir 2064 } 2065 2066 // Simple wrapper run a test with a temporary copy of the testdata, because 2067 // the test would modify the data when run. 2068 func runWithTempTestdata(t *testing.T, path string, runner func(t *testing.T, path string)) { 2069 t.Helper() 2070 tempDir := mkTempParameters("./testdata/app-parameters") 2071 runner(t, filepath.Join(tempDir, "app-parameters", path)) 2072 os.RemoveAll(tempDir) 2073 } 2074 2075 func TestGenerateManifestsWithAppParameterFile(t *testing.T) { 2076 t.Run("Single global override", func(t *testing.T) { 2077 runWithTempTestdata(t, "single-global", func(t *testing.T, path string) { 2078 t.Helper() 2079 service := newService(t, ".") 2080 manifests, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 2081 Repo: &v1alpha1.Repository{}, 2082 ApplicationSource: &v1alpha1.ApplicationSource{ 2083 Path: path, 2084 }, 2085 ProjectName: "something", 2086 ProjectSourceRepos: []string{"*"}, 2087 }) 2088 require.NoError(t, err) 2089 resourceByKindName := make(map[string]*unstructured.Unstructured) 2090 for _, manifest := range manifests.Manifests { 2091 var un unstructured.Unstructured 2092 err := yaml.Unmarshal([]byte(manifest), &un) 2093 require.NoError(t, err) 2094 resourceByKindName[fmt.Sprintf("%s/%s", un.GetKind(), un.GetName())] = &un 2095 } 2096 deployment, ok := resourceByKindName["Deployment/guestbook-ui"] 2097 require.True(t, ok) 2098 containers, ok, _ := unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "containers") 2099 require.True(t, ok) 2100 image, ok, _ := unstructured.NestedString(containers[0].(map[string]any), "image") 2101 require.True(t, ok) 2102 assert.Equal(t, "quay.io/argoprojlabs/argocd-e2e-container:0.2", image) 2103 }) 2104 }) 2105 2106 t.Run("Single global override Helm", func(t *testing.T) { 2107 runWithTempTestdata(t, "single-global-helm", func(t *testing.T, path string) { 2108 t.Helper() 2109 service := newService(t, ".") 2110 manifests, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 2111 Repo: &v1alpha1.Repository{}, 2112 ApplicationSource: &v1alpha1.ApplicationSource{ 2113 Path: path, 2114 }, 2115 ProjectName: "something", 2116 ProjectSourceRepos: []string{"*"}, 2117 }) 2118 require.NoError(t, err) 2119 resourceByKindName := make(map[string]*unstructured.Unstructured) 2120 for _, manifest := range manifests.Manifests { 2121 var un unstructured.Unstructured 2122 err := yaml.Unmarshal([]byte(manifest), &un) 2123 require.NoError(t, err) 2124 resourceByKindName[fmt.Sprintf("%s/%s", un.GetKind(), un.GetName())] = &un 2125 } 2126 deployment, ok := resourceByKindName["Deployment/guestbook-ui"] 2127 require.True(t, ok) 2128 containers, ok, _ := unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "containers") 2129 require.True(t, ok) 2130 image, ok, _ := unstructured.NestedString(containers[0].(map[string]any), "image") 2131 require.True(t, ok) 2132 assert.Equal(t, "quay.io/argoprojlabs/argocd-e2e-container:0.2", image) 2133 }) 2134 }) 2135 2136 t.Run("Application specific override", func(t *testing.T) { 2137 service := newService(t, ".") 2138 runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) { 2139 t.Helper() 2140 manifests, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 2141 Repo: &v1alpha1.Repository{}, 2142 ApplicationSource: &v1alpha1.ApplicationSource{ 2143 Path: path, 2144 }, 2145 AppName: "testapp", 2146 ProjectName: "something", 2147 ProjectSourceRepos: []string{"*"}, 2148 }) 2149 require.NoError(t, err) 2150 resourceByKindName := make(map[string]*unstructured.Unstructured) 2151 for _, manifest := range manifests.Manifests { 2152 var un unstructured.Unstructured 2153 err := yaml.Unmarshal([]byte(manifest), &un) 2154 require.NoError(t, err) 2155 resourceByKindName[fmt.Sprintf("%s/%s", un.GetKind(), un.GetName())] = &un 2156 } 2157 deployment, ok := resourceByKindName["Deployment/guestbook-ui"] 2158 require.True(t, ok) 2159 containers, ok, _ := unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "containers") 2160 require.True(t, ok) 2161 image, ok, _ := unstructured.NestedString(containers[0].(map[string]any), "image") 2162 require.True(t, ok) 2163 assert.Equal(t, "quay.io/argoprojlabs/argocd-e2e-container:0.3", image) 2164 }) 2165 }) 2166 2167 t.Run("Multi-source with source as ref only does not generate manifests", func(t *testing.T) { 2168 service := newService(t, ".") 2169 runWithTempTestdata(t, "single-app-only", func(t *testing.T, _ string) { 2170 t.Helper() 2171 manifests, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 2172 Repo: &v1alpha1.Repository{}, 2173 ApplicationSource: &v1alpha1.ApplicationSource{ 2174 Path: "", 2175 Chart: "", 2176 Ref: "test", 2177 }, 2178 AppName: "testapp-multi-ref-only", 2179 ProjectName: "something", 2180 ProjectSourceRepos: []string{"*"}, 2181 HasMultipleSources: true, 2182 }) 2183 require.NoError(t, err) 2184 assert.Empty(t, manifests.Manifests) 2185 assert.NotEmpty(t, manifests.Revision) 2186 }) 2187 }) 2188 2189 t.Run("Application specific override for other app", func(t *testing.T) { 2190 service := newService(t, ".") 2191 runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) { 2192 t.Helper() 2193 manifests, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 2194 Repo: &v1alpha1.Repository{}, 2195 ApplicationSource: &v1alpha1.ApplicationSource{ 2196 Path: path, 2197 }, 2198 AppName: "testapp2", 2199 ProjectName: "something", 2200 ProjectSourceRepos: []string{"*"}, 2201 }) 2202 require.NoError(t, err) 2203 resourceByKindName := make(map[string]*unstructured.Unstructured) 2204 for _, manifest := range manifests.Manifests { 2205 var un unstructured.Unstructured 2206 err := yaml.Unmarshal([]byte(manifest), &un) 2207 require.NoError(t, err) 2208 resourceByKindName[fmt.Sprintf("%s/%s", un.GetKind(), un.GetName())] = &un 2209 } 2210 deployment, ok := resourceByKindName["Deployment/guestbook-ui"] 2211 require.True(t, ok) 2212 containers, ok, _ := unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "containers") 2213 require.True(t, ok) 2214 image, ok, _ := unstructured.NestedString(containers[0].(map[string]any), "image") 2215 require.True(t, ok) 2216 assert.Equal(t, "quay.io/argoprojlabs/argocd-e2e-container:0.1", image) 2217 }) 2218 }) 2219 2220 t.Run("Override info does not appear in cache key", func(t *testing.T) { 2221 service := newService(t, ".") 2222 runWithTempTestdata(t, "single-global", func(t *testing.T, path string) { 2223 t.Helper() 2224 source := &v1alpha1.ApplicationSource{ 2225 Path: path, 2226 } 2227 sourceCopy := source.DeepCopy() // make a copy in case GenerateManifest mutates it. 2228 _, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{ 2229 Repo: &v1alpha1.Repository{}, 2230 ApplicationSource: sourceCopy, 2231 AppName: "test", 2232 ProjectName: "something", 2233 ProjectSourceRepos: []string{"*"}, 2234 }) 2235 require.NoError(t, err) 2236 res := &cache.CachedManifestResponse{} 2237 // Try to pull from the cache with a `source` that does not include any overrides. Overrides should not be 2238 // part of the cache key, because you can't get the overrides without a repo operation. And avoiding repo 2239 // operations is the point of the cache. 2240 err = service.cache.GetManifests(mock.Anything, source, v1alpha1.RefTargetRevisionMapping{}, &v1alpha1.ClusterInfo{}, "", "", "", "test", res, nil, "") 2241 require.NoError(t, err) 2242 }) 2243 }) 2244 } 2245 2246 func TestGenerateManifestWithAnnotatedAndRegularGitTagHashes(t *testing.T) { 2247 regularGitTagHash := "632039659e542ed7de0c170a4fcc1c571b288fc0" 2248 annotatedGitTaghash := "95249be61b028d566c29d47b19e65c5603388a41" 2249 invalidGitTaghash := "invalid-tag" 2250 actualCommitSHA := "632039659e542ed7de0c170a4fcc1c571b288fc0" 2251 2252 tests := []struct { 2253 name string 2254 ctx context.Context 2255 manifestRequest *apiclient.ManifestRequest 2256 wantError bool 2257 service *Service 2258 }{ 2259 { 2260 name: "Case: Git tag hash matches latest commit SHA (regular tag)", 2261 ctx: t.Context(), 2262 manifestRequest: &apiclient.ManifestRequest{ 2263 Repo: &v1alpha1.Repository{}, 2264 ApplicationSource: &v1alpha1.ApplicationSource{ 2265 TargetRevision: regularGitTagHash, 2266 }, 2267 NoCache: true, 2268 ProjectName: "something", 2269 ProjectSourceRepos: []string{"*"}, 2270 }, 2271 wantError: false, 2272 service: newServiceWithCommitSHA(t, ".", regularGitTagHash), 2273 }, 2274 2275 { 2276 name: "Case: Git tag hash does not match latest commit SHA (annotated tag)", 2277 ctx: t.Context(), 2278 manifestRequest: &apiclient.ManifestRequest{ 2279 Repo: &v1alpha1.Repository{}, 2280 ApplicationSource: &v1alpha1.ApplicationSource{ 2281 TargetRevision: annotatedGitTaghash, 2282 }, 2283 NoCache: true, 2284 ProjectName: "something", 2285 ProjectSourceRepos: []string{"*"}, 2286 }, 2287 wantError: false, 2288 service: newServiceWithCommitSHA(t, ".", annotatedGitTaghash), 2289 }, 2290 2291 { 2292 name: "Case: Git tag hash is invalid", 2293 ctx: t.Context(), 2294 manifestRequest: &apiclient.ManifestRequest{ 2295 Repo: &v1alpha1.Repository{}, 2296 ApplicationSource: &v1alpha1.ApplicationSource{ 2297 TargetRevision: invalidGitTaghash, 2298 }, 2299 NoCache: true, 2300 ProjectName: "something", 2301 ProjectSourceRepos: []string{"*"}, 2302 }, 2303 wantError: true, 2304 service: newServiceWithCommitSHA(t, ".", invalidGitTaghash), 2305 }, 2306 } 2307 for _, tt := range tests { 2308 t.Run(tt.name, func(t *testing.T) { 2309 manifestResponse, err := tt.service.GenerateManifest(tt.ctx, tt.manifestRequest) 2310 if !tt.wantError { 2311 require.NoError(t, err) 2312 assert.Equal(t, manifestResponse.Revision, actualCommitSHA) 2313 } else { 2314 assert.Errorf(t, err, "expected an error but did not throw one") 2315 } 2316 }) 2317 } 2318 } 2319 2320 func TestGenerateManifestWithAnnotatedTagsAndMultiSourceApp(t *testing.T) { 2321 annotatedGitTaghash := "95249be61b028d566c29d47b19e65c5603388a41" 2322 2323 service := newServiceWithCommitSHA(t, ".", annotatedGitTaghash) 2324 2325 refSources := map[string]*v1alpha1.RefTarget{} 2326 2327 refSources["$global"] = &v1alpha1.RefTarget{ 2328 TargetRevision: annotatedGitTaghash, 2329 } 2330 2331 refSources["$default"] = &v1alpha1.RefTarget{ 2332 TargetRevision: annotatedGitTaghash, 2333 } 2334 2335 manifestRequest := &apiclient.ManifestRequest{ 2336 Repo: &v1alpha1.Repository{}, 2337 ApplicationSource: &v1alpha1.ApplicationSource{ 2338 TargetRevision: annotatedGitTaghash, 2339 Helm: &v1alpha1.ApplicationSourceHelm{ 2340 ValueFiles: []string{"$global/values.yaml", "$default/secrets.yaml"}, 2341 }, 2342 }, 2343 HasMultipleSources: true, 2344 NoCache: true, 2345 RefSources: refSources, 2346 } 2347 2348 response, err := service.GenerateManifest(t.Context(), manifestRequest) 2349 require.NoError(t, err) 2350 assert.Equalf(t, response.Revision, annotatedGitTaghash, "returned SHA %s is different from expected annotated tag %s", response.Revision, annotatedGitTaghash) 2351 } 2352 2353 func TestGenerateMultiSourceHelmWithFileParameter(t *testing.T) { 2354 expectedFileContent, err := os.ReadFile("../../util/helm/testdata/external/external-secret.txt") 2355 require.NoError(t, err) 2356 2357 service := newService(t, "../../util/helm/testdata") 2358 2359 testCases := []struct { 2360 name string 2361 refSources map[string]*v1alpha1.RefTarget 2362 expectedContent string 2363 expectedErr bool 2364 }{{ 2365 name: "Successfully resolve multi-source ref for helm set-file", 2366 refSources: map[string]*v1alpha1.RefTarget{ 2367 "$global": { 2368 TargetRevision: "HEAD", 2369 }, 2370 }, 2371 expectedContent: string(expectedFileContent), 2372 expectedErr: false, 2373 }, { 2374 name: "Failed to resolve multi-source ref for helm set-file", 2375 refSources: map[string]*v1alpha1.RefTarget{}, 2376 expectedContent: "DOES-NOT-EXIST", 2377 expectedErr: true, 2378 }} 2379 2380 for i := range testCases { 2381 tc := testCases[i] 2382 t.Run(tc.name, func(t *testing.T) { 2383 manifestRequest := &apiclient.ManifestRequest{ 2384 Repo: &v1alpha1.Repository{}, 2385 ApplicationSource: &v1alpha1.ApplicationSource{ 2386 Ref: "$global", 2387 Path: "./redis", 2388 TargetRevision: "HEAD", 2389 Helm: &v1alpha1.ApplicationSourceHelm{ 2390 ValueFiles: []string{"$global/redis/values-production.yaml"}, 2391 FileParameters: []v1alpha1.HelmFileParameter{{ 2392 Name: "passwordContent", 2393 Path: "$global/external/external-secret.txt", 2394 }}, 2395 }, 2396 }, 2397 HasMultipleSources: true, 2398 NoCache: true, 2399 RefSources: tc.refSources, 2400 } 2401 2402 res, err := service.GenerateManifest(t.Context(), manifestRequest) 2403 2404 if !tc.expectedErr { 2405 require.NoError(t, err) 2406 2407 // Check that any of the manifests contains the secret 2408 idx := slices.IndexFunc(res.Manifests, func(content string) bool { 2409 return strings.Contains(content, tc.expectedContent) 2410 }) 2411 assert.GreaterOrEqual(t, idx, 0, "No manifest contains the value set with the helm fileParameters") 2412 } else { 2413 assert.Error(t, err) 2414 } 2415 }) 2416 } 2417 } 2418 2419 func TestFindResources(t *testing.T) { 2420 testCases := []struct { 2421 name string 2422 include string 2423 exclude string 2424 expectedNames []string 2425 }{{ 2426 name: "Include One Match", 2427 include: "subdir/deploymentSub.yaml", 2428 expectedNames: []string{"nginx-deployment-sub"}, 2429 }, { 2430 name: "Include Everything", 2431 include: "*.yaml", 2432 expectedNames: []string{"nginx-deployment", "nginx-deployment-sub"}, 2433 }, { 2434 name: "Include Subdirectory", 2435 include: "**/*.yaml", 2436 expectedNames: []string{"nginx-deployment-sub"}, 2437 }, { 2438 name: "Include No Matches", 2439 include: "nothing.yaml", 2440 expectedNames: []string{}, 2441 }, { 2442 name: "Exclude - One Match", 2443 exclude: "subdir/deploymentSub.yaml", 2444 expectedNames: []string{"nginx-deployment"}, 2445 }, { 2446 name: "Exclude - Everything", 2447 exclude: "*.yaml", 2448 expectedNames: []string{}, 2449 }} 2450 for i := range testCases { 2451 tc := testCases[i] 2452 t.Run(tc.name, func(t *testing.T) { 2453 objs, err := findManifests(&log.Entry{}, "testdata/app-include-exclude", ".", nil, v1alpha1.ApplicationSourceDirectory{ 2454 Recurse: true, 2455 Include: tc.include, 2456 Exclude: tc.exclude, 2457 }, map[string]bool{}, resource.MustParse("0")) 2458 require.NoError(t, err) 2459 var names []string 2460 for i := range objs { 2461 names = append(names, objs[i].GetName()) 2462 } 2463 assert.ElementsMatch(t, tc.expectedNames, names) 2464 }) 2465 } 2466 } 2467 2468 func TestFindManifests_Exclude(t *testing.T) { 2469 objs, err := findManifests(&log.Entry{}, "testdata/app-include-exclude", ".", nil, v1alpha1.ApplicationSourceDirectory{ 2470 Recurse: true, 2471 Exclude: "subdir/deploymentSub.yaml", 2472 }, map[string]bool{}, resource.MustParse("0")) 2473 2474 require.NoError(t, err) 2475 require.Len(t, objs, 1) 2476 2477 assert.Equal(t, "nginx-deployment", objs[0].GetName()) 2478 } 2479 2480 func TestFindManifests_Exclude_NothingMatches(t *testing.T) { 2481 objs, err := findManifests(&log.Entry{}, "testdata/app-include-exclude", ".", nil, v1alpha1.ApplicationSourceDirectory{ 2482 Recurse: true, 2483 Exclude: "nothing.yaml", 2484 }, map[string]bool{}, resource.MustParse("0")) 2485 2486 require.NoError(t, err) 2487 require.Len(t, objs, 2) 2488 2489 assert.ElementsMatch(t, 2490 []string{"nginx-deployment", "nginx-deployment-sub"}, []string{objs[0].GetName(), objs[1].GetName()}) 2491 } 2492 2493 func tempDir(t *testing.T) string { 2494 t.Helper() 2495 dir, err := os.MkdirTemp(".", "") 2496 require.NoError(t, err) 2497 t.Cleanup(func() { 2498 err = os.RemoveAll(dir) 2499 if err != nil { 2500 panic(err) 2501 } 2502 }) 2503 absDir, err := filepath.Abs(dir) 2504 require.NoError(t, err) 2505 return absDir 2506 } 2507 2508 func walkFor(t *testing.T, root string, testPath string, run func(info fs.FileInfo)) { 2509 t.Helper() 2510 hitExpectedPath := false 2511 err := filepath.Walk(root, func(path string, info fs.FileInfo, err error) error { 2512 if path == testPath { 2513 require.NoError(t, err) 2514 hitExpectedPath = true 2515 run(info) 2516 } 2517 return nil 2518 }) 2519 require.NoError(t, err) 2520 assert.True(t, hitExpectedPath, "did not hit expected path when walking directory") 2521 } 2522 2523 func Test_getPotentiallyValidManifestFile(t *testing.T) { 2524 // These tests use filepath.Walk instead of os.Stat to get file info, because FileInfo from os.Stat does not return 2525 // true for IsSymlink like os.Walk does. 2526 2527 // These tests do not use t.TempDir() because those directories can contain symlinks which cause test to fail 2528 // InBound checks. 2529 2530 t.Run("non-JSON/YAML is skipped with an empty ignore message", func(t *testing.T) { 2531 appDir := tempDir(t) 2532 filePath := filepath.Join(appDir, "not-json-or-yaml") 2533 file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0o644) 2534 require.NoError(t, err) 2535 err = file.Close() 2536 require.NoError(t, err) 2537 2538 walkFor(t, appDir, filePath, func(info fs.FileInfo) { 2539 realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(filePath, info, appDir, appDir, "", "") 2540 assert.Nil(t, realFileInfo) 2541 assert.Empty(t, ignoreMessage) 2542 require.NoError(t, err) 2543 }) 2544 }) 2545 2546 t.Run("circular link should throw an error", func(t *testing.T) { 2547 appDir := tempDir(t) 2548 2549 aPath := filepath.Join(appDir, "a.json") 2550 bPath := filepath.Join(appDir, "b.json") 2551 err := os.Symlink(bPath, aPath) 2552 require.NoError(t, err) 2553 err = os.Symlink(aPath, bPath) 2554 require.NoError(t, err) 2555 2556 walkFor(t, appDir, aPath, func(info fs.FileInfo) { 2557 realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(aPath, info, appDir, appDir, "", "") 2558 assert.Nil(t, realFileInfo) 2559 assert.Empty(t, ignoreMessage) 2560 assert.ErrorContains(t, err, "too many links") 2561 }) 2562 }) 2563 2564 t.Run("symlink with missing destination should throw an error", func(t *testing.T) { 2565 appDir := tempDir(t) 2566 2567 aPath := filepath.Join(appDir, "a.json") 2568 bPath := filepath.Join(appDir, "b.json") 2569 err := os.Symlink(bPath, aPath) 2570 require.NoError(t, err) 2571 2572 walkFor(t, appDir, aPath, func(info fs.FileInfo) { 2573 realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(aPath, info, appDir, appDir, "", "") 2574 assert.Nil(t, realFileInfo) 2575 assert.NotEmpty(t, ignoreMessage) 2576 require.NoError(t, err) 2577 }) 2578 }) 2579 2580 t.Run("out-of-bounds symlink should throw an error", func(t *testing.T) { 2581 appDir := tempDir(t) 2582 2583 linkPath := filepath.Join(appDir, "a.json") 2584 err := os.Symlink("..", linkPath) 2585 require.NoError(t, err) 2586 2587 walkFor(t, appDir, linkPath, func(info fs.FileInfo) { 2588 realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(linkPath, info, appDir, appDir, "", "") 2589 assert.Nil(t, realFileInfo) 2590 assert.Empty(t, ignoreMessage) 2591 assert.ErrorContains(t, err, "illegal filepath in symlink") 2592 }) 2593 }) 2594 2595 t.Run("symlink to a non-regular file should be skipped with warning", func(t *testing.T) { 2596 appDir := tempDir(t) 2597 2598 dirPath := filepath.Join(appDir, "test.dir") 2599 err := os.MkdirAll(dirPath, 0o644) 2600 require.NoError(t, err) 2601 linkPath := filepath.Join(appDir, "test.json") 2602 err = os.Symlink(dirPath, linkPath) 2603 require.NoError(t, err) 2604 2605 walkFor(t, appDir, linkPath, func(info fs.FileInfo) { 2606 realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(linkPath, info, appDir, appDir, "", "") 2607 assert.Nil(t, realFileInfo) 2608 assert.Contains(t, ignoreMessage, "non-regular file") 2609 require.NoError(t, err) 2610 }) 2611 }) 2612 2613 t.Run("non-included file should be skipped with no message", func(t *testing.T) { 2614 appDir := tempDir(t) 2615 2616 filePath := filepath.Join(appDir, "not-included.yaml") 2617 file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0o644) 2618 require.NoError(t, err) 2619 err = file.Close() 2620 require.NoError(t, err) 2621 2622 walkFor(t, appDir, filePath, func(info fs.FileInfo) { 2623 realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(filePath, info, appDir, appDir, "*.json", "") 2624 assert.Nil(t, realFileInfo) 2625 assert.Empty(t, ignoreMessage) 2626 require.NoError(t, err) 2627 }) 2628 }) 2629 2630 t.Run("excluded file should be skipped with no message", func(t *testing.T) { 2631 appDir := tempDir(t) 2632 2633 filePath := filepath.Join(appDir, "excluded.json") 2634 file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0o644) 2635 require.NoError(t, err) 2636 err = file.Close() 2637 require.NoError(t, err) 2638 2639 walkFor(t, appDir, filePath, func(info fs.FileInfo) { 2640 realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(filePath, info, appDir, appDir, "", "excluded.*") 2641 assert.Nil(t, realFileInfo) 2642 assert.Empty(t, ignoreMessage) 2643 require.NoError(t, err) 2644 }) 2645 }) 2646 2647 t.Run("symlink to a regular file is potentially valid", func(t *testing.T) { 2648 appDir := tempDir(t) 2649 2650 filePath := filepath.Join(appDir, "regular-file") 2651 file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0o644) 2652 require.NoError(t, err) 2653 err = file.Close() 2654 require.NoError(t, err) 2655 2656 linkPath := filepath.Join(appDir, "link.json") 2657 err = os.Symlink(filePath, linkPath) 2658 require.NoError(t, err) 2659 2660 walkFor(t, appDir, linkPath, func(info fs.FileInfo) { 2661 realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(linkPath, info, appDir, appDir, "", "") 2662 assert.NotNil(t, realFileInfo) 2663 assert.Empty(t, ignoreMessage) 2664 require.NoError(t, err) 2665 }) 2666 }) 2667 2668 t.Run("a regular file is potentially valid", func(t *testing.T) { 2669 appDir := tempDir(t) 2670 2671 filePath := filepath.Join(appDir, "regular-file.json") 2672 file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0o644) 2673 require.NoError(t, err) 2674 err = file.Close() 2675 require.NoError(t, err) 2676 2677 walkFor(t, appDir, filePath, func(info fs.FileInfo) { 2678 realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(filePath, info, appDir, appDir, "", "") 2679 assert.NotNil(t, realFileInfo) 2680 assert.Empty(t, ignoreMessage) 2681 require.NoError(t, err) 2682 }) 2683 }) 2684 2685 t.Run("realFileInfo is for the destination rather than the symlink", func(t *testing.T) { 2686 appDir := tempDir(t) 2687 2688 filePath := filepath.Join(appDir, "regular-file") 2689 file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0o644) 2690 require.NoError(t, err) 2691 err = file.Close() 2692 require.NoError(t, err) 2693 2694 linkPath := filepath.Join(appDir, "link.json") 2695 err = os.Symlink(filePath, linkPath) 2696 require.NoError(t, err) 2697 2698 walkFor(t, appDir, linkPath, func(info fs.FileInfo) { 2699 realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(linkPath, info, appDir, appDir, "", "") 2700 assert.NotNil(t, realFileInfo) 2701 assert.Equal(t, filepath.Base(filePath), realFileInfo.Name()) 2702 assert.Empty(t, ignoreMessage) 2703 require.NoError(t, err) 2704 }) 2705 }) 2706 } 2707 2708 func Test_getPotentiallyValidManifests(t *testing.T) { 2709 // Tests which return no manifests and an error check to make sure the directory exists before running. A missing 2710 // directory would produce those same results. 2711 2712 logCtx := log.WithField("test", "test") 2713 2714 t.Run("unreadable file throws error", func(t *testing.T) { 2715 appDir := t.TempDir() 2716 unreadablePath := filepath.Join(appDir, "unreadable.json") 2717 err := os.WriteFile(unreadablePath, []byte{}, 0o666) 2718 require.NoError(t, err) 2719 err = os.Chmod(appDir, 0o000) 2720 require.NoError(t, err) 2721 2722 manifests, err := getPotentiallyValidManifests(logCtx, appDir, appDir, false, "", "", resource.MustParse("0")) 2723 assert.Empty(t, manifests) 2724 require.Error(t, err) 2725 2726 // allow cleanup 2727 err = os.Chmod(appDir, 0o777) 2728 if err != nil { 2729 panic(err) 2730 } 2731 }) 2732 2733 t.Run("no recursion when recursion is disabled", func(t *testing.T) { 2734 manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/recurse", "./testdata/recurse", false, "", "", resource.MustParse("0")) 2735 assert.Len(t, manifests, 1) 2736 require.NoError(t, err) 2737 }) 2738 2739 t.Run("recursion when recursion is enabled", func(t *testing.T) { 2740 manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/recurse", "./testdata/recurse", true, "", "", resource.MustParse("0")) 2741 assert.Len(t, manifests, 2) 2742 require.NoError(t, err) 2743 }) 2744 2745 t.Run("non-JSON/YAML is skipped", func(t *testing.T) { 2746 manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/non-manifest-file", "./testdata/non-manifest-file", false, "", "", resource.MustParse("0")) 2747 assert.Empty(t, manifests) 2748 require.NoError(t, err) 2749 }) 2750 2751 t.Run("circular link should throw an error", func(t *testing.T) { 2752 const testDir = "./testdata/circular-link" 2753 require.DirExists(t, testDir) 2754 t.Cleanup(func() { 2755 os.Remove(path.Join(testDir, "a.json")) 2756 os.Remove(path.Join(testDir, "b.json")) 2757 }) 2758 t.Chdir(testDir) 2759 require.NoError(t, fileutil.CreateSymlink(t, "a.json", "b.json")) 2760 require.NoError(t, fileutil.CreateSymlink(t, "b.json", "a.json")) 2761 manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/circular-link", "./testdata/circular-link", false, "", "", resource.MustParse("0")) 2762 assert.Empty(t, manifests) 2763 require.Error(t, err) 2764 }) 2765 2766 t.Run("out-of-bounds symlink should throw an error", func(t *testing.T) { 2767 require.DirExists(t, "./testdata/out-of-bounds-link") 2768 manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/out-of-bounds-link", "./testdata/out-of-bounds-link", false, "", "", resource.MustParse("0")) 2769 assert.Empty(t, manifests) 2770 require.Error(t, err) 2771 }) 2772 2773 t.Run("symlink to a regular file works", func(t *testing.T) { 2774 repoRoot, err := filepath.Abs("./testdata/in-bounds-link") 2775 require.NoError(t, err) 2776 appPath, err := filepath.Abs("./testdata/in-bounds-link/app") 2777 require.NoError(t, err) 2778 manifests, err := getPotentiallyValidManifests(logCtx, appPath, repoRoot, false, "", "", resource.MustParse("0")) 2779 assert.Len(t, manifests, 1) 2780 require.NoError(t, err) 2781 }) 2782 2783 t.Run("symlink to nowhere should be ignored", func(t *testing.T) { 2784 manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/link-to-nowhere", "./testdata/link-to-nowhere", false, "", "", resource.MustParse("0")) 2785 assert.Empty(t, manifests) 2786 require.NoError(t, err) 2787 }) 2788 2789 t.Run("link to over-sized manifest fails", func(t *testing.T) { 2790 repoRoot, err := filepath.Abs("./testdata/in-bounds-link") 2791 require.NoError(t, err) 2792 appPath, err := filepath.Abs("./testdata/in-bounds-link/app") 2793 require.NoError(t, err) 2794 // The file is 35 bytes. 2795 manifests, err := getPotentiallyValidManifests(logCtx, appPath, repoRoot, false, "", "", resource.MustParse("34")) 2796 assert.Empty(t, manifests) 2797 assert.ErrorIs(t, err, ErrExceededMaxCombinedManifestFileSize) 2798 }) 2799 2800 t.Run("group of files should be limited at precisely the sum of their size", func(t *testing.T) { 2801 // There is a total of 10 files, ech file being 10 bytes. 2802 manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/several-files", "./testdata/several-files", false, "", "", resource.MustParse("365")) 2803 assert.Len(t, manifests, 10) 2804 require.NoError(t, err) 2805 2806 manifests, err = getPotentiallyValidManifests(logCtx, "./testdata/several-files", "./testdata/several-files", false, "", "", resource.MustParse("100")) 2807 assert.Empty(t, manifests) 2808 assert.ErrorIs(t, err, ErrExceededMaxCombinedManifestFileSize) 2809 }) 2810 } 2811 2812 func Test_findManifests(t *testing.T) { 2813 logCtx := log.WithField("test", "test") 2814 noRecurse := v1alpha1.ApplicationSourceDirectory{Recurse: false} 2815 2816 t.Run("unreadable file throws error", func(t *testing.T) { 2817 appDir := t.TempDir() 2818 unreadablePath := filepath.Join(appDir, "unreadable.json") 2819 err := os.WriteFile(unreadablePath, []byte{}, 0o666) 2820 require.NoError(t, err) 2821 err = os.Chmod(appDir, 0o000) 2822 require.NoError(t, err) 2823 2824 manifests, err := findManifests(logCtx, appDir, appDir, nil, noRecurse, nil, resource.MustParse("0")) 2825 assert.Empty(t, manifests) 2826 require.Error(t, err) 2827 2828 // allow cleanup 2829 err = os.Chmod(appDir, 0o777) 2830 if err != nil { 2831 panic(err) 2832 } 2833 }) 2834 2835 t.Run("no recursion when recursion is disabled", func(t *testing.T) { 2836 manifests, err := findManifests(logCtx, "./testdata/recurse", "./testdata/recurse", nil, noRecurse, nil, resource.MustParse("0")) 2837 assert.Len(t, manifests, 2) 2838 require.NoError(t, err) 2839 }) 2840 2841 t.Run("recursion when recursion is enabled", func(t *testing.T) { 2842 recurse := v1alpha1.ApplicationSourceDirectory{Recurse: true} 2843 manifests, err := findManifests(logCtx, "./testdata/recurse", "./testdata/recurse", nil, recurse, nil, resource.MustParse("0")) 2844 assert.Len(t, manifests, 4) 2845 require.NoError(t, err) 2846 }) 2847 2848 t.Run("non-JSON/YAML is skipped", func(t *testing.T) { 2849 manifests, err := findManifests(logCtx, "./testdata/non-manifest-file", "./testdata/non-manifest-file", nil, noRecurse, nil, resource.MustParse("0")) 2850 assert.Empty(t, manifests) 2851 require.NoError(t, err) 2852 }) 2853 2854 t.Run("circular link should throw an error", func(t *testing.T) { 2855 const testDir = "./testdata/circular-link" 2856 require.DirExists(t, testDir) 2857 t.Cleanup(func() { 2858 os.Remove(path.Join(testDir, "a.json")) 2859 os.Remove(path.Join(testDir, "b.json")) 2860 }) 2861 t.Chdir(testDir) 2862 require.NoError(t, fileutil.CreateSymlink(t, "a.json", "b.json")) 2863 require.NoError(t, fileutil.CreateSymlink(t, "b.json", "a.json")) 2864 manifests, err := findManifests(logCtx, "./testdata/circular-link", "./testdata/circular-link", nil, noRecurse, nil, resource.MustParse("0")) 2865 assert.Empty(t, manifests) 2866 require.Error(t, err) 2867 }) 2868 2869 t.Run("out-of-bounds symlink should throw an error", func(t *testing.T) { 2870 require.DirExists(t, "./testdata/out-of-bounds-link") 2871 manifests, err := findManifests(logCtx, "./testdata/out-of-bounds-link", "./testdata/out-of-bounds-link", nil, noRecurse, nil, resource.MustParse("0")) 2872 assert.Empty(t, manifests) 2873 require.Error(t, err) 2874 }) 2875 2876 t.Run("symlink to a regular file works", func(t *testing.T) { 2877 repoRoot, err := filepath.Abs("./testdata/in-bounds-link") 2878 require.NoError(t, err) 2879 appPath, err := filepath.Abs("./testdata/in-bounds-link/app") 2880 require.NoError(t, err) 2881 manifests, err := findManifests(logCtx, appPath, repoRoot, nil, noRecurse, nil, resource.MustParse("0")) 2882 assert.Len(t, manifests, 1) 2883 require.NoError(t, err) 2884 }) 2885 2886 t.Run("symlink to nowhere should be ignored", func(t *testing.T) { 2887 manifests, err := findManifests(logCtx, "./testdata/link-to-nowhere", "./testdata/link-to-nowhere", nil, noRecurse, nil, resource.MustParse("0")) 2888 assert.Empty(t, manifests) 2889 require.NoError(t, err) 2890 }) 2891 2892 t.Run("link to over-sized manifest fails", func(t *testing.T) { 2893 repoRoot, err := filepath.Abs("./testdata/in-bounds-link") 2894 require.NoError(t, err) 2895 appPath, err := filepath.Abs("./testdata/in-bounds-link/app") 2896 require.NoError(t, err) 2897 // The file is 35 bytes. 2898 manifests, err := findManifests(logCtx, appPath, repoRoot, nil, noRecurse, nil, resource.MustParse("34")) 2899 assert.Empty(t, manifests) 2900 assert.ErrorIs(t, err, ErrExceededMaxCombinedManifestFileSize) 2901 }) 2902 2903 t.Run("group of files should be limited at precisely the sum of their size", func(t *testing.T) { 2904 // There is a total of 10 files, each file being 10 bytes. 2905 manifests, err := findManifests(logCtx, "./testdata/several-files", "./testdata/several-files", nil, noRecurse, nil, resource.MustParse("365")) 2906 assert.Len(t, manifests, 10) 2907 require.NoError(t, err) 2908 2909 manifests, err = findManifests(logCtx, "./testdata/several-files", "./testdata/several-files", nil, noRecurse, nil, resource.MustParse("364")) 2910 assert.Empty(t, manifests) 2911 assert.ErrorIs(t, err, ErrExceededMaxCombinedManifestFileSize) 2912 }) 2913 2914 t.Run("jsonnet isn't counted against size limit", func(t *testing.T) { 2915 // Each file is 36 bytes. Only the 36-byte json file should be counted against the limit. 2916 manifests, err := findManifests(logCtx, "./testdata/jsonnet-and-json", "./testdata/jsonnet-and-json", nil, noRecurse, nil, resource.MustParse("36")) 2917 assert.Len(t, manifests, 2) 2918 require.NoError(t, err) 2919 2920 manifests, err = findManifests(logCtx, "./testdata/jsonnet-and-json", "./testdata/jsonnet-and-json", nil, noRecurse, nil, resource.MustParse("35")) 2921 assert.Empty(t, manifests) 2922 assert.ErrorIs(t, err, ErrExceededMaxCombinedManifestFileSize) 2923 }) 2924 2925 t.Run("partially valid YAML file throws an error", func(t *testing.T) { 2926 require.DirExists(t, "./testdata/partially-valid-yaml") 2927 manifests, err := findManifests(logCtx, "./testdata/partially-valid-yaml", "./testdata/partially-valid-yaml", nil, noRecurse, nil, resource.MustParse("0")) 2928 assert.Empty(t, manifests) 2929 require.Error(t, err) 2930 }) 2931 2932 t.Run("invalid manifest throws an error", func(t *testing.T) { 2933 require.DirExists(t, "./testdata/invalid-manifests") 2934 manifests, err := findManifests(logCtx, "./testdata/invalid-manifests", "./testdata/invalid-manifests", nil, noRecurse, nil, resource.MustParse("0")) 2935 assert.Empty(t, manifests) 2936 require.Error(t, err) 2937 }) 2938 2939 t.Run("invalid manifest containing '+argocd:skip-file-rendering' doesn't throw an error", func(t *testing.T) { 2940 require.DirExists(t, "./testdata/invalid-manifests-skipped") 2941 manifests, err := findManifests(logCtx, "./testdata/invalid-manifests-skipped", "./testdata/invalid-manifests-skipped", nil, noRecurse, nil, resource.MustParse("0")) 2942 assert.Empty(t, manifests) 2943 require.NoError(t, err) 2944 }) 2945 2946 t.Run("irrelevant YAML gets skipped, relevant YAML gets parsed", func(t *testing.T) { 2947 manifests, err := findManifests(logCtx, "./testdata/irrelevant-yaml", "./testdata/irrelevant-yaml", nil, noRecurse, nil, resource.MustParse("0")) 2948 assert.Len(t, manifests, 1) 2949 require.NoError(t, err) 2950 }) 2951 2952 t.Run("multiple JSON objects in one file throws an error", func(t *testing.T) { 2953 require.DirExists(t, "./testdata/json-list") 2954 manifests, err := findManifests(logCtx, "./testdata/json-list", "./testdata/json-list", nil, noRecurse, nil, resource.MustParse("0")) 2955 assert.Empty(t, manifests) 2956 require.Error(t, err) 2957 }) 2958 2959 t.Run("invalid JSON throws an error", func(t *testing.T) { 2960 require.DirExists(t, "./testdata/invalid-json") 2961 manifests, err := findManifests(logCtx, "./testdata/invalid-json", "./testdata/invalid-json", nil, noRecurse, nil, resource.MustParse("0")) 2962 assert.Empty(t, manifests) 2963 require.Error(t, err) 2964 }) 2965 2966 t.Run("valid JSON returns manifest and no error", func(t *testing.T) { 2967 manifests, err := findManifests(logCtx, "./testdata/valid-json", "./testdata/valid-json", nil, noRecurse, nil, resource.MustParse("0")) 2968 assert.Len(t, manifests, 1) 2969 require.NoError(t, err) 2970 }) 2971 2972 t.Run("YAML with an empty document doesn't throw an error", func(t *testing.T) { 2973 manifests, err := findManifests(logCtx, "./testdata/yaml-with-empty-document", "./testdata/yaml-with-empty-document", nil, noRecurse, nil, resource.MustParse("0")) 2974 assert.Len(t, manifests, 1) 2975 require.NoError(t, err) 2976 }) 2977 } 2978 2979 func TestTestRepoHelmOCI(t *testing.T) { 2980 service := newService(t, ".") 2981 _, err := service.TestRepository(t.Context(), &apiclient.TestRepositoryRequest{ 2982 Repo: &v1alpha1.Repository{ 2983 Repo: "https://demo.goharbor.io", 2984 Type: "helm", 2985 EnableOCI: true, 2986 }, 2987 }) 2988 assert.ErrorContains(t, err, "OCI Helm repository URL should include hostname and port only") 2989 } 2990 2991 func Test_getHelmDependencyRepos(t *testing.T) { 2992 repo1 := "https://charts.bitnami.com/bitnami" 2993 repo2 := "https://eventstore.github.io/EventStore.Charts" 2994 2995 repos, err := getHelmDependencyRepos("../../util/helm/testdata/dependency") 2996 require.NoError(t, err) 2997 assert.Len(t, repos, 2) 2998 assert.Equal(t, repos[0].Repo, repo1) 2999 assert.Equal(t, repos[1].Repo, repo2) 3000 } 3001 3002 func TestResolveRevision(t *testing.T) { 3003 service := newService(t, ".") 3004 repo := &v1alpha1.Repository{Repo: "https://github.com/argoproj/argo-cd"} 3005 app := &v1alpha1.Application{Spec: v1alpha1.ApplicationSpec{Source: &v1alpha1.ApplicationSource{}}} 3006 resolveRevisionResponse, err := service.ResolveRevision(t.Context(), &apiclient.ResolveRevisionRequest{ 3007 Repo: repo, 3008 App: app, 3009 AmbiguousRevision: "v2.2.2", 3010 }) 3011 3012 expectedResolveRevisionResponse := &apiclient.ResolveRevisionResponse{ 3013 Revision: "03b17e0233e64787ffb5fcf65c740cc2a20822ba", 3014 AmbiguousRevision: "v2.2.2 (03b17e0233e64787ffb5fcf65c740cc2a20822ba)", 3015 } 3016 3017 assert.NotNil(t, resolveRevisionResponse.Revision) 3018 require.NoError(t, err) 3019 assert.Equal(t, expectedResolveRevisionResponse, resolveRevisionResponse) 3020 } 3021 3022 func TestResolveRevisionNegativeScenarios(t *testing.T) { 3023 service := newService(t, ".") 3024 repo := &v1alpha1.Repository{Repo: "https://github.com/argoproj/argo-cd"} 3025 app := &v1alpha1.Application{Spec: v1alpha1.ApplicationSpec{Source: &v1alpha1.ApplicationSource{}}} 3026 resolveRevisionResponse, err := service.ResolveRevision(t.Context(), &apiclient.ResolveRevisionRequest{ 3027 Repo: repo, 3028 App: app, 3029 AmbiguousRevision: "v2.a.2", 3030 }) 3031 3032 expectedResolveRevisionResponse := &apiclient.ResolveRevisionResponse{ 3033 Revision: "", 3034 AmbiguousRevision: "", 3035 } 3036 3037 assert.NotNil(t, resolveRevisionResponse.Revision) 3038 require.Error(t, err) 3039 assert.Equal(t, expectedResolveRevisionResponse, resolveRevisionResponse) 3040 } 3041 3042 func TestDirectoryPermissionInitializer(t *testing.T) { 3043 dir := t.TempDir() 3044 3045 file, err := os.CreateTemp(dir, "") 3046 require.NoError(t, err) 3047 utilio.Close(file) 3048 3049 // remove read permissions 3050 require.NoError(t, os.Chmod(dir, 0o000)) 3051 3052 // Remember to restore permissions when the test finishes so dir can 3053 // be removed properly. 3054 t.Cleanup(func() { 3055 require.NoError(t, os.Chmod(dir, 0o777)) 3056 }) 3057 3058 // make sure permission are restored 3059 closer := directoryPermissionInitializer(dir) 3060 _, err = os.ReadFile(file.Name()) 3061 require.NoError(t, err) 3062 3063 // make sure permission are removed by closer 3064 utilio.Close(closer) 3065 _, err = os.ReadFile(file.Name()) 3066 require.Error(t, err) 3067 } 3068 3069 func addHelmToGitRepo(t *testing.T, options newGitRepoOptions) { 3070 t.Helper() 3071 err := os.WriteFile(filepath.Join(options.path, "Chart.yaml"), []byte("name: test\nversion: v1.0.0"), 0o777) 3072 require.NoError(t, err) 3073 for valuesFileName, values := range options.helmChartOptions.valuesFiles { 3074 valuesFileContents, err := yaml.Marshal(values) 3075 require.NoError(t, err) 3076 err = os.WriteFile(filepath.Join(options.path, valuesFileName), valuesFileContents, 0o777) 3077 require.NoError(t, err) 3078 } 3079 require.NoError(t, err) 3080 cmd := exec.Command("git", "add", "-A") 3081 cmd.Dir = options.path 3082 require.NoError(t, cmd.Run()) 3083 cmd = exec.Command("git", "commit", "-m", "Initial commit") 3084 cmd.Dir = options.path 3085 require.NoError(t, cmd.Run()) 3086 } 3087 3088 func initGitRepo(t *testing.T, options newGitRepoOptions) (revision string) { 3089 t.Helper() 3090 if options.createPath { 3091 require.NoError(t, os.Mkdir(options.path, 0o755)) 3092 } 3093 3094 cmd := exec.Command("git", "init", "-b", "main", options.path) 3095 cmd.Dir = options.path 3096 require.NoError(t, cmd.Run()) 3097 3098 if options.remote != "" { 3099 cmd = exec.Command("git", "remote", "add", "origin", options.path) 3100 cmd.Dir = options.path 3101 require.NoError(t, cmd.Run()) 3102 } 3103 3104 commitAdded := options.addEmptyCommit || options.helmChartOptions.chartName != "" 3105 if options.addEmptyCommit { 3106 cmd = exec.Command("git", "commit", "-m", "Initial commit", "--allow-empty") 3107 cmd.Dir = options.path 3108 require.NoError(t, cmd.Run()) 3109 } else if options.helmChartOptions.chartName != "" { 3110 addHelmToGitRepo(t, options) 3111 } 3112 3113 if commitAdded { 3114 var revB bytes.Buffer 3115 cmd = exec.Command("git", "rev-parse", "HEAD", options.path) 3116 cmd.Dir = options.path 3117 cmd.Stdout = &revB 3118 require.NoError(t, cmd.Run()) 3119 revision = strings.Split(revB.String(), "\n")[0] 3120 } 3121 return revision 3122 } 3123 3124 func TestInit(t *testing.T) { 3125 dir := t.TempDir() 3126 3127 // service.Init sets permission to 0300. Restore permissions when the test 3128 // finishes so dir can be removed properly. 3129 t.Cleanup(func() { 3130 require.NoError(t, os.Chmod(dir, 0o777)) 3131 }) 3132 3133 repoPath := path.Join(dir, "repo1") 3134 initGitRepo(t, newGitRepoOptions{path: repoPath, remote: "https://github.com/argo-cd/test-repo1", createPath: true, addEmptyCommit: false}) 3135 3136 service := newService(t, ".") 3137 service.rootDir = dir 3138 3139 require.NoError(t, service.Init()) 3140 3141 _, err := os.ReadDir(dir) 3142 require.Error(t, err) 3143 initGitRepo(t, newGitRepoOptions{path: path.Join(dir, "repo2"), remote: "https://github.com/argo-cd/test-repo2", createPath: true, addEmptyCommit: false}) 3144 } 3145 3146 // TestCheckoutRevisionCanGetNonstandardRefs shows that we can fetch a revision that points to a non-standard ref. In 3147 // other words, we haven't regressed and caused this issue again: https://github.com/argoproj/argo-cd/issues/4935 3148 func TestCheckoutRevisionCanGetNonstandardRefs(t *testing.T) { 3149 rootPath := t.TempDir() 3150 3151 sourceRepoPath, err := os.MkdirTemp(rootPath, "") 3152 require.NoError(t, err) 3153 3154 // Create a repo such that one commit is on a non-standard ref _and nowhere else_. This is meant to simulate, for 3155 // example, a GitHub ref for a pull into one repo from a fork of that repo. 3156 runGit(t, sourceRepoPath, "init") 3157 runGit(t, sourceRepoPath, "checkout", "-b", "main") // make sure there's a main branch to switch back to 3158 runGit(t, sourceRepoPath, "commit", "-m", "empty", "--allow-empty") 3159 runGit(t, sourceRepoPath, "checkout", "-b", "branch") 3160 runGit(t, sourceRepoPath, "commit", "-m", "empty", "--allow-empty") 3161 sha := runGit(t, sourceRepoPath, "rev-parse", "HEAD") 3162 runGit(t, sourceRepoPath, "update-ref", "refs/pull/123/head", strings.TrimSuffix(sha, "\n")) 3163 runGit(t, sourceRepoPath, "checkout", "main") 3164 runGit(t, sourceRepoPath, "branch", "-D", "branch") 3165 3166 destRepoPath, err := os.MkdirTemp(rootPath, "") 3167 require.NoError(t, err) 3168 3169 gitClient, err := git.NewClientExt("file://"+sourceRepoPath, destRepoPath, &git.NopCreds{}, true, false, "", "") 3170 require.NoError(t, err) 3171 3172 pullSha, err := gitClient.LsRemote("refs/pull/123/head") 3173 require.NoError(t, err) 3174 3175 err = checkoutRevision(gitClient, "does-not-exist", false) 3176 require.Error(t, err) 3177 3178 err = checkoutRevision(gitClient, pullSha, false) 3179 require.NoError(t, err) 3180 } 3181 3182 func TestCheckoutRevisionPresentSkipFetch(t *testing.T) { 3183 revision := "0123456789012345678901234567890123456789" 3184 3185 gitClient := &gitmocks.Client{} 3186 gitClient.On("Init").Return(nil) 3187 gitClient.On("IsRevisionPresent", revision).Return(true) 3188 gitClient.On("Checkout", revision, mock.Anything).Return("", nil) 3189 3190 err := checkoutRevision(gitClient, revision, false) 3191 require.NoError(t, err) 3192 } 3193 3194 func TestCheckoutRevisionNotPresentCallFetch(t *testing.T) { 3195 revision := "0123456789012345678901234567890123456789" 3196 3197 gitClient := &gitmocks.Client{} 3198 gitClient.On("Init").Return(nil) 3199 gitClient.On("IsRevisionPresent", revision).Return(false) 3200 gitClient.On("Fetch", "").Return(nil) 3201 gitClient.On("Checkout", revision, mock.Anything).Return("", nil) 3202 3203 err := checkoutRevision(gitClient, revision, false) 3204 require.NoError(t, err) 3205 } 3206 3207 func TestFetch(t *testing.T) { 3208 revision1 := "0123456789012345678901234567890123456789" 3209 revision2 := "abcdefabcdefabcdefabcdefabcdefabcdefabcd" 3210 3211 gitClient := &gitmocks.Client{} 3212 gitClient.On("Init").Return(nil) 3213 gitClient.On("IsRevisionPresent", revision1).Once().Return(true) 3214 gitClient.On("IsRevisionPresent", revision2).Once().Return(false) 3215 gitClient.On("Fetch", "").Return(nil) 3216 gitClient.On("IsRevisionPresent", revision1).Once().Return(true) 3217 gitClient.On("IsRevisionPresent", revision2).Once().Return(true) 3218 3219 err := fetch(gitClient, []string{revision1, revision2}) 3220 require.NoError(t, err) 3221 } 3222 3223 // TestFetchRevisionCanGetNonstandardRefs shows that we can fetch a revision that points to a non-standard ref. In 3224 func TestFetchRevisionCanGetNonstandardRefs(t *testing.T) { 3225 rootPath := t.TempDir() 3226 3227 sourceRepoPath, err := os.MkdirTemp(rootPath, "") 3228 require.NoError(t, err) 3229 3230 // Create a repo such that one commit is on a non-standard ref _and nowhere else_. This is meant to simulate, for 3231 // example, a GitHub ref for a pull into one repo from a fork of that repo. 3232 runGit(t, sourceRepoPath, "init") 3233 runGit(t, sourceRepoPath, "checkout", "-b", "main") // make sure there's a main branch to switch back to 3234 runGit(t, sourceRepoPath, "commit", "-m", "empty", "--allow-empty") 3235 runGit(t, sourceRepoPath, "checkout", "-b", "branch") 3236 runGit(t, sourceRepoPath, "commit", "-m", "empty", "--allow-empty") 3237 sha := runGit(t, sourceRepoPath, "rev-parse", "HEAD") 3238 runGit(t, sourceRepoPath, "update-ref", "refs/pull/123/head", strings.TrimSuffix(sha, "\n")) 3239 runGit(t, sourceRepoPath, "checkout", "main") 3240 runGit(t, sourceRepoPath, "branch", "-D", "branch") 3241 3242 destRepoPath, err := os.MkdirTemp(rootPath, "") 3243 require.NoError(t, err) 3244 3245 gitClient, err := git.NewClientExt("file://"+sourceRepoPath, destRepoPath, &git.NopCreds{}, true, false, "", "") 3246 require.NoError(t, err) 3247 3248 // We should initialize repository 3249 err = gitClient.Init() 3250 require.NoError(t, err) 3251 3252 pullSha, err := gitClient.LsRemote("refs/pull/123/head") 3253 require.NoError(t, err) 3254 3255 err = fetch(gitClient, []string{"does-not-exist"}) 3256 require.Error(t, err) 3257 3258 err = fetch(gitClient, []string{pullSha}) 3259 require.NoError(t, err) 3260 } 3261 3262 // runGit runs a git command in the given working directory. If the command succeeds, it returns the combined standard 3263 // and error output. If it fails, it stops the test with a failure message. 3264 func runGit(t *testing.T, workDir string, args ...string) string { 3265 t.Helper() 3266 cmd := exec.Command("git", args...) 3267 cmd.Dir = workDir 3268 out, err := cmd.CombinedOutput() 3269 stringOut := string(out) 3270 require.NoError(t, err, stringOut) 3271 return stringOut 3272 } 3273 3274 func Test_walkHelmValueFilesInPath(t *testing.T) { 3275 t.Run("does not exist", func(t *testing.T) { 3276 var files []string 3277 root := "/obviously/does/not/exist" 3278 err := filepath.Walk(root, walkHelmValueFilesInPath(root, &files)) 3279 require.Error(t, err) 3280 assert.Empty(t, files) 3281 }) 3282 t.Run("values files", func(t *testing.T) { 3283 var files []string 3284 root := "./testdata/values-files" 3285 err := filepath.Walk(root, walkHelmValueFilesInPath(root, &files)) 3286 require.NoError(t, err) 3287 assert.Len(t, files, 5) 3288 }) 3289 t.Run("unrelated root", func(t *testing.T) { 3290 var files []string 3291 root := "./testdata/values-files" 3292 unrelatedRoot := "/different/root/path" 3293 err := filepath.Walk(root, walkHelmValueFilesInPath(unrelatedRoot, &files)) 3294 require.Error(t, err) 3295 }) 3296 } 3297 3298 func Test_populateHelmAppDetails(t *testing.T) { 3299 emptyTempPaths := utilio.NewRandomizedTempPaths(t.TempDir()) 3300 res := apiclient.RepoAppDetailsResponse{} 3301 q := apiclient.RepoServerAppDetailsQuery{ 3302 Repo: &v1alpha1.Repository{}, 3303 Source: &v1alpha1.ApplicationSource{ 3304 Helm: &v1alpha1.ApplicationSourceHelm{ValueFiles: []string{"exclude.yaml", "has-the-word-values.yaml"}}, 3305 }, 3306 } 3307 appPath, err := filepath.Abs("./testdata/values-files/") 3308 require.NoError(t, err) 3309 err = populateHelmAppDetails(&res, appPath, appPath, &q, emptyTempPaths) 3310 require.NoError(t, err) 3311 assert.Len(t, res.Helm.Parameters, 3) 3312 assert.Len(t, res.Helm.ValueFiles, 5) 3313 } 3314 3315 func Test_populateHelmAppDetails_values_symlinks(t *testing.T) { 3316 emptyTempPaths := utilio.NewRandomizedTempPaths(t.TempDir()) 3317 t.Run("inbound", func(t *testing.T) { 3318 res := apiclient.RepoAppDetailsResponse{} 3319 q := apiclient.RepoServerAppDetailsQuery{Repo: &v1alpha1.Repository{}, Source: &v1alpha1.ApplicationSource{}} 3320 err := populateHelmAppDetails(&res, "./testdata/in-bounds-values-file-link/", "./testdata/in-bounds-values-file-link/", &q, emptyTempPaths) 3321 require.NoError(t, err) 3322 assert.NotEmpty(t, res.Helm.Values) 3323 assert.NotEmpty(t, res.Helm.Parameters) 3324 }) 3325 3326 t.Run("out of bounds", func(t *testing.T) { 3327 res := apiclient.RepoAppDetailsResponse{} 3328 q := apiclient.RepoServerAppDetailsQuery{Repo: &v1alpha1.Repository{}, Source: &v1alpha1.ApplicationSource{}} 3329 err := populateHelmAppDetails(&res, "./testdata/out-of-bounds-values-file-link/", "./testdata/out-of-bounds-values-file-link/", &q, emptyTempPaths) 3330 require.NoError(t, err) 3331 assert.Empty(t, res.Helm.Values) 3332 assert.Empty(t, res.Helm.Parameters) 3333 }) 3334 } 3335 3336 func TestGetHelmRepos_OCIHelmDependenciesWithHelmRepo(t *testing.T) { 3337 q := apiclient.ManifestRequest{Repos: []*v1alpha1.Repository{}, HelmRepoCreds: []*v1alpha1.RepoCreds{ 3338 {URL: "example.com", Username: "test", Password: "test", EnableOCI: true}, 3339 }} 3340 3341 helmRepos, err := getHelmRepos("./testdata/oci-dependencies", q.Repos, q.HelmRepoCreds) 3342 require.NoError(t, err) 3343 3344 assert.Len(t, helmRepos, 1) 3345 assert.Equal(t, "test", helmRepos[0].GetUsername()) 3346 assert.True(t, helmRepos[0].EnableOci) 3347 assert.Equal(t, "example.com/myrepo", helmRepos[0].Repo) 3348 } 3349 3350 func TestGetHelmRepos_OCIHelmDependenciesWithRepo(t *testing.T) { 3351 q := apiclient.ManifestRequest{Repos: []*v1alpha1.Repository{{Repo: "example.com", Username: "test", Password: "test", EnableOCI: true}}, HelmRepoCreds: []*v1alpha1.RepoCreds{}} 3352 3353 helmRepos, err := getHelmRepos("./testdata/oci-dependencies", q.Repos, q.HelmRepoCreds) 3354 require.NoError(t, err) 3355 3356 assert.Len(t, helmRepos, 1) 3357 assert.Equal(t, "test", helmRepos[0].GetUsername()) 3358 assert.True(t, helmRepos[0].EnableOci) 3359 assert.Equal(t, "example.com/myrepo", helmRepos[0].Repo) 3360 } 3361 3362 func TestGetHelmRepos_OCIDependenciesWithHelmRepo(t *testing.T) { 3363 q := apiclient.ManifestRequest{Repos: []*v1alpha1.Repository{}, HelmRepoCreds: []*v1alpha1.RepoCreds{ 3364 {URL: "oci://example.com", Username: "test", Password: "test", Type: "oci"}, 3365 }} 3366 3367 helmRepos, err := getHelmRepos("./testdata/oci-dependencies", q.Repos, q.HelmRepoCreds) 3368 require.NoError(t, err) 3369 3370 assert.Len(t, helmRepos, 1) 3371 assert.Equal(t, "test", helmRepos[0].GetUsername()) 3372 assert.True(t, helmRepos[0].EnableOci) 3373 assert.Equal(t, "example.com/myrepo", helmRepos[0].Repo) 3374 } 3375 3376 func TestGetHelmRepos_OCIDependenciesWithRepo(t *testing.T) { 3377 q := apiclient.ManifestRequest{Repos: []*v1alpha1.Repository{{Repo: "oci://example.com", Username: "test", Password: "test", Type: "oci"}}, HelmRepoCreds: []*v1alpha1.RepoCreds{}} 3378 3379 helmRepos, err := getHelmRepos("./testdata/oci-dependencies", q.Repos, q.HelmRepoCreds) 3380 require.NoError(t, err) 3381 3382 assert.Len(t, helmRepos, 1) 3383 assert.Equal(t, "test", helmRepos[0].GetUsername()) 3384 assert.True(t, helmRepos[0].EnableOci) 3385 assert.Equal(t, "example.com/myrepo", helmRepos[0].Repo) 3386 } 3387 3388 func TestGetHelmRepo_NamedRepos(t *testing.T) { 3389 q := apiclient.ManifestRequest{ 3390 Repos: []*v1alpha1.Repository{{ 3391 Name: "custom-repo", 3392 Repo: "https://example.com", 3393 Username: "test", 3394 }}, 3395 } 3396 3397 helmRepos, err := getHelmRepos("./testdata/helm-with-dependencies", q.Repos, q.HelmRepoCreds) 3398 require.NoError(t, err) 3399 3400 assert.Len(t, helmRepos, 1) 3401 assert.Equal(t, "test", helmRepos[0].GetUsername()) 3402 assert.Equal(t, "https://example.com", helmRepos[0].Repo) 3403 } 3404 3405 func TestGetHelmRepo_NamedReposAlias(t *testing.T) { 3406 q := apiclient.ManifestRequest{ 3407 Repos: []*v1alpha1.Repository{{ 3408 Name: "custom-repo-alias", 3409 Repo: "https://example.com", 3410 Username: "test-alias", 3411 }}, 3412 } 3413 3414 helmRepos, err := getHelmRepos("./testdata/helm-with-dependencies-alias", q.Repos, q.HelmRepoCreds) 3415 require.NoError(t, err) 3416 3417 assert.Len(t, helmRepos, 1) 3418 assert.Equal(t, "test-alias", helmRepos[0].GetUsername()) 3419 assert.Equal(t, "https://example.com", helmRepos[0].Repo) 3420 } 3421 3422 func Test_getResolvedValueFiles(t *testing.T) { 3423 t.Parallel() 3424 3425 tempDir := t.TempDir() 3426 paths := utilio.NewRandomizedTempPaths(tempDir) 3427 3428 paths.Add(git.NormalizeGitURL("https://github.com/org/repo1"), path.Join(tempDir, "repo1")) 3429 3430 testCases := []struct { 3431 name string 3432 rawPath string 3433 env *v1alpha1.Env 3434 refSources map[string]*v1alpha1.RefTarget 3435 expectedPath string 3436 expectedErr bool 3437 }{ 3438 { 3439 name: "simple path", 3440 rawPath: "values.yaml", 3441 env: &v1alpha1.Env{}, 3442 refSources: map[string]*v1alpha1.RefTarget{}, 3443 expectedPath: path.Join(tempDir, "main-repo", "values.yaml"), 3444 }, 3445 { 3446 name: "simple ref", 3447 rawPath: "$ref/values.yaml", 3448 env: &v1alpha1.Env{}, 3449 refSources: map[string]*v1alpha1.RefTarget{ 3450 "$ref": { 3451 Repo: v1alpha1.Repository{ 3452 Repo: "https://github.com/org/repo1", 3453 }, 3454 }, 3455 }, 3456 expectedPath: path.Join(tempDir, "repo1", "values.yaml"), 3457 }, 3458 { 3459 name: "only ref", 3460 rawPath: "$ref", 3461 env: &v1alpha1.Env{}, 3462 refSources: map[string]*v1alpha1.RefTarget{ 3463 "$ref": { 3464 Repo: v1alpha1.Repository{ 3465 Repo: "https://github.com/org/repo1", 3466 }, 3467 }, 3468 }, 3469 expectedErr: true, 3470 }, 3471 { 3472 name: "attempted traversal", 3473 rawPath: "$ref/../values.yaml", 3474 env: &v1alpha1.Env{}, 3475 refSources: map[string]*v1alpha1.RefTarget{ 3476 "$ref": { 3477 Repo: v1alpha1.Repository{ 3478 Repo: "https://github.com/org/repo1", 3479 }, 3480 }, 3481 }, 3482 expectedErr: true, 3483 }, 3484 { 3485 // Since $ref doesn't resolve to a ref target, we assume it's an env var. Since the env var isn't specified, 3486 // it's replaced with an empty string. This is necessary for backwards compatibility with behavior before 3487 // ref targets were introduced. 3488 name: "ref doesn't exist", 3489 rawPath: "$ref/values.yaml", 3490 env: &v1alpha1.Env{}, 3491 refSources: map[string]*v1alpha1.RefTarget{}, 3492 expectedPath: path.Join(tempDir, "main-repo", "values.yaml"), 3493 }, 3494 { 3495 name: "repo doesn't exist", 3496 rawPath: "$ref/values.yaml", 3497 env: &v1alpha1.Env{}, 3498 refSources: map[string]*v1alpha1.RefTarget{ 3499 "$ref": { 3500 Repo: v1alpha1.Repository{ 3501 Repo: "https://github.com/org/repo2", 3502 }, 3503 }, 3504 }, 3505 expectedErr: true, 3506 }, 3507 { 3508 name: "env var is resolved", 3509 rawPath: "$ref/$APP_PATH/values.yaml", 3510 env: &v1alpha1.Env{ 3511 &v1alpha1.EnvEntry{ 3512 Name: "APP_PATH", 3513 Value: "app-path", 3514 }, 3515 }, 3516 refSources: map[string]*v1alpha1.RefTarget{ 3517 "$ref": { 3518 Repo: v1alpha1.Repository{ 3519 Repo: "https://github.com/org/repo1", 3520 }, 3521 }, 3522 }, 3523 expectedPath: path.Join(tempDir, "repo1", "app-path", "values.yaml"), 3524 }, 3525 { 3526 name: "traversal in env var is blocked", 3527 rawPath: "$ref/$APP_PATH/values.yaml", 3528 env: &v1alpha1.Env{ 3529 &v1alpha1.EnvEntry{ 3530 Name: "APP_PATH", 3531 Value: "..", 3532 }, 3533 }, 3534 refSources: map[string]*v1alpha1.RefTarget{ 3535 "$ref": { 3536 Repo: v1alpha1.Repository{ 3537 Repo: "https://github.com/org/repo1", 3538 }, 3539 }, 3540 }, 3541 expectedErr: true, 3542 }, 3543 { 3544 name: "env var prefix", 3545 rawPath: "$APP_PATH/values.yaml", 3546 env: &v1alpha1.Env{ 3547 &v1alpha1.EnvEntry{ 3548 Name: "APP_PATH", 3549 Value: "app-path", 3550 }, 3551 }, 3552 refSources: map[string]*v1alpha1.RefTarget{}, 3553 expectedPath: path.Join(tempDir, "main-repo", "app-path", "values.yaml"), 3554 }, 3555 { 3556 name: "unresolved env var", 3557 rawPath: "$APP_PATH/values.yaml", 3558 env: &v1alpha1.Env{}, 3559 refSources: map[string]*v1alpha1.RefTarget{}, 3560 expectedPath: path.Join(tempDir, "main-repo", "values.yaml"), 3561 }, 3562 } 3563 3564 for _, tc := range testCases { 3565 tcc := tc 3566 t.Run(tcc.name, func(t *testing.T) { 3567 t.Parallel() 3568 resolvedPaths, err := getResolvedValueFiles(path.Join(tempDir, "main-repo"), path.Join(tempDir, "main-repo"), tcc.env, []string{}, []string{tcc.rawPath}, tcc.refSources, paths, false) 3569 if !tcc.expectedErr { 3570 require.NoError(t, err) 3571 require.Len(t, resolvedPaths, 1) 3572 assert.Equal(t, tcc.expectedPath, string(resolvedPaths[0])) 3573 } else { 3574 require.Error(t, err) 3575 assert.Empty(t, resolvedPaths) 3576 } 3577 }) 3578 } 3579 } 3580 3581 func TestErrorGetGitDirectories(t *testing.T) { 3582 // test not using the cache 3583 root := "./testdata/git-files-dirs" 3584 3585 type fields struct { 3586 service *Service 3587 } 3588 type args struct { 3589 ctx context.Context 3590 request *apiclient.GitDirectoriesRequest 3591 } 3592 tests := []struct { 3593 name string 3594 fields fields 3595 args args 3596 want *apiclient.GitDirectoriesResponse 3597 wantErr assert.ErrorAssertionFunc 3598 }{ 3599 {name: "InvalidRepo", fields: fields{service: newService(t, ".")}, args: args{ 3600 ctx: t.Context(), 3601 request: &apiclient.GitDirectoriesRequest{ 3602 Repo: nil, 3603 SubmoduleEnabled: false, 3604 Revision: "HEAD", 3605 }, 3606 }, want: nil, wantErr: assert.Error}, 3607 {name: "InvalidResolveRevision", fields: fields{service: func() *Service { 3608 s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) { 3609 gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil) 3610 gitClient.On("LsRemote", mock.Anything).Return("", errors.New("ah error")) 3611 gitClient.On("Root").Return(root) 3612 paths.On("GetPath", mock.Anything).Return(".", nil) 3613 paths.On("GetPathIfExists", mock.Anything).Return(".", nil) 3614 }, ".") 3615 return s 3616 }()}, args: args{ 3617 ctx: t.Context(), 3618 request: &apiclient.GitDirectoriesRequest{ 3619 Repo: &v1alpha1.Repository{Repo: "not-a-valid-url"}, 3620 SubmoduleEnabled: false, 3621 Revision: "sadfsadf", 3622 }, 3623 }, want: nil, wantErr: assert.Error}, 3624 {name: "ErrorVerifyCommit", fields: fields{service: func() *Service { 3625 s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) { 3626 gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil) 3627 gitClient.On("LsRemote", mock.Anything).Return("", errors.New("ah error")) 3628 gitClient.On("VerifyCommitSignature", mock.Anything).Return("", fmt.Errorf("revision %s is not signed", "sadfsadf")) 3629 gitClient.On("Root").Return(root) 3630 paths.On("GetPath", mock.Anything).Return(".", nil) 3631 paths.On("GetPathIfExists", mock.Anything).Return(".", nil) 3632 }, ".") 3633 return s 3634 }()}, args: args{ 3635 ctx: t.Context(), 3636 request: &apiclient.GitDirectoriesRequest{ 3637 Repo: &v1alpha1.Repository{Repo: "not-a-valid-url"}, 3638 SubmoduleEnabled: false, 3639 Revision: "sadfsadf", 3640 VerifyCommit: true, 3641 }, 3642 }, want: nil, wantErr: assert.Error}, 3643 } 3644 for _, tt := range tests { 3645 t.Run(tt.name, func(t *testing.T) { 3646 s := tt.fields.service 3647 got, err := s.GetGitDirectories(tt.args.ctx, tt.args.request) 3648 if !tt.wantErr(t, err, fmt.Sprintf("GetGitDirectories(%v, %v)", tt.args.ctx, tt.args.request)) { 3649 return 3650 } 3651 assert.Equalf(t, tt.want, got, "GetGitDirectories(%v, %v)", tt.args.ctx, tt.args.request) 3652 }) 3653 } 3654 } 3655 3656 func TestGetGitDirectories(t *testing.T) { 3657 // test not using the cache 3658 root := "./testdata/git-files-dirs" 3659 s, _, cacheMocks := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) { 3660 gitClient.On("Init").Return(nil) 3661 gitClient.On("IsRevisionPresent", mock.Anything).Return(false) 3662 gitClient.On("Fetch", mock.Anything).Return(nil) 3663 gitClient.On("Checkout", mock.Anything, mock.Anything).Once().Return("", nil) 3664 gitClient.On("LsRemote", "HEAD").Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil) 3665 gitClient.On("Root").Return(root) 3666 paths.On("GetPath", mock.Anything).Return(root, nil) 3667 paths.On("GetPathIfExists", mock.Anything).Return(root, nil) 3668 }, root) 3669 dirRequest := &apiclient.GitDirectoriesRequest{ 3670 Repo: &v1alpha1.Repository{Repo: "a-url.com"}, 3671 SubmoduleEnabled: false, 3672 Revision: "HEAD", 3673 } 3674 directories, err := s.GetGitDirectories(t.Context(), dirRequest) 3675 require.NoError(t, err) 3676 assert.ElementsMatch(t, directories.GetPaths(), []string{"app", "app/bar", "app/foo/bar", "somedir", "app/foo"}) 3677 3678 // do the same request again to use the cache 3679 // we only allow CheckOut to be called once in the mock 3680 directories, err = s.GetGitDirectories(t.Context(), dirRequest) 3681 require.NoError(t, err) 3682 assert.ElementsMatch(t, []string{"app", "app/bar", "app/foo/bar", "somedir", "app/foo"}, directories.GetPaths()) 3683 cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{ 3684 ExternalSets: 1, 3685 ExternalGets: 2, 3686 }) 3687 } 3688 3689 func TestGetGitDirectoriesWithHiddenDirSupported(t *testing.T) { 3690 // test not using the cache 3691 root := "./testdata/git-files-dirs" 3692 s, _, cacheMocks := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) { 3693 gitClient.On("Init").Return(nil) 3694 gitClient.On("IsRevisionPresent", mock.Anything).Return(false) 3695 gitClient.On("Fetch", mock.Anything).Return(nil) 3696 gitClient.On("Checkout", mock.Anything, mock.Anything).Once().Return("", nil) 3697 gitClient.On("LsRemote", "HEAD").Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil) 3698 gitClient.On("Root").Return(root) 3699 paths.On("GetPath", mock.Anything).Return(root, nil) 3700 paths.On("GetPathIfExists", mock.Anything).Return(root, nil) 3701 }, root) 3702 s.initConstants.IncludeHiddenDirectories = true 3703 dirRequest := &apiclient.GitDirectoriesRequest{ 3704 Repo: &v1alpha1.Repository{Repo: "a-url.com"}, 3705 SubmoduleEnabled: false, 3706 Revision: "HEAD", 3707 } 3708 directories, err := s.GetGitDirectories(t.Context(), dirRequest) 3709 require.NoError(t, err) 3710 assert.ElementsMatch(t, directories.GetPaths(), []string{"app", "app/bar", "app/foo/bar", "somedir", "app/foo", "app/bar/.hidden"}) 3711 3712 // do the same request again to use the cache 3713 // we only allow CheckOut to be called once in the mock 3714 directories, err = s.GetGitDirectories(t.Context(), dirRequest) 3715 require.NoError(t, err) 3716 assert.ElementsMatch(t, []string{"app", "app/bar", "app/foo/bar", "somedir", "app/foo", "app/bar/.hidden"}, directories.GetPaths()) 3717 cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{ 3718 ExternalSets: 1, 3719 ExternalGets: 2, 3720 }) 3721 } 3722 3723 func TestErrorGetGitFiles(t *testing.T) { 3724 // test not using the cache 3725 root := "" 3726 3727 type fields struct { 3728 service *Service 3729 } 3730 type args struct { 3731 ctx context.Context 3732 request *apiclient.GitFilesRequest 3733 } 3734 tests := []struct { 3735 name string 3736 fields fields 3737 args args 3738 want *apiclient.GitFilesResponse 3739 wantErr assert.ErrorAssertionFunc 3740 }{ 3741 {name: "InvalidRepo", fields: fields{service: newService(t, ".")}, args: args{ 3742 ctx: t.Context(), 3743 request: &apiclient.GitFilesRequest{ 3744 Repo: nil, 3745 SubmoduleEnabled: false, 3746 Revision: "HEAD", 3747 }, 3748 }, want: nil, wantErr: assert.Error}, 3749 {name: "InvalidResolveRevision", fields: fields{service: func() *Service { 3750 s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) { 3751 gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil) 3752 gitClient.On("LsRemote", mock.Anything).Return("", errors.New("ah error")) 3753 gitClient.On("Root").Return(root) 3754 paths.On("GetPath", mock.Anything).Return(".", nil) 3755 paths.On("GetPathIfExists", mock.Anything).Return(".", nil) 3756 }, ".") 3757 return s 3758 }()}, args: args{ 3759 ctx: t.Context(), 3760 request: &apiclient.GitFilesRequest{ 3761 Repo: &v1alpha1.Repository{Repo: "not-a-valid-url"}, 3762 SubmoduleEnabled: false, 3763 Revision: "sadfsadf", 3764 }, 3765 }, want: nil, wantErr: assert.Error}, 3766 } 3767 for _, tt := range tests { 3768 t.Run(tt.name, func(t *testing.T) { 3769 s := tt.fields.service 3770 got, err := s.GetGitFiles(tt.args.ctx, tt.args.request) 3771 if !tt.wantErr(t, err, fmt.Sprintf("GetGitFiles(%v, %v)", tt.args.ctx, tt.args.request)) { 3772 return 3773 } 3774 assert.Equalf(t, tt.want, got, "GetGitFiles(%v, %v)", tt.args.ctx, tt.args.request) 3775 }) 3776 } 3777 } 3778 3779 func TestGetGitFiles(t *testing.T) { 3780 // test not using the cache 3781 files := []string{ 3782 "./testdata/git-files-dirs/somedir/config.yaml", 3783 "./testdata/git-files-dirs/config.yaml", "./testdata/git-files-dirs/config.yaml", "./testdata/git-files-dirs/app/foo/bar/config.yaml", 3784 } 3785 root := "" 3786 s, _, cacheMocks := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) { 3787 gitClient.On("Init").Return(nil) 3788 gitClient.On("IsRevisionPresent", mock.Anything).Return(false) 3789 gitClient.On("Fetch", mock.Anything).Return(nil) 3790 gitClient.On("Checkout", mock.Anything, mock.Anything).Once().Return("", nil) 3791 gitClient.On("LsRemote", "HEAD").Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil) 3792 gitClient.On("Root").Return(root) 3793 gitClient.On("LsFiles", mock.Anything, mock.Anything).Once().Return(files, nil) 3794 paths.On("GetPath", mock.Anything).Return(root, nil) 3795 paths.On("GetPathIfExists", mock.Anything).Return(root, nil) 3796 }, root) 3797 filesRequest := &apiclient.GitFilesRequest{ 3798 Repo: &v1alpha1.Repository{Repo: "a-url.com"}, 3799 SubmoduleEnabled: false, 3800 Revision: "HEAD", 3801 } 3802 3803 // expected map 3804 expected := make(map[string][]byte) 3805 for _, filePath := range files { 3806 fileContents, err := os.ReadFile(filePath) 3807 require.NoError(t, err) 3808 expected[filePath] = fileContents 3809 } 3810 3811 fileResponse, err := s.GetGitFiles(t.Context(), filesRequest) 3812 require.NoError(t, err) 3813 assert.Equal(t, expected, fileResponse.GetMap()) 3814 3815 // do the same request again to use the cache 3816 // we only allow LsFiles to be called once in the mock 3817 fileResponse, err = s.GetGitFiles(t.Context(), filesRequest) 3818 require.NoError(t, err) 3819 assert.Equal(t, expected, fileResponse.GetMap()) 3820 cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{ 3821 ExternalSets: 1, 3822 ExternalGets: 2, 3823 }) 3824 } 3825 3826 func TestErrorUpdateRevisionForPaths(t *testing.T) { 3827 // test not using the cache 3828 root := "" 3829 3830 type fields struct { 3831 service *Service 3832 } 3833 type args struct { 3834 ctx context.Context 3835 request *apiclient.UpdateRevisionForPathsRequest 3836 } 3837 tests := []struct { 3838 name string 3839 fields fields 3840 args args 3841 want *apiclient.UpdateRevisionForPathsResponse 3842 wantErr assert.ErrorAssertionFunc 3843 }{ 3844 {name: "InvalidRepo", fields: fields{service: newService(t, ".")}, args: args{ 3845 ctx: t.Context(), 3846 request: &apiclient.UpdateRevisionForPathsRequest{ 3847 Repo: nil, 3848 Revision: "HEAD", 3849 SyncedRevision: "sadfsadf", 3850 }, 3851 }, want: nil, wantErr: assert.Error}, 3852 {name: "InvalidResolveRevision", fields: fields{service: func() *Service { 3853 s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) { 3854 gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil) 3855 gitClient.On("LsRemote", mock.Anything).Return("", errors.New("ah error")) 3856 gitClient.On("Root").Return(root) 3857 paths.On("GetPath", mock.Anything).Return(".", nil) 3858 paths.On("GetPathIfExists", mock.Anything).Return(".", nil) 3859 }, ".") 3860 return s 3861 }()}, args: args{ 3862 ctx: t.Context(), 3863 request: &apiclient.UpdateRevisionForPathsRequest{ 3864 Repo: &v1alpha1.Repository{Repo: "not-a-valid-url"}, 3865 Revision: "sadfsadf", 3866 SyncedRevision: "HEAD", 3867 Paths: []string{"."}, 3868 }, 3869 }, want: nil, wantErr: assert.Error}, 3870 {name: "InvalidResolveSyncedRevision", fields: fields{service: func() *Service { 3871 s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) { 3872 gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil) 3873 gitClient.On("LsRemote", "HEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil) 3874 gitClient.On("LsRemote", mock.Anything).Return("", errors.New("ah error")) 3875 gitClient.On("Root").Return(root) 3876 paths.On("GetPath", mock.Anything).Return(".", nil) 3877 paths.On("GetPathIfExists", mock.Anything).Return(".", nil) 3878 }, ".") 3879 return s 3880 }()}, args: args{ 3881 ctx: t.Context(), 3882 request: &apiclient.UpdateRevisionForPathsRequest{ 3883 Repo: &v1alpha1.Repository{Repo: "not-a-valid-url"}, 3884 Revision: "HEAD", 3885 SyncedRevision: "sadfsadf", 3886 Paths: []string{"."}, 3887 }, 3888 }, want: nil, wantErr: assert.Error}, 3889 } 3890 for _, tt := range tests { 3891 t.Run(tt.name, func(t *testing.T) { 3892 s := tt.fields.service 3893 got, err := s.UpdateRevisionForPaths(tt.args.ctx, tt.args.request) 3894 if !tt.wantErr(t, err, fmt.Sprintf("UpdateRevisionForPaths(%v, %v)", tt.args.ctx, tt.args.request)) { 3895 return 3896 } 3897 assert.Equalf(t, tt.want, got, "UpdateRevisionForPaths(%v, %v)", tt.args.ctx, tt.args.request) 3898 }) 3899 } 3900 } 3901 3902 func TestUpdateRevisionForPaths(t *testing.T) { 3903 type fields struct { 3904 service *Service 3905 cache *repoCacheMocks 3906 } 3907 type args struct { 3908 ctx context.Context 3909 request *apiclient.UpdateRevisionForPathsRequest 3910 } 3911 type cacheHit struct { 3912 revision string 3913 previousRevision string 3914 } 3915 tests := []struct { 3916 name string 3917 fields fields 3918 args args 3919 want *apiclient.UpdateRevisionForPathsResponse 3920 wantErr assert.ErrorAssertionFunc 3921 cacheHit *cacheHit 3922 }{ 3923 {name: "NoPathAbort", fields: func() fields { 3924 s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, _ *iomocks.TempPaths) { 3925 gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil) 3926 }, ".") 3927 return fields{ 3928 service: s, 3929 cache: c, 3930 } 3931 }(), args: args{ 3932 ctx: t.Context(), 3933 request: &apiclient.UpdateRevisionForPathsRequest{ 3934 Repo: &v1alpha1.Repository{Repo: "a-url.com"}, 3935 Paths: []string{}, 3936 }, 3937 }, want: &apiclient.UpdateRevisionForPathsResponse{}, wantErr: assert.NoError}, 3938 {name: "SameResolvedRevisionAbort", fields: func() fields { 3939 s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) { 3940 gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil) 3941 gitClient.On("LsRemote", "HEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil) 3942 gitClient.On("LsRemote", "SYNCEDHEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil) 3943 paths.On("GetPath", mock.Anything).Return(".", nil) 3944 paths.On("GetPathIfExists", mock.Anything).Return(".", nil) 3945 }, ".") 3946 return fields{ 3947 service: s, 3948 cache: c, 3949 } 3950 }(), args: args{ 3951 ctx: t.Context(), 3952 request: &apiclient.UpdateRevisionForPathsRequest{ 3953 Repo: &v1alpha1.Repository{Repo: "a-url.com"}, 3954 Revision: "HEAD", 3955 SyncedRevision: "SYNCEDHEAD", 3956 Paths: []string{"."}, 3957 }, 3958 }, want: &apiclient.UpdateRevisionForPathsResponse{ 3959 Revision: "632039659e542ed7de0c170a4fcc1c571b288fc0", 3960 }, wantErr: assert.NoError}, 3961 {name: "ChangedFilesDoNothing", fields: func() fields { 3962 s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) { 3963 gitClient.On("Init").Return(nil) 3964 gitClient.On("Fetch", mock.Anything).Once().Return(nil) 3965 gitClient.On("IsRevisionPresent", "632039659e542ed7de0c170a4fcc1c571b288fc0").Once().Return(false) 3966 gitClient.On("Checkout", "632039659e542ed7de0c170a4fcc1c571b288fc0", mock.Anything).Once().Return("", nil) 3967 // fetch 3968 gitClient.On("IsRevisionPresent", "1e67a504d03def3a6a1125d934cb511680f72555").Once().Return(false) 3969 gitClient.On("Fetch", mock.Anything).Once().Return(nil) 3970 gitClient.On("IsRevisionPresent", "1e67a504d03def3a6a1125d934cb511680f72555").Once().Return(true) 3971 gitClient.On("LsRemote", "HEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil) 3972 gitClient.On("LsRemote", "SYNCEDHEAD").Once().Return("1e67a504d03def3a6a1125d934cb511680f72555", nil) 3973 paths.On("GetPath", mock.Anything).Return(".", nil) 3974 paths.On("GetPathIfExists", mock.Anything).Return(".", nil) 3975 gitClient.On("Root").Return("") 3976 gitClient.On("ChangedFiles", mock.Anything, mock.Anything).Return([]string{"app.yaml"}, nil) 3977 }, ".") 3978 return fields{ 3979 service: s, 3980 cache: c, 3981 } 3982 }(), args: args{ 3983 ctx: t.Context(), 3984 request: &apiclient.UpdateRevisionForPathsRequest{ 3985 Repo: &v1alpha1.Repository{Repo: "a-url.com"}, 3986 Revision: "HEAD", 3987 SyncedRevision: "SYNCEDHEAD", 3988 Paths: []string{"."}, 3989 }, 3990 }, want: &apiclient.UpdateRevisionForPathsResponse{ 3991 Revision: "632039659e542ed7de0c170a4fcc1c571b288fc0", 3992 Changes: true, 3993 }, wantErr: assert.NoError}, 3994 {name: "NoChangesUpdateCache", fields: func() fields { 3995 s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) { 3996 gitClient.On("Init").Return(nil) 3997 gitClient.On("Fetch", mock.Anything).Once().Return(nil) 3998 gitClient.On("IsRevisionPresent", "632039659e542ed7de0c170a4fcc1c571b288fc0").Once().Return(false) 3999 gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil) 4000 gitClient.On("IsRevisionPresent", "1e67a504d03def3a6a1125d934cb511680f72555").Once().Return(false) 4001 // fetch 4002 gitClient.On("Fetch", mock.Anything).Once().Return(nil) 4003 gitClient.On("IsRevisionPresent", "1e67a504d03def3a6a1125d934cb511680f72555").Once().Return(true) 4004 gitClient.On("LsRemote", "HEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil) 4005 gitClient.On("LsRemote", "SYNCEDHEAD").Once().Return("1e67a504d03def3a6a1125d934cb511680f72555", nil) 4006 paths.On("GetPath", mock.Anything).Return(".", nil) 4007 paths.On("GetPathIfExists", mock.Anything).Return(".", nil) 4008 gitClient.On("Root").Return("") 4009 gitClient.On("ChangedFiles", mock.Anything, mock.Anything).Return([]string{}, nil) 4010 }, ".") 4011 return fields{ 4012 service: s, 4013 cache: c, 4014 } 4015 }(), args: args{ 4016 ctx: t.Context(), 4017 request: &apiclient.UpdateRevisionForPathsRequest{ 4018 Repo: &v1alpha1.Repository{Repo: "a-url.com"}, 4019 Revision: "HEAD", 4020 SyncedRevision: "SYNCEDHEAD", 4021 Paths: []string{"."}, 4022 4023 AppLabelKey: "app.kubernetes.io/name", 4024 AppName: "no-change-update-cache", 4025 Namespace: "default", 4026 TrackingMethod: "annotation+label", 4027 ApplicationSource: &v1alpha1.ApplicationSource{Path: "."}, 4028 KubeVersion: "v1.16.0", 4029 }, 4030 }, want: &apiclient.UpdateRevisionForPathsResponse{ 4031 Revision: "632039659e542ed7de0c170a4fcc1c571b288fc0", 4032 }, wantErr: assert.NoError, cacheHit: &cacheHit{ 4033 previousRevision: "1e67a504d03def3a6a1125d934cb511680f72555", 4034 revision: "632039659e542ed7de0c170a4fcc1c571b288fc0", 4035 }}, 4036 {name: "NoChangesHelmMultiSourceUpdateCache", fields: func() fields { 4037 s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) { 4038 gitClient.On("Init").Return(nil) 4039 gitClient.On("IsRevisionPresent", "632039659e542ed7de0c170a4fcc1c571b288fc0").Once().Return(false) 4040 gitClient.On("Fetch", mock.Anything).Once().Return(nil) 4041 gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil) 4042 // fetch 4043 gitClient.On("IsRevisionPresent", "1e67a504d03def3a6a1125d934cb511680f72555").Once().Return(true) 4044 gitClient.On("Fetch", mock.Anything).Once().Return(nil) 4045 gitClient.On("LsRemote", "HEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil) 4046 gitClient.On("LsRemote", "SYNCEDHEAD").Once().Return("1e67a504d03def3a6a1125d934cb511680f72555", nil) 4047 paths.On("GetPath", mock.Anything).Return(".", nil) 4048 paths.On("GetPathIfExists", mock.Anything).Return(".", nil) 4049 gitClient.On("Root").Return("") 4050 gitClient.On("ChangedFiles", mock.Anything, mock.Anything).Return([]string{}, nil) 4051 }, ".") 4052 return fields{ 4053 service: s, 4054 cache: c, 4055 } 4056 }(), args: args{ 4057 ctx: t.Context(), 4058 request: &apiclient.UpdateRevisionForPathsRequest{ 4059 Repo: &v1alpha1.Repository{Repo: "a-url.com"}, 4060 Revision: "HEAD", 4061 SyncedRevision: "SYNCEDHEAD", 4062 Paths: []string{"."}, 4063 4064 AppLabelKey: "app.kubernetes.io/name", 4065 AppName: "no-change-update-cache", 4066 Namespace: "default", 4067 TrackingMethod: "annotation+label", 4068 ApplicationSource: &v1alpha1.ApplicationSource{Path: ".", Helm: &v1alpha1.ApplicationSourceHelm{ReleaseName: "test"}}, 4069 KubeVersion: "v1.16.0", 4070 4071 HasMultipleSources: true, 4072 }, 4073 }, want: &apiclient.UpdateRevisionForPathsResponse{ 4074 Revision: "632039659e542ed7de0c170a4fcc1c571b288fc0", 4075 }, wantErr: assert.NoError, cacheHit: &cacheHit{ 4076 previousRevision: "1e67a504d03def3a6a1125d934cb511680f72555", 4077 revision: "632039659e542ed7de0c170a4fcc1c571b288fc0", 4078 }}, 4079 } 4080 for _, tt := range tests { 4081 t.Run(tt.name, func(t *testing.T) { 4082 s := tt.fields.service 4083 cache := tt.fields.cache 4084 4085 if tt.cacheHit != nil { 4086 cache.mockCache.On("Rename", tt.cacheHit.previousRevision, tt.cacheHit.revision, mock.Anything).Return(nil) 4087 } 4088 4089 got, err := s.UpdateRevisionForPaths(tt.args.ctx, tt.args.request) 4090 if !tt.wantErr(t, err, fmt.Sprintf("UpdateRevisionForPaths(%v, %v)", tt.args.ctx, tt.args.request)) { 4091 return 4092 } 4093 assert.Equalf(t, tt.want, got, "UpdateRevisionForPaths(%v, %v)", tt.args.ctx, tt.args.request) 4094 4095 if tt.cacheHit != nil { 4096 cache.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{ 4097 ExternalRenames: 1, 4098 }) 4099 } else { 4100 cache.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{ 4101 ExternalRenames: 0, 4102 }) 4103 } 4104 }) 4105 } 4106 } 4107 4108 func Test_getRepoSanitizerRegex(t *testing.T) { 4109 r := getRepoSanitizerRegex("/tmp/_argocd-repo") 4110 msg := r.ReplaceAllString("error message containing /tmp/_argocd-repo/SENSITIVE and other stuff", "<path to cached source>") 4111 assert.Equal(t, "error message containing <path to cached source> and other stuff", msg) 4112 msg = r.ReplaceAllString("error message containing /tmp/_argocd-repo/SENSITIVE/with/trailing/path and other stuff", "<path to cached source>") 4113 assert.Equal(t, "error message containing <path to cached source>/with/trailing/path and other stuff", msg) 4114 } 4115 4116 func TestGetRefs_CacheWithLockDisabled(t *testing.T) { 4117 // Test that when the lock is disabled the default behavior still works correctly 4118 // Also shows the current issue with the git requests due to cache misses 4119 dir := t.TempDir() 4120 initGitRepo(t, newGitRepoOptions{ 4121 path: dir, 4122 createPath: false, 4123 remote: "", 4124 addEmptyCommit: true, 4125 }) 4126 // Test in-memory and redis 4127 cacheMocks := newCacheMocksWithOpts(1*time.Minute, 1*time.Minute, 0) 4128 t.Cleanup(cacheMocks.mockCache.StopRedisCallback) 4129 var wg sync.WaitGroup 4130 numberOfCallers := 10 4131 for i := 0; i < numberOfCallers; i++ { 4132 wg.Add(1) 4133 go func() { 4134 defer wg.Done() 4135 client, err := git.NewClient("file://"+dir, git.NopCreds{}, true, false, "", "", git.WithCache(cacheMocks.cache, true)) 4136 require.NoError(t, err) 4137 refs, err := client.LsRefs() 4138 require.NoError(t, err) 4139 assert.NotNil(t, refs) 4140 assert.NotEmpty(t, refs.Branches, "Expected branches to be populated") 4141 assert.NotEmpty(t, refs.Branches[0]) 4142 }() 4143 } 4144 wg.Wait() 4145 // Unlock should not have been called 4146 cacheMocks.mockCache.AssertNumberOfCalls(t, "UnlockGitReferences", 0) 4147 // Lock should not have been called 4148 cacheMocks.mockCache.AssertNumberOfCalls(t, "TryLockGitRefCache", 0) 4149 } 4150 4151 func TestGetRefs_CacheDisabled(t *testing.T) { 4152 // Test that default get refs with cache disabled does not call GetOrLockGitReferences 4153 dir := t.TempDir() 4154 initGitRepo(t, newGitRepoOptions{ 4155 path: dir, 4156 createPath: false, 4157 remote: "", 4158 addEmptyCommit: true, 4159 }) 4160 cacheMocks := newCacheMocks() 4161 t.Cleanup(cacheMocks.mockCache.StopRedisCallback) 4162 client, err := git.NewClient("file://"+dir, git.NopCreds{}, true, false, "", "", git.WithCache(cacheMocks.cache, false)) 4163 require.NoError(t, err) 4164 refs, err := client.LsRefs() 4165 require.NoError(t, err) 4166 assert.NotNil(t, refs) 4167 assert.NotEmpty(t, refs.Branches, "Expected branches to be populated") 4168 assert.NotEmpty(t, refs.Branches[0]) 4169 // Unlock should not have been called 4170 cacheMocks.mockCache.AssertNumberOfCalls(t, "UnlockGitReferences", 0) 4171 cacheMocks.mockCache.AssertNumberOfCalls(t, "GetOrLockGitReferences", 0) 4172 } 4173 4174 func TestGetRefs_CacheWithLock(t *testing.T) { 4175 // Test that there is only one call to SetGitReferences for the same repo which is done after the ls-remote 4176 dir := t.TempDir() 4177 initGitRepo(t, newGitRepoOptions{ 4178 path: dir, 4179 createPath: false, 4180 remote: "", 4181 addEmptyCommit: true, 4182 }) 4183 cacheMocks := newCacheMocks() 4184 t.Cleanup(cacheMocks.mockCache.StopRedisCallback) 4185 var wg sync.WaitGroup 4186 numberOfCallers := 10 4187 for i := 0; i < numberOfCallers; i++ { 4188 wg.Add(1) 4189 go func() { 4190 defer wg.Done() 4191 client, err := git.NewClient("file://"+dir, git.NopCreds{}, true, false, "", "", git.WithCache(cacheMocks.cache, true)) 4192 require.NoError(t, err) 4193 refs, err := client.LsRefs() 4194 require.NoError(t, err) 4195 assert.NotNil(t, refs) 4196 assert.NotEmpty(t, refs.Branches, "Expected branches to be populated") 4197 assert.NotEmpty(t, refs.Branches[0]) 4198 }() 4199 } 4200 wg.Wait() 4201 // Unlock should not have been called 4202 cacheMocks.mockCache.AssertNumberOfCalls(t, "UnlockGitReferences", 0) 4203 cacheMocks.mockCache.AssertNumberOfCalls(t, "GetOrLockGitReferences", 0) 4204 } 4205 4206 func TestGetRefs_CacheUnlockedOnUpdateFailed(t *testing.T) { 4207 // Worst case the ttl on the lock expires and the lock is removed 4208 // however if the holder of the lock fails to update the cache the caller should remove the lock 4209 // to allow other callers to attempt to update the cache as quickly as possible 4210 dir := t.TempDir() 4211 initGitRepo(t, newGitRepoOptions{ 4212 path: dir, 4213 createPath: false, 4214 remote: "", 4215 addEmptyCommit: true, 4216 }) 4217 cacheMocks := newCacheMocks() 4218 t.Cleanup(cacheMocks.mockCache.StopRedisCallback) 4219 repoURL := "file://" + dir 4220 client, err := git.NewClient(repoURL, git.NopCreds{}, true, false, "", "", git.WithCache(cacheMocks.cache, true)) 4221 require.NoError(t, err) 4222 refs, err := client.LsRefs() 4223 require.NoError(t, err) 4224 assert.NotNil(t, refs) 4225 assert.NotEmpty(t, refs.Branches, "Expected branches to be populated") 4226 assert.NotEmpty(t, refs.Branches[0]) 4227 var output [][2]string 4228 err = cacheMocks.cacheutilCache.GetItem(fmt.Sprintf("git-refs|%s|%s", repoURL, common.CacheVersion), &output) 4229 require.Error(t, err, "Should be a cache miss") 4230 assert.Empty(t, output, "Expected cache to be empty for key") 4231 cacheMocks.mockCache.AssertNumberOfCalls(t, "UnlockGitReferences", 0) 4232 cacheMocks.mockCache.AssertNumberOfCalls(t, "GetOrLockGitReferences", 0) 4233 } 4234 4235 func TestGetRefs_CacheLockTryLockGitRefCacheError(t *testing.T) { 4236 // Worst case the ttl on the lock expires and the lock is removed 4237 // however if the holder of the lock fails to update the cache the caller should remove the lock 4238 // to allow other callers to attempt to update the cache as quickly as possible 4239 dir := t.TempDir() 4240 initGitRepo(t, newGitRepoOptions{ 4241 path: dir, 4242 createPath: false, 4243 remote: "", 4244 addEmptyCommit: true, 4245 }) 4246 cacheMocks := newCacheMocks() 4247 t.Cleanup(cacheMocks.mockCache.StopRedisCallback) 4248 repoURL := "file://" + dir 4249 // buf := bytes.Buffer{} 4250 // log.SetOutput(&buf) 4251 client, err := git.NewClient(repoURL, git.NopCreds{}, true, false, "", "", git.WithCache(cacheMocks.cache, true)) 4252 require.NoError(t, err) 4253 refs, err := client.LsRefs() 4254 require.NoError(t, err) 4255 assert.NotNil(t, refs) 4256 } 4257 4258 func TestGetRevisionChartDetails(t *testing.T) { 4259 t.Run("Test revision semver", func(t *testing.T) { 4260 root := t.TempDir() 4261 service := newService(t, root) 4262 _, err := service.GetRevisionChartDetails(t.Context(), &apiclient.RepoServerRevisionChartDetailsRequest{ 4263 Repo: &v1alpha1.Repository{ 4264 Repo: "file://" + root, 4265 Name: "test-repo-name", 4266 Type: "helm", 4267 }, 4268 Name: "test-name", 4269 Revision: "test-revision", 4270 }) 4271 assert.ErrorContains(t, err, "invalid revision") 4272 }) 4273 4274 t.Run("Test GetRevisionChartDetails", func(t *testing.T) { 4275 root := t.TempDir() 4276 service := newService(t, root) 4277 repoURL := "file://" + root 4278 err := service.cache.SetRevisionChartDetails(repoURL, "my-chart", "1.1.0", &v1alpha1.ChartDetails{ 4279 Description: "test-description", 4280 Home: "test-home", 4281 Maintainers: []string{"test-maintainer"}, 4282 }) 4283 require.NoError(t, err) 4284 chartDetails, err := service.GetRevisionChartDetails(t.Context(), &apiclient.RepoServerRevisionChartDetailsRequest{ 4285 Repo: &v1alpha1.Repository{ 4286 Repo: "file://" + root, 4287 Name: "test-repo-name", 4288 Type: "helm", 4289 }, 4290 Name: "my-chart", 4291 Revision: "1.1.0", 4292 }) 4293 require.NoError(t, err) 4294 assert.Equal(t, "test-description", chartDetails.Description) 4295 assert.Equal(t, "test-home", chartDetails.Home) 4296 assert.Equal(t, []string{"test-maintainer"}, chartDetails.Maintainers) 4297 }) 4298 } 4299 4300 func TestVerifyCommitSignature(t *testing.T) { 4301 repo := &v1alpha1.Repository{ 4302 Repo: "https://github.com/example/repo.git", 4303 } 4304 4305 t.Run("VerifyCommitSignature with valid signature", func(t *testing.T) { 4306 t.Setenv("ARGOCD_GPG_ENABLED", "true") 4307 mockGitClient := &gitmocks.Client{} 4308 mockGitClient.On("VerifyCommitSignature", mock.Anything, mock.Anything, mock.Anything, mock.Anything). 4309 Return(testSignature, nil) 4310 err := verifyCommitSignature(true, mockGitClient, "abcd1234", repo) 4311 require.NoError(t, err) 4312 }) 4313 4314 t.Run("VerifyCommitSignature with invalid signature", func(t *testing.T) { 4315 t.Setenv("ARGOCD_GPG_ENABLED", "true") 4316 mockGitClient := &gitmocks.Client{} 4317 mockGitClient.On("VerifyCommitSignature", mock.Anything, mock.Anything, mock.Anything, mock.Anything). 4318 Return("", nil) 4319 err := verifyCommitSignature(true, mockGitClient, "abcd1234", repo) 4320 assert.EqualError(t, err, "revision abcd1234 is not signed") 4321 }) 4322 4323 t.Run("VerifyCommitSignature with unknown signature", func(t *testing.T) { 4324 t.Setenv("ARGOCD_GPG_ENABLED", "true") 4325 mockGitClient := &gitmocks.Client{} 4326 mockGitClient.On("VerifyCommitSignature", mock.Anything, mock.Anything, mock.Anything, mock.Anything). 4327 Return("", errors.New("UNKNOWN signature: gpg: Unknown signature from ABCDEFGH")) 4328 err := verifyCommitSignature(true, mockGitClient, "abcd1234", repo) 4329 assert.EqualError(t, err, "UNKNOWN signature: gpg: Unknown signature from ABCDEFGH") 4330 }) 4331 4332 t.Run("VerifyCommitSignature with error verifying signature", func(t *testing.T) { 4333 t.Setenv("ARGOCD_GPG_ENABLED", "true") 4334 mockGitClient := &gitmocks.Client{} 4335 mockGitClient.On("VerifyCommitSignature", mock.Anything, mock.Anything, mock.Anything, mock.Anything). 4336 Return("", errors.New("error verifying signature of commit 'abcd1234' in repo 'https://github.com/example/repo.git': failed to verify signature")) 4337 err := verifyCommitSignature(true, mockGitClient, "abcd1234", repo) 4338 assert.EqualError(t, err, "error verifying signature of commit 'abcd1234' in repo 'https://github.com/example/repo.git': failed to verify signature") 4339 }) 4340 4341 t.Run("VerifyCommitSignature with signature verification disabled", func(t *testing.T) { 4342 t.Setenv("ARGOCD_GPG_ENABLED", "false") 4343 mockGitClient := &gitmocks.Client{} 4344 err := verifyCommitSignature(false, mockGitClient, "abcd1234", repo) 4345 require.NoError(t, err) 4346 }) 4347 } 4348 4349 func Test_GenerateManifests_Commands(t *testing.T) { 4350 t.Run("helm", func(t *testing.T) { 4351 service := newService(t, "testdata/my-chart") 4352 4353 // Fill the manifest request with as many parameters affecting Helm commands as possible. 4354 q := apiclient.ManifestRequest{ 4355 AppName: "test-app", 4356 Namespace: "test-namespace", 4357 KubeVersion: "1.2.3+something", 4358 ApiVersions: []string{"v1/Test", "v2/Test"}, 4359 Repo: &v1alpha1.Repository{}, 4360 ApplicationSource: &v1alpha1.ApplicationSource{ 4361 Path: ".", 4362 Helm: &v1alpha1.ApplicationSourceHelm{ 4363 FileParameters: []v1alpha1.HelmFileParameter{ 4364 { 4365 Name: "test-file-param-name", 4366 Path: "test-file-param.yaml", 4367 }, 4368 }, 4369 Parameters: []v1alpha1.HelmParameter{ 4370 { 4371 Name: "test-param-name", 4372 // Use build env var to test substitution. 4373 Value: "test-value-$ARGOCD_APP_NAME", 4374 ForceString: true, 4375 }, 4376 { 4377 Name: "test-param-bool-name", 4378 // Use build env var to test substitution. 4379 Value: "false", 4380 }, 4381 }, 4382 PassCredentials: true, 4383 SkipCrds: true, 4384 SkipSchemaValidation: false, 4385 ValueFiles: []string{ 4386 "my-chart-values.yaml", 4387 }, 4388 Values: "test: values", 4389 }, 4390 }, 4391 ProjectName: "something", 4392 ProjectSourceRepos: []string{"*"}, 4393 } 4394 4395 res, err := service.GenerateManifest(t.Context(), &q) 4396 4397 require.NoError(t, err) 4398 assert.Equal(t, []string{"helm template . --name-template test-app --namespace test-namespace --kube-version 1.2.3 --set test-param-bool-name=false --set-string test-param-name=test-value-test-app --set-file test-file-param-name=./test-file-param.yaml --values ./my-chart-values.yaml --values <temp file with values from source.helm.values/valuesObject> --api-versions v1/Test --api-versions v2/Test"}, res.Commands) 4399 4400 t.Run("with overrides", func(t *testing.T) { 4401 // These can be set explicitly instead of using inferred values. Make sure the overrides apply. 4402 q.ApplicationSource.Helm.APIVersions = []string{"v3", "v4"} 4403 q.ApplicationSource.Helm.KubeVersion = "5.6.7+something" 4404 q.ApplicationSource.Helm.Namespace = "different-namespace" 4405 q.ApplicationSource.Helm.ReleaseName = "different-release-name" 4406 4407 res, err = service.GenerateManifest(t.Context(), &q) 4408 4409 require.NoError(t, err) 4410 assert.Equal(t, []string{"helm template . --name-template different-release-name --namespace different-namespace --kube-version 5.6.7 --set test-param-bool-name=false --set-string test-param-name=test-value-test-app --set-file test-file-param-name=./test-file-param.yaml --values ./my-chart-values.yaml --values <temp file with values from source.helm.values/valuesObject> --api-versions v3 --api-versions v4"}, res.Commands) 4411 }) 4412 }) 4413 4414 t.Run("helm with dependencies", func(t *testing.T) { 4415 // This test makes sure we still get commands, even if we hit the code path that has to run "helm dependency build." 4416 // We don't actually return the "helm dependency build" command, because we expect that the user is able to read 4417 // the "helm template" and figure out how to fix it. 4418 t.Cleanup(func() { 4419 err := os.Remove("testdata/helm-with-local-dependency/Chart.lock") 4420 require.NoError(t, err) 4421 err = os.RemoveAll("testdata/helm-with-local-dependency/charts") 4422 require.NoError(t, err) 4423 err = os.Remove(path.Join("testdata/helm-with-local-dependency", helmDepUpMarkerFile)) 4424 require.NoError(t, err) 4425 }) 4426 4427 service := newService(t, "testdata/helm-with-local-dependency") 4428 4429 q := apiclient.ManifestRequest{ 4430 AppName: "test-app", 4431 Namespace: "test-namespace", 4432 Repo: &v1alpha1.Repository{}, 4433 ApplicationSource: &v1alpha1.ApplicationSource{ 4434 Path: ".", 4435 }, 4436 ProjectName: "something", 4437 ProjectSourceRepos: []string{"*"}, 4438 } 4439 4440 res, err := service.GenerateManifest(t.Context(), &q) 4441 4442 require.NoError(t, err) 4443 assert.Equal(t, []string{"helm template . --name-template test-app --namespace test-namespace --include-crds"}, res.Commands) 4444 }) 4445 4446 t.Run("kustomize", func(t *testing.T) { 4447 // Write test files to a temp dir, because the test mutates kustomization.yaml in place. 4448 tempDir := t.TempDir() 4449 err := os.WriteFile(path.Join(tempDir, "kustomization.yaml"), []byte(` 4450 resources: 4451 - guestbook.yaml 4452 `), os.FileMode(0o600)) 4453 require.NoError(t, err) 4454 err = os.WriteFile(path.Join(tempDir, "guestbook.yaml"), []byte(` 4455 apiVersion: apps/v1 4456 kind: Deployment 4457 metadata: 4458 name: guestbook-ui 4459 `), os.FileMode(0o400)) 4460 require.NoError(t, err) 4461 err = os.Mkdir(path.Join(tempDir, "component"), os.FileMode(0o700)) 4462 require.NoError(t, err) 4463 err = os.WriteFile(path.Join(tempDir, "component", "kustomization.yaml"), []byte(` 4464 apiVersion: kustomize.config.k8s.io/v1alpha1 4465 kind: Component 4466 images: 4467 - name: old 4468 newName: new 4469 `), os.FileMode(0o400)) 4470 require.NoError(t, err) 4471 4472 service := newService(t, tempDir) 4473 4474 // Fill the manifest request with as many parameters affecting Kustomize commands as possible. 4475 q := apiclient.ManifestRequest{ 4476 AppName: "test-app", 4477 Namespace: "test-namespace", 4478 KubeVersion: "1.2.3+something", 4479 ApiVersions: []string{"v1/Test", "v2/Test"}, 4480 Repo: &v1alpha1.Repository{}, 4481 KustomizeOptions: &v1alpha1.KustomizeOptions{ 4482 BuildOptions: "--enable-helm", 4483 }, 4484 ApplicationSource: &v1alpha1.ApplicationSource{ 4485 Path: ".", 4486 Kustomize: &v1alpha1.ApplicationSourceKustomize{ 4487 APIVersions: []string{"v1", "v2"}, 4488 CommonAnnotations: map[string]string{ 4489 // Use build env var to test substitution. 4490 "test": "annotation-$ARGOCD_APP_NAME", 4491 }, 4492 CommonAnnotationsEnvsubst: true, 4493 CommonLabels: map[string]string{ 4494 "test": "label", 4495 }, 4496 Components: []string{"component"}, 4497 ForceCommonAnnotations: true, 4498 ForceCommonLabels: true, 4499 Images: v1alpha1.KustomizeImages{ 4500 "image=override", 4501 }, 4502 KubeVersion: "5.6.7+something", 4503 LabelWithoutSelector: true, 4504 LabelIncludeTemplates: true, 4505 NamePrefix: "test-prefix", 4506 NameSuffix: "test-suffix", 4507 Namespace: "override-namespace", 4508 Replicas: v1alpha1.KustomizeReplicas{ 4509 { 4510 Name: "guestbook-ui", 4511 Count: intstr.Parse("1337"), 4512 }, 4513 }, 4514 }, 4515 }, 4516 ProjectName: "something", 4517 ProjectSourceRepos: []string{"*"}, 4518 } 4519 4520 res, err := service.GenerateManifest(t.Context(), &q) 4521 require.NoError(t, err) 4522 assert.Equal(t, []string{ 4523 "kustomize edit set nameprefix -- test-prefix", 4524 "kustomize edit set namesuffix -- test-suffix", 4525 "kustomize edit set image image=override", 4526 "kustomize edit set replicas guestbook-ui=1337", 4527 "kustomize edit add label --force --without-selector --include-templates test:label", 4528 "kustomize edit add annotation --force test:annotation-test-app", 4529 "kustomize edit set namespace -- override-namespace", 4530 "kustomize edit add component component", 4531 "kustomize build . --enable-helm --helm-kube-version 5.6.7 --helm-api-versions v1 --helm-api-versions v2", 4532 }, res.Commands) 4533 }) 4534 } 4535 4536 func Test_SkipSchemaValidation(t *testing.T) { 4537 t.Run("helm", func(t *testing.T) { 4538 service := newService(t, "testdata/broken-schema-verification") 4539 4540 q := apiclient.ManifestRequest{ 4541 AppName: "test-app", 4542 Repo: &v1alpha1.Repository{}, 4543 ApplicationSource: &v1alpha1.ApplicationSource{ 4544 Path: ".", 4545 Helm: &v1alpha1.ApplicationSourceHelm{ 4546 SkipSchemaValidation: true, 4547 }, 4548 }, 4549 } 4550 4551 res, err := service.GenerateManifest(t.Context(), &q) 4552 4553 require.NoError(t, err) 4554 assert.Equal(t, []string{"helm template . --name-template test-app --include-crds --skip-schema-validation"}, res.Commands) 4555 }) 4556 t.Run("helm", func(t *testing.T) { 4557 service := newService(t, "testdata/broken-schema-verification") 4558 4559 q := apiclient.ManifestRequest{ 4560 AppName: "test-app", 4561 Repo: &v1alpha1.Repository{}, 4562 ApplicationSource: &v1alpha1.ApplicationSource{ 4563 Path: ".", 4564 Helm: &v1alpha1.ApplicationSourceHelm{ 4565 SkipSchemaValidation: false, 4566 }, 4567 }, 4568 } 4569 4570 _, err := service.GenerateManifest(t.Context(), &q) 4571 4572 require.ErrorContains(t, err, "values don't meet the specifications of the schema(s)") 4573 }) 4574 } 4575 4576 func TestGenerateManifest_OCISourceSkipsGitClient(t *testing.T) { 4577 svc := newService(t, t.TempDir()) 4578 4579 gitCalled := false 4580 svc.newGitClient = func(_, _ string, _ git.Creds, _, _ bool, _, _ string, _ ...git.ClientOpts) (git.Client, error) { 4581 gitCalled = true 4582 return nil, errors.New("git should not be called for OCI") 4583 } 4584 4585 req := &apiclient.ManifestRequest{ 4586 HasMultipleSources: true, 4587 Repo: &v1alpha1.Repository{ 4588 Repo: "oci://example.com/foo", 4589 }, 4590 ApplicationSource: &v1alpha1.ApplicationSource{ 4591 Path: "", 4592 TargetRevision: "v1", 4593 Ref: "foo", 4594 RepoURL: "oci://example.com/foo", 4595 }, 4596 ProjectName: "foo-project", 4597 ProjectSourceRepos: []string{"*"}, 4598 } 4599 4600 _, err := svc.GenerateManifest(t.Context(), req) 4601 require.NoError(t, err) 4602 4603 // verify that newGitClient was never invoked 4604 assert.False(t, gitCalled, "GenerateManifest should not invoke Git for OCI sources") 4605 }