@@ -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,64 +479,16 @@ 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 {
482+ authMethod , _ := GitAutoAuth (repoURL )
483+ if authMethod != nil {
484+ _ , errClone := git .PlainClone (cloneDir , false , & git.CloneOptions {
572485 URL : repoURL ,
573486 ReferenceName : plumbing .NewBranchReferenceName (branch ),
574487 SingleBranch : true ,
575488 Auth : authMethod ,
576489 })
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" )
490+ if errClone != nil {
491+ return fmt .Errorf ("failed to clone base repo and branch: %v" , errClone )
602492 }
603493 }
604494
@@ -745,3 +635,142 @@ func getFileDiff(repoDir string, fileName string, git gitOps) (string, error) {
745635
746636 return string (diffOutput ), nil
747637}
638+
639+ // GitAutoAuth returns transport.AuthMethod for a remote URL (SSH or HTTPS)
640+ func GitAutoAuth (remoteURL string ) (transport.AuthMethod , error ) {
641+ if isSSHURL (remoteURL ) {
642+ return sshAuth ()
643+ }
644+ return httpsAuth (remoteURL )
645+ }
646+
647+ // SSH auth
648+ func sshAuth () (transport.AuthMethod , error ) {
649+ // 1. Try SSH agent
650+ auth , err := gitssh .NewSSHAgentAuth ("git" )
651+ if err == nil {
652+ return auth , nil
653+ }
654+
655+ // 2. Try SSH_PRIVATE_KEY env variable
656+ keyData := os .Getenv ("SSH_PRIVATE_KEY" )
657+ if keyData != "" {
658+ auth , err := gitssh .NewPublicKeys ("git" , []byte (keyData ), "" )
659+ if err == nil {
660+ return auth , nil
661+ }
662+ }
663+
664+ // 3. Try default key file ~/.ssh/id_rsa
665+ home := os .Getenv ("HOME" )
666+ if home != "" {
667+ defaultKey := filepath .Join (home , ".ssh" , "id_rsa" )
668+ if _ , err := os .Stat (defaultKey ); err == nil {
669+ auth , err := gitssh .NewPublicKeysFromFile ("git" , defaultKey , "" )
670+ if err == nil {
671+ return auth , nil
672+ }
673+ }
674+ }
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+ return auth , nil
691+ }
692+
693+ // Try common environment tokens
694+ if tok := os .Getenv ("GITHUB_TOKEN" ); tok != "" {
695+ return & gitHttp.BasicAuth {Username : "token" , Password : tok }, nil
696+ }
697+ if tok := os .Getenv ("GITLAB_TOKEN" ); tok != "" {
698+ return & gitHttp.BasicAuth {Username : "token" , Password : tok }, nil
699+ }
700+ if tok := os .Getenv ("BITBUCKET_TOKEN" ); tok != "" {
701+ return & gitHttp.BasicAuth {Username : "token" , Password : tok }, nil
702+ }
703+
704+ // Fallback: anonymous HTTPS
705+ return nil , nil
706+ }
707+
708+ // --------------------------
709+ // .netrc support
710+ // --------------------------
711+ type netrcMachine struct {
712+ Machine string
713+ Login string
714+ Password string
715+ }
716+
717+ func loadNetrcAuth (remoteURL string ) * gitHttp.BasicAuth {
718+ home := os .Getenv ("HOME" )
719+ if home == "" {
720+ return nil
721+ }
722+
723+ netrcPath := filepath .Join (home , ".netrc" )
724+ machines , err := parseNetrcFile (netrcPath )
725+ if err != nil {
726+ return nil
727+ }
728+
729+ m := lookupNetrcMachine (remoteURL , machines )
730+ if m == nil {
731+ return nil
732+ }
733+
734+ return & gitHttp.BasicAuth {
735+ Username : m .Login ,
736+ Password : m .Password ,
737+ }
738+ }
739+
740+ func lookupNetrcMachine (remoteURL string , machines []netrcMachine ) * netrcMachine {
741+ u , err := url .Parse (remoteURL )
742+ if err != nil {
743+ return nil
744+ }
745+
746+ host := u .Hostname ()
747+
748+ for _ , m := range machines {
749+ if m .Machine == host {
750+ return & m
751+ }
752+ }
753+ return nil
754+ }
755+
756+ func parseNetrcFile (path string ) ([]netrcMachine , error ) {
757+ data , err := os .ReadFile (path )
758+ if err != nil {
759+ return nil , err
760+ }
761+
762+ words := strings .Fields (string (data ))
763+ machines := []netrcMachine {}
764+
765+ for i := 0 ; i < len (words ); i ++ {
766+ if words [i ] == "machine" && i + 5 < len (words ) {
767+ machines = append (machines , netrcMachine {
768+ Machine : words [i + 1 ],
769+ Login : words [i + 3 ],
770+ Password : words [i + 5 ],
771+ })
772+ }
773+ }
774+
775+ return machines , nil
776+ }
0 commit comments