summaryrefslogtreecommitdiff
path: root/ext
diff options
context:
space:
mode:
authorpasha-codefresh <pavel@codefresh.io>2024-05-28 21:09:35 +0300
committerGitHub <noreply@github.com>2024-05-28 21:09:35 +0300
commitbb590d50ae5931e5dbf41f8fd6d4a30f92e22e9e (patch)
treeb1b7d6cfede237200208444e8d015a991ea4d51c /ext
parenteb1d8d30bb22cb82928df64fd5028fa978f8cb5b (diff)
parent51db19fcaaff26b3d53e4e63ddb1e4ef0f3cd695 (diff)
Merge pull request #726 from jannfis/chore/update-git-client-2_11
chore(deps): Pull in Git client changes from Argo CD v2.11.2
Diffstat (limited to 'ext')
-rw-r--r--ext/git/client.go328
-rw-r--r--ext/git/client_test.go263
-rw-r--r--ext/git/creds.go225
-rw-r--r--ext/git/creds_test.go369
-rw-r--r--ext/git/git.go10
-rw-r--r--ext/git/git_test.go163
-rw-r--r--ext/git/mocks/Client.go217
-rw-r--r--ext/git/ssh.go8
-rw-r--r--ext/git/workaround.go20
-rw-r--r--ext/git/writer.go2
10 files changed, 1418 insertions, 187 deletions
diff --git a/ext/git/client.go b/ext/git/client.go
index 11a9a75..ac9dc10 100644
--- a/ext/git/client.go
+++ b/ext/git/client.go
@@ -13,24 +13,33 @@ import (
"sort"
"strconv"
"strings"
+ "syscall"
"time"
+ argoexec "github.com/argoproj/pkg/exec"
+ "github.com/bmatcuk/doublestar/v4"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/transport"
githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-git/go-git/v5/storage/memory"
+ "github.com/google/uuid"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/knownhosts"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ utilnet "k8s.io/apimachinery/pkg/util/net"
"github.com/argoproj/argo-cd/v2/common"
certutil "github.com/argoproj/argo-cd/v2/util/cert"
+ "github.com/argoproj/argo-cd/v2/util/env"
executil "github.com/argoproj/argo-cd/v2/util/exec"
"github.com/argoproj/argo-cd/v2/util/proxy"
)
+var ErrInvalidRepoURL = fmt.Errorf("repo URL is invalid")
+
type RevisionMetadata struct {
Author string
Date time.Time
@@ -47,7 +56,8 @@ type Refs struct {
type gitRefCache interface {
SetGitReferences(repo string, references []*plumbing.Reference) error
- GetGitReferences(repo string, references *[]*plumbing.Reference) error
+ GetOrLockGitReferences(repo string, lockId string, references *[]*plumbing.Reference) (string, error)
+ UnlockGitReferences(repo string, lockId string) error
}
// Client is a generic git client interface
@@ -55,14 +65,17 @@ type Client interface {
Root() string
Init() error
Fetch(revision string) error
- Checkout(revision string) error
+ Submodule() error
+ Checkout(revision string, submoduleEnabled bool) error
LsRefs() (*Refs, error)
LsRemote(revision string) (string, error)
- LsFiles(path string) ([]string, error)
+ LsFiles(path string, enableNewGitFileGlobbing bool) ([]string, error)
LsLargeFiles() ([]string, error)
CommitSHA() (string, error)
RevisionMetadata(revision string) (*RevisionMetadata, error)
VerifyCommitSignature(string) (string, error)
+ IsAnnotatedTag(string) bool
+ ChangedFiles(revision string, targetRevision string) ([]string, error)
Commit(pathSpec string, opts *CommitOptions) error
Branch(sourceBranch string, targetBranch string) error
Push(remote string, branch string, force bool) error
@@ -98,8 +111,16 @@ type nativeGitClient struct {
proxy string
}
+type runOpts struct {
+ SkipErrorLogging bool
+ CaptureStderr bool
+}
+
var (
maxAttemptsCount = 1
+ maxRetryDuration time.Duration
+ retryDuration time.Duration
+ factor int64
)
func init() {
@@ -110,6 +131,11 @@ func init() {
maxAttemptsCount = int(math.Max(float64(cnt), 1))
}
}
+
+ maxRetryDuration = env.ParseDurationFromEnv(common.EnvGitRetryMaxDuration, common.DefaultGitRetryMaxDuration, 0, math.MaxInt64)
+ retryDuration = env.ParseDurationFromEnv(common.EnvGitRetryDuration, common.DefaultGitRetryDuration, 0, math.MaxInt64)
+ factor = env.ParseInt64FromEnv(common.EnvGitRetryFactor, common.DefaultGitRetryFactor, 0, math.MaxInt64)
+
}
type ClientOpts func(c *nativeGitClient)
@@ -131,9 +157,13 @@ func WithEventHandlers(handlers EventHandlers) ClientOpts {
func NewClient(rawRepoURL string, creds Creds, insecure bool, enableLfs bool, proxy string, opts ...ClientOpts) (Client, error) {
r := regexp.MustCompile("(/|:)")
- root := filepath.Join(os.TempDir(), r.ReplaceAllString(NormalizeGitURL(rawRepoURL), "_"))
+ normalizedGitURL := NormalizeGitURL(rawRepoURL)
+ if normalizedGitURL == "" {
+ return nil, fmt.Errorf("repository %q cannot be initialized: %w", rawRepoURL, ErrInvalidRepoURL)
+ }
+ root := filepath.Join(os.TempDir(), r.ReplaceAllString(normalizedGitURL, "_"))
if root == os.TempDir() {
- return nil, fmt.Errorf("Repository '%s' cannot be initialized, because its root would be system temp at %s", rawRepoURL, root)
+ return nil, fmt.Errorf("repository %q cannot be initialized, because its root would be system temp at %s", rawRepoURL, root)
}
return NewClientExt(rawRepoURL, root, creds, insecure, enableLfs, proxy, opts...)
}
@@ -153,6 +183,10 @@ func NewClientExt(rawRepoURL string, root string, creds Creds, insecure bool, en
return client, nil
}
+var (
+ gitClientTimeout = env.ParseDurationFromEnv("ARGOCD_GIT_REQUEST_TIMEOUT", 15*time.Second, 0, math.MaxInt64)
+)
+
// Returns a HTTP client object suitable for go-git to use using the following
// pattern:
// - If insecure is true, always returns a client with certificate verification
@@ -164,8 +198,8 @@ func NewClientExt(rawRepoURL string, root string, creds Creds, insecure bool, en
func GetRepoHTTPClient(repoURL string, insecure bool, creds Creds, proxyURL string) *http.Client {
// Default HTTP client
var customHTTPClient = &http.Client{
- // 15 second timeout
- Timeout: 15 * time.Second,
+ // 15 second timeout by default
+ Timeout: gitClientTimeout,
// don't follow redirect
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
@@ -198,46 +232,30 @@ func GetRepoHTTPClient(repoURL string, insecure bool, creds Creds, proxyURL stri
return &cert, nil
}
-
+ transport := &http.Transport{
+ Proxy: proxyFunc,
+ TLSClientConfig: &tls.Config{
+ GetClientCertificate: clientCertFunc,
+ },
+ DisableKeepAlives: true,
+ }
+ customHTTPClient.Transport = transport
if insecure {
- customHTTPClient.Transport = &http.Transport{
- Proxy: proxyFunc,
- TLSClientConfig: &tls.Config{
- InsecureSkipVerify: true,
- GetClientCertificate: clientCertFunc,
- },
- DisableKeepAlives: true,
- }
- } else {
- parsedURL, err := url.Parse(repoURL)
- if err != nil {
- return customHTTPClient
- }
- serverCertificatePem, err := certutil.GetCertificateForConnect(parsedURL.Host)
- if err != nil {
- return customHTTPClient
- } else if len(serverCertificatePem) > 0 {
- certPool := certutil.GetCertPoolFromPEMData(serverCertificatePem)
- customHTTPClient.Transport = &http.Transport{
- Proxy: proxyFunc,
- TLSClientConfig: &tls.Config{
- RootCAs: certPool,
- GetClientCertificate: clientCertFunc,
- },
- DisableKeepAlives: true,
- }
- } else {
- // else no custom certificate stored.
- customHTTPClient.Transport = &http.Transport{
- Proxy: proxyFunc,
- TLSClientConfig: &tls.Config{
- GetClientCertificate: clientCertFunc,
- },
- DisableKeepAlives: true,
- }
- }
+ transport.TLSClientConfig.InsecureSkipVerify = true
+ return customHTTPClient
+ }
+ parsedURL, err := url.Parse(repoURL)
+ if err != nil {
+ return customHTTPClient
+ }
+ serverCertificatePem, err := certutil.GetCertificateForConnect(parsedURL.Host)
+ if err != nil {
+ return customHTTPClient
+ }
+ if len(serverCertificatePem) > 0 {
+ certPool := certutil.GetCertPoolFromPEMData(serverCertificatePem)
+ transport.TLSClientConfig.RootCAs = certPool
}
-
return customHTTPClient
}
@@ -268,6 +286,9 @@ func newAuth(repoURL string, creds Creds) (transport.AuthMethod, error) {
return auth, nil
case HTTPSCreds:
auth := githttp.BasicAuth{Username: creds.username, Password: creds.password}
+ if auth.Username == "" {
+ auth.Username = "x-access-token"
+ }
return &auth, nil
case GitHubAppCreds:
token, err := creds.getAccessToken()
@@ -276,6 +297,18 @@ func newAuth(repoURL string, creds Creds) (transport.AuthMethod, error) {
}
auth := githttp.BasicAuth{Username: "x-access-token", Password: token}
return &auth, nil
+ case GoogleCloudCreds:
+ username, err := creds.getUsername()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get username from creds: %w", err)
+ }
+ token, err := creds.getAccessToken()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get access token from creds: %w", err)
+ }
+
+ auth := githttp.BasicAuth{Username: username, Password: token}
+ return &auth, nil
}
return nil, nil
}
@@ -294,7 +327,7 @@ func (m *nativeGitClient) Init() error {
return err
}
log.Infof("Initializing %s to %s", m.repoURL, m.root)
- _, err = executil.Run(exec.Command("rm", "-rf", m.root))
+ err = os.RemoveAll(m.root)
if err != nil {
return fmt.Errorf("unable to clean repo at %s: %v", m.root, err)
}
@@ -318,6 +351,16 @@ func (m *nativeGitClient) IsLFSEnabled() bool {
return m.enableLfs
}
+func (m *nativeGitClient) fetch(revision string) error {
+ var err error
+ if revision != "" {
+ err = m.runCredentialedCmd("fetch", "origin", revision, "--tags", "--force", "--prune")
+ } else {
+ err = m.runCredentialedCmd("fetch", "origin", "--tags", "--force", "--prune")
+ }
+ return err
+}
+
// Fetch fetches latest updates from origin
func (m *nativeGitClient) Fetch(revision string) error {
if m.OnFetch != nil {
@@ -325,34 +368,61 @@ func (m *nativeGitClient) Fetch(revision string) error {
defer done()
}
- var err error
- if revision != "" {
- err = m.runCredentialedCmd("git", "fetch", "origin", revision, "--tags", "--force")
- } else {
- err = m.runCredentialedCmd("git", "fetch", "origin", "--tags", "--force")
- }
+ err := m.fetch(revision)
+
// When we have LFS support enabled, check for large files and fetch them too.
if err == nil && m.IsLFSEnabled() {
largeFiles, err := m.LsLargeFiles()
if err == nil && len(largeFiles) > 0 {
- err = m.runCredentialedCmd("git", "lfs", "fetch", "--all")
+ err = m.runCredentialedCmd("lfs", "fetch", "--all")
if err != nil {
return err
}
}
}
+
return err
}
// LsFiles lists the local working tree, including only files that are under source control
-func (m *nativeGitClient) LsFiles(path string) ([]string, error) {
- out, err := m.runCmd("ls-files", "--full-name", "-z", "--", path)
- if err != nil {
- return nil, err
+func (m *nativeGitClient) LsFiles(path string, enableNewGitFileGlobbing bool) ([]string, error) {
+ if enableNewGitFileGlobbing {
+ // This is the new way with safer globbing
+ err := os.Chdir(m.root)
+ if err != nil {
+ return nil, err
+ }
+ all_files, err := doublestar.FilepathGlob(path)
+ if err != nil {
+ return nil, err
+ }
+ var files []string
+ for _, file := range all_files {
+ link, err := filepath.EvalSymlinks(file)
+ if err != nil {
+ return nil, err
+ }
+ absPath, err := filepath.Abs(link)
+ if err != nil {
+ return nil, err
+ }
+ if strings.HasPrefix(absPath, m.root) {
+ files = append(files, file)
+ } else {
+ log.Warnf("Absolute path for %s is outside of repository, removing it", file)
+ }
+ }
+ return files, nil
+ } else {
+ // This is the old and default way
+ out, err := m.runCmd("ls-files", "--full-name", "-z", "--", path)
+ if err != nil {
+ return nil, err
+ }
+ // remove last element, which is blank regardless of whether we're using nullbyte or newline
+ ss := strings.Split(out, "\000")
+ return ss[:len(ss)-1], nil
}
- // remove last element, which is blank regardless of whether we're using nullbyte or newline
- ss := strings.Split(out, "\000")
- return ss[:len(ss)-1], nil
}
// LsLargeFiles lists all files that have references to LFS storage
@@ -365,8 +435,19 @@ func (m *nativeGitClient) LsLargeFiles() ([]string, error) {
return ss, nil
}
+// Submodule embed other repositories into this repository
+func (m *nativeGitClient) Submodule() error {
+ if err := m.runCredentialedCmd("submodule", "sync", "--recursive"); err != nil {
+ return err
+ }
+ if err := m.runCredentialedCmd("submodule", "update", "--init", "--recursive"); err != nil {
+ return err
+ }
+ return nil
+}
+
// Checkout checkout specified revision
-func (m *nativeGitClient) Checkout(revision string) error {
+func (m *nativeGitClient) Checkout(revision string, submoduleEnabled bool) error {
if revision == "" || revision == "HEAD" {
revision = "origin/HEAD"
}
@@ -387,24 +468,54 @@ func (m *nativeGitClient) Checkout(revision string) error {
}
}
if _, err := os.Stat(m.root + "/.gitmodules"); !os.IsNotExist(err) {
- if submoduleEnabled := os.Getenv(common.EnvGitSubmoduleEnabled); submoduleEnabled != "false" {
- if err := m.runCredentialedCmd("git", "submodule", "update", "--init", "--recursive"); err != nil {
+ if submoduleEnabled {
+ if err := m.Submodule(); err != nil {
return err
}
}
}
- if _, err := m.runCmd("clean", "-fdx"); err != nil {
+ // NOTE
+ // The double “f” in the arguments is not a typo: the first “f” tells
+ // `git clean` to delete untracked files and directories, and the second “f”
+ // tells it to clean untractked nested Git repositories (for example a
+ // submodule which has since been removed).
+ if _, err := m.runCmd("clean", "-ffdx"); err != nil {
return err
}
return nil
}
func (m *nativeGitClient) getRefs() ([]*plumbing.Reference, error) {
+ myLockUUID, err := uuid.NewRandom()
+ myLockId := ""
+ if err != nil {
+ log.Debug("Error generating git references cache lock id: ", err)
+ } else {
+ myLockId = myLockUUID.String()
+ }
+ // Prevent an additional get call to cache if we know our state isn't stale
+ needsUnlock := true
if m.gitRefCache != nil && m.loadRefFromCache {
var res []*plumbing.Reference
- if m.gitRefCache.GetGitReferences(m.repoURL, &res) == nil {
+ foundLockId, err := m.gitRefCache.GetOrLockGitReferences(m.repoURL, myLockId, &res)
+ isLockOwner := myLockId == foundLockId
+ if !isLockOwner && err == nil {
+ // Valid value already in cache
return res, nil
+ } else if !isLockOwner && err != nil {
+ // Error getting value from cache
+ log.Debugf("Error getting git references from cache: %v", err)
+ return nil, err
}
+ // Defer a soft reset of the cache lock, if the value is set this call will be ignored
+ defer func() {
+ if needsUnlock {
+ err := m.gitRefCache.UnlockGitReferences(m.repoURL, myLockId)
+ if err != nil {
+ log.Debugf("Error unlocking git references from cache: %v", err)
+ }
+ }
+ }()
}
if m.OnLsRemote != nil {
@@ -431,6 +542,9 @@ func (m *nativeGitClient) getRefs() ([]*plumbing.Reference, error) {
if err == nil && m.gitRefCache != nil {
if err := m.gitRefCache.SetGitReferences(m.repoURL, res); err != nil {
log.Warnf("Failed to store git references to cache: %v", err)
+ } else {
+ // Since we successfully overwrote the lock with valid data, we don't need to unlock
+ needsUnlock = false
}
return res, nil
}
@@ -473,8 +587,19 @@ func (m *nativeGitClient) LsRefs() (*Refs, error) {
// repository locally cloned.
func (m *nativeGitClient) LsRemote(revision string) (res string, err error) {
for attempt := 0; attempt < maxAttemptsCount; attempt++ {
- if res, err = m.lsRemote(revision); err == nil {
+ res, err = m.lsRemote(revision)
+ if err == nil {
return
+ } else if apierrors.IsInternalError(err) || apierrors.IsTimeout(err) || apierrors.IsServerTimeout(err) ||
+ apierrors.IsTooManyRequests(err) || utilnet.IsProbableEOF(err) || utilnet.IsConnectionReset(err) {
+ // Formula: timeToWait = duration * factor^retry_number
+ // Note that timeToWait should equal to duration for the first retry attempt.
+ // When timeToWait is more than maxDuration retry should be performed at maxDuration.
+ timeToWait := float64(retryDuration) * (math.Pow(float64(factor), float64(attempt)))
+ if maxRetryDuration > 0 {
+ timeToWait = math.Min(float64(maxRetryDuration), timeToWait)
+ }
+ time.Sleep(time.Duration(timeToWait))
}
}
return
@@ -575,42 +700,87 @@ func (m *nativeGitClient) VerifyCommitSignature(revision string) (string, error)
return out, nil
}
+// IsAnnotatedTag returns true if the revision points to an annotated tag
+func (m *nativeGitClient) IsAnnotatedTag(revision string) bool {
+ cmd := exec.Command("git", "describe", "--exact-match", revision)
+ out, err := m.runCmdOutput(cmd, runOpts{SkipErrorLogging: true})
+ if out != "" && err == nil {
+ return true
+ } else {
+ return false
+ }
+}
+
+// returns the meta-data for the commit
+func (m *nativeGitClient) ChangedFiles(revision string, targetRevision string) ([]string, error) {
+ if revision == targetRevision {
+ return []string{}, nil
+ }
+
+ if !IsCommitSHA(revision) || !IsCommitSHA(targetRevision) {
+ return []string{}, fmt.Errorf("invalid revision provided, must be SHA")
+ }
+
+ out, err := m.runCmd("diff", "--name-only", fmt.Sprintf("%s..%s", revision, targetRevision))
+ if err != nil {
+ return nil, fmt.Errorf("failed to diff %s..%s: %w", revision, targetRevision, err)
+ }
+
+ if out == "" {
+ return []string{}, nil
+ }
+
+ files := strings.Split(out, "\n")
+ return files, nil
+}
+
// runWrapper runs a custom command with all the semantics of running the Git client
func (m *nativeGitClient) runGnuPGWrapper(wrapper string, args ...string) (string, error) {
cmd := exec.Command(wrapper, args...)
cmd.Env = append(cmd.Env, fmt.Sprintf("GNUPGHOME=%s", common.GetGnuPGHomePath()), "LANG=C")
- return m.runCmdOutput(cmd)
+ return m.runCmdOutput(cmd, runOpts{})
}
// runCmd is a convenience function to run a command in a given directory and return its output
func (m *nativeGitClient) runCmd(args ...string) (string, error) {
cmd := exec.Command("git", args...)
- return m.runCmdOutput(cmd)
+ return m.runCmdOutput(cmd, runOpts{})
}
// runCredentialedCmd is a convenience function to run a git command with username/password credentials
// nolint:unparam
-func (m *nativeGitClient) runCredentialedCmd(command string, args ...string) error {
- cmd := exec.Command(command, args...)
+func (m *nativeGitClient) runCredentialedCmd(args ...string) error {
closer, environ, err := m.creds.Environ()
if err != nil {
return err
}
defer func() { _ = closer.Close() }()
+
+ // If a basic auth header is explicitly set, tell Git to send it to the
+ // server to force use of basic auth instead of negotiating the auth scheme
+ for _, e := range environ {
+ if strings.HasPrefix(e, fmt.Sprintf("%s=", forceBasicAuthHeaderEnv)) {
+ args = append([]string{"--config-env", fmt.Sprintf("http.extraHeader=%s", forceBasicAuthHeaderEnv)}, args...)
+ }
+ }
+
+ cmd := exec.Command("git", args...)
cmd.Env = append(cmd.Env, environ...)
- _, err = m.runCmdOutput(cmd)
+ _, err = m.runCmdOutput(cmd, runOpts{})
return err
}
-func (m *nativeGitClient) runCmdOutput(cmd *exec.Cmd) (string, error) {
+func (m *nativeGitClient) runCmdOutput(cmd *exec.Cmd, ropts runOpts) (string, error) {
cmd.Dir = m.root
- cmd.Env = append(cmd.Env, os.Environ()...)
+ cmd.Env = append(os.Environ(), cmd.Env...)
// Set $HOME to nowhere, so we can be execute Git regardless of any external
// authentication keys (e.g. in ~/.ssh) -- this is especially important for
// running tests on local machines and/or CircleCI.
cmd.Env = append(cmd.Env, "HOME=/dev/null")
// Skip LFS for most Git operations except when explicitly requested
cmd.Env = append(cmd.Env, "GIT_LFS_SKIP_SMUDGE=1")
+ // Disable Git terminal prompts in case we're running with a tty
+ cmd.Env = append(cmd.Env, "GIT_TERMINAL_PROMPT=false")
// For HTTPS repositories, we need to consider insecure repositories as well
// as custom CA bundles from the cert database.
@@ -631,8 +801,14 @@ func (m *nativeGitClient) runCmdOutput(cmd *exec.Cmd) (string, error) {
}
}
}
-
cmd.Env = proxy.UpsertEnv(cmd, m.proxy)
-
- return executil.Run(cmd)
+ opts := executil.ExecRunOpts{
+ TimeoutBehavior: argoexec.TimeoutBehavior{
+ Signal: syscall.SIGTERM,
+ ShouldWait: true,
+ },
+ SkipErrorLogging: ropts.SkipErrorLogging,
+ CaptureStderr: ropts.CaptureStderr,
+ }
+ return executil.RunWithExecRunOpts(cmd, opts)
}
diff --git a/ext/git/client_test.go b/ext/git/client_test.go
new file mode 100644
index 0000000..b9897de
--- /dev/null
+++ b/ext/git/client_test.go
@@ -0,0 +1,263 @@
+package git
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "path"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func runCmd(workingDir string, name string, args ...string) error {
+ cmd := exec.Command(name, args...)
+ cmd.Dir = workingDir
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ return cmd.Run()
+}
+
+func _createEmptyGitRepo() (string, error) {
+ tempDir, err := os.MkdirTemp("", "")
+ if err != nil {
+ return tempDir, err
+ }
+
+ err = runCmd(tempDir, "git", "init")
+ if err != nil {
+ return tempDir, err
+ }
+
+ err = runCmd(tempDir, "git", "commit", "-m", "Initial commit", "--allow-empty")
+ return tempDir, err
+}
+
+func Test_nativeGitClient_Fetch(t *testing.T) {
+ tempDir, err := _createEmptyGitRepo()
+ require.NoError(t, err)
+
+ client, err := NewClient(fmt.Sprintf("file://%s", tempDir), NopCreds{}, true, false, "")
+ require.NoError(t, err)
+
+ err = client.Init()
+ require.NoError(t, err)
+
+ err = client.Fetch("")
+ assert.NoError(t, err)
+}
+
+func Test_nativeGitClient_Fetch_Prune(t *testing.T) {
+ tempDir, err := _createEmptyGitRepo()
+ require.NoError(t, err)
+
+ client, err := NewClient(fmt.Sprintf("file://%s", tempDir), NopCreds{}, true, false, "")
+ require.NoError(t, err)
+
+ err = client.Init()
+ require.NoError(t, err)
+
+ err = runCmd(tempDir, "git", "branch", "test/foo")
+ require.NoError(t, err)
+
+ err = client.Fetch("")
+ assert.NoError(t, err)
+
+ err = runCmd(tempDir, "git", "branch", "-d", "test/foo")
+ require.NoError(t, err)
+ err = runCmd(tempDir, "git", "branch", "test/foo/bar")
+ require.NoError(t, err)
+
+ err = client.Fetch("")
+ assert.NoError(t, err)
+}
+
+func Test_IsAnnotatedTag(t *testing.T) {
+ tempDir := t.TempDir()
+ client, err := NewClient(fmt.Sprintf("file://%s", tempDir), NopCreds{}, true, false, "")
+ require.NoError(t, err)
+
+ err = client.Init()
+ require.NoError(t, err)
+
+ p := path.Join(client.Root(), "README")
+ f, err := os.Create(p)
+ require.NoError(t, err)
+ _, err = f.WriteString("Hello.")
+ require.NoError(t, err)
+ err = f.Close()
+ require.NoError(t, err)
+
+ err = runCmd(client.Root(), "git", "add", "README")
+ require.NoError(t, err)
+
+ err = runCmd(client.Root(), "git", "commit", "-m", "Initial commit", "-a")
+ require.NoError(t, err)
+
+ atag := client.IsAnnotatedTag("master")
+ assert.False(t, atag)
+
+ err = runCmd(client.Root(), "git", "tag", "some-tag", "-a", "-m", "Create annotated tag")
+ require.NoError(t, err)
+ atag = client.IsAnnotatedTag("some-tag")
+ assert.True(t, atag)
+
+ // Tag effectually points to HEAD, so it's considered the same
+ atag = client.IsAnnotatedTag("HEAD")
+ assert.True(t, atag)
+
+ err = runCmd(client.Root(), "git", "rm", "README")
+ assert.NoError(t, err)
+ err = runCmd(client.Root(), "git", "commit", "-m", "remove README", "-a")
+ assert.NoError(t, err)
+
+ // We moved on, so tag doesn't point to HEAD anymore
+ atag = client.IsAnnotatedTag("HEAD")
+ assert.False(t, atag)
+}
+
+func Test_ChangedFiles(t *testing.T) {
+ tempDir := t.TempDir()
+
+ client, err := NewClientExt(fmt.Sprintf("file://%s", tempDir), tempDir, NopCreds{}, true, false, "")
+ require.NoError(t, err)
+
+ err = client.Init()
+ require.NoError(t, err)
+
+ err = runCmd(client.Root(), "git", "commit", "-m", "Initial commit", "--allow-empty")
+ require.NoError(t, err)
+
+ // Create a tag to have a second ref
+ err = runCmd(client.Root(), "git", "tag", "some-tag")
+ require.NoError(t, err)
+
+ p := path.Join(client.Root(), "README")
+ f, err := os.Create(p)
+ require.NoError(t, err)
+ _, err = f.WriteString("Hello.")
+ require.NoError(t, err)
+ err = f.Close()
+ require.NoError(t, err)
+
+ err = runCmd(client.Root(), "git", "add", "README")
+ require.NoError(t, err)
+
+ err = runCmd(client.Root(), "git", "commit", "-m", "Changes", "-a")
+ require.NoError(t, err)
+
+ previousSHA, err := client.LsRemote("some-tag")
+ require.NoError(t, err)
+
+ commitSHA, err := client.LsRemote("HEAD")
+ require.NoError(t, err)
+
+ // Invalid commits, error
+ _, err = client.ChangedFiles("0000000000000000000000000000000000000000", "1111111111111111111111111111111111111111")
+ require.Error(t, err)
+
+ // Not SHAs, error
+ _, err = client.ChangedFiles(previousSHA, "HEAD")
+ require.Error(t, err)
+
+ // Same commit, no changes
+ changedFiles, err := client.ChangedFiles(commitSHA, commitSHA)
+ require.NoError(t, err)
+ assert.ElementsMatch(t, []string{}, changedFiles)
+
+ // Different ref, with changes
+ changedFiles, err = client.ChangedFiles(previousSHA, commitSHA)
+ require.NoError(t, err)
+ assert.ElementsMatch(t, []string{"README"}, changedFiles)
+}
+
+func Test_nativeGitClient_Submodule(t *testing.T) {
+ tempDir, err := os.MkdirTemp("", "")
+ require.NoError(t, err)
+
+ foo := filepath.Join(tempDir, "foo")
+ err = os.Mkdir(foo, 0755)
+ require.NoError(t, err)
+
+ err = runCmd(foo, "git", "init")
+ require.NoError(t, err)
+
+ bar := filepath.Join(tempDir, "bar")
+ err = os.Mkdir(bar, 0755)
+ require.NoError(t, err)
+
+ err = runCmd(bar, "git", "init")
+ require.NoError(t, err)
+
+ err = runCmd(bar, "git", "commit", "-m", "Initial commit", "--allow-empty")
+ require.NoError(t, err)
+
+ // Embed repository bar into repository foo
+ t.Setenv("GIT_ALLOW_PROTOCOL", "file")
+ err = runCmd(foo, "git", "submodule", "add", bar)
+ require.NoError(t, err)
+
+ err = runCmd(foo, "git", "commit", "-m", "Initial commit")
+ require.NoError(t, err)
+
+ tempDir, err = os.MkdirTemp("", "")
+ require.NoError(t, err)
+
+ // Clone foo
+ err = runCmd(tempDir, "git", "clone", foo)
+ require.NoError(t, err)
+
+ client, err := NewClient(fmt.Sprintf("file://%s", foo), NopCreds{}, true, false, "")
+ require.NoError(t, err)
+
+ err = client.Init()
+ require.NoError(t, err)
+
+ err = client.Fetch("")
+ assert.NoError(t, err)
+
+ commitSHA, err := client.LsRemote("HEAD")
+ assert.NoError(t, err)
+
+ // Call Checkout() with submoduleEnabled=false.
+ err = client.Checkout(commitSHA, false)
+ assert.NoError(t, err)
+
+ // Check if submodule url does not exist in .git/config
+ err = runCmd(client.Root(), "git", "config", "submodule.bar.url")
+ assert.Error(t, err)
+
+ // Call Submodule() via Checkout() with submoduleEnabled=true.
+ err = client.Checkout(commitSHA, true)
+ assert.NoError(t, err)
+
+ // Check if the .gitmodule URL is reflected in .git/config
+ cmd := exec.Command("git", "config", "submodule.bar.url")
+ cmd.Dir = client.Root()
+ result, err := cmd.Output()
+ assert.NoError(t, err)
+ assert.Equal(t, bar+"\n", string(result))
+
+ // Change URL of submodule bar
+ err = runCmd(client.Root(), "git", "config", "--file=.gitmodules", "submodule.bar.url", bar+"baz")
+ require.NoError(t, err)
+
+ // Call Submodule()
+ err = client.Submodule()
+ assert.NoError(t, err)
+
+ // Check if the URL change in .gitmodule is reflected in .git/config
+ cmd = exec.Command("git", "config", "submodule.bar.url")
+ cmd.Dir = client.Root()
+ result, err = cmd.Output()
+ assert.NoError(t, err)
+ assert.Equal(t, bar+"baz\n", string(result))
+}
+
+func TestNewClient_invalidSSHURL(t *testing.T) {
+ client, err := NewClient("ssh://bitbucket.org:org/repo", NopCreds{}, false, false, "")
+ assert.Nil(t, client)
+ assert.ErrorIs(t, err, ErrInvalidRepoURL)
+}
diff --git a/ext/git/creds.go b/ext/git/creds.go
index c8f6493..1869844 100644
--- a/ext/git/creds.go
+++ b/ext/git/creds.go
@@ -3,27 +3,45 @@ package git
import (
"context"
"crypto/sha256"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
"fmt"
"io"
+ "net/url"
"os"
"strconv"
"strings"
"time"
+ "golang.org/x/oauth2"
+ "golang.org/x/oauth2/google"
+
gocache "github.com/patrickmn/go-cache"
argoio "github.com/argoproj/gitops-engine/pkg/utils/io"
- "github.com/bradleyfalzon/ghinstallation"
+ "github.com/argoproj/gitops-engine/pkg/utils/text"
+ "github.com/bradleyfalzon/ghinstallation/v2"
log "github.com/sirupsen/logrus"
"github.com/argoproj/argo-cd/v2/common"
-
certutil "github.com/argoproj/argo-cd/v2/util/cert"
+ argoioutils "github.com/argoproj/argo-cd/v2/util/io"
)
-// In memory cache for storing github APP api token credentials
var (
+ // In memory cache for storing github APP api token credentials
githubAppTokenCache *gocache.Cache
+ // In memory cache for storing oauth2.TokenSource used to generate Google Cloud OAuth tokens
+ googleCloudTokenSource *gocache.Cache
+)
+
+const (
+ // ASKPASS_NONCE_ENV is the environment variable that is used to pass the nonce to the askpass script
+ ASKPASS_NONCE_ENV = "ARGOCD_GIT_ASKPASS_NONCE"
+ // githubAccessTokenUsername is a username that is used to with the github access token
+ githubAccessTokenUsername = "x-access-token"
+ forceBasicAuthHeaderEnv = "ARGOCD_GIT_AUTH_HEADER"
)
func init() {
@@ -35,12 +53,38 @@ func init() {
}
githubAppTokenCache = gocache.New(githubAppCredsExp, 1*time.Minute)
+ // oauth2.TokenSource handles fetching new Tokens once they are expired. The oauth2.TokenSource itself does not expire.
+ googleCloudTokenSource = gocache.New(gocache.NoExpiration, 0)
+}
+
+type NoopCredsStore struct {
+}
+
+func (d NoopCredsStore) Add(username string, password string) string {
+ return ""
+}
+
+func (d NoopCredsStore) Remove(id string) {
+}
+
+type CredsStore interface {
+ Add(username string, password string) string
+ Remove(id string)
}
type Creds interface {
Environ() (io.Closer, []string, error)
}
+func getGitAskPassEnv(id string) []string {
+ return []string{
+ fmt.Sprintf("GIT_ASKPASS=%s", "argocd"),
+ fmt.Sprintf("%s=%s", ASKPASS_NONCE_ENV, id),
+ "GIT_TERMINAL_PROMPT=0",
+ "ARGOCD_BINARY_NAME=argocd-git-ask-pass",
+ }
+}
+
// nop implementation
type NopCloser struct {
}
@@ -83,9 +127,13 @@ type HTTPSCreds struct {
clientCertKey string
// HTTP/HTTPS proxy used to access repository
proxy string
+ // temporal credentials store
+ store CredsStore
+ // whether to force usage of basic auth
+ forceBasicAuth bool
}
-func NewHTTPSCreds(username string, password string, clientCertData string, clientCertKey string, insecure bool, proxy string) GenericHTTPSCreds {
+func NewHTTPSCreds(username string, password string, clientCertData string, clientCertKey string, insecure bool, proxy string, store CredsStore, forceBasicAuth bool) GenericHTTPSCreds {
return HTTPSCreds{
username,
password,
@@ -93,13 +141,23 @@ func NewHTTPSCreds(username string, password string, clientCertData string, clie
clientCertData,
clientCertKey,
proxy,
+ store,
+ forceBasicAuth,
}
}
+func (c HTTPSCreds) BasicAuthHeader() string {
+ h := "Authorization: Basic "
+ t := c.username + ":" + c.password
+ h += base64.StdEncoding.EncodeToString([]byte(t))
+ return h
+}
+
// Get additional required environment variables for executing git client to
// access specific repository via HTTPS.
func (c HTTPSCreds) Environ() (io.Closer, []string, error) {
- env := []string{fmt.Sprintf("GIT_ASKPASS=%s", "git-ask-pass.sh"), fmt.Sprintf("GIT_USERNAME=%s", c.username), fmt.Sprintf("GIT_PASSWORD=%s", c.password)}
+ var env []string
+
httpCloser := authFilePaths(make([]string, 0))
// GIT_SSL_NO_VERIFY is used to tell git not to validate the server's cert at
@@ -151,9 +209,19 @@ func (c HTTPSCreds) Environ() (io.Closer, []string, error) {
}
// GIT_SSL_KEY is the full path to a client certificate's key to be used
env = append(env, fmt.Sprintf("GIT_SSL_KEY=%s", keyFile.Name()))
-
}
- return httpCloser, env, nil
+ // If at least password is set, we will set ARGOCD_BASIC_AUTH_HEADER to
+ // hold the HTTP authorization header, so auth mechanism negotiation is
+ // skipped. This is insecure, but some environments may need it.
+ if c.password != "" && c.forceBasicAuth {
+ env = append(env, fmt.Sprintf("%s=%s", forceBasicAuthHeaderEnv, c.BasicAuthHeader()))
+ }
+ nonce := c.store.Add(text.FirstNonEmpty(c.username, githubAccessTokenUsername), c.password)
+ env = append(env, getGitAskPassEnv(nonce)...)
+ return argoioutils.NewCloser(func() error {
+ c.store.Remove(nonce)
+ return httpCloser.Close()
+ }), env, nil
}
func (g HTTPSCreds) HasClientCert() bool {
@@ -173,10 +241,12 @@ type SSHCreds struct {
sshPrivateKey string
caPath string
insecure bool
+ store CredsStore
+ proxy string
}
-func NewSSHCreds(sshPrivateKey string, caPath string, insecureIgnoreHostKey bool) SSHCreds {
- return SSHCreds{sshPrivateKey, caPath, insecureIgnoreHostKey}
+func NewSSHCreds(sshPrivateKey string, caPath string, insecureIgnoreHostKey bool, store CredsStore, proxy string) SSHCreds {
+ return SSHCreds{sshPrivateKey, caPath, insecureIgnoreHostKey, store, proxy}
}
type sshPrivateKeyFile string
@@ -207,7 +277,14 @@ func (c SSHCreds) Environ() (io.Closer, []string, error) {
if err != nil {
return nil, nil, err
}
- defer file.Close()
+ defer func() {
+ if err = file.Close(); err != nil {
+ log.WithFields(log.Fields{
+ common.SecurityField: common.SecurityMedium,
+ common.SecurityCWEField: common.SecurityCWEMissingReleaseOfFileDescriptor,
+ }).Errorf("error closing file %q: %v", file.Name(), err)
+ }
+ }()
_, err = file.WriteString(c.sshPrivateKey + "\n")
if err != nil {
@@ -228,7 +305,25 @@ func (c SSHCreds) Environ() (io.Closer, []string, error) {
knownHostsFile := certutil.GetSSHKnownHostsDataPath()
args = append(args, "-o", "StrictHostKeyChecking=yes", "-o", fmt.Sprintf("UserKnownHostsFile=%s", knownHostsFile))
}
+ // Handle SSH socks5 proxy settings
+ proxyEnv := []string{}
+ if c.proxy != "" {
+ parsedProxyURL, err := url.Parse(c.proxy)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to set environment variables related to socks5 proxy, could not parse proxy URL '%s': %w", c.proxy, err)
+ }
+ args = append(args, "-o", fmt.Sprintf("ProxyCommand='connect-proxy -S %s:%s -5 %%h %%p'",
+ parsedProxyURL.Hostname(),
+ parsedProxyURL.Port()))
+ if parsedProxyURL.User != nil {
+ proxyEnv = append(proxyEnv, fmt.Sprintf("SOCKS5_USER=%s", parsedProxyURL.User.Username()))
+ if socks5_passwd, isPasswdSet := parsedProxyURL.User.Password(); isPasswdSet {
+ proxyEnv = append(proxyEnv, fmt.Sprintf("SOCKS5_PASSWD=%s", socks5_passwd))
+ }
+ }
+ }
env = append(env, []string{fmt.Sprintf("GIT_SSH_COMMAND=%s", strings.Join(args, " "))}...)
+ env = append(env, proxyEnv...)
return sshPrivateKeyFile(file.Name()), env, nil
}
@@ -243,11 +338,12 @@ type GitHubAppCreds struct {
clientCertKey string
insecure bool
proxy string
+ store CredsStore
}
// NewGitHubAppCreds provide github app credentials
-func NewGitHubAppCreds(appID int64, appInstallId int64, privateKey string, baseURL string, repoURL string, clientCertData string, clientCertKey string, insecure bool) GenericHTTPSCreds {
- return GitHubAppCreds{appID: appID, appInstallId: appInstallId, privateKey: privateKey, baseURL: baseURL, repoURL: repoURL, clientCertData: clientCertData, clientCertKey: clientCertKey, insecure: insecure}
+func NewGitHubAppCreds(appID int64, appInstallId int64, privateKey string, baseURL string, repoURL string, clientCertData string, clientCertKey string, insecure bool, proxy string, store CredsStore) GenericHTTPSCreds {
+ return GitHubAppCreds{appID: appID, appInstallId: appInstallId, privateKey: privateKey, baseURL: baseURL, repoURL: repoURL, clientCertData: clientCertData, clientCertKey: clientCertKey, insecure: insecure, proxy: proxy, store: store}
}
func (g GitHubAppCreds) Environ() (io.Closer, []string, error) {
@@ -255,8 +351,7 @@ func (g GitHubAppCreds) Environ() (io.Closer, []string, error) {
if err != nil {
return NopCloser{}, nil, err
}
-
- env := []string{fmt.Sprintf("GIT_ASKPASS=%s", "git-ask-pass.sh"), "GIT_USERNAME=x-access-token", fmt.Sprintf("GIT_PASSWORD=%s", token)}
+ var env []string
httpCloser := authFilePaths(make([]string, 0))
// GIT_SSL_NO_VERIFY is used to tell git not to validate the server's cert at
@@ -310,7 +405,12 @@ func (g GitHubAppCreds) Environ() (io.Closer, []string, error) {
env = append(env, fmt.Sprintf("GIT_SSL_KEY=%s", keyFile.Name()))
}
- return httpCloser, env, nil
+ nonce := g.store.Add(githubAccessTokenUsername, token)
+ env = append(env, getGitAskPassEnv(nonce)...)
+ return argoioutils.NewCloser(func() error {
+ g.store.Remove(nonce)
+ return httpCloser.Close()
+ }), env, nil
}
// getAccessToken fetches GitHub token using the app id, install id, and private key.
@@ -372,3 +472,98 @@ func (g GitHubAppCreds) GetClientCertData() string {
func (g GitHubAppCreds) GetClientCertKey() string {
return g.clientCertKey
}
+
+// GoogleCloudCreds to authenticate to Google Cloud Source repositories
+type GoogleCloudCreds struct {
+ creds *google.Credentials
+ store CredsStore
+}
+
+func NewGoogleCloudCreds(jsonData string, store CredsStore) GoogleCloudCreds {
+ creds, err := google.CredentialsFromJSON(context.Background(), []byte(jsonData), "https://www.googleapis.com/auth/cloud-platform")
+ if err != nil {
+ // Invalid JSON
+ log.Errorf("Failed reading credentials from JSON: %+v", err)
+ }
+ return GoogleCloudCreds{creds, store}
+}
+
+func (c GoogleCloudCreds) Environ() (io.Closer, []string, error) {
+ username, err := c.getUsername()
+ if err != nil {
+ return NopCloser{}, nil, fmt.Errorf("failed to get username from creds: %w", err)
+ }
+ token, err := c.getAccessToken()
+ if err != nil {
+ return NopCloser{}, nil, fmt.Errorf("failed to get access token from creds: %w", err)
+ }
+
+ nonce := c.store.Add(username, token)
+ env := getGitAskPassEnv(nonce)
+
+ return argoioutils.NewCloser(func() error {
+ c.store.Remove(nonce)
+ return NopCloser{}.Close()
+ }), env, nil
+}
+
+func (c GoogleCloudCreds) getUsername() (string, error) {
+ type googleCredentialsFile struct {
+ Type string `json:"type"`
+
+ // Service Account fields
+ ClientEmail string `json:"client_email"`
+ PrivateKeyID string `json:"private_key_id"`
+ PrivateKey string `json:"private_key"`
+ AuthURL string `json:"auth_uri"`
+ TokenURL string `json:"token_uri"`
+ ProjectID string `json:"project_id"`
+ }
+
+ if c.creds == nil {
+ return "", errors.New("credentials for Google Cloud Source repositories are invalid")
+ }
+
+ var f googleCredentialsFile
+ if err := json.Unmarshal(c.creds.JSON, &f); err != nil {
+ return "", fmt.Errorf("failed to unmarshal Google Cloud credentials: %w", err)
+ }
+ return f.ClientEmail, nil
+}
+
+func (c GoogleCloudCreds) getAccessToken() (string, error) {
+ if c.creds == nil {
+ return "", errors.New("credentials for Google Cloud Source repositories are invalid")
+ }
+
+ // Compute hash of creds for lookup in cache
+ h := sha256.New()
+ _, err := h.Write(c.creds.JSON)
+ if err != nil {
+ return "", err
+ }
+ key := fmt.Sprintf("%x", h.Sum(nil))
+
+ t, found := googleCloudTokenSource.Get(key)
+ if found {
+ ts := t.(*oauth2.TokenSource)
+ token, err := (*ts).Token()
+ if err != nil {
+ return "", fmt.Errorf("failed to get token from Google Cloud token source: %w", err)
+ }
+ return token.AccessToken, nil
+ }
+
+ ts := c.creds.TokenSource
+
+ // Add TokenSource to cache
+ // As TokenSource handles refreshing tokens once they expire itself, TokenSource itself can be reused. Hence, no expiration.
+ googleCloudTokenSource.Set(key, &ts, gocache.NoExpiration)
+
+ token, err := ts.Token()
+ if err != nil {
+ return "", fmt.Errorf("failed to get get SHA256 hash for Google Cloud credentials: %w", err)
+ }
+
+ return token.AccessToken, nil
+}
diff --git a/ext/git/creds_test.go b/ext/git/creds_test.go
new file mode 100644
index 0000000..23a705e
--- /dev/null
+++ b/ext/git/creds_test.go
@@ -0,0 +1,369 @@
+package git
+
+import (
+ "encoding/base64"
+ "fmt"
+ "os"
+ "path"
+ "regexp"
+ "strings"
+ "testing"
+
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "golang.org/x/oauth2"
+ "golang.org/x/oauth2/google"
+
+ "github.com/argoproj/argo-cd/v2/util/cert"
+ "github.com/argoproj/argo-cd/v2/util/io"
+)
+
+type cred struct {
+ username string
+ password string
+}
+
+type memoryCredsStore struct {
+ creds map[string]cred
+}
+
+func (s *memoryCredsStore) Add(username string, password string) string {
+ id := uuid.New().String()
+ s.creds[id] = cred{
+ username: username,
+ password: password,
+ }
+ return id
+}
+
+func (s *memoryCredsStore) Remove(id string) {
+ delete(s.creds, id)
+}
+
+func TestHTTPSCreds_Environ_no_cert_cleanup(t *testing.T) {
+ store := &memoryCredsStore{creds: make(map[string]cred)}
+ creds := NewHTTPSCreds("", "", "", "", true, "", store, false)
+ closer, env, err := creds.Environ()
+ require.NoError(t, err)
+ var nonce string
+ for _, envVar := range env {
+ if strings.HasPrefix(envVar, ASKPASS_NONCE_ENV) {
+ nonce = envVar[len(ASKPASS_NONCE_ENV)+1:]
+ break
+ }
+ }
+ assert.Contains(t, store.creds, nonce)
+ io.Close(closer)
+ assert.NotContains(t, store.creds, nonce)
+}
+
+func TestHTTPSCreds_Environ_insecure_true(t *testing.T) {
+ creds := NewHTTPSCreds("", "", "", "", true, "", &NoopCredsStore{}, false)
+ closer, env, err := creds.Environ()
+ t.Cleanup(func() {
+ io.Close(closer)
+ })
+ require.NoError(t, err)
+ found := false
+ for _, envVar := range env {
+ if envVar == "GIT_SSL_NO_VERIFY=true" {
+ found = true
+ break
+ }
+ }
+ assert.True(t, found)
+}
+
+func TestHTTPSCreds_Environ_insecure_false(t *testing.T) {
+ creds := NewHTTPSCreds("", "", "", "", false, "", &NoopCredsStore{}, false)
+ closer, env, err := creds.Environ()
+ t.Cleanup(func() {
+ io.Close(closer)
+ })
+ require.NoError(t, err)
+ found := false
+ for _, envVar := range env {
+ if envVar == "GIT_SSL_NO_VERIFY=true" {
+ found = true
+ break
+ }
+ }
+ assert.False(t, found)
+}
+
+func TestHTTPSCreds_Environ_forceBasicAuth(t *testing.T) {
+ t.Run("Enabled and credentials set", func(t *testing.T) {
+ store := &memoryCredsStore{creds: make(map[string]cred)}
+ creds := NewHTTPSCreds("username", "password", "", "", false, "", store, true)
+ closer, env, err := creds.Environ()
+ require.NoError(t, err)
+ defer closer.Close()
+ var header string
+ for _, envVar := range env {
+ if strings.HasPrefix(envVar, fmt.Sprintf("%s=", forceBasicAuthHeaderEnv)) {
+ header = envVar[len(forceBasicAuthHeaderEnv)+1:]
+ }
+ if header != "" {
+ break
+ }
+ }
+ b64enc := base64.StdEncoding.EncodeToString([]byte("username:password"))
+ assert.Equal(t, "Authorization: Basic "+b64enc, header)
+ })
+ t.Run("Enabled but credentials not set", func(t *testing.T) {
+ store := &memoryCredsStore{creds: make(map[string]cred)}
+ creds := NewHTTPSCreds("", "", "", "", false, "", store, true)
+ closer, env, err := creds.Environ()
+ require.NoError(t, err)
+ defer closer.Close()
+ var header string
+ for _, envVar := range env {
+ if strings.HasPrefix(envVar, fmt.Sprintf("%s=", forceBasicAuthHeaderEnv)) {
+ header = envVar[len(forceBasicAuthHeaderEnv)+1:]
+ }
+ if header != "" {
+ break
+ }
+ }
+ assert.Empty(t, header)
+ })
+ t.Run("Disabled with credentials set", func(t *testing.T) {
+ store := &memoryCredsStore{creds: make(map[string]cred)}
+ creds := NewHTTPSCreds("username", "password", "", "", false, "", store, false)
+ closer, env, err := creds.Environ()
+ require.NoError(t, err)
+ defer closer.Close()
+ var header string
+ for _, envVar := range env {
+ if strings.HasPrefix(envVar, fmt.Sprintf("%s=", forceBasicAuthHeaderEnv)) {
+ header = envVar[len(forceBasicAuthHeaderEnv)+1:]
+ }
+ if header != "" {
+ break
+ }
+ }
+ assert.Empty(t, header)
+ })
+
+ t.Run("Disabled with credentials not set", func(t *testing.T) {
+ store := &memoryCredsStore{creds: make(map[string]cred)}
+ creds := NewHTTPSCreds("", "", "", "", false, "", store, false)
+ closer, env, err := creds.Environ()
+ require.NoError(t, err)
+ defer closer.Close()
+ var header string
+ for _, envVar := range env {
+ if strings.HasPrefix(envVar, fmt.Sprintf("%s=", forceBasicAuthHeaderEnv)) {
+ header = envVar[len(forceBasicAuthHeaderEnv)+1:]
+ }
+ if header != "" {
+ break
+ }
+ }
+ assert.Empty(t, header)
+ })
+}
+
+func TestHTTPSCreds_Environ_clientCert(t *testing.T) {
+ store := &memoryCredsStore{creds: make(map[string]cred)}
+ creds := NewHTTPSCreds("", "", "clientCertData", "clientCertKey", false, "", store, false)
+ closer, env, err := creds.Environ()
+ require.NoError(t, err)
+ var cert, key string
+ for _, envVar := range env {
+ if strings.HasPrefix(envVar, "GIT_SSL_CERT=") {
+ cert = envVar[13:]
+ } else if strings.HasPrefix(envVar, "GIT_SSL_KEY=") {
+ key = envVar[12:]
+ }
+ if cert != "" && key != "" {
+ break
+ }
+ }
+ assert.NotEmpty(t, cert)
+ assert.NotEmpty(t, key)
+
+ certBytes, err := os.ReadFile(cert)
+ assert.NoError(t, err)
+ assert.Equal(t, "clientCertData", string(certBytes))
+ keyBytes, err := os.ReadFile(key)
+ assert.Equal(t, "clientCertKey", string(keyBytes))
+ assert.NoError(t, err)
+
+ io.Close(closer)
+
+ _, err = os.Stat(cert)
+ assert.ErrorIs(t, err, os.ErrNotExist)
+ _, err = os.Stat(key)
+ assert.ErrorIs(t, err, os.ErrNotExist)
+}
+
+func Test_SSHCreds_Environ(t *testing.T) {
+ for _, insecureIgnoreHostKey := range []bool{false, true} {
+ tempDir := t.TempDir()
+ caFile := path.Join(tempDir, "caFile")
+ err := os.WriteFile(caFile, []byte(""), os.FileMode(0600))
+ require.NoError(t, err)
+ creds := NewSSHCreds("sshPrivateKey", caFile, insecureIgnoreHostKey, &NoopCredsStore{}, "")
+ closer, env, err := creds.Environ()
+ require.NoError(t, err)
+ require.Len(t, env, 2)
+
+ assert.Equal(t, fmt.Sprintf("GIT_SSL_CAINFO=%s/caFile", tempDir), env[0], "CAINFO env var must be set")
+
+ assert.True(t, strings.HasPrefix(env[1], "GIT_SSH_COMMAND="))
+
+ if insecureIgnoreHostKey {
+ assert.Contains(t, env[1], "-o StrictHostKeyChecking=no")
+ assert.Contains(t, env[1], "-o UserKnownHostsFile=/dev/null")
+ } else {
+ assert.Contains(t, env[1], "-o StrictHostKeyChecking=yes")
+ hostsPath := cert.GetSSHKnownHostsDataPath()
+ assert.Contains(t, env[1], fmt.Sprintf("-o UserKnownHostsFile=%s", hostsPath))
+ }
+
+ envRegex := regexp.MustCompile("-i ([^ ]+)")
+ assert.Regexp(t, envRegex, env[1])
+ privateKeyFile := envRegex.FindStringSubmatch(env[1])[1]
+ assert.FileExists(t, privateKeyFile)
+ io.Close(closer)
+ assert.NoFileExists(t, privateKeyFile)
+ }
+}
+
+func Test_SSHCreds_Environ_WithProxy(t *testing.T) {
+ for _, insecureIgnoreHostKey := range []bool{false, true} {
+ tempDir := t.TempDir()
+ caFile := path.Join(tempDir, "caFile")
+ err := os.WriteFile(caFile, []byte(""), os.FileMode(0600))
+ require.NoError(t, err)
+ creds := NewSSHCreds("sshPrivateKey", caFile, insecureIgnoreHostKey, &NoopCredsStore{}, "socks5://127.0.0.1:1080")
+ closer, env, err := creds.Environ()
+ require.NoError(t, err)
+ require.Len(t, env, 2)
+
+ assert.Equal(t, fmt.Sprintf("GIT_SSL_CAINFO=%s/caFile", tempDir), env[0], "CAINFO env var must be set")
+
+ assert.True(t, strings.HasPrefix(env[1], "GIT_SSH_COMMAND="))
+
+ if insecureIgnoreHostKey {
+ assert.Contains(t, env[1], "-o StrictHostKeyChecking=no")
+ assert.Contains(t, env[1], "-o UserKnownHostsFile=/dev/null")
+ } else {
+ assert.Contains(t, env[1], "-o StrictHostKeyChecking=yes")
+ hostsPath := cert.GetSSHKnownHostsDataPath()
+ assert.Contains(t, env[1], fmt.Sprintf("-o UserKnownHostsFile=%s", hostsPath))
+ }
+ assert.Contains(t, env[1], "-o ProxyCommand='connect-proxy -S 127.0.0.1:1080 -5 %h %p'")
+
+ envRegex := regexp.MustCompile("-i ([^ ]+)")
+ assert.Regexp(t, envRegex, env[1])
+ privateKeyFile := envRegex.FindStringSubmatch(env[1])[1]
+ assert.FileExists(t, privateKeyFile)
+ io.Close(closer)
+ assert.NoFileExists(t, privateKeyFile)
+ }
+}
+
+func Test_SSHCreds_Environ_WithProxyUserNamePassword(t *testing.T) {
+ for _, insecureIgnoreHostKey := range []bool{false, true} {
+ tempDir := t.TempDir()
+ caFile := path.Join(tempDir, "caFile")
+ err := os.WriteFile(caFile, []byte(""), os.FileMode(0600))
+ require.NoError(t, err)
+ creds := NewSSHCreds("sshPrivateKey", caFile, insecureIgnoreHostKey, &NoopCredsStore{}, "socks5://user:password@127.0.0.1:1080")
+ closer, env, err := creds.Environ()
+ require.NoError(t, err)
+ require.Len(t, env, 4)
+
+ assert.Equal(t, fmt.Sprintf("GIT_SSL_CAINFO=%s/caFile", tempDir), env[0], "CAINFO env var must be set")
+
+ assert.True(t, strings.HasPrefix(env[1], "GIT_SSH_COMMAND="))
+ assert.Equal(t, "SOCKS5_USER=user", env[2], "SOCKS5 user env var must be set")
+ assert.Equal(t, "SOCKS5_PASSWD=password", env[3], "SOCKS5 password env var must be set")
+
+ if insecureIgnoreHostKey {
+ assert.Contains(t, env[1], "-o StrictHostKeyChecking=no")
+ assert.Contains(t, env[1], "-o UserKnownHostsFile=/dev/null")
+ } else {
+ assert.Contains(t, env[1], "-o StrictHostKeyChecking=yes")
+ hostsPath := cert.GetSSHKnownHostsDataPath()
+ assert.Contains(t, env[1], fmt.Sprintf("-o UserKnownHostsFile=%s", hostsPath))
+ }
+ assert.Contains(t, env[1], "-o ProxyCommand='connect-proxy -S 127.0.0.1:1080 -5 %h %p'")
+
+ envRegex := regexp.MustCompile("-i ([^ ]+)")
+ assert.Regexp(t, envRegex, env[1])
+ privateKeyFile := envRegex.FindStringSubmatch(env[1])[1]
+ assert.FileExists(t, privateKeyFile)
+ io.Close(closer)
+ assert.NoFileExists(t, privateKeyFile)
+ }
+}
+
+const gcpServiceAccountKeyJSON = `{
+ "type": "service_account",
+ "project_id": "my-google-project",
+ "private_key_id": "REDACTED",
+ "private_key": "-----BEGIN PRIVATE KEY-----\nREDACTED\n-----END PRIVATE KEY-----\n",
+ "client_email": "argocd-service-account@my-google-project.iam.gserviceaccount.com",
+ "client_id": "REDACTED",
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+ "token_uri": "https://oauth2.googleapis.com/token",
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
+ "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/argocd-service-account%40my-google-project.iam.gserviceaccount.com"
+}`
+
+const invalidJSON = `{
+ "type": "service_account",
+ "project_id": "my-google-project",
+`
+
+func TestNewGoogleCloudCreds(t *testing.T) {
+ store := &memoryCredsStore{creds: make(map[string]cred)}
+ googleCloudCreds := NewGoogleCloudCreds(gcpServiceAccountKeyJSON, store)
+ assert.NotNil(t, googleCloudCreds)
+}
+
+func TestNewGoogleCloudCreds_invalidJSON(t *testing.T) {
+ store := &memoryCredsStore{creds: make(map[string]cred)}
+ googleCloudCreds := NewGoogleCloudCreds(invalidJSON, store)
+ assert.Nil(t, googleCloudCreds.creds)
+
+ token, err := googleCloudCreds.getAccessToken()
+ assert.Equal(t, "", token)
+ assert.NotNil(t, err)
+
+ username, err := googleCloudCreds.getUsername()
+ assert.Equal(t, "", username)
+ assert.NotNil(t, err)
+
+ closer, envStringSlice, err := googleCloudCreds.Environ()
+ assert.Equal(t, NopCloser{}, closer)
+ assert.Equal(t, []string(nil), envStringSlice)
+ assert.NotNil(t, err)
+}
+
+func TestGoogleCloudCreds_Environ_cleanup(t *testing.T) {
+ store := &memoryCredsStore{creds: make(map[string]cred)}
+ staticToken := &oauth2.Token{AccessToken: "token"}
+ googleCloudCreds := GoogleCloudCreds{&google.Credentials{
+ ProjectID: "my-google-project",
+ TokenSource: oauth2.StaticTokenSource(staticToken),
+ JSON: []byte(gcpServiceAccountKeyJSON),
+ }, store}
+
+ closer, env, err := googleCloudCreds.Environ()
+ assert.NoError(t, err)
+ var nonce string
+ for _, envVar := range env {
+ if strings.HasPrefix(envVar, ASKPASS_NONCE_ENV) {
+ nonce = envVar[len(ASKPASS_NONCE_ENV)+1:]
+ break
+ }
+ }
+ assert.Contains(t, store.creds, nonce)
+ io.Close(closer)
+ assert.NotContains(t, store.creds, nonce)
+}
diff --git a/ext/git/git.go b/ext/git/git.go
index b925789..d5a8652 100644
--- a/ext/git/git.go
+++ b/ext/git/git.go
@@ -14,14 +14,6 @@ func ensurePrefix(s, prefix string) string {
return s
}
-// removeSuffix idempotently removes a given suffix
-func removeSuffix(s, suffix string) string {
- if strings.HasSuffix(s, suffix) {
- return s[0 : len(s)-len(suffix)]
- }
- return s
-}
-
var (
commitSHARegex = regexp.MustCompile("^[0-9A-Fa-f]{40}$")
sshURLRegex = regexp.MustCompile("^(ssh://)?([^/:]*?)@[^@]+$")
@@ -62,7 +54,7 @@ func NormalizeGitURL(repo string) string {
repo = ensurePrefix(repo, "ssh://")
}
}
- repo = removeSuffix(repo, ".git")
+ repo = strings.TrimSuffix(repo, ".git")
repoURL, err := url.Parse(repo)
if err != nil {
return ""
diff --git a/ext/git/git_test.go b/ext/git/git_test.go
index de04454..0eebe35 100644
--- a/ext/git/git_test.go
+++ b/ext/git/git_test.go
@@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/assert"
+ "github.com/argoproj/argo-cd/v2/common"
"github.com/argoproj/argo-cd/v2/test/fixture/log"
"github.com/argoproj/argo-cd/v2/test/fixture/path"
"github.com/argoproj/argo-cd/v2/test/fixture/test"
@@ -44,18 +45,6 @@ func TestEnsurePrefix(t *testing.T) {
}
}
-func TestRemoveSuffix(t *testing.T) {
- data := [][]string{
- {"hello.git", ".git", "hello"},
- {"hello", ".git", "hello"},
- {".git", ".git", ""},
- }
- for _, table := range data {
- result := removeSuffix(table[0], table[1])
- assert.Equal(t, table[2], result)
- }
-}
-
func TestIsSSHURL(t *testing.T) {
data := map[string]bool{
"git://github.com/argoproj/test.git": false,
@@ -146,19 +135,19 @@ func TestCustomHTTPClient(t *testing.T) {
assert.NotEqual(t, "", string(keyData))
// Get HTTPSCreds with client cert creds specified, and insecure connection
- creds := NewHTTPSCreds("test", "test", string(certData), string(keyData), false, "http://proxy:5000")
+ creds := NewHTTPSCreds("test", "test", string(certData), string(keyData), false, "http://proxy:5000", &NoopCredsStore{}, false)
client := GetRepoHTTPClient("https://localhost:9443/foo/bar", false, creds, "http://proxy:5000")
assert.NotNil(t, client)
assert.NotNil(t, client.Transport)
if client.Transport != nil {
- httpClient := client.Transport.(*http.Transport)
- assert.NotNil(t, httpClient.TLSClientConfig)
-
- assert.Equal(t, false, httpClient.TLSClientConfig.InsecureSkipVerify)
-
- assert.NotNil(t, httpClient.TLSClientConfig.GetClientCertificate)
- if httpClient.TLSClientConfig.GetClientCertificate != nil {
- cert, err := httpClient.TLSClientConfig.GetClientCertificate(nil)
+ transport := client.Transport.(*http.Transport)
+ assert.NotNil(t, transport.TLSClientConfig)
+ assert.Equal(t, true, transport.DisableKeepAlives)
+ assert.Equal(t, false, transport.TLSClientConfig.InsecureSkipVerify)
+ assert.NotNil(t, transport.TLSClientConfig.GetClientCertificate)
+ assert.Nil(t, transport.TLSClientConfig.RootCAs)
+ if transport.TLSClientConfig.GetClientCertificate != nil {
+ cert, err := transport.TLSClientConfig.GetClientCertificate(nil)
assert.NoError(t, err)
if err == nil {
assert.NotNil(t, cert)
@@ -166,30 +155,27 @@ func TestCustomHTTPClient(t *testing.T) {
assert.NotNil(t, cert.PrivateKey)
}
}
- proxy, err := httpClient.Proxy(nil)
+ proxy, err := transport.Proxy(nil)
assert.Nil(t, err)
assert.Equal(t, "http://proxy:5000", proxy.String())
}
- os.Setenv("http_proxy", "http://proxy-from-env:7878")
- defer func() {
- assert.Nil(t, os.Unsetenv("http_proxy"))
- }()
+ t.Setenv("http_proxy", "http://proxy-from-env:7878")
// Get HTTPSCreds without client cert creds, but insecure connection
- creds = NewHTTPSCreds("test", "test", "", "", true, "")
+ creds = NewHTTPSCreds("test", "test", "", "", true, "", &NoopCredsStore{}, false)
client = GetRepoHTTPClient("https://localhost:9443/foo/bar", true, creds, "")
assert.NotNil(t, client)
assert.NotNil(t, client.Transport)
if client.Transport != nil {
- httpClient := client.Transport.(*http.Transport)
- assert.NotNil(t, httpClient.TLSClientConfig)
-
- assert.Equal(t, true, httpClient.TLSClientConfig.InsecureSkipVerify)
-
- assert.NotNil(t, httpClient.TLSClientConfig.GetClientCertificate)
- if httpClient.TLSClientConfig.GetClientCertificate != nil {
- cert, err := httpClient.TLSClientConfig.GetClientCertificate(nil)
+ transport := client.Transport.(*http.Transport)
+ assert.NotNil(t, transport.TLSClientConfig)
+ assert.Equal(t, true, transport.DisableKeepAlives)
+ assert.Equal(t, true, transport.TLSClientConfig.InsecureSkipVerify)
+ assert.NotNil(t, transport.TLSClientConfig.GetClientCertificate)
+ assert.Nil(t, transport.TLSClientConfig.RootCAs)
+ if transport.TLSClientConfig.GetClientCertificate != nil {
+ cert, err := transport.TLSClientConfig.GetClientCertificate(nil)
assert.NoError(t, err)
if err == nil {
assert.NotNil(t, cert)
@@ -197,12 +183,30 @@ func TestCustomHTTPClient(t *testing.T) {
assert.Nil(t, cert.PrivateKey)
}
}
- req, err := http.NewRequest("GET", "http://proxy-from-env:7878", nil)
+ req, err := http.NewRequest(http.MethodGet, "http://proxy-from-env:7878", nil)
assert.Nil(t, err)
- proxy, err := httpClient.Proxy(req)
+ proxy, err := transport.Proxy(req)
assert.Nil(t, err)
assert.Equal(t, "http://proxy-from-env:7878", proxy.String())
}
+ // GetRepoHTTPClient with root ca
+ cert, err := os.ReadFile("../../test/fixture/certs/argocd-test-server.crt")
+ assert.NoError(t, err)
+ temppath := t.TempDir()
+ defer os.RemoveAll(temppath)
+ err = os.WriteFile(filepath.Join(temppath, "127.0.0.1"), cert, 0666)
+ assert.NoError(t, err)
+ t.Setenv(common.EnvVarTLSDataPath, temppath)
+ client = GetRepoHTTPClient("https://127.0.0.1", false, creds, "")
+ assert.NotNil(t, client)
+ assert.NotNil(t, client.Transport)
+ if client.Transport != nil {
+ transport := client.Transport.(*http.Transport)
+ assert.NotNil(t, transport.TLSClientConfig)
+ assert.Equal(t, true, transport.DisableKeepAlives)
+ assert.Equal(t, false, transport.TLSClientConfig.InsecureSkipVerify)
+ assert.NotNil(t, transport.TLSClientConfig.RootCAs)
+ }
}
func TestLsRemote(t *testing.T) {
@@ -245,11 +249,7 @@ func TestLFSClient(t *testing.T) {
// TODO(alexmt): dockerize tests in and enabled it
t.Skip()
- tempDir, err := os.MkdirTemp("", "git-client-lfs-test-")
- assert.NoError(t, err)
- if err == nil {
- defer func() { _ = os.RemoveAll(tempDir) }()
- }
+ tempDir := t.TempDir()
client, err := NewClientExt("https://github.com/argoproj-labs/argocd-testrepo-lfs", tempDir, NopCreds{}, false, true, "")
assert.NoError(t, err)
@@ -264,7 +264,7 @@ func TestLFSClient(t *testing.T) {
err = client.Fetch("")
assert.NoError(t, err)
- err = client.Checkout(commitSHA)
+ err = client.Checkout(commitSHA, true)
assert.NoError(t, err)
largeFiles, err := client.LsLargeFiles()
@@ -274,7 +274,11 @@ func TestLFSClient(t *testing.T) {
fileHandle, err := os.Open(fmt.Sprintf("%s/test3.yaml", tempDir))
assert.NoError(t, err)
if err == nil {
- defer fileHandle.Close()
+ defer func() {
+ if err = fileHandle.Close(); err != nil {
+ assert.NoError(t, err)
+ }
+ }()
text, err := io.ReadAll(fileHandle)
assert.NoError(t, err)
if err == nil {
@@ -284,11 +288,7 @@ func TestLFSClient(t *testing.T) {
}
func TestVerifyCommitSignature(t *testing.T) {
- p, err := os.MkdirTemp("", "test-verify-commit-sig")
- if err != nil {
- panic(err.Error())
- }
- defer os.RemoveAll(p)
+ p := t.TempDir()
client, err := NewClientExt("https://github.com/argoproj/argo-cd.git", p, NopCreds{}, false, false, "")
assert.NoError(t, err)
@@ -302,7 +302,7 @@ func TestVerifyCommitSignature(t *testing.T) {
commitSHA, err := client.LsRemote("HEAD")
assert.NoError(t, err)
- err = client.Checkout(commitSHA)
+ err = client.Checkout(commitSHA, true)
assert.NoError(t, err)
// 28027897aad1262662096745f2ce2d4c74d02b7f is a commit that is signed in the repo
@@ -343,9 +343,7 @@ func TestNewFactory(t *testing.T) {
test.Flaky(t)
}
- dirName, err := os.MkdirTemp("", "git-client-test-")
- assert.NoError(t, err)
- defer func() { _ = os.RemoveAll(dirName) }()
+ dirName := t.TempDir()
client, err := NewClientExt(tt.args.url, dirName, NopCreds{}, tt.args.insecureIgnoreHostKey, false, "")
assert.NoError(t, err)
@@ -362,7 +360,7 @@ func TestNewFactory(t *testing.T) {
err = client.Fetch("")
assert.NoError(t, err)
- err = client.Checkout(commitSHA)
+ err = client.Checkout(commitSHA, true)
assert.NoError(t, err)
revisionMetadata, err := client.RevisionMetadata(commitSHA)
@@ -381,11 +379,7 @@ func TestNewFactory(t *testing.T) {
}
func TestListRevisions(t *testing.T) {
- dir, err := os.MkdirTemp("", "test-list-revisions")
- if err != nil {
- panic(err.Error())
- }
- defer os.RemoveAll(dir)
+ dir := t.TempDir()
repoURL := "https://github.com/argoproj/argo-cd.git"
client, err := NewClientExt(repoURL, dir, NopCreds{}, false, false, "")
@@ -402,3 +396,54 @@ func TestListRevisions(t *testing.T) {
assert.NotContains(t, lsResult.Branches, testTag)
assert.NotContains(t, lsResult.Tags, testBranch)
}
+
+func TestLsFiles(t *testing.T) {
+ tmpDir1 := t.TempDir()
+ tmpDir2 := t.TempDir()
+
+ client, err := NewClientExt("", tmpDir1, NopCreds{}, false, false, "")
+ assert.NoError(t, err)
+
+ err = runCmd(tmpDir1, "git", "init")
+ assert.NoError(t, err)
+
+ // Prepare files
+ a, err := os.Create(filepath.Join(tmpDir1, "a.yaml"))
+ assert.NoError(t, err)
+ a.Close()
+ err = os.MkdirAll(filepath.Join(tmpDir1, "subdir"), 0755)
+ assert.NoError(t, err)
+ b, err := os.Create(filepath.Join(tmpDir1, "subdir", "b.yaml"))
+ assert.NoError(t, err)
+ b.Close()
+ err = os.MkdirAll(filepath.Join(tmpDir2, "subdir"), 0755)
+ assert.NoError(t, err)
+ c, err := os.Create(filepath.Join(tmpDir2, "c.yaml"))
+ assert.NoError(t, err)
+ c.Close()
+ err = os.Symlink(filepath.Join(tmpDir2, "c.yaml"), filepath.Join(tmpDir1, "link.yaml"))
+ assert.NoError(t, err)
+
+ err = runCmd(tmpDir1, "git", "add", ".")
+ assert.NoError(t, err)
+ err = runCmd(tmpDir1, "git", "commit", "-m", "Initial commit")
+ assert.NoError(t, err)
+
+ // Old and default globbing
+ expectedResult := []string{"a.yaml", "link.yaml", "subdir/b.yaml"}
+ lsResult, err := client.LsFiles("*.yaml", false)
+ assert.NoError(t, err)
+ assert.Equal(t, lsResult, expectedResult)
+
+ // New and safer globbing, do not return symlinks resolving outside of the repo
+ expectedResult = []string{"a.yaml"}
+ lsResult, err = client.LsFiles("*.yaml", true)
+ assert.NoError(t, err)
+ assert.Equal(t, lsResult, expectedResult)
+
+ // New globbing, do not return files outside of the repo
+ var nilResult []string
+ lsResult, err = client.LsFiles(filepath.Join(tmpDir2, "*.yaml"), true)
+ assert.NoError(t, err)
+ assert.Equal(t, lsResult, nilResult)
+}
diff --git a/ext/git/mocks/Client.go b/ext/git/mocks/Client.go
index 645e065..6325228 100644
--- a/ext/git/mocks/Client.go
+++ b/ext/git/mocks/Client.go
@@ -1,11 +1,10 @@
-// Code generated by mockery v1.1.2. DO NOT EDIT.
+// Code generated by mockery v2.43.0. DO NOT EDIT.
package mocks
import (
- mock "github.com/stretchr/testify/mock"
-
git "github.com/argoproj-labs/argocd-image-updater/ext/git"
+ mock "github.com/stretchr/testify/mock"
)
// Client is an autogenerated mock type for the Client type
@@ -17,6 +16,10 @@ type Client struct {
func (_m *Client) Add(path string) error {
ret := _m.Called(path)
+ if len(ret) == 0 {
+ panic("no return value specified for Add")
+ }
+
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(path)
@@ -31,6 +34,10 @@ func (_m *Client) Add(path string) error {
func (_m *Client) Branch(sourceBranch string, targetBranch string) error {
ret := _m.Called(sourceBranch, targetBranch)
+ if len(ret) == 0 {
+ panic("no return value specified for Branch")
+ }
+
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(sourceBranch, targetBranch)
@@ -41,13 +48,47 @@ func (_m *Client) Branch(sourceBranch string, targetBranch string) error {
return r0
}
-// Checkout provides a mock function with given fields: revision
-func (_m *Client) Checkout(revision string) error {
- ret := _m.Called(revision)
+// ChangedFiles provides a mock function with given fields: revision, targetRevision
+func (_m *Client) ChangedFiles(revision string, targetRevision string) ([]string, error) {
+ ret := _m.Called(revision, targetRevision)
+
+ if len(ret) == 0 {
+ panic("no return value specified for ChangedFiles")
+ }
+
+ var r0 []string
+ var r1 error
+ if rf, ok := ret.Get(0).(func(string, string) ([]string, error)); ok {
+ return rf(revision, targetRevision)
+ }
+ if rf, ok := ret.Get(0).(func(string, string) []string); ok {
+ r0 = rf(revision, targetRevision)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).([]string)
+ }
+ }
+
+ if rf, ok := ret.Get(1).(func(string, string) error); ok {
+ r1 = rf(revision, targetRevision)
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
+}
+
+// Checkout provides a mock function with given fields: revision, submoduleEnabled
+func (_m *Client) Checkout(revision string, submoduleEnabled bool) error {
+ ret := _m.Called(revision, submoduleEnabled)
+
+ if len(ret) == 0 {
+ panic("no return value specified for Checkout")
+ }
var r0 error
- if rf, ok := ret.Get(0).(func(string) error); ok {
- r0 = rf(revision)
+ if rf, ok := ret.Get(0).(func(string, bool) error); ok {
+ r0 = rf(revision, submoduleEnabled)
} else {
r0 = ret.Error(0)
}
@@ -59,6 +100,10 @@ func (_m *Client) Checkout(revision string) error {
func (_m *Client) Commit(pathSpec string, opts *git.CommitOptions) error {
ret := _m.Called(pathSpec, opts)
+ if len(ret) == 0 {
+ panic("no return value specified for Commit")
+ }
+
var r0 error
if rf, ok := ret.Get(0).(func(string, *git.CommitOptions) error); ok {
r0 = rf(pathSpec, opts)
@@ -73,14 +118,21 @@ func (_m *Client) Commit(pathSpec string, opts *git.CommitOptions) error {
func (_m *Client) CommitSHA() (string, error) {
ret := _m.Called()
+ if len(ret) == 0 {
+ panic("no return value specified for CommitSHA")
+ }
+
var r0 string
+ var r1 error
+ if rf, ok := ret.Get(0).(func() (string, error)); ok {
+ return rf()
+ }
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
- var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
@@ -94,6 +146,10 @@ func (_m *Client) CommitSHA() (string, error) {
func (_m *Client) Config(username string, email string) error {
ret := _m.Called(username, email)
+ if len(ret) == 0 {
+ panic("no return value specified for Config")
+ }
+
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(username, email)
@@ -108,6 +164,10 @@ func (_m *Client) Config(username string, email string) error {
func (_m *Client) Fetch(revision string) error {
ret := _m.Called(revision)
+ if len(ret) == 0 {
+ panic("no return value specified for Fetch")
+ }
+
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(revision)
@@ -122,6 +182,10 @@ func (_m *Client) Fetch(revision string) error {
func (_m *Client) Init() error {
ret := _m.Called()
+ if len(ret) == 0 {
+ panic("no return value specified for Init")
+ }
+
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
@@ -132,22 +196,47 @@ func (_m *Client) Init() error {
return r0
}
-// LsFiles provides a mock function with given fields: path
-func (_m *Client) LsFiles(path string) ([]string, error) {
- ret := _m.Called(path)
+// IsAnnotatedTag provides a mock function with given fields: _a0
+func (_m *Client) IsAnnotatedTag(_a0 string) bool {
+ ret := _m.Called(_a0)
+
+ if len(ret) == 0 {
+ panic("no return value specified for IsAnnotatedTag")
+ }
+
+ var r0 bool
+ if rf, ok := ret.Get(0).(func(string) bool); ok {
+ r0 = rf(_a0)
+ } else {
+ r0 = ret.Get(0).(bool)
+ }
+
+ return r0
+}
+
+// LsFiles provides a mock function with given fields: path, enableNewGitFileGlobbing
+func (_m *Client) LsFiles(path string, enableNewGitFileGlobbing bool) ([]string, error) {
+ ret := _m.Called(path, enableNewGitFileGlobbing)
+
+ if len(ret) == 0 {
+ panic("no return value specified for LsFiles")
+ }
var r0 []string
- if rf, ok := ret.Get(0).(func(string) []string); ok {
- r0 = rf(path)
+ var r1 error
+ if rf, ok := ret.Get(0).(func(string, bool) ([]string, error)); ok {
+ return rf(path, enableNewGitFileGlobbing)
+ }
+ if rf, ok := ret.Get(0).(func(string, bool) []string); ok {
+ r0 = rf(path, enableNewGitFileGlobbing)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
- var r1 error
- if rf, ok := ret.Get(1).(func(string) error); ok {
- r1 = rf(path)
+ if rf, ok := ret.Get(1).(func(string, bool) error); ok {
+ r1 = rf(path, enableNewGitFileGlobbing)
} else {
r1 = ret.Error(1)
}
@@ -159,7 +248,15 @@ func (_m *Client) LsFiles(path string) ([]string, error) {
func (_m *Client) LsLargeFiles() ([]string, error) {
ret := _m.Called()
+ if len(ret) == 0 {
+ panic("no return value specified for LsLargeFiles")
+ }
+
var r0 []string
+ var r1 error
+ if rf, ok := ret.Get(0).(func() ([]string, error)); ok {
+ return rf()
+ }
if rf, ok := ret.Get(0).(func() []string); ok {
r0 = rf()
} else {
@@ -168,7 +265,6 @@ func (_m *Client) LsLargeFiles() ([]string, error) {
}
}
- var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
@@ -182,7 +278,15 @@ func (_m *Client) LsLargeFiles() ([]string, error) {
func (_m *Client) LsRefs() (*git.Refs, error) {
ret := _m.Called()
+ if len(ret) == 0 {
+ panic("no return value specified for LsRefs")
+ }
+
var r0 *git.Refs
+ var r1 error
+ if rf, ok := ret.Get(0).(func() (*git.Refs, error)); ok {
+ return rf()
+ }
if rf, ok := ret.Get(0).(func() *git.Refs); ok {
r0 = rf()
} else {
@@ -191,7 +295,6 @@ func (_m *Client) LsRefs() (*git.Refs, error) {
}
}
- var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
@@ -205,14 +308,21 @@ func (_m *Client) LsRefs() (*git.Refs, error) {
func (_m *Client) LsRemote(revision string) (string, error) {
ret := _m.Called(revision)
+ if len(ret) == 0 {
+ panic("no return value specified for LsRemote")
+ }
+
var r0 string
+ var r1 error
+ if rf, ok := ret.Get(0).(func(string) (string, error)); ok {
+ return rf(revision)
+ }
if rf, ok := ret.Get(0).(func(string) string); ok {
r0 = rf(revision)
} else {
r0 = ret.Get(0).(string)
}
- var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(revision)
} else {
@@ -226,6 +336,10 @@ func (_m *Client) LsRemote(revision string) (string, error) {
func (_m *Client) Push(remote string, branch string, force bool) error {
ret := _m.Called(remote, branch, force)
+ if len(ret) == 0 {
+ panic("no return value specified for Push")
+ }
+
var r0 error
if rf, ok := ret.Get(0).(func(string, string, bool) error); ok {
r0 = rf(remote, branch, force)
@@ -240,7 +354,15 @@ func (_m *Client) Push(remote string, branch string, force bool) error {
func (_m *Client) RevisionMetadata(revision string) (*git.RevisionMetadata, error) {
ret := _m.Called(revision)
+ if len(ret) == 0 {
+ panic("no return value specified for RevisionMetadata")
+ }
+
var r0 *git.RevisionMetadata
+ var r1 error
+ if rf, ok := ret.Get(0).(func(string) (*git.RevisionMetadata, error)); ok {
+ return rf(revision)
+ }
if rf, ok := ret.Get(0).(func(string) *git.RevisionMetadata); ok {
r0 = rf(revision)
} else {
@@ -249,7 +371,6 @@ func (_m *Client) RevisionMetadata(revision string) (*git.RevisionMetadata, erro
}
}
- var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(revision)
} else {
@@ -263,6 +384,10 @@ func (_m *Client) RevisionMetadata(revision string) (*git.RevisionMetadata, erro
func (_m *Client) Root() string {
ret := _m.Called()
+ if len(ret) == 0 {
+ panic("no return value specified for Root")
+ }
+
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
@@ -273,18 +398,43 @@ func (_m *Client) Root() string {
return r0
}
+// Submodule provides a mock function with given fields:
+func (_m *Client) Submodule() error {
+ ret := _m.Called()
+
+ if len(ret) == 0 {
+ panic("no return value specified for Submodule")
+ }
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func() error); ok {
+ r0 = rf()
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
// SymRefToBranch provides a mock function with given fields: symRef
func (_m *Client) SymRefToBranch(symRef string) (string, error) {
ret := _m.Called(symRef)
+ if len(ret) == 0 {
+ panic("no return value specified for SymRefToBranch")
+ }
+
var r0 string
+ var r1 error
+ if rf, ok := ret.Get(0).(func(string) (string, error)); ok {
+ return rf(symRef)
+ }
if rf, ok := ret.Get(0).(func(string) string); ok {
r0 = rf(symRef)
} else {
r0 = ret.Get(0).(string)
}
- var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(symRef)
} else {
@@ -298,14 +448,21 @@ func (_m *Client) SymRefToBranch(symRef string) (string, error) {
func (_m *Client) VerifyCommitSignature(_a0 string) (string, error) {
ret := _m.Called(_a0)
+ if len(ret) == 0 {
+ panic("no return value specified for VerifyCommitSignature")
+ }
+
var r0 string
+ var r1 error
+ if rf, ok := ret.Get(0).(func(string) (string, error)); ok {
+ return rf(_a0)
+ }
if rf, ok := ret.Get(0).(func(string) string); ok {
r0 = rf(_a0)
} else {
r0 = ret.Get(0).(string)
}
- var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(_a0)
} else {
@@ -314,3 +471,17 @@ func (_m *Client) VerifyCommitSignature(_a0 string) (string, error) {
return r0, r1
}
+
+// NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewClient(t interface {
+ mock.TestingT
+ Cleanup(func())
+}) *Client {
+ mock := &Client{}
+ mock.Mock.Test(t)
+
+ t.Cleanup(func() { mock.AssertExpectations(t) })
+
+ return mock
+}
diff --git a/ext/git/ssh.go b/ext/git/ssh.go
index eb07a05..a1cb337 100644
--- a/ext/git/ssh.go
+++ b/ext/git/ssh.go
@@ -11,14 +11,14 @@ import (
// Unfortunately, crypto/ssh does not offer public constants or list for
// this.
var SupportedSSHKeyExchangeAlgorithms = []string{
- "diffie-hellman-group1-sha1",
- "diffie-hellman-group14-sha1",
+ "curve25519-sha256",
+ "curve25519-sha256@libssh.org",
"ecdh-sha2-nistp256",
"ecdh-sha2-nistp384",
"ecdh-sha2-nistp521",
- "curve25519-sha256@libssh.org",
- "diffie-hellman-group-exchange-sha1",
"diffie-hellman-group-exchange-sha256",
+ "diffie-hellman-group14-sha256",
+ "diffie-hellman-group14-sha1",
}
// List of default key exchange algorithms to use. We use those that are
diff --git a/ext/git/workaround.go b/ext/git/workaround.go
index c364c09..4763612 100644
--- a/ext/git/workaround.go
+++ b/ext/git/workaround.go
@@ -1,6 +1,9 @@
package git
import (
+ "fmt"
+ neturl "net/url"
+
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/transport"
@@ -30,6 +33,23 @@ func newClient(url string, insecure bool, creds Creds, proxy string) (transport.
if !IsHTTPSURL(url) && !IsHTTPURL(url) {
// use the default client for protocols other than HTTP/HTTPS
+ ep.InsecureSkipTLS = insecure
+ if proxy != "" {
+ parsedProxyURL, err := neturl.Parse(proxy)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to create client for url '%s', error parsing proxy url '%s': %w", url, proxy, err)
+ }
+ var proxyUsername, proxyPasswd string
+ if parsedProxyURL.User != nil {
+ proxyUsername = parsedProxyURL.User.Username()
+ proxyPasswd, _ = parsedProxyURL.User.Password()
+ }
+ ep.Proxy = transport.ProxyOptions{
+ URL: fmt.Sprintf("%s://%s:%s", parsedProxyURL.Scheme, parsedProxyURL.Hostname(), parsedProxyURL.Port()),
+ Username: proxyUsername,
+ Password: proxyPasswd,
+ }
+ }
c, err := client.NewClient(ep)
if err != nil {
return nil, nil, err
diff --git a/ext/git/writer.go b/ext/git/writer.go
index 4dede58..a2f4cc6 100644
--- a/ext/git/writer.go
+++ b/ext/git/writer.go
@@ -79,7 +79,7 @@ func (m *nativeGitClient) Push(remote string, branch string, force bool) error {
args = append(args, "-f")
}
args = append(args, remote, branch)
- err := m.runCredentialedCmd("git", args...)
+ err := m.runCredentialedCmd(args...)
if err != nil {
return fmt.Errorf("could not push %s to %s: %v", branch, remote, err)
}