@@ -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,19 @@ 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 , errAuth := GitAutoAuth (repoURL )
483+ if errAuth != nil {
484+ return fmt .Errorf ("failed to determine authentication method: %v" , errAuth )
485+ }
486+ if authMethod != nil {
487+ _ , errClone := git .PlainClone (cloneDir , false , & git.CloneOptions {
572488 URL : repoURL ,
573489 ReferenceName : plumbing .NewBranchReferenceName (branch ),
574490 SingleBranch : true ,
575491 Auth : authMethod ,
576492 })
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" )
493+ if errClone != nil {
494+ return fmt .Errorf ("failed to clone base repo and branch: %v" , errAuth )
602495 }
603496 }
604497
@@ -745,3 +638,142 @@ func getFileDiff(repoDir string, fileName string, git gitOps) (string, error) {
745638
746639 return string (diffOutput ), nil
747640}
641+
642+ // GitAutoAuth returns transport.AuthMethod for a remote URL (SSH or HTTPS)
643+ func GitAutoAuth (remoteURL string ) (transport.AuthMethod , error ) {
644+ if isSSHURL (remoteURL ) {
645+ return sshAuth ()
646+ }
647+ return httpsAuth (remoteURL )
648+ }
649+
650+ // SSH auth
651+ func sshAuth () (transport.AuthMethod , error ) {
652+ // 1. Try SSH agent
653+ auth , err := gitssh .NewSSHAgentAuth ("git" )
654+ if err == nil {
655+ return auth , nil
656+ }
657+
658+ // 2. Try SSH_PRIVATE_KEY env variable
659+ keyData := os .Getenv ("SSH_PRIVATE_KEY" )
660+ if keyData != "" {
661+ auth , err := gitssh .NewPublicKeys ("git" , []byte (keyData ), "" )
662+ if err == nil {
663+ return auth , nil
664+ }
665+ }
666+
667+ // 3. Try default key file ~/.ssh/id_rsa
668+ home := os .Getenv ("HOME" )
669+ if home != "" {
670+ defaultKey := filepath .Join (home , ".ssh" , "id_rsa" )
671+ if _ , err := os .Stat (defaultKey ); err == nil {
672+ auth , err := gitssh .NewPublicKeysFromFile ("git" , defaultKey , "" )
673+ if err == nil {
674+ return auth , nil
675+ }
676+ }
677+ }
678+ return nil , errors .New (
679+ "SSH authentication failed: no keys found. " +
680+ "Please start ssh-agent with loaded keys, set SSH_PRIVATE_KEY, or ensure ~/.ssh/id_rsa exists." ,
681+ )
682+ }
683+
684+ func isSSHURL (raw string ) bool {
685+ return strings .HasPrefix (raw , "git@" ) ||
686+ strings .HasPrefix (raw , "ssh://" )
687+ }
688+
689+ // HTTPS auth
690+ func httpsAuth (remoteURL string ) (transport.AuthMethod , error ) {
691+ // Try .netrc first
692+ if auth := loadNetrcAuth (remoteURL ); auth != nil {
693+ return auth , nil
694+ }
695+
696+ // Try common environment tokens
697+ if tok := os .Getenv ("GITHUB_TOKEN" ); tok != "" {
698+ return & gitHttp.BasicAuth {Username : "token" , Password : tok }, nil
699+ }
700+ if tok := os .Getenv ("GITLAB_TOKEN" ); tok != "" {
701+ return & gitHttp.BasicAuth {Username : "token" , Password : tok }, nil
702+ }
703+ if tok := os .Getenv ("BITBUCKET_TOKEN" ); tok != "" {
704+ return & gitHttp.BasicAuth {Username : "token" , Password : tok }, nil
705+ }
706+
707+ // Fallback: anonymous HTTPS
708+ return nil , nil
709+ }
710+
711+ // --------------------------
712+ // .netrc support
713+ // --------------------------
714+ type netrcMachine struct {
715+ Machine string
716+ Login string
717+ Password string
718+ }
719+
720+ func loadNetrcAuth (remoteURL string ) * gitHttp.BasicAuth {
721+ home := os .Getenv ("HOME" )
722+ if home == "" {
723+ return nil
724+ }
725+
726+ netrcPath := filepath .Join (home , ".netrc" )
727+ machines , err := parseNetrcFile (netrcPath )
728+ if err != nil {
729+ return nil
730+ }
731+
732+ m := lookupNetrcMachine (remoteURL , machines )
733+ if m == nil {
734+ return nil
735+ }
736+
737+ return & gitHttp.BasicAuth {
738+ Username : m .Login ,
739+ Password : m .Password ,
740+ }
741+ }
742+
743+ func lookupNetrcMachine (remoteURL string , machines []netrcMachine ) * netrcMachine {
744+ u , err := url .Parse (remoteURL )
745+ if err != nil {
746+ return nil
747+ }
748+
749+ host := u .Hostname ()
750+
751+ for _ , m := range machines {
752+ if m .Machine == host {
753+ return & m
754+ }
755+ }
756+ return nil
757+ }
758+
759+ func parseNetrcFile (path string ) ([]netrcMachine , error ) {
760+ data , err := os .ReadFile (path )
761+ if err != nil {
762+ return nil , err
763+ }
764+
765+ words := strings .Fields (string (data ))
766+ machines := []netrcMachine {}
767+
768+ for i := 0 ; i < len (words ); i ++ {
769+ if words [i ] == "machine" && i + 5 < len (words ) {
770+ machines = append (machines , netrcMachine {
771+ Machine : words [i + 1 ],
772+ Login : words [i + 3 ],
773+ Password : words [i + 5 ],
774+ })
775+ }
776+ }
777+
778+ return machines , nil
779+ }
0 commit comments