@@ -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,55 @@ func getSshPublicKey(url string, secret map[string][]byte) (*ssh.PublicKeys, err
333352 return publicKey , nil
334353}
335354
355+ func getGitHubAppAuth (fullClient kubernetes.Interface , secret map [string ][]byte ) (* http.BasicAuth , error ) {
356+
357+ ctx , cancel := context .WithTimeout (context .Background (), 15 * time .Second )
358+ defer cancel ()
359+
360+ baseURL := "https://api.github.com"
361+
362+ if githubAppEnterpriseBaseUrl := getField (secret , "username" ); githubAppEnterpriseBaseUrl != nil {
363+ baseURL = strings .TrimSuffix (string (githubAppEnterpriseBaseUrl ), "/" )
364+ }
365+
366+ transport := getHTTPSTransport (fullClient )
367+
368+ githubAppID , err := IntOrZero (secret , "githubAppID" )
369+ if err != nil {
370+ return nil , err
371+ }
372+
373+ githubAppInstallationID , err := IntOrZero (secret , "githubAppInstallationID" )
374+ if err != nil {
375+ return nil , err
376+ }
377+
378+ itr , err := ghinstallation .New (transport ,
379+ githubAppID ,
380+ githubAppInstallationID ,
381+ getField (secret , "githubAppPrivateKey" ),
382+ )
383+
384+ if err != nil {
385+ return nil , fmt .Errorf ("failed to initialize GitHub installation transport: %w" , err )
386+ }
387+
388+ itr .BaseURL = baseURL
389+
390+ accessToken , err := itr .Token (ctx )
391+ if err != nil {
392+ return nil , fmt .Errorf ("could not get GitHub App installation token: %w" , err )
393+ }
394+
395+ auth := & http.BasicAuth {
396+ Username : "x-access-token" ,
397+ Password : accessToken ,
398+ }
399+
400+ return auth , nil
401+
402+ }
403+
336404// This returns the user prefix in git urls like:
337405// git@github.com:/foo/bar or "" when not found
338406func getUserFromURL (url string ) string {
@@ -362,14 +430,27 @@ func repoHash(directory string) (string, error) {
362430// if a secret has
363431// returns "" if a secret could not be parse, "ssh" if it is an ssh auth, and "password" if a username + pass auth
364432func detectGitAuthType (secret map [string ][]byte ) GitAuthenticationBackend {
433+ // SSH
365434 if _ , ok := secret ["sshPrivateKey" ]; ok {
366435 return GitAuthSsh
367436 }
437+
438+ // Username + Password
368439 _ , hasUser := secret ["username" ]
369440 _ , hasPassword := secret ["password" ]
370441 if hasUser && hasPassword {
371442 return GitAuthPassword
372443 }
444+
445+ // GitHub App
446+ _ , hasGithubAppID := secret ["githubAppID" ]
447+ _ , hasGithubAppInstallationID := secret ["githubAppInstallationID" ]
448+ _ , hasGithubAppPrivateKey := secret ["githubAppPrivateKey" ]
449+ if hasGithubAppID && hasGithubAppInstallationID && hasGithubAppPrivateKey {
450+ return GitAuthGitHubApp
451+ }
452+
453+ // None
373454 return GitAuthNone
374455}
375456
0 commit comments