@@ -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,15 +37,18 @@ 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
4954const GitCustomCAFile = "/tmp/vp-git-cas.pem"
@@ -176,7 +181,7 @@ func checkoutRevision(fullClient kubernetes.Interface, gitOps GitOperations, url
176181 if repo == nil { // we mocked the above OpenRepository
177182 return nil
178183 }
179- foptions , err := getFetchOptions (url , secret )
184+ foptions , err := getFetchOptions (fullClient , url , secret )
180185 if err != nil {
181186 return err
182187 }
@@ -235,7 +240,7 @@ func cloneRepo(fullClient kubernetes.Interface, gitOps GitOperations, url, direc
235240 }
236241 fmt .Printf ("git clone %s into %s\n " , url , directory )
237242
238- options , err := getCloneOptions (url , secret )
243+ options , err := getCloneOptions (fullClient , url , secret )
239244 if err != nil {
240245 return err
241246 }
@@ -259,7 +264,7 @@ func cloneRepo(fullClient kubernetes.Interface, gitOps GitOperations, url, direc
259264 return nil
260265}
261266
262- func getFetchOptions (url string , secret map [string ][]byte ) (* git.FetchOptions , error ) {
267+ func getFetchOptions (fullClient kubernetes. Interface , url string , secret map [string ][]byte ) (* git.FetchOptions , error ) {
263268 var foptions = & git.FetchOptions {
264269 RemoteName : "origin" ,
265270 Force : true ,
@@ -275,12 +280,19 @@ func getFetchOptions(url string, secret map[string][]byte) (*git.FetchOptions, e
275280 return nil , err
276281 }
277282 foptions .Auth = publicKey
283+ case GitAuthGitHubApp :
284+ gitHubAppAuth , err := getGitHubAppAuth (fullClient , secret )
285+ if err != nil {
286+ return nil , err
287+ }
288+
289+ foptions .Auth = gitHubAppAuth
278290 }
279291
280292 return foptions , nil
281293}
282294
283- func getCloneOptions (url string , secret map [string ][]byte ) (* git.CloneOptions , error ) {
295+ func getCloneOptions (fullClient kubernetes. Interface , url string , secret map [string ][]byte ) (* git.CloneOptions , error ) {
284296 // Clone the given repository to the given directory
285297 var options = & git.CloneOptions {
286298 URL : url ,
@@ -300,6 +312,13 @@ func getCloneOptions(url string, secret map[string][]byte) (*git.CloneOptions, e
300312 return nil , err
301313 }
302314 options .Auth = publicKey
315+ case GitAuthGitHubApp :
316+ gitHubAppAuth , err := getGitHubAppAuth (fullClient , secret )
317+ if err != nil {
318+ return nil , err
319+ }
320+
321+ options .Auth = gitHubAppAuth
303322 }
304323
305324 return options , nil
@@ -333,6 +352,66 @@ func getSshPublicKey(url string, secret map[string][]byte) (*ssh.PublicKeys, err
333352 return publicKey , nil
334353}
335354
355+ func getGitHubAppAuthTransport (fullClient kubernetes.Interface , secret map [string ][]byte ) (* ghinstallation.Transport , error ) {
356+
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+
391+ func getGitHubAppAuth (fullClient kubernetes.Interface , secret map [string ][]byte ) (* http.BasicAuth , error ) {
392+
393+ ctx , cancel := context .WithTimeout (context .Background (), 15 * time .Second )
394+ defer cancel ()
395+
396+ // Obtain GitHub Transport
397+ itr , err := getGitHubAppAuthTransport (fullClient , secret )
398+ if err != nil {
399+ return nil , err
400+ }
401+ accessToken , err := itr .Token (ctx )
402+ if err != nil {
403+ return nil , fmt .Errorf ("could not get GitHub App installation token: %w" , err )
404+ }
405+
406+ auth := & http.BasicAuth {
407+ Username : "x-access-token" ,
408+ Password : accessToken ,
409+ }
410+
411+ return auth , nil
412+
413+ }
414+
336415// This returns the user prefix in git urls like:
337416// git@github.com:/foo/bar or "" when not found
338417func getUserFromURL (url string ) string {
@@ -362,14 +441,27 @@ func repoHash(directory string) (string, error) {
362441// if a secret has
363442// returns "" if a secret could not be parse, "ssh" if it is an ssh auth, and "password" if a username + pass auth
364443func detectGitAuthType (secret map [string ][]byte ) GitAuthenticationBackend {
444+ // SSH
365445 if _ , ok := secret ["sshPrivateKey" ]; ok {
366446 return GitAuthSsh
367447 }
448+
449+ // Username + Password
368450 _ , hasUser := secret ["username" ]
369451 _ , hasPassword := secret ["password" ]
370452 if hasUser && hasPassword {
371453 return GitAuthPassword
372454 }
455+
456+ // GitHub App
457+ _ , hasGithubAppID := secret ["githubAppID" ]
458+ _ , hasGithubAppInstallationID := secret ["githubAppInstallationID" ]
459+ _ , hasGithubAppPrivateKey := secret ["githubAppPrivateKey" ]
460+ if hasGithubAppID && hasGithubAppInstallationID && hasGithubAppPrivateKey {
461+ return GitAuthGitHubApp
462+ }
463+
464+ // None
373465 return GitAuthNone
374466}
375467
0 commit comments