@@ -17,11 +17,13 @@ limitations under the License.
1717package controllers
1818
1919import (
20+ "context"
2021 "fmt"
2122 nethttp "net/http"
2223 "os"
2324 "regexp"
2425 "strings"
26+ "time"
2527
2628 "path/filepath"
2729
@@ -35,17 +37,21 @@ import (
3537 "github.com/go-git/go-git/v5/plumbing/transport/http"
3638 "github.com/go-git/go-git/v5/plumbing/transport/ssh"
3739
40+ "github.com/bradleyfalzon/ghinstallation/v2"
41+
3842 argogit "github.com/argoproj/argo-cd/v3/util/git"
3943)
4044
4145type GitAuthenticationBackend uint
4246
4347const (
44- GitAuthNone GitAuthenticationBackend = 0
45- GitAuthPassword GitAuthenticationBackend = 1
46- GitAuthSsh GitAuthenticationBackend = 2
48+ GitAuthNone GitAuthenticationBackend = 0
49+ GitAuthPassword GitAuthenticationBackend = 1
50+ GitAuthSsh GitAuthenticationBackend = 2
51+ GitAuthGitHubApp GitAuthenticationBackend = 3
4752)
4853
54+ const ContextTimeoutSeconds = 15 * time .Second
4955const GitCustomCAFile = "/tmp/vp-git-cas.pem"
5056const GitHEAD = "HEAD"
5157const VPTmpFolder = "vp"
@@ -176,7 +182,7 @@ func checkoutRevision(fullClient kubernetes.Interface, gitOps GitOperations, url
176182 if repo == nil { // we mocked the above OpenRepository
177183 return nil
178184 }
179- foptions , err := getFetchOptions (url , secret )
185+ foptions , err := getFetchOptions (fullClient , url , secret )
180186 if err != nil {
181187 return err
182188 }
@@ -235,7 +241,7 @@ func cloneRepo(fullClient kubernetes.Interface, gitOps GitOperations, url, direc
235241 }
236242 fmt .Printf ("git clone %s into %s\n " , url , directory )
237243
238- options , err := getCloneOptions (url , secret )
244+ options , err := getCloneOptions (fullClient , url , secret )
239245 if err != nil {
240246 return err
241247 }
@@ -259,7 +265,7 @@ func cloneRepo(fullClient kubernetes.Interface, gitOps GitOperations, url, direc
259265 return nil
260266}
261267
262- func getFetchOptions (url string , secret map [string ][]byte ) (* git.FetchOptions , error ) {
268+ func getFetchOptions (fullClient kubernetes. Interface , url string , secret map [string ][]byte ) (* git.FetchOptions , error ) {
263269 var foptions = & git.FetchOptions {
264270 RemoteName : "origin" ,
265271 Force : true ,
@@ -275,12 +281,19 @@ func getFetchOptions(url string, secret map[string][]byte) (*git.FetchOptions, e
275281 return nil , err
276282 }
277283 foptions .Auth = publicKey
284+ case GitAuthGitHubApp :
285+ gitHubAppAuth , err := getGitHubAppAuth (fullClient , secret )
286+ if err != nil {
287+ return nil , err
288+ }
289+
290+ foptions .Auth = gitHubAppAuth
278291 }
279292
280293 return foptions , nil
281294}
282295
283- func getCloneOptions (url string , secret map [string ][]byte ) (* git.CloneOptions , error ) {
296+ func getCloneOptions (fullClient kubernetes. Interface , url string , secret map [string ][]byte ) (* git.CloneOptions , error ) {
284297 // Clone the given repository to the given directory
285298 var options = & git.CloneOptions {
286299 URL : url ,
@@ -300,6 +313,13 @@ func getCloneOptions(url string, secret map[string][]byte) (*git.CloneOptions, e
300313 return nil , err
301314 }
302315 options .Auth = publicKey
316+ case GitAuthGitHubApp :
317+ gitHubAppAuth , err := getGitHubAppAuth (fullClient , secret )
318+ if err != nil {
319+ return nil , err
320+ }
321+
322+ options .Auth = gitHubAppAuth
303323 }
304324
305325 return options , nil
@@ -333,6 +353,62 @@ func getSshPublicKey(url string, secret map[string][]byte) (*ssh.PublicKeys, err
333353 return publicKey , nil
334354}
335355
356+ func getGitHubAppAuthTransport (fullClient kubernetes.Interface , secret map [string ][]byte ) (* ghinstallation.Transport , error ) {
357+ baseURL := "https://api.github.com"
358+
359+ if githubAppEnterpriseBaseUrl := getField (secret , "githubAppEnterpriseBaseUrl" ); githubAppEnterpriseBaseUrl != nil {
360+ baseURL = strings .TrimSuffix (string (githubAppEnterpriseBaseUrl ), "/" )
361+ }
362+
363+ transport := getHTTPSTransport (fullClient )
364+
365+ githubAppID , err := IntOrZero (secret , "githubAppID" )
366+ if err != nil {
367+ return nil , err
368+ }
369+
370+ githubAppInstallationID , err := IntOrZero (secret , "githubAppInstallationID" )
371+ if err != nil {
372+ return nil , err
373+ }
374+
375+ itr , err := ghinstallation .New (transport ,
376+ githubAppID ,
377+ githubAppInstallationID ,
378+ getField (secret , "githubAppPrivateKey" ),
379+ )
380+
381+ if err != nil {
382+ return nil , fmt .Errorf ("failed to initialize GitHub installation transport: %w" , err )
383+ }
384+
385+ itr .BaseURL = baseURL
386+
387+ return itr , nil
388+ }
389+
390+ func getGitHubAppAuth (fullClient kubernetes.Interface , secret map [string ][]byte ) (* http.BasicAuth , error ) {
391+ ctx , cancel := context .WithTimeout (context .Background (), ContextTimeoutSeconds )
392+ defer cancel ()
393+
394+ // Obtain GitHub Transport
395+ itr , err := getGitHubAppAuthTransport (fullClient , secret )
396+ if err != nil {
397+ return nil , err
398+ }
399+ accessToken , err := itr .Token (ctx )
400+ if err != nil {
401+ return nil , fmt .Errorf ("could not get GitHub App installation token: %w" , err )
402+ }
403+
404+ auth := & http.BasicAuth {
405+ Username : "x-access-token" ,
406+ Password : accessToken ,
407+ }
408+
409+ return auth , nil
410+ }
411+
336412// This returns the user prefix in git urls like:
337413// git@github.com:/foo/bar or "" when not found
338414func getUserFromURL (url string ) string {
@@ -362,14 +438,27 @@ func repoHash(directory string) (string, error) {
362438// if a secret has
363439// returns "" if a secret could not be parse, "ssh" if it is an ssh auth, and "password" if a username + pass auth
364440func detectGitAuthType (secret map [string ][]byte ) GitAuthenticationBackend {
441+ // SSH
365442 if _ , ok := secret ["sshPrivateKey" ]; ok {
366443 return GitAuthSsh
367444 }
445+
446+ // Username + Password
368447 _ , hasUser := secret ["username" ]
369448 _ , hasPassword := secret ["password" ]
370449 if hasUser && hasPassword {
371450 return GitAuthPassword
372451 }
452+
453+ // GitHub App
454+ _ , hasGithubAppID := secret ["githubAppID" ]
455+ _ , hasGithubAppInstallationID := secret ["githubAppInstallationID" ]
456+ _ , hasGithubAppPrivateKey := secret ["githubAppPrivateKey" ]
457+ if hasGithubAppID && hasGithubAppInstallationID && hasGithubAppPrivateKey {
458+ return GitAuthGitHubApp
459+ }
460+
461+ // None
373462 return GitAuthNone
374463}
375464
0 commit comments