github.com/argoproj/argo-cd@v1.8.7/reposerver/repository/repository_test.go (about)

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