Repository: argoproj/argo-cd
Pull request #8517 (2022-02-15 22:25:22)

Unique repo path and permissions
Signed-off-by: Alexander Matyushentsev [AMatyushentsev@gmail.com](mailto:AMatyushentsev@gmail.com)

PR implements two changes:

* Git and Helm repositories are cloned/fetched into random/non-predicable local directory
* Repo server remove read permissions from Git repos when not using it for manifest generation

codecov[bot]: # [Codecov](https://codecov.io/gh/argoproj/argo-cd/pull/8517?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj) Report
> Merging [#8517](https://codecov.io/gh/argoproj/argo-cd/pull/8517?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj) (7967831) into [master](https://codecov.io/gh/argoproj/argo-cd/commit/c078bbb5cd90d392888e4f851471dffce85be150?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj) (c078bbb) will **increase** coverage by `0.01%`.
> The diff coverage is `63.63%`.

[![Impacted file tree graph](https://codecov.io/gh/argoproj/argo-cd/pull/8517/graphs/tree.svg?width=650&height=150&src=pr&token=kf5kwuegwE&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj)](https://codecov.io/gh/argoproj/argo-cd/pull/8517?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj)

```diff
@@            Coverage Diff             @@
##           master    #8517      +/-   ##
==========================================
+ Coverage   42.31%   42.32%   +0.01%     
==========================================
  Files         176      182       +6     
  Lines       22798    22908     +110     
==========================================
+ Hits         9647     9696      +49     
- Misses      11775    11824      +49     
- Partials     1376     1388      +12     
```


| [Impacted Files](https://codecov.io/gh/argoproj/argo-cd/pull/8517?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj) | Coverage Δ | |
|---|---|---|
| [util/helm/client.go](https://codecov.io/gh/argoproj/argo-cd/pull/8517/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj#diff-dXRpbC9oZWxtL2NsaWVudC5nbw==) | `45.14% <55.55%> (-3.37%)` | :arrow_down: |
| [reposerver/repository/repository.go](https://codecov.io/gh/argoproj/argo-cd/pull/8517/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj#diff-cmVwb3NlcnZlci9yZXBvc2l0b3J5L3JlcG9zaXRvcnkuZ28=) | `58.54% <57.14%> (-0.18%)` | :arrow_down: |
| [util/io/paths.go](https://codecov.io/gh/argoproj/argo-cd/pull/8517/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj#diff-dXRpbC9pby9wYXRocy5nbw==) | `68.75% <68.75%> (ø)` | |
| [util/grpc/sanitizer.go](https://codecov.io/gh/argoproj/argo-cd/pull/8517/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj#diff-dXRpbC9ncnBjL3Nhbml0aXplci5nbw==) | `81.25% <81.25%> (ø)` | |
| [reposerver/repository/lock.go](https://codecov.io/gh/argoproj/argo-cd/pull/8517/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj#diff-cmVwb3NlcnZlci9yZXBvc2l0b3J5L2xvY2suZ28=) | `100.00% <100.00%> (ø)` | |
| [util/io/closer.go](https://codecov.io/gh/argoproj/argo-cd/pull/8517/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj#diff-dXRpbC9pby9jbG9zZXIuZ28=) | `14.28% <0.00%> (ø)` | |
| [util/io/bytereadseeker.go](https://codecov.io/gh/argoproj/argo-cd/pull/8517/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj#diff-dXRpbC9pby9ieXRlcmVhZHNlZWtlci5nbw==) | `0.00% <0.00%> (ø)` | |
| [util/io/subdirfs.go](https://codecov.io/gh/argoproj/argo-cd/pull/8517/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj#diff-dXRpbC9pby9zdWJkaXJmcy5nbw==) | `0.00% <0.00%> (ø)` | |
| [util/io/componsablefs.go](https://codecov.io/gh/argoproj/argo-cd/pull/8517/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj#diff-dXRpbC9pby9jb21wb25zYWJsZWZzLmdv) | `0.00% <0.00%> (ø)` | |
| ... and [1 more](https://codecov.io/gh/argoproj/argo-cd/pull/8517/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/argoproj/argo-cd/pull/8517?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/argoproj/argo-cd/pull/8517?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj). Last update [c078bbb...7967831](https://codecov.io/gh/argoproj/argo-cd/pull/8517?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj).

alexmt: @crenshaw-dev request about `file, err := ioutil.TempFile("", "values-*.yaml") ` already implemented in another PR

Patch:
diff --git a/reposerver/repository/lock.go b/reposerver/repository/lock.go
index 14a2e6f07..06c4aa4ac 100644
--- a/reposerver/repository/lock.go
+++ b/reposerver/repository/lock.go
@@ -17,7 +17,7 @@ type repositoryLock struct {
 }
 
 // Lock acquires lock unless lock is already acquired with the same commit and allowConcurrent is set to true
-func (r *repositoryLock) Lock(path string, revision string, allowConcurrent bool, init func() error) (io.Closer, error) {
+func (r *repositoryLock) Lock(path string, revision string, allowConcurrent bool, init func() (io.Closer, error)) (io.Closer, error) {
 	r.lock.Lock()
 	state, ok := r.stateByKey[path]
 	if !ok {
@@ -30,26 +30,30 @@ func (r *repositoryLock) Lock(path string, revision string, allowConcurrent bool
 		state.cond.L.Lock()
 		notify := false
 		state.processCount--
+		var err error
 		if state.processCount == 0 {
 			notify = true
 			state.revision = ""
+			err = state.initCloser.Close()
 		}
+
 		state.cond.L.Unlock()
 		if notify {
 			state.cond.Broadcast()
 		}
-		return nil
+		return err
 	})
 
 	for {
 		state.cond.L.Lock()
 		if state.revision == "" {
 			// no in progress operation for that repo. Go ahead.
-			if err := init(); err != nil {
+			initCloser, err := init()
+			if err != nil {
 				state.cond.L.Unlock()
 				return nil, err
 			}
-
+			state.initCloser = initCloser
 			state.revision = revision
 			state.processCount = 1
 			state.allowConcurrent = allowConcurrent
@@ -71,6 +75,7 @@ func (r *repositoryLock) Lock(path string, revision string, allowConcurrent bool
 type repositoryState struct {
 	cond            *sync.Cond
 	revision        string
+	initCloser      io.Closer
 	processCount    int
 	allowConcurrent bool
 }
diff --git a/reposerver/repository/repository.go b/reposerver/repository/repository.go
index 82cb9fa18..39c717829 100644
--- a/reposerver/repository/repository.go
+++ b/reposerver/repository/repository.go
@@ -6,6 +6,8 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	goio "io"
+	"io/fs"
 	"io/ioutil"
 	"net/url"
 	"os"
@@ -16,10 +18,6 @@ import (
 	"strings"
 	"time"
 
-	"github.com/google/go-jsonnet"
-
-	"github.com/argoproj/argo-cd/v2/util/argo"
-
 	"github.com/Masterminds/semver/v3"
 	"github.com/TomOnTime/utfutil"
 	"github.com/argoproj/gitops-engine/pkg/utils/kube"
@@ -27,6 +25,8 @@ import (
 	"github.com/argoproj/pkg/sync"
 	jsonpatch "github.com/evanphx/json-patch"
 	"github.com/ghodss/yaml"
+	gogit "github.com/go-git/go-git/v5"
+	"github.com/google/go-jsonnet"
 	log "github.com/sirupsen/logrus"
 	"golang.org/x/sync/semaphore"
 	"google.golang.org/grpc/codes"
@@ -44,10 +44,12 @@ import (
 	"github.com/argoproj/argo-cd/v2/reposerver/metrics"
 	"github.com/argoproj/argo-cd/v2/util/app/discovery"
 	argopath "github.com/argoproj/argo-cd/v2/util/app/path"
+	"github.com/argoproj/argo-cd/v2/util/argo"
 	executil "github.com/argoproj/argo-cd/v2/util/exec"
 	"github.com/argoproj/argo-cd/v2/util/git"
 	"github.com/argoproj/argo-cd/v2/util/glob"
 	"github.com/argoproj/argo-cd/v2/util/gpg"
+	"github.com/argoproj/argo-cd/v2/util/grpc"
 	"github.com/argoproj/argo-cd/v2/util/helm"
 	"github.com/argoproj/argo-cd/v2/util/io"
 	"github.com/argoproj/argo-cd/v2/util/ksonnet"
@@ -68,12 +70,16 @@ const (
 // Service implements ManifestService interface
 type Service struct {
 	gitCredsStore             git.CredsStore
+	rootDir                   string
+	gitRepoPaths              *io.TempPaths
+	chartPaths                *io.TempPaths
+	gitRepoInitializer        func(rootPath string) goio.Closer
 	repoLock                  *repositoryLock
 	cache                     *reposervercache.Cache
 	parallelismLimitSemaphore *semaphore.Weighted
 	metricsServer             *metrics.MetricsServer
 	resourceTracking          argo.ResourceTracking
-	newGitClient              func(rawRepoURL string, creds git.Creds, insecure bool, enableLfs bool, proxy string, opts ...git.ClientOpts) (git.Client, error)
+	newGitClient              func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, proxy string, opts ...git.ClientOpts) (git.Client, error)
 	newHelmClient             func(repoURL string, creds helm.Creds, enableOci bool, proxy string, opts ...helm.ClientOpts) helm.Client
 	initConstants             RepoServerInitConstants
 	// now is usually just time.Now, but may be replaced by unit tests for testing purposes
@@ -89,7 +95,7 @@ type RepoServerInitConstants struct {
 }
 
 // NewService returns a new instance of the Manifest service
-func NewService(metricsServer *metrics.MetricsServer, cache *reposervercache.Cache, initConstants RepoServerInitConstants, resourceTracking argo.ResourceTracking, gitCredsStore git.CredsStore) *Service {
+func NewService(metricsServer *metrics.MetricsServer, cache *reposervercache.Cache, initConstants RepoServerInitConstants, resourceTracking argo.ResourceTracking, gitCredsStore git.CredsStore, rootDir string) *Service {
 	var parallelismLimitSemaphore *semaphore.Weighted
 	if initConstants.ParallelismLimit > 0 {
 		parallelismLimitSemaphore = semaphore.NewWeighted(initConstants.ParallelismLimit)
@@ -100,15 +106,54 @@ func NewService(metricsServer *metrics.MetricsServer, cache *reposervercache.Cac
 		repoLock:                  repoLock,
 		cache:                     cache,
 		metricsServer:             metricsServer,
-		newGitClient:              git.NewClient,
+		newGitClient:              git.NewClientExt,
 		resourceTracking:          resourceTracking,
 		newHelmClient: func(repoURL string, creds helm.Creds, enableOci bool, proxy string, opts ...helm.ClientOpts) helm.Client {
 			return helm.NewClientWithLock(repoURL, creds, sync.NewKeyLock(), enableOci, proxy, opts...)
 		},
-		initConstants: initConstants,
-		now:           time.Now,
-		gitCredsStore: gitCredsStore,
+		initConstants:      initConstants,
+		now:                time.Now,
+		gitCredsStore:      gitCredsStore,
+		gitRepoPaths:       io.NewTempPaths(rootDir),
+		chartPaths:         io.NewTempPaths(rootDir),
+		gitRepoInitializer: directoryPermissionInitializer,
+		rootDir:            rootDir,
+	}
+}
+
+func (s *Service) Init() error {
+	_, err := os.Stat(s.rootDir)
+	if os.IsNotExist(err) {
+		return os.MkdirAll(s.rootDir, 0300)
+	}
+	if err == nil {
+		// give itself read permissions to list previously written directories
+		err = os.Chmod(s.rootDir, 0700)
+	}
+	var files []fs.FileInfo
+	if err == nil {
+		files, err = ioutil.ReadDir(s.rootDir)
+	}
+	if err != nil {
+		log.Warnf("Failed to restore cloned repositories paths: %v", err)
+		return nil
+	}
+
+	for _, file := range files {
+		if !file.IsDir() {
+			continue
+		}
+		fullPath := filepath.Join(s.rootDir, file.Name())
+		closer := s.gitRepoInitializer(fullPath)
+		if repo, err := gogit.PlainOpen(fullPath); err == nil {
+			if remotes, err := repo.Remotes(); err == nil && len(remotes) > 0 && len(remotes[0].Config().URLs) > 0 {
+				s.gitRepoPaths.Add(git.NormalizeGitURL(remotes[0].Config().URLs[0]), fullPath)
+			}
+		}
+		io.Close(closer)
 	}
+	// remove read permissions since no-one should be able to list the directories
+	return os.Chmod(s.rootDir, 0300)
 }
 
 // List a subset of the refs (currently, branches and tags) of a git repo
@@ -148,8 +193,8 @@ func (s *Service) ListApps(ctx context.Context, q *apiclient.ListAppsRequest) (*
 	s.metricsServer.IncPendingRepoRequest(q.Repo.Repo)
 	defer s.metricsServer.DecPendingRepoRequest(q.Repo.Repo)
 
-	closer, err := s.repoLock.Lock(gitClient.Root(), commitSHA, true, func() error {
-		return checkoutRevision(gitClient, commitSHA, s.initConstants.SubmoduleEnabled)
+	closer, err := s.repoLock.Lock(gitClient.Root(), commitSHA, true, func() (goio.Closer, error) {
+		return s.checkoutRevision(gitClient, commitSHA, s.initConstants.SubmoduleEnabled)
 	})
 
 	if err != nil {
@@ -207,6 +252,11 @@ func (s *Service) runRepoOperation(
 	operation func(repoRoot, commitSHA, cacheKey string, ctxSrc operationContextSrc) error,
 	settings operationSettings) error {
 
+	if sanitizer, ok := grpc.SanitizerFromContext(ctx); ok {
+		// make sure randomized path replaced with '.' in the error message
+		sanitizer.AddRegexReplacement(regexp.MustCompile(`(`+s.rootDir+`/.*?)/`), ".")
+	}
+
 	var gitClient git.Client
 	var helmClient helm.Client
 	var err error
@@ -260,8 +310,8 @@ func (s *Service) runRepoOperation(
 			return &operationContext{chartPath, ""}, nil
 		})
 	} else {
-		closer, err := s.repoLock.Lock(gitClient.Root(), revision, settings.allowConcurrent, func() error {
-			return checkoutRevision(gitClient, revision, s.initConstants.SubmoduleEnabled)
+		closer, err := s.repoLock.Lock(gitClient.Root(), revision, settings.allowConcurrent, func() (goio.Closer, error) {
+			return s.checkoutRevision(gitClient, revision, s.initConstants.SubmoduleEnabled)
 		})
 
 		if err != nil {
@@ -1626,8 +1676,8 @@ func (s *Service) GetRevisionMetadata(ctx context.Context, q *apiclient.RepoServ
 	s.metricsServer.IncPendingRepoRequest(q.Repo.Repo)
 	defer s.metricsServer.DecPendingRepoRequest(q.Repo.Repo)
 
-	closer, err := s.repoLock.Lock(gitClient.Root(), q.Revision, true, func() error {
-		return checkoutRevision(gitClient, q.Revision, s.initConstants.SubmoduleEnabled)
+	closer, err := s.repoLock.Lock(gitClient.Root(), q.Revision, true, func() (goio.Closer, error) {
+		return s.checkoutRevision(gitClient, q.Revision, s.initConstants.SubmoduleEnabled)
 	})
 
 	if err != nil {
@@ -1675,8 +1725,12 @@ func fileParameters(q *apiclient.RepoServerAppDetailsQuery) []v1alpha1.HelmFileP
 }
 
 func (s *Service) newClient(repo *v1alpha1.Repository, opts ...git.ClientOpts) (git.Client, error) {
+	repoPath, err := s.gitRepoPaths.GetPath(git.NormalizeGitURL(repo.Repo))
+	if err != nil {
+		return nil, err
+	}
 	opts = append(opts, git.WithEventHandlers(metrics.NewGitClientEventHandlers(s.metricsServer)))
-	return s.newGitClient(repo.Repo, repo.GetGitCreds(s.gitCredsStore), repo.IsInsecure(), repo.EnableLFS, repo.Proxy, opts...)
+	return s.newGitClient(repo.Repo, repoPath, repo.GetGitCreds(s.gitCredsStore), repo.IsInsecure(), repo.EnableLFS, repo.Proxy, opts...)
 }
 
 // newClientResolveRevision is a helper to perform the common task of instantiating a git client
@@ -1695,7 +1749,7 @@ func (s *Service) newClientResolveRevision(repo *v1alpha1.Repository, revision s
 
 func (s *Service) newHelmClientResolveRevision(repo *v1alpha1.Repository, revision string, chart string, noRevisionCache bool) (helm.Client, string, error) {
 	enableOCI := repo.EnableOCI || helm.IsHelmOciRepo(repo.Repo)
-	helmClient := s.newHelmClient(repo.Repo, repo.GetHelmCreds(), enableOCI, repo.Proxy, helm.WithIndexCache(s.cache))
+	helmClient := s.newHelmClient(repo.Repo, repo.GetHelmCreds(), enableOCI, repo.Proxy, helm.WithIndexCache(s.cache), helm.WithChartPaths(s.chartPaths))
 	// OCI helm registers don't support semver ranges. Assuming that given revision is exact version
 	if helm.IsVersion(revision) || enableOCI {
 		return helmClient, revision, nil
@@ -1719,13 +1773,35 @@ func (s *Service) newHelmClientResolveRevision(repo *v1alpha1.Repository, revisi
 	return helmClient, version.String(), nil
 }
 
+// directoryPermissionInitializer ensures the directory has read/write/execute permissions and returns
+// a function that can be used to remove all permissions.
+func directoryPermissionInitializer(rootPath string) goio.Closer {
+	if _, err := os.Stat(rootPath); err == nil {
+		if err := os.Chmod(rootPath, 0700); err != nil {
+			log.Warnf("Failed to restore read/write/execute permissions on %s: %v", rootPath, err)
+		} else {
+			log.Debugf("Successfully restored read/write/execute permissions on %s", rootPath)
+		}
+	}
+
+	return io.NewCloser(func() error {
+		if err := os.Chmod(rootPath, 0000); err != nil {
+			log.Warnf("Failed to remove permissions on %s: %v", rootPath, err)
+		} else {
+			log.Debugf("Successfully removed permissions on %s", rootPath)
+		}
+		return nil
+	})
+}
+
 // checkoutRevision is a convenience function to initialize a repo, fetch, and checkout a revision
 // Returns the 40 character commit SHA after the checkout has been performed
 // nolint:unparam
-func checkoutRevision(gitClient git.Client, revision string, submoduleEnabled bool) error {
+func (s *Service) checkoutRevision(gitClient git.Client, revision string, submoduleEnabled bool) (goio.Closer, error) {
+	closer := s.gitRepoInitializer(gitClient.Root())
 	err := gitClient.Init()
 	if err != nil {
-		return status.Errorf(codes.Internal, "Failed to initialize git repo: %v", err)
+		return closer, status.Errorf(codes.Internal, "Failed to initialize git repo: %v", err)
 	}
 
 	err = gitClient.Fetch(revision)
@@ -1735,25 +1811,25 @@ func checkoutRevision(gitClient git.Client, revision string, submoduleEnabled bo
 		log.Infof("Fallback to fetch default")
 		err = gitClient.Fetch("")
 		if err != nil {
-			return status.Errorf(codes.Internal, "Failed to fetch default: %v", err)
+			return closer, status.Errorf(codes.Internal, "Failed to fetch default: %v", err)
 		}
 		err = gitClient.Checkout(revision, submoduleEnabled)
 		if err != nil {
-			return status.Errorf(codes.Internal, "Failed to checkout revision %s: %v", revision, err)
+			return closer, status.Errorf(codes.Internal, "Failed to checkout revision %s: %v", revision, err)
 		}
-		return err
+		return closer, err
 	}
 
 	err = gitClient.Checkout("FETCH_HEAD", submoduleEnabled)
 	if err != nil {
-		return status.Errorf(codes.Internal, "Failed to checkout FETCH_HEAD: %v", err)
+		return closer, status.Errorf(codes.Internal, "Failed to checkout FETCH_HEAD: %v", err)
 	}
 
-	return err
+	return closer, err
 }
 
 func (s *Service) GetHelmCharts(ctx context.Context, q *apiclient.HelmChartsRequest) (*apiclient.HelmChartsResponse, error) {
-	index, err := s.newHelmClient(q.Repo.Repo, q.Repo.GetHelmCreds(), q.Repo.EnableOCI, q.Repo.Proxy).GetIndex(true)
+	index, err := s.newHelmClient(q.Repo.Repo, q.Repo.GetHelmCreds(), q.Repo.EnableOCI, q.Repo.Proxy, helm.WithChartPaths(s.chartPaths)).GetIndex(true)
 	if err != nil {
 		return nil, err
 	}
@@ -1814,7 +1890,7 @@ func (s *Service) ResolveRevision(ctx context.Context, q *apiclient.ResolveRevis
 		if helm.IsVersion(ambiguousRevision) {
 			return &apiclient.ResolveRevisionResponse{Revision: ambiguousRevision, AmbiguousRevision: ambiguousRevision}, nil
 		}
-		client := helm.NewClient(repo.Repo, repo.GetHelmCreds(), repo.EnableOCI || app.Spec.Source.IsHelmOci(), repo.Proxy)
+		client := helm.NewClient(repo.Repo, repo.GetHelmCreds(), repo.EnableOCI || app.Spec.Source.IsHelmOci(), repo.Proxy, helm.WithChartPaths(s.chartPaths))
 		index, err := client.GetIndex(false)
 		if err != nil {
 			return &apiclient.ResolveRevisionResponse{Revision: "", AmbiguousRevision: ""}, err
diff --git a/reposerver/server.go b/reposerver/server.go
index fb2e861c3..75ddbfd2c 100644
--- a/reposerver/server.go
+++ b/reposerver/server.go
@@ -4,6 +4,7 @@ import (
 	"crypto/tls"
 	"fmt"
 	"os"
+	"path/filepath"
 
 	grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
 	grpc_logrus "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus"
@@ -32,6 +33,7 @@ import (
 // ArgoCDRepoServer is the repo server implementation
 type ArgoCDRepoServer struct {
 	log           *log.Entry
+	repoService   *repository.Service
 	metricsServer *metrics.MetricsServer
 	gitCredsStore git.CredsStore
 	cache         *reposervercache.Cache
@@ -65,7 +67,12 @@ func NewServer(metricsServer *metrics.MetricsServer, cache *reposervercache.Cach
 
 	serverLog := log.NewEntry(log.StandardLogger())
 	streamInterceptors := []grpc.StreamServerInterceptor{grpc_logrus.StreamServerInterceptor(serverLog), grpc_prometheus.StreamServerInterceptor, grpc_util.PanicLoggerStreamServerInterceptor(serverLog)}
-	unaryInterceptors := []grpc.UnaryServerInterceptor{grpc_logrus.UnaryServerInterceptor(serverLog), grpc_prometheus.UnaryServerInterceptor, grpc_util.PanicLoggerUnaryServerInterceptor(serverLog)}
+	unaryInterceptors := []grpc.UnaryServerInterceptor{
+		grpc_logrus.UnaryServerInterceptor(serverLog),
+		grpc_prometheus.UnaryServerInterceptor,
+		grpc_util.PanicLoggerUnaryServerInterceptor(serverLog),
+		grpc_util.ErrorSanitizerUnaryServerInterceptor(),
+	}
 
 	serverOpts := []grpc.ServerOption{
 		grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(unaryInterceptors...)),
@@ -79,6 +86,10 @@ func NewServer(metricsServer *metrics.MetricsServer, cache *reposervercache.Cach
 	if tlsConfig != nil {
 		serverOpts = append(serverOpts, grpc.Creds(credentials.NewTLS(tlsConfig)))
 	}
+	repoService := repository.NewService(metricsServer, cache, initConstants, argo.NewResourceTracking(), gitCredsStore, filepath.Join(os.TempDir(), "_argocd-repo"))
+	if err := repoService.Init(); err != nil {
+		return nil, err
+	}
 
 	return &ArgoCDRepoServer{
 		log:           serverLog,
@@ -87,6 +98,7 @@ func NewServer(metricsServer *metrics.MetricsServer, cache *reposervercache.Cach
 		initConstants: initConstants,
 		opts:          serverOpts,
 		gitCredsStore: gitCredsStore,
+		repoService:   repoService,
 	}, nil
 }
 
@@ -96,8 +108,7 @@ func (a *ArgoCDRepoServer) CreateGRPC() *grpc.Server {
 	versionpkg.RegisterVersionServiceServer(server, version.NewServer(nil, func() (bool, error) {
 		return true, nil
 	}))
-	manifestService := repository.NewService(a.metricsServer, a.cache, a.initConstants, argo.NewResourceTracking(), a.gitCredsStore)
-	apiclient.RegisterRepoServerServiceServer(server, manifestService)
+	apiclient.RegisterRepoServerServiceServer(server, a.repoService)
 
 	healthService := health.NewServer()
 	grpc_health_v1.RegisterHealthServer(server, healthService)
diff --git a/ui/package.json b/ui/package.json
index 121f7c813..489bb8b4d 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -39,7 +39,7 @@
     "react-router": "^4.3.1",
     "react-router-dom": "^4.2.2",
     "react-svg-piechart": "^2.1.1",
-    "redoc": "^2.0.0-rc.63",
+    "redoc": "^2.0.0-rc.64",
     "rxjs": "^6.6.6",
     "superagent": "^3.8.2",
     "superagent-promise": "^1.1.0",
diff --git a/ui/src/app/applications/components/application-sync-options/application-sync-options.tsx b/ui/src/app/applications/components/application-sync-options/application-sync-options.tsx
index 269c99fe0..8f6f16b74 100644
--- a/ui/src/app/applications/components/application-sync-options/application-sync-options.tsx
+++ b/ui/src/app/applications/components/application-sync-options/application-sync-options.tsx
@@ -6,6 +6,7 @@ import * as ReactForm from 'react-form';
 require('./application-sync-options.scss');
 
 export const REPLACE_WARNING = `The resources will be synced using 'kubectl replace/create' command that is a potentially destructive action and might cause resources recreation.`;
+export const FORCE_WARNING = `The resources will be synced using '--force' that is a potentially destructive action and will immediately remove resources from the API and bypasses graceful deletion. Immediate deletion of some resources may result in inconsistency or data loss.`;
 
 export interface ApplicationSyncOptionProps {
     options: string[];
diff --git a/ui/src/app/applications/components/application-sync-panel/application-sync-panel.tsx b/ui/src/app/applications/components/application-sync-panel/application-sync-panel.tsx
index 93b38e706..ed3226143 100644
--- a/ui/src/app/applications/components/application-sync-panel/application-sync-panel.tsx
+++ b/ui/src/app/applications/components/application-sync-panel/application-sync-panel.tsx
@@ -7,7 +7,7 @@ import {Consumer} from '../../../shared/context';
 import * as models from '../../../shared/models';
 import {services} from '../../../shared/services';
 import {ApplicationRetryOptions} from '../application-retry-options/application-retry-options';
-import {ApplicationManualSyncFlags, ApplicationSyncOptions, SyncFlags, REPLACE_WARNING} from '../application-sync-options/application-sync-options';
+import {ApplicationManualSyncFlags, ApplicationSyncOptions, FORCE_WARNING, SyncFlags, REPLACE_WARNING} from '../application-sync-options/application-sync-options';
 import {ComparisonStatusIcon, nodeKey} from '../utils';
 
 require('./application-sync-panel.scss');
@@ -60,7 +60,6 @@ export const ApplicationSyncPanel = ({application, selectedResource, hide}: {app
                                 if (resources.length === appResources.length) {
                                     resources = null;
                                 }
-
                                 const replace = params.syncOptions?.findIndex((opt: string) => opt === 'Replace=true') > -1;
                                 if (replace) {
                                     const confirmed = await ctx.popup.confirm('Synchronize using replace?', () => (
@@ -75,11 +74,23 @@ export const ApplicationSyncPanel = ({application, selectedResource, hide}: {app
                                 }
 
                                 const syncFlags = {...params.syncFlags} as SyncFlags;
+                                const force = syncFlags.Force || false;
 
                                 if (syncFlags.ApplyOnly) {
-                                    syncStrategy.apply = {force: syncFlags.Force || false};
+                                    syncStrategy.apply = {force};
                                 } else {
-                                    syncStrategy.hook = {force: syncFlags.Force || false};
+                                    syncStrategy.hook = {force};
+                                }
+                                if (force) {
+                                    const confirmed = await ctx.popup.confirm('Synchronize with force?', () => (
+                                        <div>
+                                            <i className='fa fa-exclamation-triangle' style={{color: ARGO_WARNING_COLOR}} /> {FORCE_WARNING} Are you sure you want to continue?
+                                        </div>
+                                    ));
+                                    if (!confirmed) {
+                                        setPending(false);
+                                        return;
+                                    }
                                 }
 
                                 try {
diff --git a/ui/src/app/applications/components/applications-sync-panel/applications-sync-panel.tsx b/ui/src/app/applications/components/applications-sync-panel/applications-sync-panel.tsx
index a80afb731..8ca9e2d74 100644
--- a/ui/src/app/applications/components/applications-sync-panel/applications-sync-panel.tsx
+++ b/ui/src/app/applications/components/applications-sync-panel/applications-sync-panel.tsx
@@ -1,12 +1,12 @@
 import {ErrorNotification, FormField, NotificationType, SlidingPanel} from 'argo-ui';
 import * as React from 'react';
 import {Form, FormApi} from 'react-form';
-import {ProgressPopup} from '../../../shared/components';
+import {ARGO_WARNING_COLOR, ProgressPopup, Spinner} from '../../../shared/components';
 import {Consumer} from '../../../shared/context';
 import * as models from '../../../shared/models';
 import {services} from '../../../shared/services';
 import {ApplicationRetryOptions} from '../application-retry-options/application-retry-options';
-import {ApplicationManualSyncFlags, ApplicationSyncOptions, SyncFlags} from '../application-sync-options/application-sync-options';
+import {ApplicationManualSyncFlags, ApplicationSyncOptions, FORCE_WARNING, REPLACE_WARNING, SyncFlags} from '../application-sync-options/application-sync-options';
 import {ApplicationSelector} from '../../../shared/components';
 
 interface Progress {
@@ -18,6 +18,7 @@ export const ApplicationsSyncPanel = ({show, apps, hide}: {show: boolean; apps:
     const [form, setForm] = React.useState<FormApi>(null);
     const [progress, setProgress] = React.useState<Progress>(null);
     const getSelectedApps = (params: any) => apps.filter((_, i) => params['app/' + i]);
+    const [isPending, setPending] = React.useState(false);
     return (
         <Consumer>
             {ctx => (
@@ -27,7 +28,8 @@ export const ApplicationsSyncPanel = ({show, apps, hide}: {show: boolean; apps:
                     onClose={() => hide()}
                     header={
                         <div>
-                            <button className='argo-button argo-button--base' onClick={() => form.submitForm(null)}>
+                            <button className='argo-button argo-button--base' disabled={isPending} onClick={() => form.submitForm(null)}>
+                                <Spinner show={isPending} style={{marginRight: '5px'}} />
                                 Sync
                             </button>{' '}
                             <button onClick={() => hide()} className='argo-button argo-button--base-o'>
@@ -38,16 +40,40 @@ export const ApplicationsSyncPanel = ({show, apps, hide}: {show: boolean; apps:
                     <Form
                         defaultValues={{syncFlags: []}}
                         onSubmit={async (params: any) => {
+                            setPending(true);
                             const selectedApps = getSelectedApps(params);
+                            const syncFlags = {...params.syncFlags} as SyncFlags;
+                            const force = syncFlags.Force || false;
+                            if (force) {
+                                const confirmed = await ctx.popup.confirm('Synchronize with force?', () => (
+                                    <div>
+                                        <i className='fa fa-exclamation-triangle' style={{color: ARGO_WARNING_COLOR}} /> {FORCE_WARNING} Are you sure you want to continue?
+                                    </div>
+                                ));
+                                if (!confirmed) {
+                                    setPending(false);
+                                    return;
+                                }
+                            }
+                            const replace = params.syncOptions?.findIndex((opt: string) => opt === 'Replace=true') > -1;
+                            if (replace) {
+                                const confirmed = await ctx.popup.confirm('Synchronize using replace?', () => (
+                                    <div>
+                                        <i className='fa fa-exclamation-triangle' style={{color: ARGO_WARNING_COLOR}} /> {REPLACE_WARNING} Are you sure you want to continue?
+                                    </div>
+                                ));
+                                if (!confirmed) {
+                                    setPending(false);
+                                    return;
+                                }
+                            }
                             if (selectedApps.length === 0) {
                                 ctx.notifications.show({content: `No apps selected`, type: NotificationType.Error});
+                                setPending(false);
                                 return;
                             }
 
-                            const syncFlags = {...params.syncFlags} as SyncFlags;
-
-                            const syncStrategy: models.SyncStrategy =
-                                syncFlags.ApplyOnly || false ? {apply: {force: syncFlags.Force || false}} : {hook: {force: syncFlags.Force || false}};
+                            const syncStrategy: models.SyncStrategy = syncFlags.ApplyOnly || false ? {apply: {force}} : {hook: {force}};
 
                             setProgress({percentage: 0, title: 'Starting...'});
                             let i = 0;
@@ -68,6 +94,9 @@ export const ApplicationsSyncPanel = ({show, apps, hide}: {show: boolean; apps:
                                             content: <ErrorNotification title={`Unable to sync ${app.metadata.name}`} e={e} />,
                                             type: NotificationType.Error
                                         });
+                                    })
+                                    .finally(() => {
+                                        setPending(false);
                                     });
                                 i++;
                                 setProgress({
diff --git a/ui/yarn.lock b/ui/yarn.lock
index 8f1f34738..0c4b75e56 100644
--- a/ui/yarn.lock
+++ b/ui/yarn.lock
@@ -5560,10 +5560,10 @@ json-parse-even-better-errors@^2.3.0:
   resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
   integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
 
-json-pointer@^0.6.1:
-  version "0.6.1"
-  resolved "https://registry.yarnpkg.com/json-pointer/-/json-pointer-0.6.1.tgz#3c6caa6ac139e2599f5a1659d39852154015054d"
-  integrity sha512-3OvjqKdCBvH41DLpV4iSt6v2XhZXV1bPB4OROuknvUXI7ZQNofieCPkmE26stEJ9zdQuvIxDHCuYhfgxFAAs+Q==
+json-pointer@0.6.2, json-pointer@^0.6.2:
+  version "0.6.2"
+  resolved "https://registry.yarnpkg.com/json-pointer/-/json-pointer-0.6.2.tgz#f97bd7550be5e9ea901f8c9264c9d436a22a93cd"
+  integrity sha512-vLWcKbOaXlO+jvRy4qNd+TI1QUPZzfJj1tpJ3vAXDych5XJf93ftpUKe5pKCrzyIIwgBJcOcCVRUfqQP25afBw==
   dependencies:
     foreach "^2.0.4"
 
@@ -6614,13 +6614,13 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0:
   dependencies:
     wrappy "1"
 
-openapi-sampler@^1.1.1:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/openapi-sampler/-/openapi-sampler-1.2.0.tgz#2e82e4e32c1506a42f370be726d759d5275a9d4d"
-  integrity sha512-Y0tFg2iH7NWnNHYnesxhMfkXc7wWXyJXYMUTTxkGkfpl0U9u/ZOf6BxrdEXBD4sgs9uMlVWsbWLDLesVmSUU7Q==
+openapi-sampler@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/openapi-sampler/-/openapi-sampler-1.2.1.tgz#2ca9eea527f8f2ddb32c3ae1dda31afd8bf0833f"
+  integrity sha512-mHrYmyvcLD0qrfqPkPRBAL2z16hGT2rW0d0B7nklfoTcc3pmkJLkSZlKSeFgerUM41E5c7jlxf0Y19xrM7mWQQ==
   dependencies:
     "@types/json-schema" "^7.0.7"
-    json-pointer "^0.6.1"
+    json-pointer "0.6.2"
 
 opn@^5.5.0:
   version "5.5.0"
@@ -8002,10 +8002,10 @@ redent@^3.0.0:
     indent-string "^4.0.0"
     strip-indent "^3.0.0"
 
-redoc@^2.0.0-rc.63:
-  version "2.0.0-rc.63"
-  resolved "https://registry.yarnpkg.com/redoc/-/redoc-2.0.0-rc.63.tgz#d1bea51d3fdf2a6f5371a9bf7f06253ac5c1a487"
-  integrity sha512-PsoPqRyNqHi7+jKUyFBwJhHrzjMl4N5vieTeBloRGbhWuY3PPH2DJ3ihgrLfdEV0glzq/LMTaqfarm8WLqCc4Q==
+redoc@^2.0.0-rc.64:
+  version "2.0.0-rc.64"
+  resolved "https://registry.yarnpkg.com/redoc/-/redoc-2.0.0-rc.64.tgz#6fc326092a826d6e0b631cde41980ba3bc8d7e73"
+  integrity sha512-zrM/vcONpbmyDUOpk7Ai7R/yZrT7W1+8ANJUB3b5kzaLQUx4VbrDoT4D6HHHquOnKx+5We4nOPPAi8fi/cqa8g==
   dependencies:
     "@redocly/openapi-core" "^1.0.0-beta.54"
     "@redocly/react-dropdown-aria" "^2.0.11"
@@ -8013,12 +8013,12 @@ redoc@^2.0.0-rc.63:
     decko "^1.2.0"
     dompurify "^2.2.8"
     eventemitter3 "^4.0.7"
-    json-pointer "^0.6.1"
+    json-pointer "^0.6.2"
     lunr "^2.3.9"
     mark.js "^8.11.1"
     marked "^4.0.10"
     mobx-react "^7.2.0"
-    openapi-sampler "^1.1.1"
+    openapi-sampler "^1.2.1"
     path-browserify "^1.0.1"
     perfect-scrollbar "^1.5.1"
     polished "^4.1.3"
diff --git a/util/grpc/sanitizer.go b/util/grpc/sanitizer.go
new file mode 100644
index 000000000..90eafd62f
--- /dev/null
+++ b/util/grpc/sanitizer.go
@@ -0,0 +1,82 @@
+package grpc
+
+import (
+	"errors"
+	"regexp"
+	"strings"
+
+	"golang.org/x/net/context"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/status"
+)
+
+const (
+	contextKey = "sanitizer"
+)
+
+// ErrorSanitizerUnaryServerInterceptor returns a new unary server interceptor that sanitizes error messages
+// and provides Sanitizer to define replacements.
+func ErrorSanitizerUnaryServerInterceptor() grpc.UnaryServerInterceptor {
+	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
+		sanitizer := NewSanitizer()
+		resp, err = handler(ContextWithSanitizer(ctx, sanitizer), req)
+		if err == nil {
+			return resp, nil
+		}
+
+		if se, ok := err.(interface{ GRPCStatus() *status.Status }); ok {
+			return resp, status.Error(se.GRPCStatus().Code(), sanitizer.Replace(se.GRPCStatus().Message()))
+		}
+
+		return resp, errors.New(sanitizer.Replace(err.Error()))
+	}
+}
+
+// ContextWithSanitizer returns a new context with sanitizer set.
+func ContextWithSanitizer(ctx context.Context, sanitizer Sanitizer) context.Context {
+	return context.WithValue(ctx, contextKey, sanitizer)
+}
+
+// SanitizerFromContext returns sanitizer from context.
+func SanitizerFromContext(ctx context.Context) (Sanitizer, bool) {
+	res, ok := ctx.Value(contextKey).(Sanitizer)
+	return res, ok
+}
+
+// Sanitizer provides methods to define list of strings and replacements
+type Sanitizer interface {
+	Replace(s string) string
+	AddReplacement(val string, replacement string)
+	AddRegexReplacement(regex *regexp.Regexp, replacement string)
+}
+
+type sanitizer struct {
+	replacers []func(in string) string
+}
+
+// NewSanitizer returns a new Sanitizer instance
+func NewSanitizer() *sanitizer {
+	return &sanitizer{}
+}
+
+// AddReplacement adds a replacement to the Sanitizer
+func (s *sanitizer) AddReplacement(val string, replacement string) {
+	s.replacers = append(s.replacers, func(in string) string {
+		return strings.Replace(in, val, replacement, -1)
+	})
+}
+
+// AddRegexReplacement adds a replacement to the sanitizer using regexp
+func (s *sanitizer) AddRegexReplacement(regex *regexp.Regexp, replacement string) {
+	s.replacers = append(s.replacers, func(in string) string {
+		return regex.ReplaceAllString(in, replacement)
+	})
+}
+
+// Replace replaces all occurrences of the configured values in the sanitizer with the replacements
+func (s *sanitizer) Replace(val string) string {
+	for _, replacer := range s.replacers {
+		val = replacer(val)
+	}
+	return val
+}
diff --git a/util/helm/client.go b/util/helm/client.go
index 2911c68cb..c3c9ed7e7 100644
--- a/util/helm/client.go
+++ b/util/helm/client.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"crypto/tls"
 	"crypto/x509"
+	"encoding/json"
 	"errors"
 	"fmt"
 	"io/ioutil"
@@ -17,6 +18,7 @@ import (
 	"time"
 
 	"github.com/argoproj/pkg/sync"
+	"github.com/google/uuid"
 	log "github.com/sirupsen/logrus"
 	"gopkg.in/yaml.v2"
 
@@ -60,18 +62,24 @@ func WithIndexCache(indexCache indexCache) ClientOpts {
 	}
 }
 
+func WithChartPaths(chartPaths *io.TempPaths) ClientOpts {
+	return func(c *nativeHelmChart) {
+		c.chartCachePaths = chartPaths
+	}
+}
+
 func NewClient(repoURL string, creds Creds, enableOci bool, proxy string, opts ...ClientOpts) Client {
 	return NewClientWithLock(repoURL, creds, globalLock, enableOci, proxy, opts...)
 }
 
 func NewClientWithLock(repoURL string, creds Creds, repoLock sync.KeyLock, enableOci bool, proxy string, opts ...ClientOpts) Client {
 	c := &nativeHelmChart{
-		repoURL:   repoURL,
-		creds:     creds,
-		repoPath:  filepath.Join(os.TempDir(), strings.Replace(repoURL, "/", "_", -1)),
-		repoLock:  repoLock,
-		enableOci: enableOci,
-		proxy:     proxy,
+		repoURL:         repoURL,
+		creds:           creds,
+		repoLock:        repoLock,
+		enableOci:       enableOci,
+		proxy:           proxy,
+		chartCachePaths: io.NewTempPaths(os.TempDir()),
 	}
 	for i := range opts {
 		opts[i](c)
@@ -82,13 +90,13 @@ func NewClientWithLock(repoURL string, creds Creds, repoLock sync.KeyLock, enabl
 var _ Client = &nativeHelmChart{}
 
 type nativeHelmChart struct {
-	repoPath   string
-	repoURL    string
-	creds      Creds
-	repoLock   sync.KeyLock
-	enableOci  bool
-	indexCache indexCache
-	proxy      string
+	chartCachePaths *io.TempPaths
+	repoURL         string
+	creds           Creds
+	repoLock        sync.KeyLock
+	enableOci       bool
+	indexCache      indexCache
+	proxy           string
 }
 
 func fileExist(filePath string) (bool, error) {
@@ -102,29 +110,29 @@ func fileExist(filePath string) (bool, error) {
 	return true, nil
 }
 
-func (c *nativeHelmChart) ensureHelmChartRepoPath() error {
-	c.repoLock.Lock(c.repoPath)
-	defer c.repoLock.Unlock(c.repoPath)
-
-	err := os.Mkdir(c.repoPath, 0700)
-	if err != nil && !os.IsExist(err) {
+func (c *nativeHelmChart) CleanChartCache(chart string, version string) error {
+	cachePath, err := c.getCachedChartPath(chart, version)
+	if err != nil {
 		return err
 	}
-	return nil
-}
-
-func (c *nativeHelmChart) CleanChartCache(chart string, version string) error {
-	return os.RemoveAll(c.getCachedChartPath(chart, version))
+	return os.RemoveAll(cachePath)
 }
 
-func (c *nativeHelmChart) ExtractChart(chart string, version string, passCredentials bool) (string, io.Closer, error) {
-	err := c.ensureHelmChartRepoPath()
+func createTempDir() (string, error) {
+	newUUID, err := uuid.NewRandom()
 	if err != nil {
-		return "", nil, err
+		return "", err
+	}
+	tempDir := path.Join(os.TempDir(), newUUID.String())
+	if err := os.Mkdir(tempDir, 0755); err != nil {
+		return "", err
 	}
+	return tempDir, nil
+}
 
+func (c *nativeHelmChart) ExtractChart(chart string, version string, passCredentials bool) (string, io.Closer, error) {
 	// always use Helm V3 since we don't have chart content to determine correct Helm version
-	helmCmd, err := NewCmdWithVersion(c.repoPath, HelmV3, c.enableOci, c.proxy)
+	helmCmd, err := NewCmdWithVersion("", HelmV3, c.enableOci, c.proxy)
 
 	if err != nil {
 		return "", nil, err
@@ -137,12 +145,15 @@ func (c *nativeHelmChart) ExtractChart(chart string, version string, passCredent
 	}
 
 	// throw away temp directory that stores extracted chart and should be deleted as soon as no longer needed by returned closer
-	tempDir, err := ioutil.TempDir("", "helm")
+	tempDir, err := createTempDir()
 	if err != nil {
 		return "", nil, err
 	}
 
-	cachedChartPath := c.getCachedChartPath(chart, version)
+	cachedChartPath, err := c.getCachedChartPath(chart, version)
+	if err != nil {
+		return "", nil, err
+	}
 
 	c.repoLock.Lock(cachedChartPath)
 	defer c.repoLock.Unlock(cachedChartPath)
@@ -155,7 +166,7 @@ func (c *nativeHelmChart) ExtractChart(chart string, version string, passCredent
 
 	if !exists {
 		// create empty temp directory to extract chart from the registry
-		tempDest, err := ioutil.TempDir("", "helm")
+		tempDest, err := createTempDir()
 		if err != nil {
 			return "", nil, err
 		}
@@ -355,8 +366,12 @@ func normalizeChartName(chart string) string {
 	return nc
 }
 
-func (c *nativeHelmChart) getCachedChartPath(chart string, version string) string {
-	return path.Join(c.repoPath, fmt.Sprintf("%s-%s.tgz", strings.ReplaceAll(chart, "/", "_"), version))
+func (c *nativeHelmChart) getCachedChartPath(chart string, version string) (string, error) {
+	keyData, err := json.Marshal(map[string]string{"url": c.repoURL, "chart": chart, "version": version})
+	if err != nil {
+		return "", err
+	}
+	return c.chartCachePaths.GetPath(string(keyData))
 }
 
 // Ensures that given OCI registries URL does not have protocol
diff --git a/util/io/paths.go b/util/io/paths.go
new file mode 100644
index 000000000..b9c20283b
--- /dev/null
+++ b/util/io/paths.go
@@ -0,0 +1,44 @@
+package io
+
+import (
+	"path/filepath"
+	"sync"
+
+	"github.com/google/uuid"
+)
+
+// TempPaths allows generating and memoizing random paths, each path being mapped to a specific key.
+type TempPaths struct {
+	root  string
+	paths map[string]string
+	lock  sync.Mutex
+}
+
+func NewTempPaths(root string) *TempPaths {
+	return &TempPaths{
+		root:  root,
+		paths: map[string]string{},
+	}
+}
+
+func (p *TempPaths) Add(key string, value string) {
+	p.lock.Lock()
+	defer p.lock.Unlock()
+	p.paths[key] = value
+}
+
+// GetPath generates a path for the given key or returns previously generated one.
+func (p *TempPaths) GetPath(key string) (string, error) {
+	p.lock.Lock()
+	defer p.lock.Unlock()
+	if val, ok := p.paths[key]; ok {
+		return val, nil
+	}
+	uniqueId, err := uuid.NewRandom()
+	if err != nil {
+		return "", err
+	}
+	repoPath := filepath.Join(p.root, uniqueId.String())
+	p.paths[key] = repoPath
+	return repoPath, nil
+}


Test patch:
diff --git a/reposerver/repository/lock_test.go b/reposerver/repository/lock_test.go
index e3000128c..db2bf2558 100644
--- a/reposerver/repository/lock_test.go
+++ b/reposerver/repository/lock_test.go
@@ -26,10 +26,10 @@ func lockQuickly(action func() (io.Closer, error)) (io.Closer, bool) {
 	}
 }
 
-func numberOfInits(initializedTimes *int) func() error {
-	return func() error {
+func numberOfInits(initializedTimes *int) func() (io.Closer, error) {
+	return func() (io.Closer, error) {
 		*initializedTimes++
-		return nil
+		return util.NopCloser, nil
 	}
 }
 
@@ -120,8 +120,8 @@ func TestLock_FailedInitialization(t *testing.T) {
 	lock := NewRepositoryLock()
 
 	closer1, done := lockQuickly(func() (io.Closer, error) {
-		return lock.Lock("myRepo", "1", true, func() error {
-			return errors.New("failed")
+		return lock.Lock("myRepo", "1", true, func() (io.Closer, error) {
+			return util.NopCloser, errors.New("failed")
 		})
 	})
 
@@ -132,8 +132,8 @@ func TestLock_FailedInitialization(t *testing.T) {
 	assert.Nil(t, closer1)
 
 	closer2, done := lockQuickly(func() (io.Closer, error) {
-		return lock.Lock("myRepo", "1", true, func() error {
-			return nil
+		return lock.Lock("myRepo", "1", true, func() (io.Closer, error) {
+			return util.NopCloser, nil
 		})
 	})
 
diff --git a/reposerver/repository/repository_test.go b/reposerver/repository/repository_test.go
index 6c8162f4c..62d66d51d 100644
--- a/reposerver/repository/repository_test.go
+++ b/reposerver/repository/repository_test.go
@@ -5,17 +5,17 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	goio "io"
 	"io/ioutil"
 	"os"
 	"os/exec"
+	"path"
 	"path/filepath"
 	"regexp"
 	"strings"
 	"testing"
 	"time"
 
-	"github.com/argoproj/argo-cd/v2/util/argo"
-
 	"github.com/ghodss/yaml"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
@@ -29,6 +29,7 @@ import (
 	"github.com/argoproj/argo-cd/v2/reposerver/cache"
 	"github.com/argoproj/argo-cd/v2/reposerver/metrics"
 	fileutil "github.com/argoproj/argo-cd/v2/test/fixture/path"
+	"github.com/argoproj/argo-cd/v2/util/argo"
 	cacheutil "github.com/argoproj/argo-cd/v2/util/cache"
 	"github.com/argoproj/argo-cd/v2/util/git"
 	gitmocks "github.com/argoproj/argo-cd/v2/util/git/mocks"
@@ -72,7 +73,7 @@ func newServiceWithOpt(cf clientFunc) (*Service, *gitmocks.Client) {
 		cacheutil.NewCache(cacheutil.NewInMemoryCache(1*time.Minute)),
 		1*time.Minute,
 		1*time.Minute,
-	), RepoServerInitConstants{ParallelismLimit: 1}, argo.NewResourceTracking(), &git.NoopCredsStore{})
+	), RepoServerInitConstants{ParallelismLimit: 1}, argo.NewResourceTracking(), &git.NoopCredsStore{}, os.TempDir())
 
 	chart := "my-chart"
 	version := "1.1.0"
@@ -82,12 +83,15 @@ func newServiceWithOpt(cf clientFunc) (*Service, *gitmocks.Client) {
 	helmClient.On("ExtractChart", chart, version).Return("./testdata/my-chart", io.NopCloser, nil)
 	helmClient.On("CleanChartCache", chart, version).Return(nil)
 
-	service.newGitClient = func(rawRepoURL string, creds git.Creds, insecure bool, enableLfs bool, prosy string, opts ...git.ClientOpts) (client git.Client, e error) {
+	service.newGitClient = func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, prosy string, opts ...git.ClientOpts) (client git.Client, e error) {
 		return gitClient, nil
 	}
 	service.newHelmClient = func(repoURL string, creds helm.Creds, enableOci bool, proxy string, opts ...helm.ClientOpts) helm.Client {
 		return helmClient
 	}
+	service.gitRepoInitializer = func(rootPath string) goio.Closer {
+		return io.NopCloser
+	}
 	return service, gitClient
 }
 
@@ -118,7 +122,7 @@ func newServiceWithCommitSHA(root, revision string) *Service {
 		gitClient.On("Root").Return(root)
 	})
 
-	service.newGitClient = func(rawRepoURL string, creds git.Creds, insecure bool, enableLfs bool, proxy string, opts ...git.ClientOpts) (client git.Client, e error) {
+	service.newGitClient = func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, proxy string, opts ...git.ClientOpts) (client git.Client, e error) {
 		return gitClient, nil
 	}
 
@@ -1904,3 +1908,67 @@ func Test_resolveHelmValueFilePath(t *testing.T) {
 		assert.Equal(t, "", p)
 	})
 }
+
+func TestDirectoryPermissionInitializer(t *testing.T) {
+	dir, err := ioutil.TempDir("", "")
+	require.NoError(t, err)
+	defer func() {
+		_ = os.RemoveAll(dir)
+	}()
+
+	file, err := ioutil.TempFile(dir, "")
+	require.NoError(t, err)
+	io.Close(file)
+
+	// remove read permissions
+	assert.NoError(t, os.Chmod(dir, 0000))
+	closer := directoryPermissionInitializer(dir)
+
+	// make sure permission are restored
+	_, err = ioutil.ReadFile(file.Name())
+	require.NoError(t, err)
+
+	// make sure permission are removed by closer
+	io.Close(closer)
+	_, err = ioutil.ReadFile(file.Name())
+	require.Error(t, err)
+}
+
+func initGitRepo(repoPath string, remote string) error {
+	if err := os.Mkdir(repoPath, 0755); err != nil {
+		return err
+	}
+
+	cmd := exec.Command("git", "init", repoPath)
+	cmd.Dir = repoPath
+	if err := cmd.Run(); err != nil {
+		return err
+	}
+	cmd = exec.Command("git", "remote", "add", "origin", remote)
+	cmd.Dir = repoPath
+	return cmd.Run()
+}
+
+func TestInit(t *testing.T) {
+	dir, err := ioutil.TempDir("", "")
+	require.NoError(t, err)
+	defer func() {
+		_ = os.RemoveAll(dir)
+	}()
+
+	repoPath := path.Join(dir, "repo1")
+	require.NoError(t, initGitRepo(repoPath, "https://github.com/argo-cd/test-repo1"))
+
+	service := newService(".")
+	service.rootDir = dir
+
+	require.NoError(t, service.Init())
+
+	repo1Path, err := service.gitRepoPaths.GetPath(git.NormalizeGitURL("https://github.com/argo-cd/test-repo1"))
+	assert.NoError(t, err)
+	assert.Equal(t, repoPath, repo1Path)
+
+	_, err = ioutil.ReadDir(dir)
+	require.Error(t, err)
+	require.NoError(t, initGitRepo(path.Join(dir, "repo2"), "https://github.com/argo-cd/test-repo2"))
+}
diff --git a/util/grpc/sanitizer_test.go b/util/grpc/sanitizer_test.go
new file mode 100644
index 000000000..bd7108b6d
--- /dev/null
+++ b/util/grpc/sanitizer_test.go
@@ -0,0 +1,51 @@
+package grpc
+
+import (
+	"context"
+	"regexp"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
+)
+
+func TestSanitizer(t *testing.T) {
+	s := NewSanitizer()
+
+	ctx := ContextWithSanitizer(context.TODO(), s)
+
+	sanitizer, ok := SanitizerFromContext(ctx)
+	require.True(t, ok)
+	sanitizer.AddReplacement("/my-random/path", ".")
+
+	res := s.Replace("error at /my-random/path/sub-dir: something went wrong")
+	assert.Equal(t, "error at ./sub-dir: something went wrong", res)
+}
+
+func TestSanitizer_RegexReplacement(t *testing.T) {
+	s := NewSanitizer()
+
+	ctx := ContextWithSanitizer(context.TODO(), s)
+
+	sanitizer, ok := SanitizerFromContext(ctx)
+	require.True(t, ok)
+
+	sanitizer.AddRegexReplacement(regexp.MustCompile("(/my-random/path)"), ".")
+	res := s.Replace("error at /my-random/path/something: something went wrong")
+	assert.Equal(t, "error at ./something: something went wrong", res)
+}
+
+func TestErrorSanitizerUnaryServerInterceptor(t *testing.T) {
+	interceptor := ErrorSanitizerUnaryServerInterceptor()
+
+	_, err := interceptor(context.Background(), nil, nil, func(ctx context.Context, req interface{}) (interface{}, error) {
+		sanitizer, ok := SanitizerFromContext(ctx)
+		require.True(t, ok)
+		sanitizer.AddReplacement("/my-random/path", ".")
+		return nil, status.Error(codes.Internal, "error at /my-random/path/sub-dir: something went wrong")
+	})
+
+	assert.Equal(t, "rpc error: code = Internal desc = error at ./sub-dir: something went wrong", err.Error())
+}
diff --git a/util/io/paths_test.go b/util/io/paths_test.go
new file mode 100644
index 000000000..ea98dbaaf
--- /dev/null
+++ b/util/io/paths_test.go
@@ -0,0 +1,37 @@
+package io
+
+import (
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestGetPath_SameURLs(t *testing.T) {
+	paths := NewTempPaths(os.TempDir())
+	res1, err := paths.GetPath("https://localhost/test.txt")
+	require.NoError(t, err)
+	res2, err := paths.GetPath("https://localhost/test.txt")
+	require.NoError(t, err)
+	assert.Equal(t, res1, res2)
+}
+
+func TestGetPath_DifferentURLs(t *testing.T) {
+	paths := NewTempPaths(os.TempDir())
+	res1, err := paths.GetPath("https://localhost/test1.txt")
+	require.NoError(t, err)
+	res2, err := paths.GetPath("https://localhost/test2.txt")
+	require.NoError(t, err)
+	assert.NotEqual(t, res1, res2)
+}
+
+func TestGetPath_SameURLsDifferentInstances(t *testing.T) {
+	paths1 := NewTempPaths(os.TempDir())
+	res1, err := paths1.GetPath("https://localhost/test.txt")
+	require.NoError(t, err)
+	paths2 := NewTempPaths(os.TempDir())
+	res2, err := paths2.GetPath("https://localhost/test.txt")
+	require.NoError(t, err)
+	assert.NotEqual(t, res1, res2)
+}