mirror of https://github.com/rancher/gitjob.git
294 lines
7.0 KiB
Go
294 lines
7.0 KiB
Go
package git
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
gogit "github.com/go-git/go-git/v5"
|
|
"github.com/go-git/go-git/v5/config"
|
|
"github.com/go-git/go-git/v5/plumbing/transport"
|
|
httpgit "github.com/go-git/go-git/v5/plumbing/transport/http"
|
|
gossh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
|
|
"github.com/go-git/go-git/v5/storage/memory"
|
|
giturls "github.com/rancher/gitjob/pkg/git-urls"
|
|
"github.com/sirupsen/logrus"
|
|
"golang.org/x/crypto/ssh"
|
|
corev1 "k8s.io/api/core/v1"
|
|
)
|
|
|
|
type options struct {
|
|
Credential *corev1.Secret
|
|
CABundle []byte
|
|
InsecureTLSVerify bool
|
|
Headers map[string]string
|
|
}
|
|
|
|
func newGit(directory, url string, opts *options) (*git, error) {
|
|
if err := validateURL(url); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if opts == nil {
|
|
opts = &options{}
|
|
}
|
|
|
|
g := &git{
|
|
URL: url,
|
|
Directory: directory,
|
|
caBundle: opts.CABundle,
|
|
insecureTLSVerify: opts.InsecureTLSVerify,
|
|
secret: opts.Credential,
|
|
headers: opts.Headers,
|
|
}
|
|
return g, g.setCredential(opts.Credential)
|
|
}
|
|
|
|
type git struct {
|
|
URL string
|
|
Directory string
|
|
caBundle []byte
|
|
insecureTLSVerify bool
|
|
secret *corev1.Secret
|
|
headers map[string]string
|
|
auth transport.AuthMethod
|
|
}
|
|
|
|
// LsRemote runs ls-remote on git repo and returns the HEAD commit SHA
|
|
func (g *git) lsRemote(branch string, commit string) (string, error) {
|
|
if err := validateBranch(branch); err != nil {
|
|
return "", err
|
|
}
|
|
if commit != "" {
|
|
if err := validateCommit(commit); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
if changed, err := g.remoteSHAChanged(branch, commit); err != nil || !changed {
|
|
return commit, err
|
|
}
|
|
|
|
refBranch := formatRefForBranch(branch)
|
|
rem := gogit.NewRemote(memory.NewStorage(), &config.RemoteConfig{
|
|
URLs: []string{g.URL},
|
|
})
|
|
|
|
refs, err := rem.List(&gogit.ListOptions{
|
|
Auth: g.auth,
|
|
CABundle: g.caBundle,
|
|
InsecureSkipTLS: g.insecureTLSVerify,
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
for _, ref := range refs {
|
|
if ref.Name().IsBranch() && ref.Name().String() == refBranch {
|
|
return ref.Hash().String(), nil
|
|
}
|
|
}
|
|
|
|
return "", errors.New("commit not found")
|
|
}
|
|
|
|
func (g *git) httpClientWithCreds() (*http.Client, error) {
|
|
var (
|
|
username string
|
|
password string
|
|
tlsConfig tls.Config
|
|
)
|
|
|
|
if g.secret != nil {
|
|
switch g.secret.Type {
|
|
case corev1.SecretTypeBasicAuth:
|
|
username = string(g.secret.Data[corev1.BasicAuthUsernameKey])
|
|
password = string(g.secret.Data[corev1.BasicAuthPasswordKey])
|
|
case corev1.SecretTypeTLS:
|
|
cert, err := tls.X509KeyPair(g.secret.Data[corev1.TLSCertKey], g.secret.Data[corev1.TLSPrivateKeyKey])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tlsConfig.Certificates = append(tlsConfig.Certificates, cert)
|
|
}
|
|
}
|
|
|
|
if len(g.caBundle) > 0 {
|
|
cert, err := x509.ParseCertificate(g.caBundle)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pool, err := x509.SystemCertPool()
|
|
if err != nil {
|
|
pool = x509.NewCertPool()
|
|
}
|
|
pool.AddCert(cert)
|
|
tlsConfig.RootCAs = pool
|
|
}
|
|
|
|
transport := http.DefaultTransport.(*http.Transport).Clone()
|
|
transport.TLSClientConfig = &tlsConfig
|
|
transport.TLSClientConfig.InsecureSkipVerify = g.insecureTLSVerify
|
|
|
|
client := &http.Client{
|
|
Transport: transport,
|
|
Timeout: 30 * time.Second,
|
|
}
|
|
if username != "" || password != "" {
|
|
client.Transport = &basicRoundTripper{
|
|
username: username,
|
|
password: password,
|
|
next: client.Transport,
|
|
}
|
|
}
|
|
|
|
return client, nil
|
|
}
|
|
|
|
func (g *git) remoteSHAChanged(branch, sha string) (bool, error) {
|
|
formattedURL := formatGitURL(g.URL, branch)
|
|
if formattedURL == "" {
|
|
return true, nil
|
|
}
|
|
|
|
client, err := g.httpClientWithCreds()
|
|
if err != nil {
|
|
logrus.Warnf("Problem creating http client to check git remote sha of repo [%v]: %v", g.URL, err)
|
|
return true, nil
|
|
}
|
|
defer client.CloseIdleConnections()
|
|
|
|
req, err := http.NewRequest("GET", formattedURL, nil)
|
|
if err != nil {
|
|
logrus.Warnf("Problem creating request to check git remote sha of repo [%v]: %v", g.URL, err)
|
|
return true, nil
|
|
}
|
|
|
|
req.Header.Set("Accept", "application/vnd.github.v3.sha")
|
|
req.Header.Set("If-None-Match", fmt.Sprintf("\"%s\"", sha))
|
|
for k, v := range g.headers {
|
|
req.Header.Set(k, v)
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
// Return timeout errors so caller can decide whether or not to proceed with updating the repo
|
|
uErr := &url.Error{}
|
|
if ok := errors.As(err, &uErr); ok && uErr.Timeout() {
|
|
return false, fmt.Errorf("%w: Repo [%v] is not accessible", uErr, g.URL)
|
|
}
|
|
return true, nil
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusNotModified {
|
|
return false, nil
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (g *git) setCredential(cred *corev1.Secret) error {
|
|
if cred == nil {
|
|
return nil
|
|
}
|
|
|
|
if cred.Type == corev1.SecretTypeBasicAuth {
|
|
username, password := cred.Data[corev1.BasicAuthUsernameKey], cred.Data[corev1.BasicAuthPasswordKey]
|
|
if len(password) == 0 && len(username) == 0 {
|
|
return nil
|
|
}
|
|
u, err := url.Parse(g.URL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
g.URL = u.String()
|
|
g.auth = &httpgit.BasicAuth{
|
|
Username: string(username),
|
|
Password: string(password),
|
|
}
|
|
} else if cred.Type == corev1.SecretTypeSSHAuth {
|
|
gitURL, err := giturls.Parse(g.URL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
auth, err := gossh.NewPublicKeys(gitURL.User.Username(), cred.Data[corev1.SSHAuthPrivateKey], "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if cred.Data["known_hosts"] != nil {
|
|
auth.HostKeyCallback, err = createKnownHosts(cred.Data["known_hosts"])
|
|
} else {
|
|
//nolint G106: Use of ssh InsecureIgnoreHostKey should be audited
|
|
auth.HostKeyCallback = ssh.InsecureIgnoreHostKey()
|
|
}
|
|
g.auth = auth
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func createKnownHosts(knownHosts []byte) (ssh.HostKeyCallback, error) {
|
|
f, err := os.CreateTemp("", "known_hosts")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer os.RemoveAll(f.Name())
|
|
defer f.Close()
|
|
|
|
if _, err := f.Write(knownHosts); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := f.Close(); err != nil {
|
|
return nil, fmt.Errorf("closing knownHosts file %s: %w", f.Name(), err)
|
|
}
|
|
|
|
return gossh.NewKnownHostsCallback(f.Name())
|
|
}
|
|
|
|
func formatGitURL(endpoint, branch string) string {
|
|
u, err := url.Parse(endpoint)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
pathParts := strings.Split(u.Path, "/")
|
|
switch u.Hostname() {
|
|
case "github.com":
|
|
if len(pathParts) >= 3 {
|
|
org := pathParts[1]
|
|
repo := strings.TrimSuffix(pathParts[2], ".git")
|
|
return fmt.Sprintf("https://api.github.com/repos/%s/%s/commits/%s", org, repo, branch)
|
|
}
|
|
case "git.rancher.io":
|
|
repo := strings.TrimSuffix(pathParts[1], ".git")
|
|
u.Path = fmt.Sprintf("/repos/%s/commits/%s", repo, branch)
|
|
return u.String()
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func formatRefForBranch(branch string) string {
|
|
return fmt.Sprintf("refs/heads/%s", branch)
|
|
}
|
|
|
|
type basicRoundTripper struct {
|
|
username string
|
|
password string
|
|
next http.RoundTripper
|
|
}
|
|
|
|
func (b *basicRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
request.SetBasicAuth(b.username, b.password)
|
|
return b.next.RoundTrip(request)
|
|
}
|