Merge pull request #14 from fluxcd/git-auth-helper

Internal helpers for Git auth methods from secrets
This commit is contained in:
Hidde Beydals 2020-04-13 13:44:00 +02:00 committed by GitHub
commit f8e0685af1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 98 additions and 88 deletions

View File

@ -21,16 +21,12 @@ import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
"github.com/blang/semver"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -40,6 +36,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1"
internalgit "github.com/fluxcd/source-controller/internal/git"
)
// GitRepositoryReconciler reconciles a GitRepository object
@ -78,7 +75,7 @@ func (r *GitRepositoryReconciler) Reconcile(req ctrl.Request) (ctrl.Result, erro
r.gc(repo)
// try git clone
syncedRepo, err := r.sync(*repo.DeepCopy())
syncedRepo, err := r.sync(ctx, *repo.DeepCopy())
if err != nil {
log.Info("Git repository sync failed", "error", err.Error())
}
@ -103,7 +100,7 @@ func (r *GitRepositoryReconciler) SetupWithManager(mgr ctrl.Manager) error {
Complete(r)
}
func (r *GitRepositoryReconciler) sync(repository sourcev1.GitRepository) (sourcev1.GitRepository, error) {
func (r *GitRepositoryReconciler) sync(ctx context.Context, repository sourcev1.GitRepository) (sourcev1.GitRepository, error) {
// set defaults: master branch, no tags fetching, max two commits
branch := "master"
revision := ""
@ -129,18 +126,29 @@ func (r *GitRepositoryReconciler) sync(repository sourcev1.GitRepository) (sourc
}
}
// create tmp dir for SSH known_hosts
tmpSSH, err := ioutil.TempDir("", repository.Name)
if err != nil {
err = fmt.Errorf("tmp dir error: %w", err)
return sourcev1.GitRepositoryNotReady(repository, sourcev1.StorageOperationFailedReason, err.Error()), err
}
defer os.RemoveAll(tmpSSH)
var auth transport.AuthMethod
if repository.Spec.SecretRef != nil {
name := types.NamespacedName{
Namespace: repository.GetNamespace(),
Name: repository.Spec.SecretRef.Name,
}
auth, err := r.auth(repository, tmpSSH)
if err != nil {
err = fmt.Errorf("auth error: %w", err)
return sourcev1.GitRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err
var secret corev1.Secret
err := r.Client.Get(ctx, name, &secret)
if err != nil {
err = fmt.Errorf("auth secret error: %w", err)
return sourcev1.GitRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err
}
method, cleanup, err := internalgit.AuthMethodFromSecret(repository.Spec.URL, secret)
if err != nil {
err = fmt.Errorf("auth error: %w", err)
return sourcev1.GitRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err
}
if cleanup != nil {
defer cleanup()
}
auth = method
}
// create tmp dir for the Git clone
@ -321,74 +329,3 @@ func (r *GitRepositoryReconciler) gc(repository sourcev1.GitRepository) {
}
}
}
func (r *GitRepositoryReconciler) auth(repository sourcev1.GitRepository, tmp string) (transport.AuthMethod, error) {
if repository.Spec.SecretRef == nil {
return nil, nil
}
name := types.NamespacedName{
Namespace: repository.GetNamespace(),
Name: repository.Spec.SecretRef.Name,
}
var secret corev1.Secret
err := r.Client.Get(context.TODO(), name, &secret)
if err != nil {
return nil, err
}
credentials := secret.Data
// HTTP auth
if strings.HasPrefix(repository.Spec.URL, "http") {
auth := &http.BasicAuth{}
if username, ok := credentials["username"]; ok {
auth.Username = string(username)
}
if password, ok := credentials["password"]; ok {
auth.Password = string(password)
}
if auth.Username == "" || auth.Password == "" {
return nil, fmt.Errorf("invalid '%s' secret data: required fields username and password",
repository.Spec.SecretRef.Name)
}
return auth, nil
}
// SSH auth
if strings.HasPrefix(repository.Spec.URL, "ssh") {
var privateKey []byte
if identity, ok := credentials["identity"]; ok {
privateKey = identity
} else {
return nil, fmt.Errorf("invalid '%s' secret data: required field identity", repository.Spec.SecretRef.Name)
}
pk, err := ssh.NewPublicKeys("git", privateKey, "")
if err != nil {
return nil, err
}
known_hosts := filepath.Join(tmp, "known_hosts")
if kh, ok := credentials["known_hosts"]; ok {
if err := ioutil.WriteFile(filepath.Join(tmp, "known_hosts"), kh, 0644); err != nil {
return nil, err
}
} else {
return nil, fmt.Errorf("invalid '%s' secret data: required field known_hosts", repository.Spec.SecretRef.Name)
}
callback, err := ssh.NewKnownHostsCallback(known_hosts)
if err != nil {
return nil, err
}
pk.HostKeyCallback = callback
return pk, nil
}
return nil, nil
}

73
internal/git/transport.go Normal file
View File

@ -0,0 +1,73 @@
package git
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
corev1 "k8s.io/api/core/v1"
)
func AuthMethodFromSecret(url string, secret corev1.Secret) (transport.AuthMethod, func(), error) {
switch {
case strings.HasPrefix(url, "http"):
auth, err := BasicAuthFromSecret(secret)
return auth, nil, err
case strings.HasPrefix(url, "ssh"):
return PublicKeysFromSecret(secret)
}
return nil, nil, nil
}
func BasicAuthFromSecret(secret corev1.Secret) (*http.BasicAuth, error) {
auth := &http.BasicAuth{}
if username, ok := secret.Data["username"]; ok {
auth.Username = string(username)
}
if password, ok := secret.Data["password"]; ok {
auth.Password = string(password)
}
if auth.Username == "" || auth.Password == "" {
return nil, fmt.Errorf("invalid '%s' secret data: required fields 'username' and 'password'", secret.Name)
}
return auth, nil
}
func PublicKeysFromSecret(secret corev1.Secret) (*ssh.PublicKeys, func(), error) {
identity := secret.Data["identity"]
knownHosts := secret.Data["known_hosts"]
if len(identity) == 0 || len(knownHosts) == 0 {
return nil, nil, fmt.Errorf("invalid '%s' secret data: required fields 'identity' and 'known_hosts'", secret.Name)
}
pk, err := ssh.NewPublicKeys("git", identity, "")
if err != nil {
return nil, nil, err
}
// create tmp dir for known_hosts
tmp, err := ioutil.TempDir("", "ssh-"+secret.Name)
if err != nil {
return nil, nil, err
}
cleanup := func() { os.RemoveAll(tmp) }
knownHostsPath := filepath.Join(tmp, "known_hosts")
if err := ioutil.WriteFile(knownHostsPath, knownHosts, 0644); err != nil {
cleanup()
return nil, nil, err
}
callback, err := ssh.NewKnownHostsCallback(knownHostsPath)
if err != nil {
cleanup()
return nil, nil, err
}
pk.HostKeyCallback = callback
return pk, cleanup, nil
}