github.com/argoproj/argo-cd/v2@v2.10.9/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 "os" 12 "os/exec" 13 "path" 14 "path/filepath" 15 "regexp" 16 "sort" 17 "strings" 18 "testing" 19 "time" 20 21 cacheutil "github.com/argoproj/argo-cd/v2/util/cache" 22 log "github.com/sirupsen/logrus" 23 "k8s.io/apimachinery/pkg/api/resource" 24 25 "github.com/stretchr/testify/assert" 26 "github.com/stretchr/testify/mock" 27 "github.com/stretchr/testify/require" 28 v1 "k8s.io/api/apps/v1" 29 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 30 "k8s.io/apimachinery/pkg/runtime" 31 "sigs.k8s.io/yaml" 32 33 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 34 argoappv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 35 "github.com/argoproj/argo-cd/v2/reposerver/apiclient" 36 "github.com/argoproj/argo-cd/v2/reposerver/cache" 37 repositorymocks "github.com/argoproj/argo-cd/v2/reposerver/cache/mocks" 38 "github.com/argoproj/argo-cd/v2/reposerver/metrics" 39 fileutil "github.com/argoproj/argo-cd/v2/test/fixture/path" 40 "github.com/argoproj/argo-cd/v2/util/argo" 41 dbmocks "github.com/argoproj/argo-cd/v2/util/db/mocks" 42 "github.com/argoproj/argo-cd/v2/util/git" 43 gitmocks "github.com/argoproj/argo-cd/v2/util/git/mocks" 44 "github.com/argoproj/argo-cd/v2/util/helm" 45 helmmocks "github.com/argoproj/argo-cd/v2/util/helm/mocks" 46 "github.com/argoproj/argo-cd/v2/util/io" 47 iomocks "github.com/argoproj/argo-cd/v2/util/io/mocks" 48 ) 49 50 const testSignature = `gpg: Signature made Wed Feb 26 23:22:34 2020 CET 51 gpg: using RSA key 4AEE18F83AFDEB23 52 gpg: Good signature from "GitHub (web-flow commit signing) <noreply@github.com>" [ultimate] 53 ` 54 55 type clientFunc func(*gitmocks.Client, *helmmocks.Client, *iomocks.TempPaths) 56 57 type repoCacheMocks struct { 58 mock.Mock 59 cacheutilCache *cacheutil.Cache 60 cache *cache.Cache 61 mockCache *repositorymocks.MockRepoCache 62 } 63 64 type newGitRepoHelmChartOptions struct { 65 chartName string 66 chartVersion string 67 // valuesFiles is a map of the values file name to the key/value pairs to be written to the file 68 valuesFiles map[string]map[string]string 69 } 70 71 type newGitRepoOptions struct { 72 path string 73 createPath bool 74 remote string 75 addEmptyCommit bool 76 helmChartOptions newGitRepoHelmChartOptions 77 } 78 79 func newCacheMocks() *repoCacheMocks { 80 mockRepoCache := repositorymocks.NewMockRepoCache(&repositorymocks.MockCacheOptions{ 81 RepoCacheExpiration: 1 * time.Minute, 82 RevisionCacheExpiration: 1 * time.Minute, 83 ReadDelay: 0, 84 WriteDelay: 0, 85 }) 86 cacheutilCache := cacheutil.NewCache(mockRepoCache.RedisClient) 87 return &repoCacheMocks{ 88 cacheutilCache: cacheutilCache, 89 cache: cache.NewCache(cacheutilCache, 1*time.Minute, 1*time.Minute), 90 mockCache: mockRepoCache, 91 } 92 } 93 94 func newServiceWithMocks(t *testing.T, root string, signed bool) (*Service, *gitmocks.Client, *repoCacheMocks) { 95 root, err := filepath.Abs(root) 96 if err != nil { 97 panic(err) 98 } 99 return newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) { 100 gitClient.On("Init").Return(nil) 101 gitClient.On("Fetch", mock.Anything).Return(nil) 102 gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil) 103 gitClient.On("LsRemote", mock.Anything).Return(mock.Anything, nil) 104 gitClient.On("CommitSHA").Return(mock.Anything, nil) 105 gitClient.On("Root").Return(root) 106 gitClient.On("IsAnnotatedTag").Return(false) 107 if signed { 108 gitClient.On("VerifyCommitSignature", mock.Anything).Return(testSignature, nil) 109 } else { 110 gitClient.On("VerifyCommitSignature", mock.Anything).Return("", nil) 111 } 112 113 chart := "my-chart" 114 oobChart := "out-of-bounds-chart" 115 version := "1.1.0" 116 helmClient.On("GetIndex", mock.AnythingOfType("bool"), mock.Anything).Return(&helm.Index{Entries: map[string]helm.Entries{ 117 chart: {{Version: "1.0.0"}, {Version: version}}, 118 oobChart: {{Version: "1.0.0"}, {Version: version}}, 119 }}, nil) 120 helmClient.On("ExtractChart", chart, version).Return("./testdata/my-chart", io.NopCloser, nil) 121 helmClient.On("ExtractChart", oobChart, version).Return("./testdata2/out-of-bounds-chart", io.NopCloser, nil) 122 helmClient.On("CleanChartCache", chart, version).Return(nil) 123 helmClient.On("CleanChartCache", oobChart, version).Return(nil) 124 helmClient.On("DependencyBuild").Return(nil) 125 126 paths.On("Add", mock.Anything, mock.Anything).Return(root, nil) 127 paths.On("GetPath", mock.Anything).Return(root, nil) 128 paths.On("GetPathIfExists", mock.Anything).Return(root, nil) 129 }, root) 130 } 131 132 func newServiceWithOpt(t *testing.T, cf clientFunc, root string) (*Service, *gitmocks.Client, *repoCacheMocks) { 133 helmClient := &helmmocks.Client{} 134 gitClient := &gitmocks.Client{} 135 paths := &iomocks.TempPaths{} 136 cf(gitClient, helmClient, paths) 137 cacheMocks := newCacheMocks() 138 t.Cleanup(cacheMocks.mockCache.StopRedisCallback) 139 service := NewService(metrics.NewMetricsServer(), cacheMocks.cache, RepoServerInitConstants{ParallelismLimit: 1}, argo.NewResourceTracking(), &git.NoopCredsStore{}, root) 140 141 service.newGitClient = func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, proxy string, opts ...git.ClientOpts) (client git.Client, e error) { 142 return gitClient, nil 143 } 144 service.newHelmClient = func(repoURL string, creds helm.Creds, enableOci bool, proxy string, opts ...helm.ClientOpts) helm.Client { 145 return helmClient 146 } 147 service.gitRepoInitializer = func(rootPath string) goio.Closer { 148 return io.NopCloser 149 } 150 service.gitRepoPaths = paths 151 return service, gitClient, cacheMocks 152 } 153 154 func newService(t *testing.T, root string) *Service { 155 service, _, _ := newServiceWithMocks(t, root, false) 156 return service 157 } 158 159 func newServiceWithSignature(t *testing.T, root string) *Service { 160 service, _, _ := newServiceWithMocks(t, root, true) 161 return service 162 } 163 164 func newServiceWithCommitSHA(t *testing.T, root, revision string) *Service { 165 var revisionErr error 166 167 commitSHARegex := regexp.MustCompile("^[0-9A-Fa-f]{40}$") 168 if !commitSHARegex.MatchString(revision) { 169 revisionErr = errors.New("not a commit SHA") 170 } 171 172 service, gitClient, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) { 173 gitClient.On("Init").Return(nil) 174 gitClient.On("Fetch", mock.Anything).Return(nil) 175 gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil) 176 gitClient.On("LsRemote", revision).Return(revision, revisionErr) 177 gitClient.On("CommitSHA").Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil) 178 gitClient.On("Root").Return(root) 179 paths.On("GetPath", mock.Anything).Return(root, nil) 180 paths.On("GetPathIfExists", mock.Anything).Return(root, nil) 181 }, root) 182 183 service.newGitClient = func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, proxy string, opts ...git.ClientOpts) (client git.Client, e error) { 184 return gitClient, nil 185 } 186 187 return service 188 } 189 190 func TestGenerateYamlManifestInDir(t *testing.T) { 191 service := newService(t, "../../manifests/base") 192 193 src := argoappv1.ApplicationSource{Path: "."} 194 q := apiclient.ManifestRequest{ 195 Repo: &argoappv1.Repository{}, 196 ApplicationSource: &src, 197 ProjectName: "something", 198 ProjectSourceRepos: []string{"*"}, 199 } 200 201 // update this value if we add/remove manifests 202 const countOfManifests = 48 203 204 res1, err := service.GenerateManifest(context.Background(), &q) 205 206 assert.NoError(t, err) 207 assert.Equal(t, countOfManifests, len(res1.Manifests)) 208 209 // this will test concatenated manifests to verify we split YAMLs correctly 210 res2, err := GenerateManifests(context.Background(), "./testdata/concatenated", "/", "", &q, false, &git.NoopCredsStore{}, resource.MustParse("0"), nil) 211 assert.NoError(t, err) 212 assert.Equal(t, 3, len(res2.Manifests)) 213 } 214 215 func Test_GenerateManifests_NoOutOfBoundsAccess(t *testing.T) { 216 testCases := []struct { 217 name string 218 outOfBoundsFilename string 219 outOfBoundsFileContents string 220 mustNotContain string // Optional string that must not appear in error or manifest output. If empty, use outOfBoundsFileContents. 221 }{ 222 { 223 name: "out of bounds JSON file should not appear in error output", 224 outOfBoundsFilename: "test.json", 225 outOfBoundsFileContents: `{"some": "json"}`, 226 }, 227 { 228 name: "malformed JSON file contents should not appear in error output", 229 outOfBoundsFilename: "test.json", 230 outOfBoundsFileContents: "$", 231 }, 232 { 233 name: "out of bounds JSON manifest should not appear in manifest output", 234 outOfBoundsFilename: "test.json", 235 // JSON marshalling is deterministic. So if there's a leak, exactly this should appear in the manifests. 236 outOfBoundsFileContents: `{"apiVersion":"v1","kind":"Secret","metadata":{"name":"test","namespace":"default"},"type":"Opaque"}`, 237 }, 238 { 239 name: "out of bounds YAML manifest should not appear in manifest output", 240 outOfBoundsFilename: "test.yaml", 241 outOfBoundsFileContents: "apiVersion: v1\nkind: Secret\nmetadata:\n name: test\n namespace: default\ntype: Opaque", 242 mustNotContain: `{"apiVersion":"v1","kind":"Secret","metadata":{"name":"test","namespace":"default"},"type":"Opaque"}`, 243 }, 244 } 245 246 for _, testCase := range testCases { 247 testCaseCopy := testCase 248 t.Run(testCaseCopy.name, func(t *testing.T) { 249 t.Parallel() 250 251 outOfBoundsDir := t.TempDir() 252 outOfBoundsFile := path.Join(outOfBoundsDir, testCaseCopy.outOfBoundsFilename) 253 err := os.WriteFile(outOfBoundsFile, []byte(testCaseCopy.outOfBoundsFileContents), os.FileMode(0444)) 254 require.NoError(t, err) 255 256 repoDir := t.TempDir() 257 err = os.Symlink(outOfBoundsFile, path.Join(repoDir, testCaseCopy.outOfBoundsFilename)) 258 require.NoError(t, err) 259 260 var mustNotContain = testCaseCopy.outOfBoundsFileContents 261 if testCaseCopy.mustNotContain != "" { 262 mustNotContain = testCaseCopy.mustNotContain 263 } 264 265 q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &argoappv1.ApplicationSource{}, ProjectName: "something", 266 ProjectSourceRepos: []string{"*"}} 267 res, err := GenerateManifests(context.Background(), repoDir, "", "", &q, false, &git.NoopCredsStore{}, resource.MustParse("0"), nil) 268 require.Error(t, err) 269 assert.NotContains(t, err.Error(), mustNotContain) 270 assert.Contains(t, err.Error(), "illegal filepath") 271 assert.Nil(t, res) 272 }) 273 } 274 } 275 276 func TestGenerateManifests_MissingSymlinkDestination(t *testing.T) { 277 repoDir := t.TempDir() 278 err := os.Symlink("/obviously/does/not/exist", path.Join(repoDir, "test.yaml")) 279 require.NoError(t, err) 280 281 q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &argoappv1.ApplicationSource{}, ProjectName: "something", 282 ProjectSourceRepos: []string{"*"}} 283 _, err = GenerateManifests(context.Background(), repoDir, "", "", &q, false, &git.NoopCredsStore{}, resource.MustParse("0"), nil) 284 require.NoError(t, err) 285 } 286 287 func TestGenerateManifests_K8SAPIResetCache(t *testing.T) { 288 service := newService(t, "../../manifests/base") 289 290 src := argoappv1.ApplicationSource{Path: "."} 291 q := apiclient.ManifestRequest{ 292 KubeVersion: "v1.16.0", 293 Repo: &argoappv1.Repository{}, 294 ApplicationSource: &src, 295 ProjectName: "something", 296 ProjectSourceRepos: []string{"*"}, 297 } 298 299 cachedFakeResponse := &apiclient.ManifestResponse{Manifests: []string{"Fake"}} 300 301 err := service.cache.SetManifests(mock.Anything, &src, q.RefSources, &q, "", "", "", "", &cache.CachedManifestResponse{ManifestResponse: cachedFakeResponse}, nil) 302 assert.NoError(t, err) 303 304 res, err := service.GenerateManifest(context.Background(), &q) 305 assert.NoError(t, err) 306 assert.Equal(t, cachedFakeResponse, res) 307 308 q.KubeVersion = "v1.17.0" 309 res, err = service.GenerateManifest(context.Background(), &q) 310 assert.NoError(t, err) 311 assert.NotEqual(t, cachedFakeResponse, res) 312 assert.True(t, len(res.Manifests) > 1) 313 } 314 315 func TestGenerateManifests_EmptyCache(t *testing.T) { 316 service, gitMocks, mockCache := newServiceWithMocks(t, "../../manifests/base", false) 317 318 src := argoappv1.ApplicationSource{Path: "."} 319 q := apiclient.ManifestRequest{ 320 Repo: &argoappv1.Repository{}, 321 ApplicationSource: &src, 322 ProjectName: "something", 323 ProjectSourceRepos: []string{"*"}, 324 } 325 326 err := service.cache.SetManifests(mock.Anything, &src, q.RefSources, &q, "", "", "", "", &cache.CachedManifestResponse{ManifestResponse: nil}, nil) 327 assert.NoError(t, err) 328 329 res, err := service.GenerateManifest(context.Background(), &q) 330 assert.NoError(t, err) 331 assert.True(t, len(res.Manifests) > 0) 332 mockCache.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{ 333 ExternalSets: 2, 334 ExternalGets: 2, 335 ExternalDeletes: 1}) 336 gitMocks.AssertCalled(t, "LsRemote", mock.Anything) 337 gitMocks.AssertCalled(t, "Fetch", mock.Anything) 338 } 339 340 // 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 341 // but it does resolve and cache the revision 342 func TestGenerateManifest_RefOnlyShortCircuit(t *testing.T) { 343 lsremoteCalled := false 344 dir := t.TempDir() 345 repopath := fmt.Sprintf("%s/tmprepo", dir) 346 repoRemote := fmt.Sprintf("file://%s", repopath) 347 cacheMocks := newCacheMocks() 348 t.Cleanup(cacheMocks.mockCache.StopRedisCallback) 349 service := NewService(metrics.NewMetricsServer(), cacheMocks.cache, RepoServerInitConstants{ParallelismLimit: 1}, argo.NewResourceTracking(), &git.NoopCredsStore{}, repopath) 350 service.newGitClient = func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, proxy string, opts ...git.ClientOpts) (client git.Client, e error) { 351 opts = append(opts, git.WithEventHandlers(git.EventHandlers{ 352 // Primary check, we want to make sure ls-remote is not called when the item is in cache 353 OnLsRemote: func(repo string) func() { 354 return func() { 355 lsremoteCalled = true 356 } 357 }, 358 OnFetch: func(repo string) func() { 359 return func() { 360 assert.Fail(t, "Fetch should not be called from GenerateManifest when the source is ref only") 361 } 362 }, 363 })) 364 gitClient, err := git.NewClientExt(rawRepoURL, root, creds, insecure, enableLfs, proxy, opts...) 365 return gitClient, err 366 } 367 revision := initGitRepo(t, newGitRepoOptions{ 368 path: repopath, 369 createPath: true, 370 remote: repoRemote, 371 addEmptyCommit: true, 372 }) 373 src := argoappv1.ApplicationSource{RepoURL: repoRemote, TargetRevision: "HEAD", Ref: "test-ref"} 374 repo := &argoappv1.Repository{ 375 Repo: repoRemote, 376 } 377 q := apiclient.ManifestRequest{ 378 Repo: repo, 379 Revision: "HEAD", 380 HasMultipleSources: true, 381 ApplicationSource: &src, 382 ProjectName: "default", 383 ProjectSourceRepos: []string{"*"}, 384 } 385 _, err := service.GenerateManifest(context.Background(), &q) 386 assert.NoError(t, err) 387 cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{ 388 ExternalSets: 1, 389 ExternalGets: 1}) 390 assert.True(t, lsremoteCalled, "ls-remote should be called when the source is ref only") 391 var revisions [][2]string 392 assert.NoError(t, cacheMocks.cacheutilCache.GetItem(fmt.Sprintf("git-refs|%s", repoRemote), &revisions)) 393 assert.ElementsMatch(t, [][2]string{{"refs/heads/main", revision}, {"HEAD", "ref: refs/heads/main"}}, revisions) 394 } 395 396 // Test that calling manifest generation on source helm reference helm files that when the revision is cached it does not call ls-remote 397 func TestGenerateManifestsHelmWithRefs_CachedNoLsRemote(t *testing.T) { 398 dir := t.TempDir() 399 repopath := fmt.Sprintf("%s/tmprepo", dir) 400 cacheMocks := newCacheMocks() 401 t.Cleanup(func() { 402 cacheMocks.mockCache.StopRedisCallback() 403 err := filepath.WalkDir(dir, 404 func(path string, di fs.DirEntry, err error) error { 405 if err == nil { 406 return os.Chmod(path, 0777) 407 } 408 return err 409 }) 410 if err != nil { 411 t.Fatal(err) 412 } 413 }) 414 service := NewService(metrics.NewMetricsServer(), cacheMocks.cache, RepoServerInitConstants{ParallelismLimit: 1}, argo.NewResourceTracking(), &git.NoopCredsStore{}, repopath) 415 var gitClient git.Client 416 var err error 417 service.newGitClient = func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, proxy string, opts ...git.ClientOpts) (client git.Client, e error) { 418 opts = append(opts, git.WithEventHandlers(git.EventHandlers{ 419 // Primary check, we want to make sure ls-remote is not called when the item is in cache 420 OnLsRemote: func(repo string) func() { 421 return func() { 422 assert.Fail(t, "LsRemote should not be called when the item is in cache") 423 } 424 }, 425 })) 426 gitClient, err = git.NewClientExt(rawRepoURL, root, creds, insecure, enableLfs, proxy, opts...) 427 return gitClient, err 428 } 429 repoRemote := fmt.Sprintf("file://%s", repopath) 430 revision := initGitRepo(t, newGitRepoOptions{ 431 path: repopath, 432 createPath: true, 433 remote: repoRemote, 434 helmChartOptions: newGitRepoHelmChartOptions{ 435 chartName: "my-chart", 436 chartVersion: "v1.0.0", 437 valuesFiles: map[string]map[string]string{"test.yaml": {"testval": "test"}}}, 438 }) 439 src := argoappv1.ApplicationSource{RepoURL: repoRemote, Path: ".", TargetRevision: "HEAD", Helm: &argoappv1.ApplicationSourceHelm{ 440 ValueFiles: []string{"$ref/test.yaml"}, 441 }} 442 repo := &argoappv1.Repository{ 443 Repo: repoRemote, 444 } 445 q := apiclient.ManifestRequest{ 446 Repo: repo, 447 Revision: "HEAD", 448 HasMultipleSources: true, 449 ApplicationSource: &src, 450 ProjectName: "default", 451 ProjectSourceRepos: []string{"*"}, 452 RefSources: map[string]*argoappv1.RefTarget{"$ref": {TargetRevision: "HEAD", Repo: *repo}}, 453 } 454 err = cacheMocks.cacheutilCache.SetItem(fmt.Sprintf("git-refs|%s", repoRemote), [][2]string{{"HEAD", revision}}, 30*time.Second, false) 455 assert.NoError(t, err) 456 _, err = service.GenerateManifest(context.Background(), &q) 457 assert.NoError(t, err) 458 cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{ 459 ExternalSets: 2, 460 ExternalGets: 5}) 461 } 462 463 // ensure we can use a semver constraint range (>= 1.0.0) and get back the correct chart (1.0.0) 464 func TestHelmManifestFromChartRepo(t *testing.T) { 465 root := t.TempDir() 466 service, gitMocks, mockCache := newServiceWithMocks(t, root, false) 467 source := &argoappv1.ApplicationSource{Chart: "my-chart", TargetRevision: ">= 1.0.0"} 468 request := &apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: source, NoCache: true, ProjectName: "something", 469 ProjectSourceRepos: []string{"*"}} 470 response, err := service.GenerateManifest(context.Background(), request) 471 assert.NoError(t, err) 472 assert.NotNil(t, response) 473 assert.Equal(t, &apiclient.ManifestResponse{ 474 Manifests: []string{"{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"name\":\"my-map\"}}"}, 475 Namespace: "", 476 Server: "", 477 Revision: "1.1.0", 478 SourceType: "Helm", 479 }, response) 480 mockCache.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{ 481 ExternalSets: 1, 482 ExternalGets: 0}) 483 gitMocks.AssertNotCalled(t, "LsRemote", mock.Anything) 484 } 485 486 func TestHelmChartReferencingExternalValues(t *testing.T) { 487 service := newService(t, ".") 488 spec := argoappv1.ApplicationSpec{ 489 Sources: []argoappv1.ApplicationSource{ 490 {RepoURL: "https://helm.example.com", Chart: "my-chart", TargetRevision: ">= 1.0.0", Helm: &argoappv1.ApplicationSourceHelm{ 491 ValueFiles: []string{"$ref/testdata/my-chart/my-chart-values.yaml"}, 492 }}, 493 {Ref: "ref", RepoURL: "https://git.example.com/test/repo"}, 494 }, 495 } 496 repoDB := &dbmocks.ArgoDB{} 497 repoDB.On("GetRepository", context.Background(), "https://git.example.com/test/repo").Return(&argoappv1.Repository{ 498 Repo: "https://git.example.com/test/repo", 499 }, nil) 500 refSources, err := argo.GetRefSources(context.Background(), spec, repoDB) 501 require.NoError(t, err) 502 request := &apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &spec.Sources[0], NoCache: true, RefSources: refSources, HasMultipleSources: true, ProjectName: "something", 503 ProjectSourceRepos: []string{"*"}} 504 response, err := service.GenerateManifest(context.Background(), request) 505 assert.NoError(t, err) 506 assert.NotNil(t, response) 507 assert.Equal(t, &apiclient.ManifestResponse{ 508 Manifests: []string{"{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"name\":\"my-map\"}}"}, 509 Namespace: "", 510 Server: "", 511 Revision: "1.1.0", 512 SourceType: "Helm", 513 }, response) 514 } 515 516 func TestHelmChartReferencingExternalValues_OutOfBounds_Symlink(t *testing.T) { 517 service := newService(t, ".") 518 err := os.Mkdir("testdata/oob-symlink", 0755) 519 require.NoError(t, err) 520 t.Cleanup(func() { 521 err = os.RemoveAll("testdata/oob-symlink") 522 require.NoError(t, err) 523 }) 524 // Create a symlink to a file outside of the repo 525 err = os.Symlink("../../../values.yaml", "./testdata/oob-symlink/oob-symlink.yaml") 526 // Create a regular file to reference from another source 527 err = os.WriteFile("./testdata/oob-symlink/values.yaml", []byte("foo: bar"), 0644) 528 require.NoError(t, err) 529 spec := argoappv1.ApplicationSpec{ 530 Sources: []argoappv1.ApplicationSource{ 531 {RepoURL: "https://helm.example.com", Chart: "my-chart", TargetRevision: ">= 1.0.0", Helm: &argoappv1.ApplicationSourceHelm{ 532 // Reference `ref` but do not use the oob symlink. The mere existence of the link should be enough to 533 // cause an error. 534 ValueFiles: []string{"$ref/testdata/oob-symlink/values.yaml"}, 535 }}, 536 {Ref: "ref", RepoURL: "https://git.example.com/test/repo"}, 537 }, 538 } 539 repoDB := &dbmocks.ArgoDB{} 540 repoDB.On("GetRepository", context.Background(), "https://git.example.com/test/repo").Return(&argoappv1.Repository{ 541 Repo: "https://git.example.com/test/repo", 542 }, nil) 543 refSources, err := argo.GetRefSources(context.Background(), spec, repoDB) 544 require.NoError(t, err) 545 request := &apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &spec.Sources[0], NoCache: true, RefSources: refSources, HasMultipleSources: true} 546 _, err = service.GenerateManifest(context.Background(), request) 547 assert.Error(t, err) 548 } 549 550 func TestGenerateManifestsUseExactRevision(t *testing.T) { 551 service, gitClient, _ := newServiceWithMocks(t, ".", false) 552 553 src := argoappv1.ApplicationSource{Path: "./testdata/recurse", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}} 554 555 q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, Revision: "abc", ProjectName: "something", 556 ProjectSourceRepos: []string{"*"}} 557 558 res1, err := service.GenerateManifest(context.Background(), &q) 559 assert.Nil(t, err) 560 assert.Equal(t, 2, len(res1.Manifests)) 561 assert.Equal(t, gitClient.Calls[0].Arguments[0], "abc") 562 } 563 564 func TestRecurseManifestsInDir(t *testing.T) { 565 service := newService(t, ".") 566 567 src := argoappv1.ApplicationSource{Path: "./testdata/recurse", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}} 568 569 q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, ProjectName: "something", 570 ProjectSourceRepos: []string{"*"}} 571 572 res1, err := service.GenerateManifest(context.Background(), &q) 573 assert.Nil(t, err) 574 assert.Equal(t, 2, len(res1.Manifests)) 575 } 576 577 func TestInvalidManifestsInDir(t *testing.T) { 578 service := newService(t, ".") 579 580 src := argoappv1.ApplicationSource{Path: "./testdata/invalid-manifests", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}} 581 582 q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src} 583 584 _, err := service.GenerateManifest(context.Background(), &q) 585 assert.NotNil(t, err) 586 } 587 588 func TestInvalidMetadata(t *testing.T) { 589 service := newService(t, ".") 590 591 src := argoappv1.ApplicationSource{Path: "./testdata/invalid-metadata", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}} 592 q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, AppLabelKey: "test", AppName: "invalid-metadata", TrackingMethod: "annotation+label"} 593 _, err := service.GenerateManifest(context.Background(), &q) 594 assert.Error(t, err) 595 assert.Contains(t, err.Error(), "contains non-string key in the map") 596 } 597 598 func TestNilMetadataAccessors(t *testing.T) { 599 service := newService(t, ".") 600 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\"}}" 601 602 src := argoappv1.ApplicationSource{Path: "./testdata/nil-metadata-accessors", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}} 603 q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, AppLabelKey: "test", AppName: "nil-metadata-accessors", TrackingMethod: "annotation+label"} 604 res, err := service.GenerateManifest(context.Background(), &q) 605 assert.NoError(t, err) 606 assert.Equal(t, len(res.Manifests), 1) 607 assert.Equal(t, expected, res.Manifests[0]) 608 } 609 610 func TestGenerateJsonnetManifestInDir(t *testing.T) { 611 service := newService(t, ".") 612 613 q := apiclient.ManifestRequest{ 614 Repo: &argoappv1.Repository{}, 615 ApplicationSource: &argoappv1.ApplicationSource{ 616 Path: "./testdata/jsonnet", 617 Directory: &argoappv1.ApplicationSourceDirectory{ 618 Jsonnet: argoappv1.ApplicationSourceJsonnet{ 619 ExtVars: []argoappv1.JsonnetVar{{Name: "extVarString", Value: "extVarString"}, {Name: "extVarCode", Value: "\"extVarCode\"", Code: true}}, 620 TLAs: []argoappv1.JsonnetVar{{Name: "tlaString", Value: "tlaString"}, {Name: "tlaCode", Value: "\"tlaCode\"", Code: true}}, 621 Libs: []string{"testdata/jsonnet/vendor"}, 622 }, 623 }, 624 }, 625 ProjectName: "something", 626 ProjectSourceRepos: []string{"*"}, 627 } 628 res1, err := service.GenerateManifest(context.Background(), &q) 629 assert.Nil(t, err) 630 assert.Equal(t, 2, len(res1.Manifests)) 631 } 632 633 func TestGenerateJsonnetManifestInRootDir(t *testing.T) { 634 service := newService(t, "testdata/jsonnet-1") 635 636 q := apiclient.ManifestRequest{ 637 Repo: &argoappv1.Repository{}, 638 ApplicationSource: &argoappv1.ApplicationSource{ 639 Path: ".", 640 Directory: &argoappv1.ApplicationSourceDirectory{ 641 Jsonnet: argoappv1.ApplicationSourceJsonnet{ 642 ExtVars: []argoappv1.JsonnetVar{{Name: "extVarString", Value: "extVarString"}, {Name: "extVarCode", Value: "\"extVarCode\"", Code: true}}, 643 TLAs: []argoappv1.JsonnetVar{{Name: "tlaString", Value: "tlaString"}, {Name: "tlaCode", Value: "\"tlaCode\"", Code: true}}, 644 Libs: []string{"."}, 645 }, 646 }, 647 }, 648 ProjectName: "something", 649 ProjectSourceRepos: []string{"*"}, 650 } 651 res1, err := service.GenerateManifest(context.Background(), &q) 652 assert.Nil(t, err) 653 assert.Equal(t, 2, len(res1.Manifests)) 654 } 655 656 func TestGenerateJsonnetLibOutside(t *testing.T) { 657 service := newService(t, ".") 658 659 q := apiclient.ManifestRequest{ 660 Repo: &argoappv1.Repository{}, 661 ApplicationSource: &argoappv1.ApplicationSource{ 662 Path: "./testdata/jsonnet", 663 Directory: &argoappv1.ApplicationSourceDirectory{ 664 Jsonnet: argoappv1.ApplicationSourceJsonnet{ 665 Libs: []string{"../../../testdata/jsonnet/vendor"}, 666 }, 667 }, 668 }, 669 ProjectName: "something", 670 ProjectSourceRepos: []string{"*"}, 671 } 672 _, err := service.GenerateManifest(context.Background(), &q) 673 require.Error(t, err) 674 require.Contains(t, err.Error(), "file '../../../testdata/jsonnet/vendor' resolved to outside repository root") 675 } 676 677 func TestManifestGenErrorCacheByNumRequests(t *testing.T) { 678 679 // Returns the state of the manifest generation cache, by querying the cache for the previously set result 680 getRecentCachedEntry := func(service *Service, manifestRequest *apiclient.ManifestRequest) *cache.CachedManifestResponse { 681 assert.NotNil(t, service) 682 assert.NotNil(t, manifestRequest) 683 684 cachedManifestResponse := &cache.CachedManifestResponse{} 685 err := service.cache.GetManifests(mock.Anything, manifestRequest.ApplicationSource, manifestRequest.RefSources, manifestRequest, manifestRequest.Namespace, "", manifestRequest.AppLabelKey, manifestRequest.AppName, cachedManifestResponse, nil) 686 assert.Nil(t, err) 687 return cachedManifestResponse 688 } 689 690 // Example: 691 // With repo server (test) parameters: 692 // - PauseGenerationAfterFailedGenerationAttempts: 2 693 // - PauseGenerationOnFailureForRequests: 4 694 // - TotalCacheInvocations: 10 695 // 696 // After 2 manifest generation failures in a row, the next 4 manifest generation requests should be cached, 697 // with the next 2 after that being uncached. Here's how it looks... 698 // 699 // request count) result 700 // -------------------------- 701 // 1) Attempt to generate manifest, fails. 702 // 2) Second attempt to generate manifest, fails. 703 // 3) Return cached error attempt from #2 704 // 4) Return cached error attempt from #2 705 // 5) Return cached error attempt from #2 706 // 6) Return cached error attempt from #2. Max response limit hit, so reset cache entry. 707 // 7) Attempt to generate manifest, fails. 708 // 8) Attempt to generate manifest, fails. 709 // 9) Return cached error attempt from #8 710 // 10) Return cached error attempt from #8 711 712 // The same pattern PauseGenerationAfterFailedGenerationAttempts generation attempts, followed by 713 // PauseGenerationOnFailureForRequests cached responses, should apply for various combinations of 714 // both parameters. 715 716 tests := []struct { 717 PauseGenerationAfterFailedGenerationAttempts int 718 PauseGenerationOnFailureForRequests int 719 TotalCacheInvocations int 720 }{ 721 {2, 4, 10}, 722 {3, 5, 10}, 723 {1, 2, 5}, 724 } 725 for _, tt := range tests { 726 testName := fmt.Sprintf("gen-attempts-%d-pause-%d-total-%d", tt.PauseGenerationAfterFailedGenerationAttempts, tt.PauseGenerationOnFailureForRequests, tt.TotalCacheInvocations) 727 t.Run(testName, func(t *testing.T) { 728 service := newService(t, ".") 729 730 service.initConstants = RepoServerInitConstants{ 731 ParallelismLimit: 1, 732 PauseGenerationAfterFailedGenerationAttempts: tt.PauseGenerationAfterFailedGenerationAttempts, 733 PauseGenerationOnFailureForMinutes: 0, 734 PauseGenerationOnFailureForRequests: tt.PauseGenerationOnFailureForRequests, 735 } 736 737 totalAttempts := service.initConstants.PauseGenerationAfterFailedGenerationAttempts + service.initConstants.PauseGenerationOnFailureForRequests 738 739 for invocationCount := 0; invocationCount < tt.TotalCacheInvocations; invocationCount++ { 740 adjustedInvocation := invocationCount % totalAttempts 741 742 fmt.Printf("%d )-------------------------------------------\n", invocationCount) 743 744 manifestRequest := &apiclient.ManifestRequest{ 745 Repo: &argoappv1.Repository{}, 746 AppName: "test", 747 ApplicationSource: &argoappv1.ApplicationSource{ 748 Path: "./testdata/invalid-helm", 749 }, 750 } 751 752 res, err := service.GenerateManifest(context.Background(), manifestRequest) 753 754 // Verify invariant: res != nil xor err != nil 755 if err != nil { 756 assert.True(t, res == nil, "both err and res are non-nil res: %v err: %v", res, err) 757 } else { 758 assert.True(t, res != nil, "both err and res are nil") 759 } 760 761 cachedManifestResponse := getRecentCachedEntry(service, manifestRequest) 762 763 isCachedError := err != nil && strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix) 764 765 if adjustedInvocation < service.initConstants.PauseGenerationAfterFailedGenerationAttempts { 766 // GenerateManifest should not return cached errors for the first X responses, where X is the FailGenAttempts constants 767 require.False(t, isCachedError) 768 769 require.NotNil(t, cachedManifestResponse) 770 // nolint:staticcheck 771 assert.Nil(t, cachedManifestResponse.ManifestResponse) 772 // nolint:staticcheck 773 assert.True(t, cachedManifestResponse.FirstFailureTimestamp != 0) 774 775 // Internal cache consec failures value should increase with invocations, cached response should stay the same, 776 // nolint:staticcheck 777 assert.True(t, cachedManifestResponse.NumberOfConsecutiveFailures == adjustedInvocation+1) 778 // nolint:staticcheck 779 assert.True(t, cachedManifestResponse.NumberOfCachedResponsesReturned == 0) 780 781 } else { 782 // GenerateManifest SHOULD return cached errors for the next X responses, where X is the 783 // PauseGenerationOnFailureForRequests constant 784 assert.True(t, isCachedError) 785 require.NotNil(t, cachedManifestResponse) 786 // nolint:staticcheck 787 assert.Nil(t, cachedManifestResponse.ManifestResponse) 788 // nolint:staticcheck 789 assert.True(t, cachedManifestResponse.FirstFailureTimestamp != 0) 790 791 // Internal cache values should update correctly based on number of return cache entries, consecutive failures should stay the same 792 // nolint:staticcheck 793 assert.True(t, cachedManifestResponse.NumberOfConsecutiveFailures == service.initConstants.PauseGenerationAfterFailedGenerationAttempts) 794 // nolint:staticcheck 795 assert.True(t, cachedManifestResponse.NumberOfCachedResponsesReturned == (adjustedInvocation-service.initConstants.PauseGenerationAfterFailedGenerationAttempts+1)) 796 } 797 } 798 }) 799 } 800 } 801 802 func TestManifestGenErrorCacheFileContentsChange(t *testing.T) { 803 804 tmpDir := t.TempDir() 805 806 service := newService(t, tmpDir) 807 808 service.initConstants = RepoServerInitConstants{ 809 ParallelismLimit: 1, 810 PauseGenerationAfterFailedGenerationAttempts: 2, 811 PauseGenerationOnFailureForMinutes: 0, 812 PauseGenerationOnFailureForRequests: 4, 813 } 814 815 for step := 0; step < 3; step++ { 816 817 // step 1) Attempt to generate manifests against invalid helm chart (should return uncached error) 818 // step 2) Attempt to generate manifest against valid helm chart (should succeed and return valid response) 819 // step 3) Attempt to generate manifest against invalid helm chart (should return cached value from step 2) 820 821 errorExpected := step%2 == 0 822 823 // Ensure that the target directory will succeed or fail, so we can verify the cache correctly handles it 824 err := os.RemoveAll(tmpDir) 825 assert.NoError(t, err) 826 err = os.MkdirAll(tmpDir, 0777) 827 assert.NoError(t, err) 828 if errorExpected { 829 // Copy invalid helm chart into temporary directory, ensuring manifest generation will fail 830 err = fileutil.CopyDir("./testdata/invalid-helm", tmpDir) 831 assert.NoError(t, err) 832 833 } else { 834 // Copy valid helm chart into temporary directory, ensuring generation will succeed 835 err = fileutil.CopyDir("./testdata/my-chart", tmpDir) 836 assert.NoError(t, err) 837 } 838 839 res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 840 Repo: &argoappv1.Repository{}, 841 AppName: "test", 842 ApplicationSource: &argoappv1.ApplicationSource{ 843 Path: ".", 844 }, 845 ProjectName: "something", 846 ProjectSourceRepos: []string{"*"}, 847 }) 848 849 fmt.Println("-", step, "-", res != nil, err != nil, errorExpected) 850 fmt.Println(" err: ", err) 851 fmt.Println(" res: ", res) 852 853 if step < 2 { 854 assert.True(t, (err != nil) == errorExpected, "error return value and error expected did not match") 855 assert.True(t, (res != nil) == !errorExpected, "GenerateManifest return value and expected value did not match") 856 } 857 858 if step == 2 { 859 assert.NoError(t, err, "error ret val was non-nil on step 3") 860 assert.NotNil(t, res, "GenerateManifest ret val was nil on step 3") 861 } 862 } 863 } 864 865 func TestManifestGenErrorCacheByMinutesElapsed(t *testing.T) { 866 867 tests := []struct { 868 // Test with a range of pause expiration thresholds 869 PauseGenerationOnFailureForMinutes int 870 }{ 871 {1}, {2}, {10}, {24 * 60}, 872 } 873 for _, tt := range tests { 874 testName := fmt.Sprintf("pause-time-%d", tt.PauseGenerationOnFailureForMinutes) 875 t.Run(testName, func(t *testing.T) { 876 service := newService(t, ".") 877 878 // Here we simulate the passage of time by overriding the now() function of Service 879 currentTime := time.Now() 880 service.now = func() time.Time { 881 return currentTime 882 } 883 884 service.initConstants = RepoServerInitConstants{ 885 ParallelismLimit: 1, 886 PauseGenerationAfterFailedGenerationAttempts: 1, 887 PauseGenerationOnFailureForMinutes: tt.PauseGenerationOnFailureForMinutes, 888 PauseGenerationOnFailureForRequests: 0, 889 } 890 891 // 1) Put the cache into the failure state 892 for x := 0; x < 2; x++ { 893 res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 894 Repo: &argoappv1.Repository{}, 895 AppName: "test", 896 ApplicationSource: &argoappv1.ApplicationSource{ 897 Path: "./testdata/invalid-helm", 898 }, 899 }) 900 901 assert.True(t, err != nil && res == nil) 902 903 // Ensure that the second invocation triggers the cached error state 904 if x == 1 { 905 assert.True(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix)) 906 } 907 908 } 909 910 // 2) Jump forward X-1 minutes in time, where X is the expiration boundary 911 currentTime = currentTime.Add(time.Duration(tt.PauseGenerationOnFailureForMinutes-1) * time.Minute) 912 res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 913 Repo: &argoappv1.Repository{}, 914 AppName: "test", 915 ApplicationSource: &argoappv1.ApplicationSource{ 916 Path: "./testdata/invalid-helm", 917 }, 918 }) 919 920 // 3) Ensure that the cache still returns a cached copy of the last error 921 assert.True(t, err != nil && res == nil) 922 assert.True(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix)) 923 924 // 4) Jump forward 2 minutes in time, such that the pause generation time has elapsed and we should return to normal state 925 currentTime = currentTime.Add(2 * time.Minute) 926 927 res, err = service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 928 Repo: &argoappv1.Repository{}, 929 AppName: "test", 930 ApplicationSource: &argoappv1.ApplicationSource{ 931 Path: "./testdata/invalid-helm", 932 }, 933 }) 934 935 // 5) Ensure that the service no longer returns a cached copy of the last error 936 assert.True(t, err != nil && res == nil) 937 assert.True(t, !strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix)) 938 939 }) 940 } 941 942 } 943 944 func TestManifestGenErrorCacheRespectsNoCache(t *testing.T) { 945 946 service := newService(t, ".") 947 948 service.initConstants = RepoServerInitConstants{ 949 ParallelismLimit: 1, 950 PauseGenerationAfterFailedGenerationAttempts: 1, 951 PauseGenerationOnFailureForMinutes: 0, 952 PauseGenerationOnFailureForRequests: 4, 953 } 954 955 // 1) Put the cache into the failure state 956 for x := 0; x < 2; x++ { 957 res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 958 Repo: &argoappv1.Repository{}, 959 AppName: "test", 960 ApplicationSource: &argoappv1.ApplicationSource{ 961 Path: "./testdata/invalid-helm", 962 }, 963 }) 964 965 assert.True(t, err != nil && res == nil) 966 967 // Ensure that the second invocation is cached 968 if x == 1 { 969 assert.True(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix)) 970 } 971 } 972 973 // 2) Call generateManifest with NoCache enabled 974 res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 975 Repo: &argoappv1.Repository{}, 976 AppName: "test", 977 ApplicationSource: &argoappv1.ApplicationSource{ 978 Path: "./testdata/invalid-helm", 979 }, 980 NoCache: true, 981 }) 982 983 // 3) Ensure that the cache returns a new generation attempt, rather than a previous cached error 984 assert.True(t, err != nil && res == nil) 985 assert.True(t, !strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix)) 986 987 // 4) Call generateManifest 988 res, err = service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 989 Repo: &argoappv1.Repository{}, 990 AppName: "test", 991 ApplicationSource: &argoappv1.ApplicationSource{ 992 Path: "./testdata/invalid-helm", 993 }, 994 }) 995 996 // 5) Ensure that the subsequent invocation, after nocache, is cached 997 assert.True(t, err != nil && res == nil) 998 assert.True(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix)) 999 1000 } 1001 1002 func TestGenerateHelmWithValues(t *testing.T) { 1003 service := newService(t, "../../util/helm/testdata/redis") 1004 1005 res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 1006 Repo: &argoappv1.Repository{}, 1007 AppName: "test", 1008 ApplicationSource: &argoappv1.ApplicationSource{ 1009 Path: ".", 1010 Helm: &argoappv1.ApplicationSourceHelm{ 1011 ValueFiles: []string{"values-production.yaml"}, 1012 ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)}, 1013 }, 1014 }, 1015 ProjectName: "something", 1016 ProjectSourceRepos: []string{"*"}, 1017 }) 1018 1019 assert.NoError(t, err) 1020 1021 replicasVerified := false 1022 for _, src := range res.Manifests { 1023 obj := unstructured.Unstructured{} 1024 err = json.Unmarshal([]byte(src), &obj) 1025 assert.NoError(t, err) 1026 1027 if obj.GetKind() == "Deployment" && obj.GetName() == "test-redis-slave" { 1028 var dep v1.Deployment 1029 err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &dep) 1030 assert.NoError(t, err) 1031 assert.Equal(t, int32(2), *dep.Spec.Replicas) 1032 replicasVerified = true 1033 } 1034 } 1035 assert.True(t, replicasVerified) 1036 1037 } 1038 1039 func TestHelmWithMissingValueFiles(t *testing.T) { 1040 service := newService(t, "../../util/helm/testdata/redis") 1041 missingValuesFile := "values-prod-overrides.yaml" 1042 1043 req := &apiclient.ManifestRequest{ 1044 Repo: &argoappv1.Repository{}, 1045 AppName: "test", 1046 ApplicationSource: &argoappv1.ApplicationSource{ 1047 Path: ".", 1048 Helm: &argoappv1.ApplicationSourceHelm{ 1049 ValueFiles: []string{"values-production.yaml", missingValuesFile}, 1050 }, 1051 }, 1052 ProjectName: "something", 1053 ProjectSourceRepos: []string{"*"}, 1054 } 1055 1056 // Should fail since we're passing a non-existent values file, and error should indicate that 1057 _, err := service.GenerateManifest(context.Background(), req) 1058 assert.Error(t, err) 1059 assert.Contains(t, err.Error(), fmt.Sprintf("%s: no such file or directory", missingValuesFile)) 1060 1061 // Should template without error even if defining a non-existent values file 1062 req.ApplicationSource.Helm.IgnoreMissingValueFiles = true 1063 _, err = service.GenerateManifest(context.Background(), req) 1064 assert.NoError(t, err) 1065 } 1066 1067 func TestGenerateHelmWithEnvVars(t *testing.T) { 1068 service := newService(t, "../../util/helm/testdata/redis") 1069 1070 res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 1071 Repo: &argoappv1.Repository{}, 1072 AppName: "production", 1073 ApplicationSource: &argoappv1.ApplicationSource{ 1074 Path: ".", 1075 Helm: &argoappv1.ApplicationSourceHelm{ 1076 ValueFiles: []string{"values-$ARGOCD_APP_NAME.yaml"}, 1077 }, 1078 }, 1079 ProjectName: "something", 1080 ProjectSourceRepos: []string{"*"}, 1081 }) 1082 1083 assert.NoError(t, err) 1084 1085 replicasVerified := false 1086 for _, src := range res.Manifests { 1087 obj := unstructured.Unstructured{} 1088 err = json.Unmarshal([]byte(src), &obj) 1089 assert.NoError(t, err) 1090 1091 if obj.GetKind() == "Deployment" && obj.GetName() == "production-redis-slave" { 1092 var dep v1.Deployment 1093 err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &dep) 1094 assert.NoError(t, err) 1095 assert.Equal(t, int32(3), *dep.Spec.Replicas) 1096 replicasVerified = true 1097 } 1098 } 1099 assert.True(t, replicasVerified) 1100 } 1101 1102 // The requested value file (`../minio/values.yaml`) is outside the app path (`./util/helm/testdata/redis`), however 1103 // since the requested value is still under the repo directory (`~/go/src/github.com/argoproj/argo-cd`), it is allowed 1104 func TestGenerateHelmWithValuesDirectoryTraversal(t *testing.T) { 1105 service := newService(t, "../../util/helm/testdata") 1106 _, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 1107 Repo: &argoappv1.Repository{}, 1108 AppName: "test", 1109 ApplicationSource: &argoappv1.ApplicationSource{ 1110 Path: "./redis", 1111 Helm: &argoappv1.ApplicationSourceHelm{ 1112 ValueFiles: []string{"../minio/values.yaml"}, 1113 ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)}, 1114 }, 1115 }, 1116 ProjectName: "something", 1117 ProjectSourceRepos: []string{"*"}, 1118 }) 1119 assert.NoError(t, err) 1120 1121 // Test the case where the path is "." 1122 service = newService(t, "./testdata") 1123 _, err = service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 1124 Repo: &argoappv1.Repository{}, 1125 AppName: "test", 1126 ApplicationSource: &argoappv1.ApplicationSource{ 1127 Path: "./my-chart", 1128 }, 1129 ProjectName: "something", 1130 ProjectSourceRepos: []string{"*"}, 1131 }) 1132 assert.NoError(t, err) 1133 } 1134 1135 func TestChartRepoWithOutOfBoundsSymlink(t *testing.T) { 1136 service := newService(t, ".") 1137 source := &argoappv1.ApplicationSource{Chart: "out-of-bounds-chart", TargetRevision: ">= 1.0.0"} 1138 request := &apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: source, NoCache: true} 1139 _, err := service.GenerateManifest(context.Background(), request) 1140 assert.ErrorContains(t, err, "chart contains out-of-bounds symlinks") 1141 } 1142 1143 // This is a Helm first-class app with a values file inside the repo directory 1144 // (`~/go/src/github.com/argoproj/argo-cd/reposerver/repository`), so it is allowed 1145 func TestHelmManifestFromChartRepoWithValueFile(t *testing.T) { 1146 service := newService(t, ".") 1147 source := &argoappv1.ApplicationSource{ 1148 Chart: "my-chart", 1149 TargetRevision: ">= 1.0.0", 1150 Helm: &argoappv1.ApplicationSourceHelm{ 1151 ValueFiles: []string{"./my-chart-values.yaml"}, 1152 }, 1153 } 1154 request := &apiclient.ManifestRequest{ 1155 Repo: &argoappv1.Repository{}, 1156 ApplicationSource: source, 1157 NoCache: true, 1158 ProjectName: "something", 1159 ProjectSourceRepos: []string{"*"}} 1160 response, err := service.GenerateManifest(context.Background(), request) 1161 assert.NoError(t, err) 1162 assert.NotNil(t, response) 1163 assert.Equal(t, &apiclient.ManifestResponse{ 1164 Manifests: []string{"{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"name\":\"my-map\"}}"}, 1165 Namespace: "", 1166 Server: "", 1167 Revision: "1.1.0", 1168 SourceType: "Helm", 1169 }, response) 1170 } 1171 1172 // This is a Helm first-class app with a values file outside the repo directory 1173 // (`~/go/src/github.com/argoproj/argo-cd/reposerver/repository`), so it is not allowed 1174 func TestHelmManifestFromChartRepoWithValueFileOutsideRepo(t *testing.T) { 1175 service := newService(t, ".") 1176 source := &argoappv1.ApplicationSource{ 1177 Chart: "my-chart", 1178 TargetRevision: ">= 1.0.0", 1179 Helm: &argoappv1.ApplicationSourceHelm{ 1180 ValueFiles: []string{"../my-chart-2/my-chart-2-values.yaml"}, 1181 }, 1182 } 1183 request := &apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: source, NoCache: true} 1184 _, err := service.GenerateManifest(context.Background(), request) 1185 assert.Error(t, err) 1186 } 1187 1188 func TestHelmManifestFromChartRepoWithValueFileLinks(t *testing.T) { 1189 t.Run("Valid symlink", func(t *testing.T) { 1190 service := newService(t, ".") 1191 source := &argoappv1.ApplicationSource{ 1192 Chart: "my-chart", 1193 TargetRevision: ">= 1.0.0", 1194 Helm: &argoappv1.ApplicationSourceHelm{ 1195 ValueFiles: []string{"my-chart-link.yaml"}, 1196 }, 1197 } 1198 request := &apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: source, NoCache: true, ProjectName: "something", 1199 ProjectSourceRepos: []string{"*"}} 1200 _, err := service.GenerateManifest(context.Background(), request) 1201 assert.NoError(t, err) 1202 }) 1203 } 1204 1205 func TestGenerateHelmWithURL(t *testing.T) { 1206 service := newService(t, "../../util/helm/testdata/redis") 1207 1208 _, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 1209 Repo: &argoappv1.Repository{}, 1210 AppName: "test", 1211 ApplicationSource: &argoappv1.ApplicationSource{ 1212 Path: ".", 1213 Helm: &argoappv1.ApplicationSourceHelm{ 1214 ValueFiles: []string{"https://raw.githubusercontent.com/argoproj/argocd-example-apps/master/helm-guestbook/values.yaml"}, 1215 ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)}, 1216 }, 1217 }, 1218 ProjectName: "something", 1219 ProjectSourceRepos: []string{"*"}, 1220 HelmOptions: &argoappv1.HelmOptions{ValuesFileSchemes: []string{"https"}}, 1221 }) 1222 assert.NoError(t, err) 1223 } 1224 1225 // The requested value file (`../minio/values.yaml`) is outside the repo directory 1226 // (`~/go/src/github.com/argoproj/argo-cd/util/helm/testdata/redis`), so it is blocked 1227 func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) { 1228 t.Run("Values file with relative path pointing outside repo root", func(t *testing.T) { 1229 service := newService(t, "../../util/helm/testdata/redis") 1230 _, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 1231 Repo: &argoappv1.Repository{}, 1232 AppName: "test", 1233 ApplicationSource: &argoappv1.ApplicationSource{ 1234 Path: ".", 1235 Helm: &argoappv1.ApplicationSourceHelm{ 1236 ValueFiles: []string{"../minio/values.yaml"}, 1237 ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)}, 1238 }, 1239 }, 1240 ProjectName: "something", 1241 ProjectSourceRepos: []string{"*"}, 1242 }) 1243 assert.Error(t, err) 1244 assert.Contains(t, err.Error(), "outside repository root") 1245 }) 1246 1247 t.Run("Values file with relative path pointing inside repo root", func(t *testing.T) { 1248 service := newService(t, "./testdata") 1249 _, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 1250 Repo: &argoappv1.Repository{}, 1251 AppName: "test", 1252 ApplicationSource: &argoappv1.ApplicationSource{ 1253 Path: "./my-chart", 1254 Helm: &argoappv1.ApplicationSourceHelm{ 1255 ValueFiles: []string{"../my-chart/my-chart-values.yaml"}, 1256 ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)}, 1257 }, 1258 }, 1259 ProjectName: "something", 1260 ProjectSourceRepos: []string{"*"}, 1261 }) 1262 assert.NoError(t, err) 1263 }) 1264 1265 t.Run("Values file with absolute path stays within repo root", func(t *testing.T) { 1266 service := newService(t, "./testdata") 1267 _, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 1268 Repo: &argoappv1.Repository{}, 1269 AppName: "test", 1270 ApplicationSource: &argoappv1.ApplicationSource{ 1271 Path: "./my-chart", 1272 Helm: &argoappv1.ApplicationSourceHelm{ 1273 ValueFiles: []string{"/my-chart/my-chart-values.yaml"}, 1274 ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)}, 1275 }, 1276 }, 1277 ProjectName: "something", 1278 ProjectSourceRepos: []string{"*"}, 1279 }) 1280 assert.NoError(t, err) 1281 }) 1282 1283 t.Run("Values file with absolute path using back-references outside repo root", func(t *testing.T) { 1284 service := newService(t, "./testdata") 1285 _, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 1286 Repo: &argoappv1.Repository{}, 1287 AppName: "test", 1288 ApplicationSource: &argoappv1.ApplicationSource{ 1289 Path: "./my-chart", 1290 Helm: &argoappv1.ApplicationSourceHelm{ 1291 ValueFiles: []string{"/../../../my-chart-values.yaml"}, 1292 ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)}, 1293 }, 1294 }, 1295 ProjectName: "something", 1296 ProjectSourceRepos: []string{"*"}, 1297 }) 1298 assert.Error(t, err) 1299 assert.Contains(t, err.Error(), "outside repository root") 1300 }) 1301 1302 t.Run("Remote values file from forbidden protocol", func(t *testing.T) { 1303 service := newService(t, "./testdata") 1304 _, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 1305 Repo: &argoappv1.Repository{}, 1306 AppName: "test", 1307 ApplicationSource: &argoappv1.ApplicationSource{ 1308 Path: "./my-chart", 1309 Helm: &argoappv1.ApplicationSourceHelm{ 1310 ValueFiles: []string{"file://../../../../my-chart-values.yaml"}, 1311 ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)}, 1312 }, 1313 }, 1314 ProjectName: "something", 1315 ProjectSourceRepos: []string{"*"}, 1316 }) 1317 assert.Error(t, err) 1318 assert.Contains(t, err.Error(), "is not allowed") 1319 }) 1320 1321 t.Run("Remote values file from custom allowed protocol", func(t *testing.T) { 1322 service := newService(t, "./testdata") 1323 _, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 1324 Repo: &argoappv1.Repository{}, 1325 AppName: "test", 1326 ApplicationSource: &argoappv1.ApplicationSource{ 1327 Path: "./my-chart", 1328 Helm: &argoappv1.ApplicationSourceHelm{ 1329 ValueFiles: []string{"s3://my-bucket/my-chart-values.yaml"}, 1330 }, 1331 }, 1332 HelmOptions: &argoappv1.HelmOptions{ValuesFileSchemes: []string{"s3"}}, 1333 ProjectName: "something", 1334 ProjectSourceRepos: []string{"*"}, 1335 }) 1336 assert.Error(t, err) 1337 assert.Contains(t, err.Error(), "s3://my-bucket/my-chart-values.yaml: no such file or directory") 1338 }) 1339 } 1340 1341 // File parameter should not allow traversal outside of the repository root 1342 func TestGenerateHelmWithAbsoluteFileParameter(t *testing.T) { 1343 service := newService(t, "../..") 1344 1345 file, err := os.CreateTemp("", "external-secret.txt") 1346 assert.NoError(t, err) 1347 externalSecretPath := file.Name() 1348 defer func() { _ = os.RemoveAll(externalSecretPath) }() 1349 expectedFileContent, err := os.ReadFile("../../util/helm/testdata/external/external-secret.txt") 1350 assert.NoError(t, err) 1351 err = os.WriteFile(externalSecretPath, expectedFileContent, 0644) 1352 assert.NoError(t, err) 1353 defer func() { 1354 if err = file.Close(); err != nil { 1355 panic(err) 1356 } 1357 }() 1358 1359 _, err = service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 1360 Repo: &argoappv1.Repository{}, 1361 AppName: "test", 1362 ApplicationSource: &argoappv1.ApplicationSource{ 1363 Path: "./util/helm/testdata/redis", 1364 Helm: &argoappv1.ApplicationSourceHelm{ 1365 ValueFiles: []string{"values-production.yaml"}, 1366 ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)}, 1367 FileParameters: []argoappv1.HelmFileParameter{{ 1368 Name: "passwordContent", 1369 Path: externalSecretPath, 1370 }}, 1371 }, 1372 }, 1373 ProjectName: "something", 1374 ProjectSourceRepos: []string{"*"}, 1375 }) 1376 assert.Error(t, err) 1377 } 1378 1379 // The requested file parameter (`../external/external-secret.txt`) is outside the app path 1380 // (`./util/helm/testdata/redis`), however since the requested value is still under the repo 1381 // directory (`~/go/src/github.com/argoproj/argo-cd`), it is allowed. It is used as a means of 1382 // providing direct content to a helm chart via a specific key. 1383 func TestGenerateHelmWithFileParameter(t *testing.T) { 1384 service := newService(t, "../../util/helm/testdata") 1385 1386 res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 1387 Repo: &argoappv1.Repository{}, 1388 AppName: "test", 1389 ApplicationSource: &argoappv1.ApplicationSource{ 1390 Path: "./redis", 1391 Helm: &argoappv1.ApplicationSourceHelm{ 1392 ValueFiles: []string{"values-production.yaml"}, 1393 Values: `cluster: {slaveCount: 10}`, 1394 ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)}, 1395 FileParameters: []argoappv1.HelmFileParameter{{ 1396 Name: "passwordContent", 1397 Path: "../external/external-secret.txt", 1398 }}, 1399 }, 1400 }, 1401 ProjectName: "something", 1402 ProjectSourceRepos: []string{"*"}, 1403 }) 1404 assert.NoError(t, err) 1405 assert.Contains(t, res.Manifests[6], `"replicas":2`, "ValuesObject should override Values") 1406 } 1407 1408 func TestGenerateNullList(t *testing.T) { 1409 service := newService(t, ".") 1410 1411 t.Run("null list", func(t *testing.T) { 1412 res1, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 1413 Repo: &argoappv1.Repository{}, 1414 ApplicationSource: &argoappv1.ApplicationSource{Path: "./testdata/null-list"}, 1415 NoCache: true, 1416 ProjectName: "something", 1417 ProjectSourceRepos: []string{"*"}, 1418 }) 1419 assert.Nil(t, err) 1420 assert.Equal(t, len(res1.Manifests), 1) 1421 assert.Contains(t, res1.Manifests[0], "prometheus-operator-operator") 1422 }) 1423 1424 t.Run("empty list", func(t *testing.T) { 1425 res1, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 1426 Repo: &argoappv1.Repository{}, 1427 ApplicationSource: &argoappv1.ApplicationSource{Path: "./testdata/empty-list"}, 1428 NoCache: true, 1429 ProjectName: "something", 1430 ProjectSourceRepos: []string{"*"}, 1431 }) 1432 assert.Nil(t, err) 1433 assert.Equal(t, len(res1.Manifests), 1) 1434 assert.Contains(t, res1.Manifests[0], "prometheus-operator-operator") 1435 }) 1436 1437 t.Run("weird list", func(t *testing.T) { 1438 res1, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 1439 Repo: &argoappv1.Repository{}, 1440 ApplicationSource: &argoappv1.ApplicationSource{Path: "./testdata/weird-list"}, 1441 NoCache: true, 1442 ProjectName: "something", 1443 ProjectSourceRepos: []string{"*"}, 1444 }) 1445 assert.Nil(t, err) 1446 assert.Len(t, res1.Manifests, 2) 1447 }) 1448 } 1449 1450 func TestIdentifyAppSourceTypeByAppDirWithKustomizations(t *testing.T) { 1451 sourceType, err := GetAppSourceType(context.Background(), &argoappv1.ApplicationSource{}, "./testdata/kustomization_yaml", "./testdata", "testapp", map[string]bool{}, []string{}) 1452 assert.Nil(t, err) 1453 assert.Equal(t, argoappv1.ApplicationSourceTypeKustomize, sourceType) 1454 1455 sourceType, err = GetAppSourceType(context.Background(), &argoappv1.ApplicationSource{}, "./testdata/kustomization_yml", "./testdata", "testapp", map[string]bool{}, []string{}) 1456 assert.Nil(t, err) 1457 assert.Equal(t, argoappv1.ApplicationSourceTypeKustomize, sourceType) 1458 1459 sourceType, err = GetAppSourceType(context.Background(), &argoappv1.ApplicationSource{}, "./testdata/Kustomization", "./testdata", "testapp", map[string]bool{}, []string{}) 1460 assert.Nil(t, err) 1461 assert.Equal(t, argoappv1.ApplicationSourceTypeKustomize, sourceType) 1462 } 1463 1464 func TestGenerateFromUTF16(t *testing.T) { 1465 q := apiclient.ManifestRequest{ 1466 Repo: &argoappv1.Repository{}, 1467 ApplicationSource: &argoappv1.ApplicationSource{}, 1468 ProjectName: "something", 1469 ProjectSourceRepos: []string{"*"}, 1470 } 1471 res1, err := GenerateManifests(context.Background(), "./testdata/utf-16", "/", "", &q, false, &git.NoopCredsStore{}, resource.MustParse("0"), nil) 1472 assert.Nil(t, err) 1473 assert.Equal(t, 2, len(res1.Manifests)) 1474 } 1475 1476 func TestListApps(t *testing.T) { 1477 service := newService(t, "./testdata") 1478 1479 res, err := service.ListApps(context.Background(), &apiclient.ListAppsRequest{Repo: &argoappv1.Repository{}}) 1480 assert.NoError(t, err) 1481 1482 expectedApps := map[string]string{ 1483 "Kustomization": "Kustomize", 1484 "app-parameters/multi": "Kustomize", 1485 "app-parameters/single-app-only": "Kustomize", 1486 "app-parameters/single-global": "Kustomize", 1487 "app-parameters/single-global-helm": "Helm", 1488 "in-bounds-values-file-link": "Helm", 1489 "invalid-helm": "Helm", 1490 "invalid-kustomize": "Kustomize", 1491 "kustomization_yaml": "Kustomize", 1492 "kustomization_yml": "Kustomize", 1493 "my-chart": "Helm", 1494 "my-chart-2": "Helm", 1495 "oci-dependencies": "Helm", 1496 "out-of-bounds-values-file-link": "Helm", 1497 "values-files": "Helm", 1498 "helm-with-dependencies": "Helm", 1499 "helm-with-dependencies-alias": "Helm", 1500 } 1501 assert.Equal(t, expectedApps, res.Apps) 1502 } 1503 1504 func TestGetAppDetailsHelm(t *testing.T) { 1505 service := newService(t, "../../util/helm/testdata/dependency") 1506 1507 res, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{ 1508 Repo: &argoappv1.Repository{}, 1509 Source: &argoappv1.ApplicationSource{ 1510 Path: ".", 1511 }, 1512 }) 1513 1514 assert.NoError(t, err) 1515 assert.NotNil(t, res.Helm) 1516 1517 assert.Equal(t, "Helm", res.Type) 1518 assert.EqualValues(t, []string{"values-production.yaml", "values.yaml"}, res.Helm.ValueFiles) 1519 } 1520 1521 func TestGetAppDetailsHelmUsesCache(t *testing.T) { 1522 service := newService(t, "../../util/helm/testdata/dependency") 1523 1524 res, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{ 1525 Repo: &argoappv1.Repository{}, 1526 Source: &argoappv1.ApplicationSource{ 1527 Path: ".", 1528 }, 1529 }) 1530 1531 assert.NoError(t, err) 1532 assert.NotNil(t, res.Helm) 1533 1534 assert.Equal(t, "Helm", res.Type) 1535 assert.EqualValues(t, []string{"values-production.yaml", "values.yaml"}, res.Helm.ValueFiles) 1536 } 1537 1538 func TestGetAppDetailsHelm_WithNoValuesFile(t *testing.T) { 1539 service := newService(t, "../../util/helm/testdata/api-versions") 1540 1541 res, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{ 1542 Repo: &argoappv1.Repository{}, 1543 Source: &argoappv1.ApplicationSource{ 1544 Path: ".", 1545 }, 1546 }) 1547 1548 assert.NoError(t, err) 1549 assert.NotNil(t, res.Helm) 1550 1551 assert.Equal(t, "Helm", res.Type) 1552 assert.Empty(t, res.Helm.ValueFiles) 1553 assert.Equal(t, "", res.Helm.Values) 1554 } 1555 1556 func TestGetAppDetailsKustomize(t *testing.T) { 1557 service := newService(t, "../../util/kustomize/testdata/kustomization_yaml") 1558 1559 res, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{ 1560 Repo: &argoappv1.Repository{}, 1561 Source: &argoappv1.ApplicationSource{ 1562 Path: ".", 1563 }, 1564 }) 1565 1566 assert.NoError(t, err) 1567 1568 assert.Equal(t, "Kustomize", res.Type) 1569 assert.NotNil(t, res.Kustomize) 1570 assert.EqualValues(t, []string{"nginx:1.15.4", "registry.k8s.io/nginx-slim:0.8"}, res.Kustomize.Images) 1571 } 1572 1573 func TestGetHelmCharts(t *testing.T) { 1574 service := newService(t, "../..") 1575 res, err := service.GetHelmCharts(context.Background(), &apiclient.HelmChartsRequest{Repo: &argoappv1.Repository{}}) 1576 1577 // fix flakiness 1578 sort.Slice(res.Items, func(i, j int) bool { 1579 return res.Items[i].Name < res.Items[j].Name 1580 }) 1581 1582 assert.NoError(t, err) 1583 assert.Len(t, res.Items, 2) 1584 1585 item := res.Items[0] 1586 assert.Equal(t, "my-chart", item.Name) 1587 assert.EqualValues(t, []string{"1.0.0", "1.1.0"}, item.Versions) 1588 1589 item2 := res.Items[1] 1590 assert.Equal(t, "out-of-bounds-chart", item2.Name) 1591 assert.EqualValues(t, []string{"1.0.0", "1.1.0"}, item2.Versions) 1592 } 1593 1594 func TestGetRevisionMetadata(t *testing.T) { 1595 service, gitClient, _ := newServiceWithMocks(t, "../..", false) 1596 now := time.Now() 1597 1598 gitClient.On("RevisionMetadata", mock.Anything).Return(&git.RevisionMetadata{ 1599 Message: "test", 1600 Author: "author", 1601 Date: now, 1602 Tags: []string{"tag1", "tag2"}, 1603 }, nil) 1604 1605 res, err := service.GetRevisionMetadata(context.Background(), &apiclient.RepoServerRevisionMetadataRequest{ 1606 Repo: &argoappv1.Repository{}, 1607 Revision: "c0b400fc458875d925171398f9ba9eabd5529923", 1608 CheckSignature: true, 1609 }) 1610 1611 assert.NoError(t, err) 1612 assert.Equal(t, "test", res.Message) 1613 assert.Equal(t, now, res.Date.Time) 1614 assert.Equal(t, "author", res.Author) 1615 assert.EqualValues(t, []string{"tag1", "tag2"}, res.Tags) 1616 assert.NotEmpty(t, res.SignatureInfo) 1617 1618 // Check for truncated revision value 1619 res, err = service.GetRevisionMetadata(context.Background(), &apiclient.RepoServerRevisionMetadataRequest{ 1620 Repo: &argoappv1.Repository{}, 1621 Revision: "c0b400f", 1622 CheckSignature: true, 1623 }) 1624 1625 assert.NoError(t, err) 1626 assert.Equal(t, "test", res.Message) 1627 assert.Equal(t, now, res.Date.Time) 1628 assert.Equal(t, "author", res.Author) 1629 assert.EqualValues(t, []string{"tag1", "tag2"}, res.Tags) 1630 assert.NotEmpty(t, res.SignatureInfo) 1631 1632 // Cache hit - signature info should not be in result 1633 res, err = service.GetRevisionMetadata(context.Background(), &apiclient.RepoServerRevisionMetadataRequest{ 1634 Repo: &argoappv1.Repository{}, 1635 Revision: "c0b400fc458875d925171398f9ba9eabd5529923", 1636 CheckSignature: false, 1637 }) 1638 assert.NoError(t, err) 1639 assert.Empty(t, res.SignatureInfo) 1640 1641 // Enforce cache miss - signature info should not be in result 1642 res, err = service.GetRevisionMetadata(context.Background(), &apiclient.RepoServerRevisionMetadataRequest{ 1643 Repo: &argoappv1.Repository{}, 1644 Revision: "da52afd3b2df1ec49470603d8bbb46954dab1091", 1645 CheckSignature: false, 1646 }) 1647 assert.NoError(t, err) 1648 assert.Empty(t, res.SignatureInfo) 1649 1650 // Cache hit on previous entry that did not have signature info 1651 res, err = service.GetRevisionMetadata(context.Background(), &apiclient.RepoServerRevisionMetadataRequest{ 1652 Repo: &argoappv1.Repository{}, 1653 Revision: "da52afd3b2df1ec49470603d8bbb46954dab1091", 1654 CheckSignature: true, 1655 }) 1656 assert.NoError(t, err) 1657 assert.NotEmpty(t, res.SignatureInfo) 1658 } 1659 1660 func TestGetSignatureVerificationResult(t *testing.T) { 1661 // Commit with signature and verification requested 1662 { 1663 service := newServiceWithSignature(t, "../../manifests/base") 1664 1665 src := argoappv1.ApplicationSource{Path: "."} 1666 q := apiclient.ManifestRequest{ 1667 Repo: &argoappv1.Repository{}, 1668 ApplicationSource: &src, 1669 VerifySignature: true, 1670 ProjectName: "something", 1671 ProjectSourceRepos: []string{"*"}, 1672 } 1673 1674 res, err := service.GenerateManifest(context.Background(), &q) 1675 assert.NoError(t, err) 1676 assert.Equal(t, testSignature, res.VerifyResult) 1677 } 1678 // Commit with signature and verification not requested 1679 { 1680 service := newServiceWithSignature(t, "../../manifests/base") 1681 1682 src := argoappv1.ApplicationSource{Path: "."} 1683 q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, ProjectName: "something", 1684 ProjectSourceRepos: []string{"*"}} 1685 1686 res, err := service.GenerateManifest(context.Background(), &q) 1687 assert.NoError(t, err) 1688 assert.Empty(t, res.VerifyResult) 1689 } 1690 // Commit without signature and verification requested 1691 { 1692 service := newService(t, "../../manifests/base") 1693 1694 src := argoappv1.ApplicationSource{Path: "."} 1695 q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, VerifySignature: true, ProjectName: "something", 1696 ProjectSourceRepos: []string{"*"}} 1697 1698 res, err := service.GenerateManifest(context.Background(), &q) 1699 assert.NoError(t, err) 1700 assert.Empty(t, res.VerifyResult) 1701 } 1702 // Commit without signature and verification not requested 1703 { 1704 service := newService(t, "../../manifests/base") 1705 1706 src := argoappv1.ApplicationSource{Path: "."} 1707 q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, VerifySignature: true, ProjectName: "something", 1708 ProjectSourceRepos: []string{"*"}} 1709 1710 res, err := service.GenerateManifest(context.Background(), &q) 1711 assert.NoError(t, err) 1712 assert.Empty(t, res.VerifyResult) 1713 } 1714 } 1715 1716 func Test_newEnv(t *testing.T) { 1717 assert.Equal(t, &argoappv1.Env{ 1718 &argoappv1.EnvEntry{Name: "ARGOCD_APP_NAME", Value: "my-app-name"}, 1719 &argoappv1.EnvEntry{Name: "ARGOCD_APP_NAMESPACE", Value: "my-namespace"}, 1720 &argoappv1.EnvEntry{Name: "ARGOCD_APP_REVISION", Value: "my-revision"}, 1721 &argoappv1.EnvEntry{Name: "ARGOCD_APP_REVISION_SHORT", Value: "my-revi"}, 1722 &argoappv1.EnvEntry{Name: "ARGOCD_APP_SOURCE_REPO_URL", Value: "https://github.com/my-org/my-repo"}, 1723 &argoappv1.EnvEntry{Name: "ARGOCD_APP_SOURCE_PATH", Value: "my-path"}, 1724 &argoappv1.EnvEntry{Name: "ARGOCD_APP_SOURCE_TARGET_REVISION", Value: "my-target-revision"}, 1725 }, newEnv(&apiclient.ManifestRequest{ 1726 AppName: "my-app-name", 1727 Namespace: "my-namespace", 1728 Repo: &argoappv1.Repository{Repo: "https://github.com/my-org/my-repo"}, 1729 ApplicationSource: &argoappv1.ApplicationSource{ 1730 Path: "my-path", 1731 TargetRevision: "my-target-revision", 1732 }, 1733 }, "my-revision")) 1734 } 1735 1736 func TestService_newHelmClientResolveRevision(t *testing.T) { 1737 service := newService(t, ".") 1738 1739 t.Run("EmptyRevision", func(t *testing.T) { 1740 _, _, err := service.newHelmClientResolveRevision(&argoappv1.Repository{}, "", "", true) 1741 assert.EqualError(t, err, "invalid revision '': improper constraint: ") 1742 }) 1743 t.Run("InvalidRevision", func(t *testing.T) { 1744 _, _, err := service.newHelmClientResolveRevision(&argoappv1.Repository{}, "???", "", true) 1745 assert.EqualError(t, err, "invalid revision '???': improper constraint: ???", true) 1746 }) 1747 } 1748 1749 func TestGetAppDetailsWithAppParameterFile(t *testing.T) { 1750 t.Run("No app name set and app specific file exists", func(t *testing.T) { 1751 service := newService(t, ".") 1752 runWithTempTestdata(t, "multi", func(t *testing.T, path string) { 1753 details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{ 1754 Repo: &argoappv1.Repository{}, 1755 Source: &argoappv1.ApplicationSource{ 1756 Path: path, 1757 }, 1758 }) 1759 require.NoError(t, err) 1760 assert.EqualValues(t, []string{"gcr.io/heptio-images/ks-guestbook-demo:0.2"}, details.Kustomize.Images) 1761 }) 1762 }) 1763 t.Run("No app specific override", func(t *testing.T) { 1764 service := newService(t, ".") 1765 runWithTempTestdata(t, "single-global", func(t *testing.T, path string) { 1766 details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{ 1767 Repo: &argoappv1.Repository{}, 1768 Source: &argoappv1.ApplicationSource{ 1769 Path: path, 1770 }, 1771 AppName: "testapp", 1772 }) 1773 require.NoError(t, err) 1774 assert.EqualValues(t, []string{"gcr.io/heptio-images/ks-guestbook-demo:0.2"}, details.Kustomize.Images) 1775 }) 1776 }) 1777 t.Run("Only app specific override", func(t *testing.T) { 1778 service := newService(t, ".") 1779 runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) { 1780 details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{ 1781 Repo: &argoappv1.Repository{}, 1782 Source: &argoappv1.ApplicationSource{ 1783 Path: path, 1784 }, 1785 AppName: "testapp", 1786 }) 1787 require.NoError(t, err) 1788 assert.EqualValues(t, []string{"gcr.io/heptio-images/ks-guestbook-demo:0.3"}, details.Kustomize.Images) 1789 }) 1790 }) 1791 t.Run("App specific override", func(t *testing.T) { 1792 service := newService(t, ".") 1793 runWithTempTestdata(t, "multi", func(t *testing.T, path string) { 1794 details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{ 1795 Repo: &argoappv1.Repository{}, 1796 Source: &argoappv1.ApplicationSource{ 1797 Path: path, 1798 }, 1799 AppName: "testapp", 1800 }) 1801 require.NoError(t, err) 1802 assert.EqualValues(t, []string{"gcr.io/heptio-images/ks-guestbook-demo:0.3"}, details.Kustomize.Images) 1803 }) 1804 }) 1805 t.Run("App specific overrides containing non-mergeable field", func(t *testing.T) { 1806 service := newService(t, ".") 1807 runWithTempTestdata(t, "multi", func(t *testing.T, path string) { 1808 details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{ 1809 Repo: &argoappv1.Repository{}, 1810 Source: &argoappv1.ApplicationSource{ 1811 Path: path, 1812 }, 1813 AppName: "unmergeable", 1814 }) 1815 require.NoError(t, err) 1816 assert.EqualValues(t, []string{"gcr.io/heptio-images/ks-guestbook-demo:0.3"}, details.Kustomize.Images) 1817 }) 1818 }) 1819 t.Run("Broken app-specific overrides", func(t *testing.T) { 1820 service := newService(t, ".") 1821 runWithTempTestdata(t, "multi", func(t *testing.T, path string) { 1822 _, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{ 1823 Repo: &argoappv1.Repository{}, 1824 Source: &argoappv1.ApplicationSource{ 1825 Path: path, 1826 }, 1827 AppName: "broken", 1828 }) 1829 assert.Error(t, err) 1830 }) 1831 }) 1832 } 1833 1834 // There are unit test that will use kustomize set and by that modify the 1835 // kustomization.yaml. For proper testing, we need to copy the testdata to a 1836 // temporary path, run the tests, and then throw the copy away again. 1837 func mkTempParameters(source string) string { 1838 tempDir, err := os.MkdirTemp("./testdata", "app-parameters") 1839 if err != nil { 1840 panic(err) 1841 } 1842 cmd := exec.Command("cp", "-R", source, tempDir) 1843 err = cmd.Run() 1844 if err != nil { 1845 os.RemoveAll(tempDir) 1846 panic(err) 1847 } 1848 return tempDir 1849 } 1850 1851 // Simple wrapper run a test with a temporary copy of the testdata, because 1852 // the test would modify the data when run. 1853 func runWithTempTestdata(t *testing.T, path string, runner func(t *testing.T, path string)) { 1854 tempDir := mkTempParameters("./testdata/app-parameters") 1855 runner(t, filepath.Join(tempDir, "app-parameters", path)) 1856 os.RemoveAll(tempDir) 1857 } 1858 1859 func TestGenerateManifestsWithAppParameterFile(t *testing.T) { 1860 t.Run("Single global override", func(t *testing.T) { 1861 runWithTempTestdata(t, "single-global", func(t *testing.T, path string) { 1862 service := newService(t, ".") 1863 manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 1864 Repo: &argoappv1.Repository{}, 1865 ApplicationSource: &argoappv1.ApplicationSource{ 1866 Path: path, 1867 }, 1868 ProjectName: "something", 1869 ProjectSourceRepos: []string{"*"}, 1870 }) 1871 require.NoError(t, err) 1872 resourceByKindName := make(map[string]*unstructured.Unstructured) 1873 for _, manifest := range manifests.Manifests { 1874 var un unstructured.Unstructured 1875 err := yaml.Unmarshal([]byte(manifest), &un) 1876 if !assert.NoError(t, err) { 1877 return 1878 } 1879 resourceByKindName[fmt.Sprintf("%s/%s", un.GetKind(), un.GetName())] = &un 1880 } 1881 deployment, ok := resourceByKindName["Deployment/guestbook-ui"] 1882 require.True(t, ok) 1883 containers, ok, _ := unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "containers") 1884 require.True(t, ok) 1885 image, ok, _ := unstructured.NestedString(containers[0].(map[string]interface{}), "image") 1886 require.True(t, ok) 1887 assert.Equal(t, "gcr.io/heptio-images/ks-guestbook-demo:0.2", image) 1888 }) 1889 }) 1890 1891 t.Run("Single global override Helm", func(t *testing.T) { 1892 runWithTempTestdata(t, "single-global-helm", func(t *testing.T, path string) { 1893 service := newService(t, ".") 1894 manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 1895 Repo: &argoappv1.Repository{}, 1896 ApplicationSource: &argoappv1.ApplicationSource{ 1897 Path: path, 1898 }, 1899 ProjectName: "something", 1900 ProjectSourceRepos: []string{"*"}, 1901 }) 1902 require.NoError(t, err) 1903 resourceByKindName := make(map[string]*unstructured.Unstructured) 1904 for _, manifest := range manifests.Manifests { 1905 var un unstructured.Unstructured 1906 err := yaml.Unmarshal([]byte(manifest), &un) 1907 if !assert.NoError(t, err) { 1908 return 1909 } 1910 resourceByKindName[fmt.Sprintf("%s/%s", un.GetKind(), un.GetName())] = &un 1911 } 1912 deployment, ok := resourceByKindName["Deployment/guestbook-ui"] 1913 require.True(t, ok) 1914 containers, ok, _ := unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "containers") 1915 require.True(t, ok) 1916 image, ok, _ := unstructured.NestedString(containers[0].(map[string]interface{}), "image") 1917 require.True(t, ok) 1918 assert.Equal(t, "gcr.io/heptio-images/ks-guestbook-demo:0.2", image) 1919 }) 1920 }) 1921 1922 t.Run("Application specific override", func(t *testing.T) { 1923 service := newService(t, ".") 1924 runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) { 1925 manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 1926 Repo: &argoappv1.Repository{}, 1927 ApplicationSource: &argoappv1.ApplicationSource{ 1928 Path: path, 1929 }, 1930 AppName: "testapp", 1931 ProjectName: "something", 1932 ProjectSourceRepos: []string{"*"}, 1933 }) 1934 require.NoError(t, err) 1935 resourceByKindName := make(map[string]*unstructured.Unstructured) 1936 for _, manifest := range manifests.Manifests { 1937 var un unstructured.Unstructured 1938 err := yaml.Unmarshal([]byte(manifest), &un) 1939 if !assert.NoError(t, err) { 1940 return 1941 } 1942 resourceByKindName[fmt.Sprintf("%s/%s", un.GetKind(), un.GetName())] = &un 1943 } 1944 deployment, ok := resourceByKindName["Deployment/guestbook-ui"] 1945 require.True(t, ok) 1946 containers, ok, _ := unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "containers") 1947 require.True(t, ok) 1948 image, ok, _ := unstructured.NestedString(containers[0].(map[string]interface{}), "image") 1949 require.True(t, ok) 1950 assert.Equal(t, "gcr.io/heptio-images/ks-guestbook-demo:0.3", image) 1951 }) 1952 }) 1953 1954 t.Run("Multi-source with source as ref only does not generate manifests", func(t *testing.T) { 1955 service := newService(t, ".") 1956 runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) { 1957 manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 1958 Repo: &argoappv1.Repository{}, 1959 ApplicationSource: &argoappv1.ApplicationSource{ 1960 Path: "", 1961 Chart: "", 1962 Ref: "test", 1963 }, 1964 AppName: "testapp-multi-ref-only", 1965 ProjectName: "something", 1966 ProjectSourceRepos: []string{"*"}, 1967 HasMultipleSources: true, 1968 }) 1969 assert.NoError(t, err) 1970 assert.Empty(t, manifests.Manifests) 1971 assert.NotEmpty(t, manifests.Revision) 1972 }) 1973 }) 1974 1975 t.Run("Application specific override for other app", func(t *testing.T) { 1976 service := newService(t, ".") 1977 runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) { 1978 manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 1979 Repo: &argoappv1.Repository{}, 1980 ApplicationSource: &argoappv1.ApplicationSource{ 1981 Path: path, 1982 }, 1983 AppName: "testapp2", 1984 ProjectName: "something", 1985 ProjectSourceRepos: []string{"*"}, 1986 }) 1987 require.NoError(t, err) 1988 resourceByKindName := make(map[string]*unstructured.Unstructured) 1989 for _, manifest := range manifests.Manifests { 1990 var un unstructured.Unstructured 1991 err := yaml.Unmarshal([]byte(manifest), &un) 1992 if !assert.NoError(t, err) { 1993 return 1994 } 1995 resourceByKindName[fmt.Sprintf("%s/%s", un.GetKind(), un.GetName())] = &un 1996 } 1997 deployment, ok := resourceByKindName["Deployment/guestbook-ui"] 1998 require.True(t, ok) 1999 containers, ok, _ := unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "containers") 2000 require.True(t, ok) 2001 image, ok, _ := unstructured.NestedString(containers[0].(map[string]interface{}), "image") 2002 require.True(t, ok) 2003 assert.Equal(t, "gcr.io/heptio-images/ks-guestbook-demo:0.1", image) 2004 }) 2005 }) 2006 2007 t.Run("Override info does not appear in cache key", func(t *testing.T) { 2008 service := newService(t, ".") 2009 runWithTempTestdata(t, "single-global", func(t *testing.T, path string) { 2010 source := &argoappv1.ApplicationSource{ 2011 Path: path, 2012 } 2013 sourceCopy := source.DeepCopy() // make a copy in case GenerateManifest mutates it. 2014 _, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ 2015 Repo: &argoappv1.Repository{}, 2016 ApplicationSource: sourceCopy, 2017 AppName: "test", 2018 ProjectName: "something", 2019 ProjectSourceRepos: []string{"*"}, 2020 }) 2021 assert.NoError(t, err) 2022 res := &cache.CachedManifestResponse{} 2023 // Try to pull from the cache with a `source` that does not include any overrides. Overrides should not be 2024 // part of the cache key, because you can't get the overrides without a repo operation. And avoiding repo 2025 // operations is the point of the cache. 2026 err = service.cache.GetManifests(mock.Anything, source, argoappv1.RefTargetRevisionMapping{}, &argoappv1.ClusterInfo{}, "", "", "", "test", res, nil) 2027 assert.NoError(t, err) 2028 }) 2029 }) 2030 } 2031 2032 func TestGenerateManifestWithAnnotatedAndRegularGitTagHashes(t *testing.T) { 2033 regularGitTagHash := "632039659e542ed7de0c170a4fcc1c571b288fc0" 2034 annotatedGitTaghash := "95249be61b028d566c29d47b19e65c5603388a41" 2035 invalidGitTaghash := "invalid-tag" 2036 actualCommitSHA := "632039659e542ed7de0c170a4fcc1c571b288fc0" 2037 2038 tests := []struct { 2039 name string 2040 ctx context.Context 2041 manifestRequest *apiclient.ManifestRequest 2042 wantError bool 2043 service *Service 2044 }{ 2045 { 2046 name: "Case: Git tag hash matches latest commit SHA (regular tag)", 2047 ctx: context.Background(), 2048 manifestRequest: &apiclient.ManifestRequest{ 2049 Repo: &argoappv1.Repository{}, 2050 ApplicationSource: &argoappv1.ApplicationSource{ 2051 TargetRevision: regularGitTagHash, 2052 }, 2053 NoCache: true, 2054 ProjectName: "something", 2055 ProjectSourceRepos: []string{"*"}, 2056 }, 2057 wantError: false, 2058 service: newServiceWithCommitSHA(t, ".", regularGitTagHash), 2059 }, 2060 2061 { 2062 name: "Case: Git tag hash does not match latest commit SHA (annotated tag)", 2063 ctx: context.Background(), 2064 manifestRequest: &apiclient.ManifestRequest{ 2065 Repo: &argoappv1.Repository{}, 2066 ApplicationSource: &argoappv1.ApplicationSource{ 2067 TargetRevision: annotatedGitTaghash, 2068 }, 2069 NoCache: true, 2070 ProjectName: "something", 2071 ProjectSourceRepos: []string{"*"}, 2072 }, 2073 wantError: false, 2074 service: newServiceWithCommitSHA(t, ".", annotatedGitTaghash), 2075 }, 2076 2077 { 2078 name: "Case: Git tag hash is invalid", 2079 ctx: context.Background(), 2080 manifestRequest: &apiclient.ManifestRequest{ 2081 Repo: &argoappv1.Repository{}, 2082 ApplicationSource: &argoappv1.ApplicationSource{ 2083 TargetRevision: invalidGitTaghash, 2084 }, 2085 NoCache: true, 2086 ProjectName: "something", 2087 ProjectSourceRepos: []string{"*"}, 2088 }, 2089 wantError: true, 2090 service: newServiceWithCommitSHA(t, ".", invalidGitTaghash), 2091 }, 2092 } 2093 for _, tt := range tests { 2094 t.Run(tt.name, func(t *testing.T) { 2095 manifestResponse, err := tt.service.GenerateManifest(tt.ctx, tt.manifestRequest) 2096 if !tt.wantError { 2097 if err == nil { 2098 assert.Equal(t, manifestResponse.Revision, actualCommitSHA) 2099 } else { 2100 t.Errorf("unexpected error") 2101 } 2102 } else { 2103 if err == nil { 2104 t.Errorf("expected an error but did not throw one") 2105 } 2106 } 2107 2108 }) 2109 } 2110 } 2111 2112 func TestGenerateManifestWithAnnotatedTagsAndMultiSourceApp(t *testing.T) { 2113 annotatedGitTaghash := "95249be61b028d566c29d47b19e65c5603388a41" 2114 2115 service := newServiceWithCommitSHA(t, ".", annotatedGitTaghash) 2116 2117 refSources := map[string]*argoappv1.RefTarget{} 2118 2119 refSources["$global"] = &argoappv1.RefTarget{ 2120 TargetRevision: annotatedGitTaghash, 2121 } 2122 2123 refSources["$default"] = &argoappv1.RefTarget{ 2124 TargetRevision: annotatedGitTaghash, 2125 } 2126 2127 manifestRequest := &apiclient.ManifestRequest{ 2128 Repo: &argoappv1.Repository{}, 2129 ApplicationSource: &argoappv1.ApplicationSource{ 2130 TargetRevision: annotatedGitTaghash, 2131 Helm: &argoappv1.ApplicationSourceHelm{ 2132 ValueFiles: []string{"$global/values.yaml", "$default/secrets.yaml"}, 2133 }, 2134 }, 2135 HasMultipleSources: true, 2136 NoCache: true, 2137 RefSources: refSources, 2138 } 2139 2140 response, err := service.GenerateManifest(context.Background(), manifestRequest) 2141 if err != nil { 2142 t.Errorf("unexpected %s", err) 2143 } 2144 2145 if response.Revision != annotatedGitTaghash { 2146 t.Errorf("returned SHA %s is different from expected annotated tag %s", response.Revision, annotatedGitTaghash) 2147 } 2148 } 2149 2150 func TestFindResources(t *testing.T) { 2151 testCases := []struct { 2152 name string 2153 include string 2154 exclude string 2155 expectedNames []string 2156 }{{ 2157 name: "Include One Match", 2158 include: "subdir/deploymentSub.yaml", 2159 expectedNames: []string{"nginx-deployment-sub"}, 2160 }, { 2161 name: "Include Everything", 2162 include: "*.yaml", 2163 expectedNames: []string{"nginx-deployment", "nginx-deployment-sub"}, 2164 }, { 2165 name: "Include Subdirectory", 2166 include: "**/*.yaml", 2167 expectedNames: []string{"nginx-deployment-sub"}, 2168 }, { 2169 name: "Include No Matches", 2170 include: "nothing.yaml", 2171 expectedNames: []string{}, 2172 }, { 2173 name: "Exclude - One Match", 2174 exclude: "subdir/deploymentSub.yaml", 2175 expectedNames: []string{"nginx-deployment"}, 2176 }, { 2177 name: "Exclude - Everything", 2178 exclude: "*.yaml", 2179 expectedNames: []string{}, 2180 }} 2181 for i := range testCases { 2182 tc := testCases[i] 2183 t.Run(tc.name, func(t *testing.T) { 2184 objs, err := findManifests(&log.Entry{}, "testdata/app-include-exclude", ".", nil, argoappv1.ApplicationSourceDirectory{ 2185 Recurse: true, 2186 Include: tc.include, 2187 Exclude: tc.exclude, 2188 }, map[string]bool{}, resource.MustParse("0")) 2189 if !assert.NoError(t, err) { 2190 return 2191 } 2192 var names []string 2193 for i := range objs { 2194 names = append(names, objs[i].GetName()) 2195 } 2196 assert.ElementsMatch(t, tc.expectedNames, names) 2197 }) 2198 } 2199 } 2200 2201 func TestFindManifests_Exclude(t *testing.T) { 2202 objs, err := findManifests(&log.Entry{}, "testdata/app-include-exclude", ".", nil, argoappv1.ApplicationSourceDirectory{ 2203 Recurse: true, 2204 Exclude: "subdir/deploymentSub.yaml", 2205 }, map[string]bool{}, resource.MustParse("0")) 2206 2207 if !assert.NoError(t, err) || !assert.Len(t, objs, 1) { 2208 return 2209 } 2210 2211 assert.Equal(t, "nginx-deployment", objs[0].GetName()) 2212 } 2213 2214 func TestFindManifests_Exclude_NothingMatches(t *testing.T) { 2215 objs, err := findManifests(&log.Entry{}, "testdata/app-include-exclude", ".", nil, argoappv1.ApplicationSourceDirectory{ 2216 Recurse: true, 2217 Exclude: "nothing.yaml", 2218 }, map[string]bool{}, resource.MustParse("0")) 2219 2220 if !assert.NoError(t, err) || !assert.Len(t, objs, 2) { 2221 return 2222 } 2223 2224 assert.ElementsMatch(t, 2225 []string{"nginx-deployment", "nginx-deployment-sub"}, []string{objs[0].GetName(), objs[1].GetName()}) 2226 } 2227 2228 func tempDir(t *testing.T) string { 2229 dir, err := os.MkdirTemp(".", "") 2230 require.NoError(t, err) 2231 t.Cleanup(func() { 2232 err = os.RemoveAll(dir) 2233 if err != nil { 2234 panic(err) 2235 } 2236 }) 2237 absDir, err := filepath.Abs(dir) 2238 require.NoError(t, err) 2239 return absDir 2240 } 2241 2242 func walkFor(t *testing.T, root string, testPath string, run func(info fs.FileInfo)) { 2243 var hitExpectedPath = false 2244 err := filepath.Walk(root, func(path string, info fs.FileInfo, err error) error { 2245 if path == testPath { 2246 require.NoError(t, err) 2247 hitExpectedPath = true 2248 run(info) 2249 } 2250 return nil 2251 }) 2252 require.NoError(t, err) 2253 assert.True(t, hitExpectedPath, "did not hit expected path when walking directory") 2254 } 2255 2256 func Test_getPotentiallyValidManifestFile(t *testing.T) { 2257 // These tests use filepath.Walk instead of os.Stat to get file info, because FileInfo from os.Stat does not return 2258 // true for IsSymlink like os.Walk does. 2259 2260 // These tests do not use t.TempDir() because those directories can contain symlinks which cause test to fail 2261 // InBound checks. 2262 2263 t.Run("non-JSON/YAML is skipped with an empty ignore message", func(t *testing.T) { 2264 appDir := tempDir(t) 2265 filePath := filepath.Join(appDir, "not-json-or-yaml") 2266 file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0644) 2267 require.NoError(t, err) 2268 err = file.Close() 2269 require.NoError(t, err) 2270 2271 walkFor(t, appDir, filePath, func(info fs.FileInfo) { 2272 realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(filePath, info, appDir, appDir, "", "") 2273 assert.Nil(t, realFileInfo) 2274 assert.Empty(t, ignoreMessage) 2275 assert.NoError(t, err) 2276 }) 2277 }) 2278 2279 t.Run("circular link should throw an error", func(t *testing.T) { 2280 appDir := tempDir(t) 2281 2282 aPath := filepath.Join(appDir, "a.json") 2283 bPath := filepath.Join(appDir, "b.json") 2284 err := os.Symlink(bPath, aPath) 2285 require.NoError(t, err) 2286 err = os.Symlink(aPath, bPath) 2287 require.NoError(t, err) 2288 2289 walkFor(t, appDir, aPath, func(info fs.FileInfo) { 2290 realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(aPath, info, appDir, appDir, "", "") 2291 assert.Nil(t, realFileInfo) 2292 assert.Empty(t, ignoreMessage) 2293 assert.ErrorContains(t, err, "too many links") 2294 }) 2295 }) 2296 2297 t.Run("symlink with missing destination should throw an error", func(t *testing.T) { 2298 appDir := tempDir(t) 2299 2300 aPath := filepath.Join(appDir, "a.json") 2301 bPath := filepath.Join(appDir, "b.json") 2302 err := os.Symlink(bPath, aPath) 2303 require.NoError(t, err) 2304 2305 walkFor(t, appDir, aPath, func(info fs.FileInfo) { 2306 realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(aPath, info, appDir, appDir, "", "") 2307 assert.Nil(t, realFileInfo) 2308 assert.NotEmpty(t, ignoreMessage) 2309 assert.NoError(t, err) 2310 }) 2311 }) 2312 2313 t.Run("out-of-bounds symlink should throw an error", func(t *testing.T) { 2314 appDir := tempDir(t) 2315 2316 linkPath := filepath.Join(appDir, "a.json") 2317 err := os.Symlink("..", linkPath) 2318 require.NoError(t, err) 2319 2320 walkFor(t, appDir, linkPath, func(info fs.FileInfo) { 2321 realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(linkPath, info, appDir, appDir, "", "") 2322 assert.Nil(t, realFileInfo) 2323 assert.Empty(t, ignoreMessage) 2324 assert.ErrorContains(t, err, "illegal filepath in symlink") 2325 }) 2326 }) 2327 2328 t.Run("symlink to a non-regular file should be skipped with warning", func(t *testing.T) { 2329 appDir := tempDir(t) 2330 2331 dirPath := filepath.Join(appDir, "test.dir") 2332 err := os.MkdirAll(dirPath, 0644) 2333 require.NoError(t, err) 2334 linkPath := filepath.Join(appDir, "test.json") 2335 err = os.Symlink(dirPath, linkPath) 2336 require.NoError(t, err) 2337 2338 walkFor(t, appDir, linkPath, func(info fs.FileInfo) { 2339 realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(linkPath, info, appDir, appDir, "", "") 2340 assert.Nil(t, realFileInfo) 2341 assert.Contains(t, ignoreMessage, "non-regular file") 2342 assert.NoError(t, err) 2343 }) 2344 }) 2345 2346 t.Run("non-included file should be skipped with no message", func(t *testing.T) { 2347 appDir := tempDir(t) 2348 2349 filePath := filepath.Join(appDir, "not-included.yaml") 2350 file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0644) 2351 require.NoError(t, err) 2352 err = file.Close() 2353 require.NoError(t, err) 2354 2355 walkFor(t, appDir, filePath, func(info fs.FileInfo) { 2356 realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(filePath, info, appDir, appDir, "*.json", "") 2357 assert.Nil(t, realFileInfo) 2358 assert.Empty(t, ignoreMessage) 2359 assert.NoError(t, err) 2360 }) 2361 }) 2362 2363 t.Run("excluded file should be skipped with no message", func(t *testing.T) { 2364 appDir := tempDir(t) 2365 2366 filePath := filepath.Join(appDir, "excluded.json") 2367 file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0644) 2368 require.NoError(t, err) 2369 err = file.Close() 2370 require.NoError(t, err) 2371 2372 walkFor(t, appDir, filePath, func(info fs.FileInfo) { 2373 realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(filePath, info, appDir, appDir, "", "excluded.*") 2374 assert.Nil(t, realFileInfo) 2375 assert.Empty(t, ignoreMessage) 2376 assert.NoError(t, err) 2377 }) 2378 }) 2379 2380 t.Run("symlink to a regular file is potentially valid", func(t *testing.T) { 2381 appDir := tempDir(t) 2382 2383 filePath := filepath.Join(appDir, "regular-file") 2384 file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0644) 2385 require.NoError(t, err) 2386 err = file.Close() 2387 require.NoError(t, err) 2388 2389 linkPath := filepath.Join(appDir, "link.json") 2390 err = os.Symlink(filePath, linkPath) 2391 require.NoError(t, err) 2392 2393 walkFor(t, appDir, linkPath, func(info fs.FileInfo) { 2394 realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(linkPath, info, appDir, appDir, "", "") 2395 assert.NotNil(t, realFileInfo) 2396 assert.Empty(t, ignoreMessage) 2397 assert.NoError(t, err) 2398 }) 2399 }) 2400 2401 t.Run("a regular file is potentially valid", func(t *testing.T) { 2402 appDir := tempDir(t) 2403 2404 filePath := filepath.Join(appDir, "regular-file.json") 2405 file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0644) 2406 require.NoError(t, err) 2407 err = file.Close() 2408 require.NoError(t, err) 2409 2410 walkFor(t, appDir, filePath, func(info fs.FileInfo) { 2411 realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(filePath, info, appDir, appDir, "", "") 2412 assert.NotNil(t, realFileInfo) 2413 assert.Empty(t, ignoreMessage) 2414 assert.NoError(t, err) 2415 }) 2416 }) 2417 2418 t.Run("realFileInfo is for the destination rather than the symlink", func(t *testing.T) { 2419 appDir := tempDir(t) 2420 2421 filePath := filepath.Join(appDir, "regular-file") 2422 file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0644) 2423 require.NoError(t, err) 2424 err = file.Close() 2425 require.NoError(t, err) 2426 2427 linkPath := filepath.Join(appDir, "link.json") 2428 err = os.Symlink(filePath, linkPath) 2429 require.NoError(t, err) 2430 2431 walkFor(t, appDir, linkPath, func(info fs.FileInfo) { 2432 realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(linkPath, info, appDir, appDir, "", "") 2433 assert.NotNil(t, realFileInfo) 2434 assert.Equal(t, filepath.Base(filePath), realFileInfo.Name()) 2435 assert.Empty(t, ignoreMessage) 2436 assert.NoError(t, err) 2437 }) 2438 }) 2439 } 2440 2441 func Test_getPotentiallyValidManifests(t *testing.T) { 2442 // Tests which return no manifests and an error check to make sure the directory exists before running. A missing 2443 // directory would produce those same results. 2444 2445 logCtx := log.WithField("test", "test") 2446 2447 t.Run("unreadable file throws error", func(t *testing.T) { 2448 appDir := t.TempDir() 2449 unreadablePath := filepath.Join(appDir, "unreadable.json") 2450 err := os.WriteFile(unreadablePath, []byte{}, 0666) 2451 require.NoError(t, err) 2452 err = os.Chmod(appDir, 0000) 2453 require.NoError(t, err) 2454 2455 manifests, err := getPotentiallyValidManifests(logCtx, appDir, appDir, false, "", "", resource.MustParse("0")) 2456 assert.Empty(t, manifests) 2457 assert.Error(t, err) 2458 2459 // allow cleanup 2460 err = os.Chmod(appDir, 0777) 2461 if err != nil { 2462 panic(err) 2463 } 2464 }) 2465 2466 t.Run("no recursion when recursion is disabled", func(t *testing.T) { 2467 manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/recurse", "./testdata/recurse", false, "", "", resource.MustParse("0")) 2468 assert.Len(t, manifests, 1) 2469 assert.NoError(t, err) 2470 }) 2471 2472 t.Run("recursion when recursion is enabled", func(t *testing.T) { 2473 manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/recurse", "./testdata/recurse", true, "", "", resource.MustParse("0")) 2474 assert.Len(t, manifests, 2) 2475 assert.NoError(t, err) 2476 }) 2477 2478 t.Run("non-JSON/YAML is skipped", func(t *testing.T) { 2479 manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/non-manifest-file", "./testdata/non-manifest-file", false, "", "", resource.MustParse("0")) 2480 assert.Empty(t, manifests) 2481 assert.NoError(t, err) 2482 }) 2483 2484 t.Run("circular link should throw an error", func(t *testing.T) { 2485 const testDir = "./testdata/circular-link" 2486 require.DirExists(t, testDir) 2487 require.NoError(t, fileutil.CreateSymlink(t, testDir, "a.json", "b.json")) 2488 defer os.Remove(path.Join(testDir, "a.json")) 2489 require.NoError(t, fileutil.CreateSymlink(t, testDir, "b.json", "a.json")) 2490 defer os.Remove(path.Join(testDir, "b.json")) 2491 manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/circular-link", "./testdata/circular-link", false, "", "", resource.MustParse("0")) 2492 assert.Empty(t, manifests) 2493 assert.Error(t, err) 2494 }) 2495 2496 t.Run("out-of-bounds symlink should throw an error", func(t *testing.T) { 2497 require.DirExists(t, "./testdata/out-of-bounds-link") 2498 manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/out-of-bounds-link", "./testdata/out-of-bounds-link", false, "", "", resource.MustParse("0")) 2499 assert.Empty(t, manifests) 2500 assert.Error(t, err) 2501 }) 2502 2503 t.Run("symlink to a regular file works", func(t *testing.T) { 2504 repoRoot, err := filepath.Abs("./testdata/in-bounds-link") 2505 require.NoError(t, err) 2506 appPath, err := filepath.Abs("./testdata/in-bounds-link/app") 2507 require.NoError(t, err) 2508 manifests, err := getPotentiallyValidManifests(logCtx, appPath, repoRoot, false, "", "", resource.MustParse("0")) 2509 assert.Len(t, manifests, 1) 2510 assert.NoError(t, err) 2511 }) 2512 2513 t.Run("symlink to nowhere should be ignored", func(t *testing.T) { 2514 manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/link-to-nowhere", "./testdata/link-to-nowhere", false, "", "", resource.MustParse("0")) 2515 assert.Empty(t, manifests) 2516 assert.NoError(t, err) 2517 }) 2518 2519 t.Run("link to over-sized manifest fails", func(t *testing.T) { 2520 repoRoot, err := filepath.Abs("./testdata/in-bounds-link") 2521 require.NoError(t, err) 2522 appPath, err := filepath.Abs("./testdata/in-bounds-link/app") 2523 require.NoError(t, err) 2524 // The file is 35 bytes. 2525 manifests, err := getPotentiallyValidManifests(logCtx, appPath, repoRoot, false, "", "", resource.MustParse("34")) 2526 assert.Empty(t, manifests) 2527 assert.ErrorIs(t, err, ErrExceededMaxCombinedManifestFileSize) 2528 }) 2529 2530 t.Run("group of files should be limited at precisely the sum of their size", func(t *testing.T) { 2531 // There is a total of 10 files, ech file being 10 bytes. 2532 manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/several-files", "./testdata/several-files", false, "", "", resource.MustParse("365")) 2533 assert.Len(t, manifests, 10) 2534 assert.NoError(t, err) 2535 2536 manifests, err = getPotentiallyValidManifests(logCtx, "./testdata/several-files", "./testdata/several-files", false, "", "", resource.MustParse("100")) 2537 assert.Empty(t, manifests) 2538 assert.ErrorIs(t, err, ErrExceededMaxCombinedManifestFileSize) 2539 }) 2540 } 2541 2542 func Test_findManifests(t *testing.T) { 2543 logCtx := log.WithField("test", "test") 2544 noRecurse := argoappv1.ApplicationSourceDirectory{Recurse: false} 2545 2546 t.Run("unreadable file throws error", func(t *testing.T) { 2547 appDir := t.TempDir() 2548 unreadablePath := filepath.Join(appDir, "unreadable.json") 2549 err := os.WriteFile(unreadablePath, []byte{}, 0666) 2550 require.NoError(t, err) 2551 err = os.Chmod(appDir, 0000) 2552 require.NoError(t, err) 2553 2554 manifests, err := findManifests(logCtx, appDir, appDir, nil, noRecurse, nil, resource.MustParse("0")) 2555 assert.Empty(t, manifests) 2556 assert.Error(t, err) 2557 2558 // allow cleanup 2559 err = os.Chmod(appDir, 0777) 2560 if err != nil { 2561 panic(err) 2562 } 2563 }) 2564 2565 t.Run("no recursion when recursion is disabled", func(t *testing.T) { 2566 manifests, err := findManifests(logCtx, "./testdata/recurse", "./testdata/recurse", nil, noRecurse, nil, resource.MustParse("0")) 2567 assert.Len(t, manifests, 2) 2568 assert.NoError(t, err) 2569 }) 2570 2571 t.Run("recursion when recursion is enabled", func(t *testing.T) { 2572 recurse := argoappv1.ApplicationSourceDirectory{Recurse: true} 2573 manifests, err := findManifests(logCtx, "./testdata/recurse", "./testdata/recurse", nil, recurse, nil, resource.MustParse("0")) 2574 assert.Len(t, manifests, 4) 2575 assert.NoError(t, err) 2576 }) 2577 2578 t.Run("non-JSON/YAML is skipped", func(t *testing.T) { 2579 manifests, err := findManifests(logCtx, "./testdata/non-manifest-file", "./testdata/non-manifest-file", nil, noRecurse, nil, resource.MustParse("0")) 2580 assert.Empty(t, manifests) 2581 assert.NoError(t, err) 2582 }) 2583 2584 t.Run("circular link should throw an error", func(t *testing.T) { 2585 const testDir = "./testdata/circular-link" 2586 require.DirExists(t, testDir) 2587 require.NoError(t, fileutil.CreateSymlink(t, testDir, "a.json", "b.json")) 2588 defer os.Remove(path.Join(testDir, "a.json")) 2589 require.NoError(t, fileutil.CreateSymlink(t, testDir, "b.json", "a.json")) 2590 defer os.Remove(path.Join(testDir, "b.json")) 2591 manifests, err := findManifests(logCtx, "./testdata/circular-link", "./testdata/circular-link", nil, noRecurse, nil, resource.MustParse("0")) 2592 assert.Empty(t, manifests) 2593 assert.Error(t, err) 2594 }) 2595 2596 t.Run("out-of-bounds symlink should throw an error", func(t *testing.T) { 2597 require.DirExists(t, "./testdata/out-of-bounds-link") 2598 manifests, err := findManifests(logCtx, "./testdata/out-of-bounds-link", "./testdata/out-of-bounds-link", nil, noRecurse, nil, resource.MustParse("0")) 2599 assert.Empty(t, manifests) 2600 assert.Error(t, err) 2601 }) 2602 2603 t.Run("symlink to a regular file works", func(t *testing.T) { 2604 repoRoot, err := filepath.Abs("./testdata/in-bounds-link") 2605 require.NoError(t, err) 2606 appPath, err := filepath.Abs("./testdata/in-bounds-link/app") 2607 require.NoError(t, err) 2608 manifests, err := findManifests(logCtx, appPath, repoRoot, nil, noRecurse, nil, resource.MustParse("0")) 2609 assert.Len(t, manifests, 1) 2610 assert.NoError(t, err) 2611 }) 2612 2613 t.Run("symlink to nowhere should be ignored", func(t *testing.T) { 2614 manifests, err := findManifests(logCtx, "./testdata/link-to-nowhere", "./testdata/link-to-nowhere", nil, noRecurse, nil, resource.MustParse("0")) 2615 assert.Empty(t, manifests) 2616 assert.NoError(t, err) 2617 }) 2618 2619 t.Run("link to over-sized manifest fails", func(t *testing.T) { 2620 repoRoot, err := filepath.Abs("./testdata/in-bounds-link") 2621 require.NoError(t, err) 2622 appPath, err := filepath.Abs("./testdata/in-bounds-link/app") 2623 require.NoError(t, err) 2624 // The file is 35 bytes. 2625 manifests, err := findManifests(logCtx, appPath, repoRoot, nil, noRecurse, nil, resource.MustParse("34")) 2626 assert.Empty(t, manifests) 2627 assert.ErrorIs(t, err, ErrExceededMaxCombinedManifestFileSize) 2628 }) 2629 2630 t.Run("group of files should be limited at precisely the sum of their size", func(t *testing.T) { 2631 // There is a total of 10 files, each file being 10 bytes. 2632 manifests, err := findManifests(logCtx, "./testdata/several-files", "./testdata/several-files", nil, noRecurse, nil, resource.MustParse("365")) 2633 assert.Len(t, manifests, 10) 2634 assert.NoError(t, err) 2635 2636 manifests, err = findManifests(logCtx, "./testdata/several-files", "./testdata/several-files", nil, noRecurse, nil, resource.MustParse("364")) 2637 assert.Empty(t, manifests) 2638 assert.ErrorIs(t, err, ErrExceededMaxCombinedManifestFileSize) 2639 }) 2640 2641 t.Run("jsonnet isn't counted against size limit", func(t *testing.T) { 2642 // Each file is 36 bytes. Only the 36-byte json file should be counted against the limit. 2643 manifests, err := findManifests(logCtx, "./testdata/jsonnet-and-json", "./testdata/jsonnet-and-json", nil, noRecurse, nil, resource.MustParse("36")) 2644 assert.Len(t, manifests, 2) 2645 assert.NoError(t, err) 2646 2647 manifests, err = findManifests(logCtx, "./testdata/jsonnet-and-json", "./testdata/jsonnet-and-json", nil, noRecurse, nil, resource.MustParse("35")) 2648 assert.Empty(t, manifests) 2649 assert.ErrorIs(t, err, ErrExceededMaxCombinedManifestFileSize) 2650 }) 2651 2652 t.Run("partially valid YAML file throws an error", func(t *testing.T) { 2653 require.DirExists(t, "./testdata/partially-valid-yaml") 2654 manifests, err := findManifests(logCtx, "./testdata/partially-valid-yaml", "./testdata/partially-valid-yaml", nil, noRecurse, nil, resource.MustParse("0")) 2655 assert.Empty(t, manifests) 2656 assert.Error(t, err) 2657 }) 2658 2659 t.Run("invalid manifest throws an error", func(t *testing.T) { 2660 require.DirExists(t, "./testdata/invalid-manifests") 2661 manifests, err := findManifests(logCtx, "./testdata/invalid-manifests", "./testdata/invalid-manifests", nil, noRecurse, nil, resource.MustParse("0")) 2662 assert.Empty(t, manifests) 2663 assert.Error(t, err) 2664 }) 2665 2666 t.Run("irrelevant YAML gets skipped, relevant YAML gets parsed", func(t *testing.T) { 2667 manifests, err := findManifests(logCtx, "./testdata/irrelevant-yaml", "./testdata/irrelevant-yaml", nil, noRecurse, nil, resource.MustParse("0")) 2668 assert.Len(t, manifests, 1) 2669 assert.NoError(t, err) 2670 }) 2671 2672 t.Run("multiple JSON objects in one file throws an error", func(t *testing.T) { 2673 require.DirExists(t, "./testdata/json-list") 2674 manifests, err := findManifests(logCtx, "./testdata/json-list", "./testdata/json-list", nil, noRecurse, nil, resource.MustParse("0")) 2675 assert.Empty(t, manifests) 2676 assert.Error(t, err) 2677 }) 2678 2679 t.Run("invalid JSON throws an error", func(t *testing.T) { 2680 require.DirExists(t, "./testdata/invalid-json") 2681 manifests, err := findManifests(logCtx, "./testdata/invalid-json", "./testdata/invalid-json", nil, noRecurse, nil, resource.MustParse("0")) 2682 assert.Empty(t, manifests) 2683 assert.Error(t, err) 2684 }) 2685 2686 t.Run("valid JSON returns manifest and no error", func(t *testing.T) { 2687 manifests, err := findManifests(logCtx, "./testdata/valid-json", "./testdata/valid-json", nil, noRecurse, nil, resource.MustParse("0")) 2688 assert.Len(t, manifests, 1) 2689 assert.NoError(t, err) 2690 }) 2691 2692 t.Run("YAML with an empty document doesn't throw an error", func(t *testing.T) { 2693 manifests, err := findManifests(logCtx, "./testdata/yaml-with-empty-document", "./testdata/yaml-with-empty-document", nil, noRecurse, nil, resource.MustParse("0")) 2694 assert.Len(t, manifests, 1) 2695 assert.NoError(t, err) 2696 }) 2697 } 2698 2699 func TestTestRepoOCI(t *testing.T) { 2700 service := newService(t, ".") 2701 _, err := service.TestRepository(context.Background(), &apiclient.TestRepositoryRequest{ 2702 Repo: &argoappv1.Repository{ 2703 Repo: "https://demo.goharbor.io", 2704 Type: "helm", 2705 EnableOCI: true, 2706 }, 2707 }) 2708 require.Error(t, err) 2709 assert.Contains(t, err.Error(), "OCI Helm repository URL should include hostname and port only") 2710 } 2711 2712 func Test_getHelmDependencyRepos(t *testing.T) { 2713 repo1 := "https://charts.bitnami.com/bitnami" 2714 repo2 := "https://eventstore.github.io/EventStore.Charts" 2715 2716 repos, err := getHelmDependencyRepos("../../util/helm/testdata/dependency") 2717 assert.NoError(t, err) 2718 assert.Equal(t, len(repos), 2) 2719 assert.Equal(t, repos[0].Repo, repo1) 2720 assert.Equal(t, repos[1].Repo, repo2) 2721 } 2722 2723 func TestResolveRevision(t *testing.T) { 2724 2725 service := newService(t, ".") 2726 repo := &argoappv1.Repository{Repo: "https://github.com/argoproj/argo-cd"} 2727 app := &argoappv1.Application{Spec: argoappv1.ApplicationSpec{Source: &argoappv1.ApplicationSource{}}} 2728 resolveRevisionResponse, err := service.ResolveRevision(context.Background(), &apiclient.ResolveRevisionRequest{ 2729 Repo: repo, 2730 App: app, 2731 AmbiguousRevision: "v2.2.2", 2732 }) 2733 2734 expectedResolveRevisionResponse := &apiclient.ResolveRevisionResponse{ 2735 Revision: "03b17e0233e64787ffb5fcf65c740cc2a20822ba", 2736 AmbiguousRevision: "v2.2.2 (03b17e0233e64787ffb5fcf65c740cc2a20822ba)", 2737 } 2738 2739 assert.NotNil(t, resolveRevisionResponse.Revision) 2740 assert.Nil(t, err) 2741 assert.Equal(t, expectedResolveRevisionResponse, resolveRevisionResponse) 2742 2743 } 2744 2745 func TestResolveRevisionNegativeScenarios(t *testing.T) { 2746 2747 service := newService(t, ".") 2748 repo := &argoappv1.Repository{Repo: "https://github.com/argoproj/argo-cd"} 2749 app := &argoappv1.Application{Spec: argoappv1.ApplicationSpec{Source: &argoappv1.ApplicationSource{}}} 2750 resolveRevisionResponse, err := service.ResolveRevision(context.Background(), &apiclient.ResolveRevisionRequest{ 2751 Repo: repo, 2752 App: app, 2753 AmbiguousRevision: "v2.a.2", 2754 }) 2755 2756 expectedResolveRevisionResponse := &apiclient.ResolveRevisionResponse{ 2757 Revision: "", 2758 AmbiguousRevision: "", 2759 } 2760 2761 assert.NotNil(t, resolveRevisionResponse.Revision) 2762 assert.NotNil(t, err) 2763 assert.Equal(t, expectedResolveRevisionResponse, resolveRevisionResponse) 2764 2765 } 2766 2767 func TestDirectoryPermissionInitializer(t *testing.T) { 2768 dir := t.TempDir() 2769 2770 file, err := os.CreateTemp(dir, "") 2771 require.NoError(t, err) 2772 io.Close(file) 2773 2774 // remove read permissions 2775 assert.NoError(t, os.Chmod(dir, 0000)) 2776 2777 // Remember to restore permissions when the test finishes so dir can 2778 // be removed properly. 2779 t.Cleanup(func() { 2780 require.NoError(t, os.Chmod(dir, 0777)) 2781 }) 2782 2783 // make sure permission are restored 2784 closer := directoryPermissionInitializer(dir) 2785 _, err = os.ReadFile(file.Name()) 2786 require.NoError(t, err) 2787 2788 // make sure permission are removed by closer 2789 io.Close(closer) 2790 _, err = os.ReadFile(file.Name()) 2791 require.Error(t, err) 2792 } 2793 2794 func addHelmToGitRepo(t *testing.T, options newGitRepoOptions) { 2795 err := os.WriteFile(filepath.Join(options.path, "Chart.yaml"), []byte("name: test\nversion: v1.0.0"), 0777) 2796 assert.NoError(t, err) 2797 for valuesFileName, values := range options.helmChartOptions.valuesFiles { 2798 valuesFileContents, err := yaml.Marshal(values) 2799 assert.NoError(t, err) 2800 err = os.WriteFile(filepath.Join(options.path, valuesFileName), valuesFileContents, 0777) 2801 assert.NoError(t, err) 2802 } 2803 assert.NoError(t, err) 2804 cmd := exec.Command("git", "add", "-A") 2805 cmd.Dir = options.path 2806 assert.NoError(t, cmd.Run()) 2807 cmd = exec.Command("git", "commit", "-m", "Initial commit") 2808 cmd.Dir = options.path 2809 assert.NoError(t, cmd.Run()) 2810 } 2811 2812 func initGitRepo(t *testing.T, options newGitRepoOptions) (revision string) { 2813 if options.createPath { 2814 assert.NoError(t, os.Mkdir(options.path, 0755)) 2815 } 2816 2817 cmd := exec.Command("git", "init", "-b", "main", options.path) 2818 cmd.Dir = options.path 2819 assert.NoError(t, cmd.Run()) 2820 2821 if options.remote != "" { 2822 cmd = exec.Command("git", "remote", "add", "origin", options.path) 2823 cmd.Dir = options.path 2824 assert.NoError(t, cmd.Run()) 2825 } 2826 2827 commitAdded := options.addEmptyCommit || options.helmChartOptions.chartName != "" 2828 if options.addEmptyCommit { 2829 cmd = exec.Command("git", "commit", "-m", "Initial commit", "--allow-empty") 2830 cmd.Dir = options.path 2831 assert.NoError(t, cmd.Run()) 2832 } else if options.helmChartOptions.chartName != "" { 2833 addHelmToGitRepo(t, options) 2834 } 2835 2836 if commitAdded { 2837 var revB bytes.Buffer 2838 cmd = exec.Command("git", "rev-parse", "HEAD", options.path) 2839 cmd.Dir = options.path 2840 cmd.Stdout = &revB 2841 assert.NoError(t, cmd.Run()) 2842 revision = strings.Split(revB.String(), "\n")[0] 2843 } 2844 return revision 2845 } 2846 2847 func TestInit(t *testing.T) { 2848 dir := t.TempDir() 2849 2850 // service.Init sets permission to 0300. Restore permissions when the test 2851 // finishes so dir can be removed properly. 2852 t.Cleanup(func() { 2853 require.NoError(t, os.Chmod(dir, 0777)) 2854 }) 2855 2856 repoPath := path.Join(dir, "repo1") 2857 initGitRepo(t, newGitRepoOptions{path: repoPath, remote: "https://github.com/argo-cd/test-repo1", createPath: true, addEmptyCommit: false}) 2858 2859 service := newService(t, ".") 2860 service.rootDir = dir 2861 2862 require.NoError(t, service.Init()) 2863 2864 _, err := os.ReadDir(dir) 2865 require.Error(t, err) 2866 initGitRepo(t, newGitRepoOptions{path: path.Join(dir, "repo2"), remote: "https://github.com/argo-cd/test-repo2", createPath: true, addEmptyCommit: false}) 2867 } 2868 2869 // TestCheckoutRevisionCanGetNonstandardRefs shows that we can fetch a revision that points to a non-standard ref. In 2870 // other words, we haven't regressed and caused this issue again: https://github.com/argoproj/argo-cd/issues/4935 2871 func TestCheckoutRevisionCanGetNonstandardRefs(t *testing.T) { 2872 rootPath := t.TempDir() 2873 2874 sourceRepoPath, err := os.MkdirTemp(rootPath, "") 2875 require.NoError(t, err) 2876 2877 // Create a repo such that one commit is on a non-standard ref _and nowhere else_. This is meant to simulate, for 2878 // example, a GitHub ref for a pull into one repo from a fork of that repo. 2879 runGit(t, sourceRepoPath, "init") 2880 runGit(t, sourceRepoPath, "checkout", "-b", "main") // make sure there's a main branch to switch back to 2881 runGit(t, sourceRepoPath, "commit", "-m", "empty", "--allow-empty") 2882 runGit(t, sourceRepoPath, "checkout", "-b", "branch") 2883 runGit(t, sourceRepoPath, "commit", "-m", "empty", "--allow-empty") 2884 sha := runGit(t, sourceRepoPath, "rev-parse", "HEAD") 2885 runGit(t, sourceRepoPath, "update-ref", "refs/pull/123/head", strings.TrimSuffix(sha, "\n")) 2886 runGit(t, sourceRepoPath, "checkout", "main") 2887 runGit(t, sourceRepoPath, "branch", "-D", "branch") 2888 2889 destRepoPath, err := os.MkdirTemp(rootPath, "") 2890 require.NoError(t, err) 2891 2892 gitClient, err := git.NewClientExt("file://"+sourceRepoPath, destRepoPath, &git.NopCreds{}, true, false, "") 2893 require.NoError(t, err) 2894 2895 pullSha, err := gitClient.LsRemote("refs/pull/123/head") 2896 require.NoError(t, err) 2897 2898 err = checkoutRevision(gitClient, "does-not-exist", false) 2899 assert.Error(t, err) 2900 2901 err = checkoutRevision(gitClient, pullSha, false) 2902 assert.NoError(t, err) 2903 } 2904 2905 // runGit runs a git command in the given working directory. If the command succeeds, it returns the combined standard 2906 // and error output. If it fails, it stops the test with a failure message. 2907 func runGit(t *testing.T, workDir string, args ...string) string { 2908 cmd := exec.Command("git", args...) 2909 cmd.Dir = workDir 2910 out, err := cmd.CombinedOutput() 2911 stringOut := string(out) 2912 require.NoError(t, err, stringOut) 2913 return stringOut 2914 } 2915 2916 func Test_walkHelmValueFilesInPath(t *testing.T) { 2917 t.Run("does not exist", func(t *testing.T) { 2918 var files []string 2919 root := "/obviously/does/not/exist" 2920 err := filepath.Walk(root, walkHelmValueFilesInPath(root, &files)) 2921 assert.Error(t, err) 2922 assert.Empty(t, files) 2923 }) 2924 t.Run("values files", func(t *testing.T) { 2925 var files []string 2926 root := "./testdata/values-files" 2927 err := filepath.Walk(root, walkHelmValueFilesInPath(root, &files)) 2928 assert.NoError(t, err) 2929 assert.Len(t, files, 5) 2930 }) 2931 t.Run("unrelated root", func(t *testing.T) { 2932 var files []string 2933 root := "./testdata/values-files" 2934 unrelated_root := "/different/root/path" 2935 err := filepath.Walk(root, walkHelmValueFilesInPath(unrelated_root, &files)) 2936 assert.Error(t, err) 2937 }) 2938 } 2939 2940 func Test_populateHelmAppDetails(t *testing.T) { 2941 var emptyTempPaths = io.NewRandomizedTempPaths(t.TempDir()) 2942 res := apiclient.RepoAppDetailsResponse{} 2943 q := apiclient.RepoServerAppDetailsQuery{ 2944 Repo: &argoappv1.Repository{}, 2945 Source: &argoappv1.ApplicationSource{ 2946 Helm: &argoappv1.ApplicationSourceHelm{ValueFiles: []string{"exclude.yaml", "has-the-word-values.yaml"}}, 2947 }, 2948 } 2949 appPath, err := filepath.Abs("./testdata/values-files/") 2950 require.NoError(t, err) 2951 err = populateHelmAppDetails(&res, appPath, appPath, &q, emptyTempPaths) 2952 require.NoError(t, err) 2953 assert.Len(t, res.Helm.Parameters, 3) 2954 assert.Len(t, res.Helm.ValueFiles, 5) 2955 } 2956 2957 func Test_populateHelmAppDetails_values_symlinks(t *testing.T) { 2958 var emptyTempPaths = io.NewRandomizedTempPaths(t.TempDir()) 2959 t.Run("inbound", func(t *testing.T) { 2960 res := apiclient.RepoAppDetailsResponse{} 2961 q := apiclient.RepoServerAppDetailsQuery{Repo: &argoappv1.Repository{}, Source: &argoappv1.ApplicationSource{}} 2962 err := populateHelmAppDetails(&res, "./testdata/in-bounds-values-file-link/", "./testdata/in-bounds-values-file-link/", &q, emptyTempPaths) 2963 require.NoError(t, err) 2964 assert.NotEmpty(t, res.Helm.Values) 2965 assert.NotEmpty(t, res.Helm.Parameters) 2966 }) 2967 2968 t.Run("out of bounds", func(t *testing.T) { 2969 res := apiclient.RepoAppDetailsResponse{} 2970 q := apiclient.RepoServerAppDetailsQuery{Repo: &argoappv1.Repository{}, Source: &argoappv1.ApplicationSource{}} 2971 err := populateHelmAppDetails(&res, "./testdata/out-of-bounds-values-file-link/", "./testdata/out-of-bounds-values-file-link/", &q, emptyTempPaths) 2972 require.NoError(t, err) 2973 assert.Empty(t, res.Helm.Values) 2974 assert.Empty(t, res.Helm.Parameters) 2975 }) 2976 } 2977 2978 func TestGetHelmRepos_OCIDependencies(t *testing.T) { 2979 src := argoappv1.ApplicationSource{Path: "."} 2980 q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, HelmRepoCreds: []*argoappv1.RepoCreds{ 2981 {URL: "example.com", Username: "test", Password: "test", EnableOCI: true}, 2982 }} 2983 2984 helmRepos, err := getHelmRepos("./testdata/oci-dependencies", q.Repos, q.HelmRepoCreds) 2985 assert.Nil(t, err) 2986 2987 assert.Equal(t, len(helmRepos), 1) 2988 assert.Equal(t, helmRepos[0].Username, "test") 2989 assert.Equal(t, helmRepos[0].EnableOci, true) 2990 assert.Equal(t, helmRepos[0].Repo, "example.com/myrepo") 2991 } 2992 2993 func TestGetHelmRepo_NamedRepos(t *testing.T) { 2994 src := argoappv1.ApplicationSource{Path: "."} 2995 q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, Repos: []*argoappv1.Repository{{ 2996 Name: "custom-repo", 2997 Repo: "https://example.com", 2998 Username: "test", 2999 }}} 3000 3001 helmRepos, err := getHelmRepos("./testdata/helm-with-dependencies", q.Repos, q.HelmRepoCreds) 3002 assert.Nil(t, err) 3003 3004 assert.Equal(t, len(helmRepos), 1) 3005 assert.Equal(t, helmRepos[0].Username, "test") 3006 assert.Equal(t, helmRepos[0].Repo, "https://example.com") 3007 } 3008 3009 func TestGetHelmRepo_NamedReposAlias(t *testing.T) { 3010 src := argoappv1.ApplicationSource{Path: "."} 3011 q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, Repos: []*argoappv1.Repository{{ 3012 Name: "custom-repo-alias", 3013 Repo: "https://example.com", 3014 Username: "test-alias", 3015 }}} 3016 3017 helmRepos, err := getHelmRepos("./testdata/helm-with-dependencies-alias", q.Repos, q.HelmRepoCreds) 3018 assert.Nil(t, err) 3019 3020 assert.Equal(t, len(helmRepos), 1) 3021 assert.Equal(t, helmRepos[0].Username, "test-alias") 3022 assert.Equal(t, helmRepos[0].Repo, "https://example.com") 3023 } 3024 3025 func Test_getResolvedValueFiles(t *testing.T) { 3026 tempDir := t.TempDir() 3027 paths := io.NewRandomizedTempPaths(tempDir) 3028 paths.Add(git.NormalizeGitURL("https://github.com/org/repo1"), path.Join(tempDir, "repo1")) 3029 3030 testCases := []struct { 3031 name string 3032 rawPath string 3033 env *argoappv1.Env 3034 refSources map[string]*argoappv1.RefTarget 3035 expectedPath string 3036 expectedErr bool 3037 }{ 3038 { 3039 name: "simple path", 3040 rawPath: "values.yaml", 3041 env: &argoappv1.Env{}, 3042 refSources: map[string]*argoappv1.RefTarget{}, 3043 expectedPath: path.Join(tempDir, "main-repo", "values.yaml"), 3044 }, 3045 { 3046 name: "simple ref", 3047 rawPath: "$ref/values.yaml", 3048 env: &argoappv1.Env{}, 3049 refSources: map[string]*argoappv1.RefTarget{ 3050 "$ref": { 3051 Repo: argoappv1.Repository{ 3052 Repo: "https://github.com/org/repo1", 3053 }, 3054 }, 3055 }, 3056 expectedPath: path.Join(tempDir, "repo1", "values.yaml"), 3057 }, 3058 { 3059 name: "only ref", 3060 rawPath: "$ref", 3061 env: &argoappv1.Env{}, 3062 refSources: map[string]*argoappv1.RefTarget{ 3063 "$ref": { 3064 Repo: argoappv1.Repository{ 3065 Repo: "https://github.com/org/repo1", 3066 }, 3067 }, 3068 }, 3069 expectedErr: true, 3070 }, 3071 { 3072 name: "attempted traversal", 3073 rawPath: "$ref/../values.yaml", 3074 env: &argoappv1.Env{}, 3075 refSources: map[string]*argoappv1.RefTarget{ 3076 "$ref": { 3077 Repo: argoappv1.Repository{ 3078 Repo: "https://github.com/org/repo1", 3079 }, 3080 }, 3081 }, 3082 expectedErr: true, 3083 }, 3084 { 3085 // Since $ref doesn't resolve to a ref target, we assume it's an env var. Since the env var isn't specified, 3086 // it's replaced with an empty string. This is necessary for backwards compatibility with behavior before 3087 // ref targets were introduced. 3088 name: "ref doesn't exist", 3089 rawPath: "$ref/values.yaml", 3090 env: &argoappv1.Env{}, 3091 refSources: map[string]*argoappv1.RefTarget{}, 3092 expectedPath: path.Join(tempDir, "main-repo", "values.yaml"), 3093 }, 3094 { 3095 name: "repo doesn't exist", 3096 rawPath: "$ref/values.yaml", 3097 env: &argoappv1.Env{}, 3098 refSources: map[string]*argoappv1.RefTarget{ 3099 "$ref": { 3100 Repo: argoappv1.Repository{ 3101 Repo: "https://github.com/org/repo2", 3102 }, 3103 }, 3104 }, 3105 expectedErr: true, 3106 }, 3107 { 3108 name: "env var is resolved", 3109 rawPath: "$ref/$APP_PATH/values.yaml", 3110 env: &argoappv1.Env{ 3111 &argoappv1.EnvEntry{ 3112 Name: "APP_PATH", 3113 Value: "app-path", 3114 }, 3115 }, 3116 refSources: map[string]*argoappv1.RefTarget{ 3117 "$ref": { 3118 Repo: argoappv1.Repository{ 3119 Repo: "https://github.com/org/repo1", 3120 }, 3121 }, 3122 }, 3123 expectedPath: path.Join(tempDir, "repo1", "app-path", "values.yaml"), 3124 }, 3125 { 3126 name: "traversal in env var is blocked", 3127 rawPath: "$ref/$APP_PATH/values.yaml", 3128 env: &argoappv1.Env{ 3129 &argoappv1.EnvEntry{ 3130 Name: "APP_PATH", 3131 Value: "..", 3132 }, 3133 }, 3134 refSources: map[string]*argoappv1.RefTarget{ 3135 "$ref": { 3136 Repo: argoappv1.Repository{ 3137 Repo: "https://github.com/org/repo1", 3138 }, 3139 }, 3140 }, 3141 expectedErr: true, 3142 }, 3143 { 3144 name: "env var prefix", 3145 rawPath: "$APP_PATH/values.yaml", 3146 env: &argoappv1.Env{ 3147 &argoappv1.EnvEntry{ 3148 Name: "APP_PATH", 3149 Value: "app-path", 3150 }, 3151 }, 3152 refSources: map[string]*argoappv1.RefTarget{}, 3153 expectedPath: path.Join(tempDir, "main-repo", "app-path", "values.yaml"), 3154 }, 3155 { 3156 name: "unresolved env var", 3157 rawPath: "$APP_PATH/values.yaml", 3158 env: &argoappv1.Env{}, 3159 refSources: map[string]*argoappv1.RefTarget{}, 3160 expectedPath: path.Join(tempDir, "main-repo", "values.yaml"), 3161 }, 3162 } 3163 3164 for _, tc := range testCases { 3165 tcc := tc 3166 t.Run(tcc.name, func(t *testing.T) { 3167 t.Parallel() 3168 resolvedPaths, err := getResolvedValueFiles(path.Join(tempDir, "main-repo"), path.Join(tempDir, "main-repo"), tcc.env, []string{}, []string{tcc.rawPath}, tcc.refSources, paths, false) 3169 if !tcc.expectedErr { 3170 assert.NoError(t, err) 3171 require.Len(t, resolvedPaths, 1) 3172 assert.Equal(t, tcc.expectedPath, string(resolvedPaths[0])) 3173 } else { 3174 assert.Error(t, err) 3175 assert.Empty(t, resolvedPaths) 3176 } 3177 }) 3178 } 3179 } 3180 func TestErrorGetGitDirectories(t *testing.T) { 3181 type fields struct { 3182 service *Service 3183 } 3184 type args struct { 3185 ctx context.Context 3186 request *apiclient.GitDirectoriesRequest 3187 } 3188 tests := []struct { 3189 name string 3190 fields fields 3191 args args 3192 want *apiclient.GitDirectoriesResponse 3193 wantErr assert.ErrorAssertionFunc 3194 }{ 3195 {name: "InvalidRepo", fields: fields{service: newService(t, ".")}, args: args{ 3196 ctx: context.TODO(), 3197 request: &apiclient.GitDirectoriesRequest{ 3198 Repo: nil, 3199 SubmoduleEnabled: false, 3200 Revision: "HEAD", 3201 }, 3202 }, want: nil, wantErr: assert.Error}, 3203 {name: "InvalidResolveRevision", fields: fields{service: func() *Service { 3204 s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) { 3205 gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil) 3206 gitClient.On("LsRemote", mock.Anything).Return("", fmt.Errorf("ah error")) 3207 paths.On("GetPath", mock.Anything).Return(".", nil) 3208 paths.On("GetPathIfExists", mock.Anything).Return(".", nil) 3209 }, ".") 3210 return s 3211 }()}, args: args{ 3212 ctx: context.TODO(), 3213 request: &apiclient.GitDirectoriesRequest{ 3214 Repo: &argoappv1.Repository{Repo: "not-a-valid-url"}, 3215 SubmoduleEnabled: false, 3216 Revision: "sadfsadf", 3217 }, 3218 }, want: nil, wantErr: assert.Error}, 3219 } 3220 for _, tt := range tests { 3221 t.Run(tt.name, func(t *testing.T) { 3222 s := tt.fields.service 3223 got, err := s.GetGitDirectories(tt.args.ctx, tt.args.request) 3224 if !tt.wantErr(t, err, fmt.Sprintf("GetGitDirectories(%v, %v)", tt.args.ctx, tt.args.request)) { 3225 return 3226 } 3227 assert.Equalf(t, tt.want, got, "GetGitDirectories(%v, %v)", tt.args.ctx, tt.args.request) 3228 }) 3229 } 3230 } 3231 3232 func TestGetGitDirectories(t *testing.T) { 3233 // test not using the cache 3234 root := "./testdata/git-files-dirs" 3235 s, _, cacheMocks := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) { 3236 gitClient.On("Init").Return(nil) 3237 gitClient.On("Fetch", mock.Anything).Return(nil) 3238 gitClient.On("Checkout", mock.Anything, mock.Anything).Once().Return(nil) 3239 gitClient.On("LsRemote", "HEAD").Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil) 3240 gitClient.On("Root").Return(root) 3241 paths.On("GetPath", mock.Anything).Return(root, nil) 3242 paths.On("GetPathIfExists", mock.Anything).Return(root, nil) 3243 }, root) 3244 dirRequest := &apiclient.GitDirectoriesRequest{ 3245 Repo: &argoappv1.Repository{Repo: "a-url.com"}, 3246 SubmoduleEnabled: false, 3247 Revision: "HEAD", 3248 } 3249 directories, err := s.GetGitDirectories(context.TODO(), dirRequest) 3250 assert.Nil(t, err) 3251 assert.ElementsMatch(t, directories.GetPaths(), []string{"app", "app/bar", "app/foo/bar", "somedir", "app/foo"}) 3252 3253 // do the same request again to use the cache 3254 // we only allow CheckOut to be called once in the mock 3255 directories, err = s.GetGitDirectories(context.TODO(), dirRequest) 3256 assert.Nil(t, err) 3257 assert.ElementsMatch(t, []string{"app", "app/bar", "app/foo/bar", "somedir", "app/foo"}, directories.GetPaths()) 3258 cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{ 3259 ExternalSets: 1, 3260 ExternalGets: 2, 3261 }) 3262 } 3263 3264 func TestErrorGetGitFiles(t *testing.T) { 3265 type fields struct { 3266 service *Service 3267 } 3268 type args struct { 3269 ctx context.Context 3270 request *apiclient.GitFilesRequest 3271 } 3272 tests := []struct { 3273 name string 3274 fields fields 3275 args args 3276 want *apiclient.GitFilesResponse 3277 wantErr assert.ErrorAssertionFunc 3278 }{ 3279 {name: "InvalidRepo", fields: fields{service: newService(t, ".")}, args: args{ 3280 ctx: context.TODO(), 3281 request: &apiclient.GitFilesRequest{ 3282 Repo: nil, 3283 SubmoduleEnabled: false, 3284 Revision: "HEAD", 3285 }, 3286 }, want: nil, wantErr: assert.Error}, 3287 {name: "InvalidResolveRevision", fields: fields{service: func() *Service { 3288 s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) { 3289 gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil) 3290 gitClient.On("LsRemote", mock.Anything).Return("", fmt.Errorf("ah error")) 3291 paths.On("GetPath", mock.Anything).Return(".", nil) 3292 paths.On("GetPathIfExists", mock.Anything).Return(".", nil) 3293 }, ".") 3294 return s 3295 }()}, args: args{ 3296 ctx: context.TODO(), 3297 request: &apiclient.GitFilesRequest{ 3298 Repo: &argoappv1.Repository{Repo: "not-a-valid-url"}, 3299 SubmoduleEnabled: false, 3300 Revision: "sadfsadf", 3301 }, 3302 }, want: nil, wantErr: assert.Error}, 3303 } 3304 for _, tt := range tests { 3305 t.Run(tt.name, func(t *testing.T) { 3306 s := tt.fields.service 3307 got, err := s.GetGitFiles(tt.args.ctx, tt.args.request) 3308 if !tt.wantErr(t, err, fmt.Sprintf("GetGitFiles(%v, %v)", tt.args.ctx, tt.args.request)) { 3309 return 3310 } 3311 assert.Equalf(t, tt.want, got, "GetGitFiles(%v, %v)", tt.args.ctx, tt.args.request) 3312 }) 3313 } 3314 } 3315 3316 func TestGetGitFiles(t *testing.T) { 3317 // test not using the cache 3318 files := []string{"./testdata/git-files-dirs/somedir/config.yaml", 3319 "./testdata/git-files-dirs/config.yaml", "./testdata/git-files-dirs/config.yaml", "./testdata/git-files-dirs/app/foo/bar/config.yaml"} 3320 root := "" 3321 s, _, cacheMocks := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) { 3322 gitClient.On("Init").Return(nil) 3323 gitClient.On("Fetch", mock.Anything).Return(nil) 3324 gitClient.On("Checkout", mock.Anything, mock.Anything).Once().Return(nil) 3325 gitClient.On("LsRemote", "HEAD").Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil) 3326 gitClient.On("Root").Return(root) 3327 gitClient.On("LsFiles", mock.Anything, mock.Anything).Once().Return(files, nil) 3328 paths.On("GetPath", mock.Anything).Return(root, nil) 3329 paths.On("GetPathIfExists", mock.Anything).Return(root, nil) 3330 }, root) 3331 filesRequest := &apiclient.GitFilesRequest{ 3332 Repo: &argoappv1.Repository{Repo: "a-url.com"}, 3333 SubmoduleEnabled: false, 3334 Revision: "HEAD", 3335 } 3336 3337 // expected map 3338 expected := make(map[string][]byte) 3339 for _, filePath := range files { 3340 fileContents, err := os.ReadFile(filePath) 3341 assert.Nil(t, err) 3342 expected[filePath] = fileContents 3343 } 3344 3345 fileResponse, err := s.GetGitFiles(context.TODO(), filesRequest) 3346 assert.Nil(t, err) 3347 assert.Equal(t, fileResponse.GetMap(), expected) 3348 3349 // do the same request again to use the cache 3350 // we only allow LsFiles to be called once in the mock 3351 fileResponse, err = s.GetGitFiles(context.TODO(), filesRequest) 3352 assert.Nil(t, err) 3353 assert.Equal(t, expected, fileResponse.GetMap()) 3354 cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{ 3355 ExternalSets: 1, 3356 ExternalGets: 2, 3357 }) 3358 } 3359 3360 func Test_getRepoSanitizerRegex(t *testing.T) { 3361 r := getRepoSanitizerRegex("/tmp/_argocd-repo") 3362 msg := r.ReplaceAllString("error message containing /tmp/_argocd-repo/SENSITIVE and other stuff", "<path to cached source>") 3363 assert.Equal(t, "error message containing <path to cached source> and other stuff", msg) 3364 msg = r.ReplaceAllString("error message containing /tmp/_argocd-repo/SENSITIVE/with/trailing/path and other stuff", "<path to cached source>") 3365 assert.Equal(t, "error message containing <path to cached source>/with/trailing/path and other stuff", msg) 3366 } 3367 3368 func TestGetRevisionChartDetails(t *testing.T) { 3369 t.Run("Test revision semvar", func(t *testing.T) { 3370 root := t.TempDir() 3371 service := newService(t, root) 3372 _, err := service.GetRevisionChartDetails(context.Background(), &apiclient.RepoServerRevisionChartDetailsRequest{ 3373 Repo: &v1alpha1.Repository{ 3374 Repo: fmt.Sprintf("file://%s", root), 3375 Name: "test-repo-name", 3376 Type: "helm", 3377 }, 3378 Name: "test-name", 3379 Revision: "test-revision", 3380 }) 3381 assert.ErrorContains(t, err, "invalid revision") 3382 }) 3383 3384 t.Run("Test GetRevisionChartDetails", func(t *testing.T) { 3385 root := t.TempDir() 3386 service := newService(t, root) 3387 repoUrl := fmt.Sprintf("file://%s", root) 3388 err := service.cache.SetRevisionChartDetails(repoUrl, "my-chart", "1.1.0", &argoappv1.ChartDetails{ 3389 Description: "test-description", 3390 Home: "test-home", 3391 Maintainers: []string{"test-maintainer"}, 3392 }) 3393 assert.NoError(t, err) 3394 chartDetails, err := service.GetRevisionChartDetails(context.Background(), &apiclient.RepoServerRevisionChartDetailsRequest{ 3395 Repo: &v1alpha1.Repository{ 3396 Repo: fmt.Sprintf("file://%s", root), 3397 Name: "test-repo-name", 3398 Type: "helm", 3399 }, 3400 Name: "my-chart", 3401 Revision: "1.1.0", 3402 }) 3403 assert.NoError(t, err) 3404 assert.Equal(t, "test-description", chartDetails.Description) 3405 assert.Equal(t, "test-home", chartDetails.Home) 3406 assert.Equal(t, []string{"test-maintainer"}, chartDetails.Maintainers) 3407 }) 3408 }