@@ -5,8 +5,10 @@ import (
55 "errors"
66 "fmt"
77 "log"
8+ "net/url"
89 "os"
910 "os/exec"
11+ "path/filepath"
1012 "regexp"
1113 "strings"
1214 "testing"
@@ -16,7 +18,7 @@ import (
1618 "github.com/go-git/go-git/v5/plumbing"
1719 "github.com/go-git/go-git/v5/plumbing/object"
1820 "github.com/go-git/go-git/v5/plumbing/transport"
19- "github.com/go-git/go-git/v5/plumbing/transport/http"
21+ gitHttp "github.com/go-git/go-git/v5/plumbing/transport/http"
2022 gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
2123 "github.com/go-git/go-git/v5/storage/memory"
2224 "github.com/gruntwork-io/terratest/modules/logger"
@@ -182,9 +184,11 @@ func (g *realGitOps) commitExistsInRemote(remoteURL, commitID string) (bool, err
182184 config .RefSpec (fmt .Sprintf ("+refs/heads/*:refs/remotes/%s/*" , tempRemoteName )),
183185 config .RefSpec (fmt .Sprintf ("+refs/pull/*/head:refs/remotes/%s/pr/*" , tempRemoteName )),
184186 }
187+ auth , _ := GitAutoAuth (remoteURL )
185188 err = tempRemote .Fetch (& git.FetchOptions {
186189 RemoteName : tempRemoteName ,
187190 RefSpecs : refSpecs ,
191+ Auth : auth ,
188192 })
189193 if err != nil && err != git .NoErrAlreadyUpToDate {
190194 return false , fmt .Errorf ("fetch failed: %w" , err )
@@ -397,72 +401,6 @@ func getCurrentPrRepoAndBranch(git gitOps) (string, string, error) {
397401 return repoURL , branch , nil
398402}
399403
400- // DetermineAuthMethod determines the appropriate authentication method for a given repository URL.
401- // The function supports both HTTPS and SSH-based repositories.
402- //
403- // For HTTPS repositories:
404- // - It first checks if the GIT_TOKEN environment variable is set. If so, it uses this as the Personal Access Token (PAT).
405- // - If the GIT_TOKEN environment variable is not set, no authentication is used for HTTPS repositories.
406- //
407- // For SSH repositories:
408- // - It first checks if the SSH_PRIVATE_KEY environment variable is set. If so, it uses this as the SSH private key.
409- // - If the SSH_PRIVATE_KEY environment variable is not set, it attempts to use the default SSH key located at ~/.ssh/id_rsa.
410- // - If neither the environment variable nor the default key is available, no authentication is used for SSH repositories.
411- //
412- // Parameters:
413- // - repoURL: The URL of the Git repository.
414- //
415- // Returns:
416- // - An appropriate AuthMethod based on the repository URL and available credentials.
417- // - An error if there's an issue parsing the SSH private key or if the private key cannot be cast to an ssh.Signer.
418- func DetermineAuthMethod (repoURL string ) (transport.AuthMethod , error ) {
419- var pat string
420- var sshPrivateKey string
421- if strings .HasPrefix (repoURL , "https://" ) {
422- // Check for Personal Access Token (PAT) in environment variable
423- envPat , exists := os .LookupEnv ("GIT_TOKEN" )
424- if exists {
425- pat = envPat
426- }
427- if pat != "" {
428- return & http.BasicAuth {
429- Username : "git" , // This can be anything except an empty string
430- Password : pat ,
431- }, nil
432- }
433- } else if strings .HasPrefix (repoURL , "git@" ) {
434- // SSH authentication
435- envSSHKey , exists := os .LookupEnv ("SSH_PRIVATE_KEY" )
436- if exists {
437- sshPrivateKey = envSSHKey
438- }
439- if sshPrivateKey == "" {
440- // Attempt to use the default SSH key if none is provided
441- defaultKeyPath := os .ExpandEnv ("$HOME/.ssh/id_rsa" )
442- if _ , err := os .Stat (defaultKeyPath ); ! os .IsNotExist (err ) {
443- // Read the default key
444- keyBytes , err := os .ReadFile (defaultKeyPath )
445- if err != nil {
446- return nil , err
447- }
448- sshPrivateKey = string (keyBytes )
449- }
450- }
451- if sshPrivateKey != "" {
452- key , err := RetrievePrivateKey (sshPrivateKey )
453- if err != nil {
454- return nil , err
455- }
456- signer , ok := key .(ssh.Signer )
457- if ! ok {
458- return nil , errors .New ("unable to cast private key to ssh.Signer" )
459- }
460- return & gitssh.PublicKeys {User : "git" , Signer : signer }, nil
461- }
462- }
463- return nil , nil // No authentication
464- }
465-
466404// RetrievePrivateKey is a function that takes a string sshPvtKey as input and returns an interface{} and error as output.
467405// IF the SSH_PASSPHRASE environment variable is set:
468406// - It will parse the raw private key with passphrase using the ParseRawPrivateKeyWithPassphrase method of the ssh package.
@@ -541,65 +479,15 @@ func SkipUpgradeTest(testing *testing.T, source_repo string, source_branch strin
541479}
542480
543481func CloneAndCheckoutBranch (testing * testing.T , repoURL string , branch string , cloneDir string ) error {
544-
545- authMethod , authErr := DetermineAuthMethod (repoURL )
546- if authErr != nil {
547- logger .Log (testing , "Failed to determine authentication method, trying without authentication..." )
548-
549- // Convert SSH URL to HTTPS URL
550- if strings .HasPrefix (repoURL , "git@" ) {
551- repoURL = strings .Replace (repoURL , ":" , "/" , 1 )
552- repoURL = strings .Replace (repoURL , "git@" , "https://" , 1 )
553- repoURL = strings .TrimSuffix (repoURL , ".git" ) + ".git"
554- }
555-
556- // Try to clone without authentication
557- _ , errUnauth := git .PlainClone (cloneDir , false , & git.CloneOptions {
558- URL : repoURL ,
559- ReferenceName : plumbing .NewBranchReferenceName (branch ),
560- SingleBranch : true ,
561- })
562-
563- if errUnauth != nil {
564- // If unauthenticated clone fails and we cannot determine authentication, return the error from the unauthenticated approach
565- return fmt .Errorf ("failed to determine authentication method and clone base repo and branch without authentication: %v" , errUnauth )
566- } else {
567- logger .Log (testing , "Cloned base repo and branch without authentication" )
568- }
569- } else {
570- // Authentication method determined, try with authentication
571- _ , errAuth := git .PlainClone (cloneDir , false , & git.CloneOptions {
572- URL : repoURL ,
573- ReferenceName : plumbing .NewBranchReferenceName (branch ),
574- SingleBranch : true ,
575- Auth : authMethod ,
576- })
577-
578- if errAuth != nil {
579- logger .Log (testing , "Failed to clone base repo and branch with authentication, trying without authentication..." )
580- // Convert SSH URL to HTTPS URL
581- if strings .HasPrefix (repoURL , "git@" ) {
582- repoURL = strings .Replace (repoURL , ":" , "/" , 1 )
583- repoURL = strings .Replace (repoURL , "git@" , "https://" , 1 )
584- repoURL = strings .TrimSuffix (repoURL , ".git" ) + ".git"
585- }
586-
587- // Try to clone without authentication
588- _ , errUnauth := git .PlainClone (cloneDir , false , & git.CloneOptions {
589- URL : repoURL ,
590- ReferenceName : plumbing .NewBranchReferenceName (branch ),
591- SingleBranch : true ,
592- })
593-
594- if errUnauth != nil {
595- // If unauthenticated clone also fails, return the error from the authenticated approach
596- return fmt .Errorf ("failed to clone base repo and branch with authentication: %v" , errAuth )
597- } else {
598- logger .Log (testing , "Cloned base repo and branch without authentication" )
599- }
600- } else {
601- logger .Log (testing , "Cloned base repo and branch with authentication" )
602- }
482+ authMethod , _ := GitAutoAuth (repoURL )
483+ _ , errClone := git .PlainClone (cloneDir , false , & git.CloneOptions {
484+ URL : repoURL ,
485+ ReferenceName : plumbing .NewBranchReferenceName (branch ),
486+ SingleBranch : true ,
487+ Auth : authMethod ,
488+ })
489+ if errClone != nil {
490+ return fmt .Errorf ("failed to clone base repo and branch: %v" , errClone )
603491 }
604492
605493 return nil
@@ -745,3 +633,148 @@ func getFileDiff(repoDir string, fileName string, git gitOps) (string, error) {
745633
746634 return string (diffOutput ), nil
747635}
636+
637+ // GitAutoAuth returns transport.AuthMethod for a remote URL (SSH or HTTPS)
638+ func GitAutoAuth (remoteURL string ) (transport.AuthMethod , error ) {
639+ if isSSHURL (remoteURL ) {
640+ return sshAuth ()
641+ }
642+ return httpsAuth (remoteURL )
643+ }
644+
645+ // SSH auth
646+ func sshAuth () (transport.AuthMethod , error ) {
647+ // 1. Try SSH agent
648+ auth , err := gitssh .NewSSHAgentAuth ("git" )
649+ if err == nil {
650+ return auth , nil
651+ }
652+
653+ // 2. Try SSH_PRIVATE_KEY env variable
654+ keyData := os .Getenv ("SSH_PRIVATE_KEY" )
655+ if keyData != "" {
656+ auth , err := gitssh .NewPublicKeys ("git" , []byte (keyData ), "" )
657+ if err == nil {
658+ return auth , nil
659+ }
660+ }
661+
662+ // 3. Try default key file ~/.ssh/id_rsa
663+ home := os .Getenv ("HOME" )
664+ if home != "" {
665+ defaultKey := filepath .Join (home , ".ssh" , "id_rsa" )
666+ if _ , err := os .Stat (defaultKey ); err == nil {
667+ auth , err := gitssh .NewPublicKeysFromFile ("git" , defaultKey , "" )
668+ if err == nil {
669+ println ("auth with ssh default" )
670+ return auth , nil
671+ }
672+ }
673+ }
674+ println ("auth with ssh anonymous" )
675+ return nil , errors .New (
676+ "SSH authentication failed: no keys found. " +
677+ "Please start ssh-agent with loaded keys, set SSH_PRIVATE_KEY, or ensure ~/.ssh/id_rsa exists." ,
678+ )
679+ }
680+
681+ func isSSHURL (raw string ) bool {
682+ return strings .HasPrefix (raw , "git@" ) ||
683+ strings .HasPrefix (raw , "ssh://" )
684+ }
685+
686+ // HTTPS auth
687+ func httpsAuth (remoteURL string ) (transport.AuthMethod , error ) {
688+ // Try .netrc first
689+ if auth := loadNetrcAuth (remoteURL ); auth != nil {
690+ println ("auth with https netrc" )
691+ return auth , nil
692+ }
693+
694+ // Try common environment tokens
695+ if tok := os .Getenv ("GITHUB_TOKEN" ); tok != "" {
696+ return & gitHttp.BasicAuth {Username : "token" , Password : tok }, nil
697+ }
698+ if tok := os .Getenv ("GIT_TOKEN" ); tok != "" {
699+ return & gitHttp.BasicAuth {Username : "token" , Password : tok }, nil
700+ }
701+ if tok := os .Getenv ("GITLAB_TOKEN" ); tok != "" {
702+ return & gitHttp.BasicAuth {Username : "token" , Password : tok }, nil
703+ }
704+ if tok := os .Getenv ("BITBUCKET_TOKEN" ); tok != "" {
705+ return & gitHttp.BasicAuth {Username : "token" , Password : tok }, nil
706+ }
707+
708+ // Fallback: anonymous HTTPS
709+ return nil , nil
710+ }
711+
712+ // --------------------------
713+ // .netrc support
714+ // --------------------------
715+ type netrcMachine struct {
716+ Machine string
717+ Login string
718+ Password string
719+ }
720+
721+ func loadNetrcAuth (remoteURL string ) * gitHttp.BasicAuth {
722+ home := os .Getenv ("HOME" )
723+ if home == "" {
724+ return nil
725+ }
726+
727+ netrcPath := filepath .Join (home , ".netrc" )
728+ machines , err := parseNetrcFile (netrcPath )
729+ if err != nil {
730+ return nil
731+ }
732+
733+ m := lookupNetrcMachine (remoteURL , machines )
734+ if m == nil {
735+ return nil
736+ }
737+
738+ return & gitHttp.BasicAuth {
739+ Username : m .Login ,
740+ Password : m .Password ,
741+ }
742+ }
743+
744+ func lookupNetrcMachine (remoteURL string , machines []netrcMachine ) * netrcMachine {
745+ u , err := url .Parse (remoteURL )
746+ if err != nil {
747+ return nil
748+ }
749+
750+ host := u .Hostname ()
751+
752+ for _ , m := range machines {
753+ if m .Machine == host {
754+ return & m
755+ }
756+ }
757+ return nil
758+ }
759+
760+ func parseNetrcFile (path string ) ([]netrcMachine , error ) {
761+ data , err := os .ReadFile (path )
762+ if err != nil {
763+ return nil , err
764+ }
765+
766+ words := strings .Fields (string (data ))
767+ machines := []netrcMachine {}
768+
769+ for i := 0 ; i < len (words ); i ++ {
770+ if words [i ] == "machine" && i + 5 < len (words ) {
771+ machines = append (machines , netrcMachine {
772+ Machine : words [i + 1 ],
773+ Login : words [i + 3 ],
774+ Password : words [i + 5 ],
775+ })
776+ }
777+ }
778+
779+ return machines , nil
780+ }
0 commit comments