Skip to content

Commit e3ab966

Browse files
jannfissribiere-jellysmack
authored andcommitted
chore: Update and refactor Git client (argoproj-labs#283)
Signed-off-by: jannfis <[email protected]>
1 parent 967dfb9 commit e3ab966

File tree

16 files changed

+692
-248
lines changed

16 files changed

+692
-248
lines changed

ext/git/client.go

Lines changed: 111 additions & 159 deletions
Large diffs are not rendered by default.

ext/git/creds.go

Lines changed: 196 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,43 @@
11
package git
22

33
import (
4+
"context"
5+
"crypto/sha256"
46
"fmt"
57
"io"
68
"io/ioutil"
79
"os"
10+
"strconv"
811
"strings"
12+
"time"
13+
14+
gocache "github.com/patrickmn/go-cache"
915

1016
argoio "github.com/argoproj/gitops-engine/pkg/utils/io"
17+
"github.com/bradleyfalzon/ghinstallation"
1118
log "github.com/sirupsen/logrus"
1219

20+
"github.com/argoproj/argo-cd/v2/common"
21+
1322
certutil "github.com/argoproj/argo-cd/v2/util/cert"
1423
)
1524

25+
// In memory cache for storing github APP api token credentials
26+
var (
27+
githubAppTokenCache *gocache.Cache
28+
)
29+
30+
func init() {
31+
githubAppCredsExp := common.GithubAppCredsExpirationDuration
32+
if exp := os.Getenv(common.EnvGithubAppCredsExpirationDuration); exp != "" {
33+
if qps, err := strconv.Atoi(exp); err != nil {
34+
githubAppCredsExp = time.Duration(qps) * time.Minute
35+
}
36+
}
37+
38+
githubAppTokenCache = gocache.New(githubAppCredsExp, 1*time.Minute)
39+
}
40+
1641
type Creds interface {
1742
Environ() (io.Closer, []string, error)
1843
}
@@ -25,13 +50,26 @@ func (c NopCloser) Close() error {
2550
return nil
2651
}
2752

53+
var _ Creds = NopCreds{}
54+
2855
type NopCreds struct {
2956
}
3057

3158
func (c NopCreds) Environ() (io.Closer, []string, error) {
3259
return NopCloser{}, nil, nil
3360
}
3461

62+
var _ io.Closer = NopCloser{}
63+
64+
type GenericHTTPSCreds interface {
65+
HasClientCert() bool
66+
GetClientCertData() string
67+
GetClientCertKey() string
68+
Environ() (io.Closer, []string, error)
69+
}
70+
71+
var _ GenericHTTPSCreds = HTTPSCreds{}
72+
3573
// HTTPS creds implementation
3674
type HTTPSCreds struct {
3775
// Username for authentication
@@ -44,15 +82,18 @@ type HTTPSCreds struct {
4482
clientCertData string
4583
// Client certificate key to use
4684
clientCertKey string
85+
// HTTP/HTTPS proxy used to access repository
86+
proxy string
4787
}
4888

49-
func NewHTTPSCreds(username string, password string, clientCertData string, clientCertKey string, insecure bool) HTTPSCreds {
89+
func NewHTTPSCreds(username string, password string, clientCertData string, clientCertKey string, insecure bool, proxy string) GenericHTTPSCreds {
5090
return HTTPSCreds{
5191
username,
5292
password,
5393
insecure,
5494
clientCertData,
5595
clientCertKey,
96+
proxy,
5697
}
5798
}
5899

@@ -71,7 +112,7 @@ func (c HTTPSCreds) Environ() (io.Closer, []string, error) {
71112
// In case the repo is configured for using a TLS client cert, we need to make
72113
// sure git client will use it. The certificate's key must not be password
73114
// protected.
74-
if c.clientCertData != "" && c.clientCertKey != "" {
115+
if c.HasClientCert() {
75116
var certFile, keyFile *os.File
76117

77118
// We need to actually create two temp files, one for storing cert data and
@@ -116,6 +157,18 @@ func (c HTTPSCreds) Environ() (io.Closer, []string, error) {
116157
return httpCloser, env, nil
117158
}
118159

160+
func (g HTTPSCreds) HasClientCert() bool {
161+
return g.clientCertData != "" && g.clientCertKey != ""
162+
}
163+
164+
func (c HTTPSCreds) GetClientCertData() string {
165+
return c.clientCertData
166+
}
167+
168+
func (c HTTPSCreds) GetClientCertKey() string {
169+
return c.clientCertKey
170+
}
171+
119172
// SSH implementation
120173
type SSHCreds struct {
121174
sshPrivateKey string
@@ -179,3 +232,144 @@ func (c SSHCreds) Environ() (io.Closer, []string, error) {
179232
env = append(env, []string{fmt.Sprintf("GIT_SSH_COMMAND=%s", strings.Join(args, " "))}...)
180233
return sshPrivateKeyFile(file.Name()), env, nil
181234
}
235+
236+
// GitHubAppCreds to authenticate as GitHub application
237+
type GitHubAppCreds struct {
238+
appID int64
239+
appInstallId int64
240+
privateKey string
241+
baseURL string
242+
repoURL string
243+
clientCertData string
244+
clientCertKey string
245+
insecure bool
246+
proxy string
247+
}
248+
249+
// NewGitHubAppCreds provide github app credentials
250+
func NewGitHubAppCreds(appID int64, appInstallId int64, privateKey string, baseURL string, repoURL string, clientCertData string, clientCertKey string, insecure bool) GenericHTTPSCreds {
251+
return GitHubAppCreds{appID: appID, appInstallId: appInstallId, privateKey: privateKey, baseURL: baseURL, repoURL: repoURL, clientCertData: clientCertData, clientCertKey: clientCertKey, insecure: insecure}
252+
}
253+
254+
func (g GitHubAppCreds) Environ() (io.Closer, []string, error) {
255+
token, err := g.getAccessToken()
256+
if err != nil {
257+
return NopCloser{}, nil, err
258+
}
259+
260+
env := []string{fmt.Sprintf("GIT_ASKPASS=%s", "git-ask-pass.sh"), "GIT_USERNAME=x-access-token", fmt.Sprintf("GIT_PASSWORD=%s", token)}
261+
httpCloser := authFilePaths(make([]string, 0))
262+
263+
// GIT_SSL_NO_VERIFY is used to tell git not to validate the server's cert at
264+
// all.
265+
if g.insecure {
266+
env = append(env, "GIT_SSL_NO_VERIFY=true")
267+
}
268+
269+
// In case the repo is configured for using a TLS client cert, we need to make
270+
// sure git client will use it. The certificate's key must not be password
271+
// protected.
272+
if g.HasClientCert() {
273+
var certFile, keyFile *os.File
274+
275+
// We need to actually create two temp files, one for storing cert data and
276+
// another for storing the key. If we fail to create second fail, the first
277+
// must be removed.
278+
certFile, err := ioutil.TempFile(argoio.TempDir, "")
279+
if err == nil {
280+
defer certFile.Close()
281+
keyFile, err = ioutil.TempFile(argoio.TempDir, "")
282+
if err != nil {
283+
removeErr := os.Remove(certFile.Name())
284+
if removeErr != nil {
285+
log.Errorf("Could not remove previously created tempfile %s: %v", certFile.Name(), removeErr)
286+
}
287+
return NopCloser{}, nil, err
288+
}
289+
defer keyFile.Close()
290+
} else {
291+
return NopCloser{}, nil, err
292+
}
293+
294+
// We should have both temp files by now
295+
httpCloser = authFilePaths([]string{certFile.Name(), keyFile.Name()})
296+
297+
_, err = certFile.WriteString(g.clientCertData)
298+
if err != nil {
299+
httpCloser.Close()
300+
return NopCloser{}, nil, err
301+
}
302+
// GIT_SSL_CERT is the full path to a client certificate to be used
303+
env = append(env, fmt.Sprintf("GIT_SSL_CERT=%s", certFile.Name()))
304+
305+
_, err = keyFile.WriteString(g.clientCertKey)
306+
if err != nil {
307+
httpCloser.Close()
308+
return NopCloser{}, nil, err
309+
}
310+
// GIT_SSL_KEY is the full path to a client certificate's key to be used
311+
env = append(env, fmt.Sprintf("GIT_SSL_KEY=%s", keyFile.Name()))
312+
313+
}
314+
return httpCloser, env, nil
315+
}
316+
317+
// getAccessToken fetches GitHub token using the app id, install id, and private key.
318+
// the token is then cached for re-use.
319+
func (g GitHubAppCreds) getAccessToken() (string, error) {
320+
// Timeout
321+
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
322+
defer cancel()
323+
324+
// Compute hash of creds for lookup in cache
325+
h := sha256.New()
326+
_, err := h.Write([]byte(fmt.Sprintf("%s %d %d %s", g.privateKey, g.appID, g.appInstallId, g.baseURL)))
327+
if err != nil {
328+
return "", err
329+
}
330+
key := fmt.Sprintf("%x", h.Sum(nil))
331+
332+
// Check cache for GitHub transport which helps fetch an API token
333+
t, found := githubAppTokenCache.Get(key)
334+
if found {
335+
itr := t.(*ghinstallation.Transport)
336+
// This method caches the token and if it's expired retrieves a new one
337+
return itr.Token(ctx)
338+
}
339+
340+
// GitHub API url
341+
baseUrl := "https://api.github.com"
342+
if g.baseURL != "" {
343+
baseUrl = strings.TrimSuffix(g.baseURL, "/")
344+
}
345+
346+
// Create a new GitHub transport
347+
c := GetRepoHTTPClient(baseUrl, g.insecure, g, g.proxy)
348+
itr, err := ghinstallation.New(c.Transport,
349+
g.appID,
350+
g.appInstallId,
351+
[]byte(g.privateKey),
352+
)
353+
if err != nil {
354+
return "", err
355+
}
356+
357+
itr.BaseURL = baseUrl
358+
359+
// Add transport to cache
360+
githubAppTokenCache.Set(key, itr, time.Minute*60)
361+
362+
return itr.Token(ctx)
363+
}
364+
365+
func (g GitHubAppCreds) HasClientCert() bool {
366+
return g.clientCertData != "" && g.clientCertKey != ""
367+
}
368+
369+
func (g GitHubAppCreds) GetClientCertData() string {
370+
return g.clientCertData
371+
}
372+
373+
func (g GitHubAppCreds) GetClientCertKey() string {
374+
return g.clientCertKey
375+
}

ext/git/git.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ var (
2626
commitSHARegex = regexp.MustCompile("^[0-9A-Fa-f]{40}$")
2727
sshURLRegex = regexp.MustCompile("^(ssh://)?([^/:]*?)@[^@]+$")
2828
httpsURLRegex = regexp.MustCompile("^(https://).*")
29+
httpURLRegex = regexp.MustCompile("^(http://).*")
2930
)
3031

3132
// IsCommitSHA returns whether or not a string is a 40 character SHA-1
@@ -84,9 +85,14 @@ func IsHTTPSURL(url string) bool {
8485
return httpsURLRegex.MatchString(url)
8586
}
8687

88+
// IsHTTPURL returns true if supplied URL is HTTP URL
89+
func IsHTTPURL(url string) bool {
90+
return httpURLRegex.MatchString(url)
91+
}
92+
8793
// TestRepo tests if a repo exists and is accessible with the given credentials
88-
func TestRepo(repo string, creds Creds, insecure bool, enableLfs bool) error {
89-
clnt, err := NewClient(repo, creds, insecure, enableLfs)
94+
func TestRepo(repo string, creds Creds, insecure bool, enableLfs bool, proxy string) error {
95+
clnt, err := NewClient(repo, creds, insecure, enableLfs, proxy)
9096
if err != nil {
9197
return err
9298
}

0 commit comments

Comments
 (0)