controllers: implement checkout strategies

This commit is contained in:
Hidde Beydals 2020-05-03 22:20:16 +02:00
parent 40b1369ace
commit 9c67baa158
2 changed files with 89 additions and 187 deletions

View File

@ -22,9 +22,6 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"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/object" "github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/plumbing/transport" "github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-logr/logr" "github.com/go-logr/logr"
@ -122,36 +119,18 @@ func (r *GitRepositoryReconciler) SetupWithManagerAndOptions(mgr ctrl.Manager, o
} }
func (r *GitRepositoryReconciler) sync(ctx context.Context, 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 // create tmp dir for the Git clone
branch := "master" tmpGit, err := ioutil.TempDir("", repository.Name)
revision := "" if err != nil {
tagMode := git.NoTags err = fmt.Errorf("tmp dir error: %w", err)
depth := 2 return sourcev1.GitRepositoryNotReady(repository, sourcev1.StorageOperationFailedReason, err.Error()), err
// determine ref
refName := plumbing.NewBranchReferenceName(branch)
if repository.Spec.Reference != nil {
if repository.Spec.Reference.Branch != "" {
branch = repository.Spec.Reference.Branch
refName = plumbing.NewBranchReferenceName(branch)
}
if repository.Spec.Reference.Commit != "" {
depth = 0
} else {
if repository.Spec.Reference.Tag != "" {
refName = plumbing.NewTagReferenceName(repository.Spec.Reference.Tag)
}
if repository.Spec.Reference.SemVer != "" {
tagMode = git.AllTags
}
}
} }
defer os.RemoveAll(tmpGit)
// determine auth method // determine auth method
strategy := intgit.AuthSecretStrategyForURL(repository.Spec.URL)
var auth transport.AuthMethod var auth transport.AuthMethod
if repository.Spec.SecretRef != nil { authStrategy := intgit.AuthSecretStrategyForURL(repository.Spec.URL)
if repository.Spec.SecretRef != nil && authStrategy != nil {
name := types.NamespacedName{ name := types.NamespacedName{
Namespace: repository.GetNamespace(), Namespace: repository.GetNamespace(),
Name: repository.Spec.SecretRef.Name, Name: repository.Spec.SecretRef.Name,
@ -164,124 +143,16 @@ func (r *GitRepositoryReconciler) sync(ctx context.Context, repository sourcev1.
return sourcev1.GitRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err return sourcev1.GitRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err
} }
auth, err = strategy.Method(secret) auth, err = authStrategy.Method(secret)
if err != nil { if err != nil {
err = fmt.Errorf("auth error: %w", err) err = fmt.Errorf("auth error: %w", err)
return sourcev1.GitRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err return sourcev1.GitRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err
} }
} }
// create tmp dir for the Git clone checkoutStrategy := intgit.CheckoutStrategyForRef(repository.Spec.Reference)
tmpGit, err := ioutil.TempDir("", repository.Name) commit, revision, err := checkoutStrategy.Checkout(ctx, tmpGit, repository.Spec.URL, auth)
if err != nil { if err != nil {
err = fmt.Errorf("tmp dir error: %w", err)
return sourcev1.GitRepositoryNotReady(repository, sourcev1.StorageOperationFailedReason, err.Error()), err
}
defer os.RemoveAll(tmpGit)
// clone to tmp
gitCtx, cancel := context.WithTimeout(ctx, repository.GetTimeout())
repo, err := git.PlainCloneContext(gitCtx, tmpGit, false, &git.CloneOptions{
URL: repository.Spec.URL,
Auth: auth,
RemoteName: "origin",
ReferenceName: refName,
SingleBranch: true,
NoCheckout: false,
Depth: depth,
RecurseSubmodules: 0,
Progress: nil,
Tags: tagMode,
})
cancel()
if err != nil {
err = fmt.Errorf("git clone error: %w", err)
return sourcev1.GitRepositoryNotReady(repository, sourcev1.GitOperationFailedReason, err.Error()), err
}
// checkout commit or tag
if repository.Spec.Reference != nil {
if commit := repository.Spec.Reference.Commit; commit != "" {
w, err := repo.Worktree()
if err != nil {
err = fmt.Errorf("git worktree error: %w", err)
return sourcev1.GitRepositoryNotReady(repository, sourcev1.GitOperationFailedReason, err.Error()), err
}
err = w.Checkout(&git.CheckoutOptions{
Hash: plumbing.NewHash(commit),
Force: true,
})
if err != nil {
err = fmt.Errorf("git checkout '%s' for '%s' error: %w", commit, branch, err)
return sourcev1.GitRepositoryNotReady(repository, sourcev1.GitOperationFailedReason, err.Error()), err
}
} else if exp := repository.Spec.Reference.SemVer; exp != "" {
rng, err := semver.ParseRange(exp)
if err != nil {
err = fmt.Errorf("semver parse range error: %w", err)
return sourcev1.GitRepositoryNotReady(repository, sourcev1.GitOperationFailedReason, err.Error()), err
}
repoTags, err := repo.Tags()
if err != nil {
err = fmt.Errorf("git list tags error: %w", err)
return sourcev1.GitRepositoryNotReady(repository, sourcev1.GitOperationFailedReason, err.Error()), err
}
tags := make(map[string]string)
_ = repoTags.ForEach(func(t *plumbing.Reference) error {
tags[t.Name().Short()] = t.Strings()[1]
return nil
})
svTags := make(map[string]string)
var svers []semver.Version
for tag, _ := range tags {
v, _ := semver.ParseTolerant(tag)
if rng(v) {
svers = append(svers, v)
svTags[v.String()] = tag
}
}
if len(svers) > 0 {
semver.Sort(svers)
v := svers[len(svers)-1]
t := svTags[v.String()]
commit := tags[t]
revision = fmt.Sprintf("%s/%s", t, commit)
w, err := repo.Worktree()
if err != nil {
err = fmt.Errorf("git worktree error: %w", err)
return sourcev1.GitRepositoryNotReady(repository, sourcev1.GitOperationFailedReason, err.Error()), err
}
err = w.Checkout(&git.CheckoutOptions{
Hash: plumbing.NewHash(commit),
})
if err != nil {
err = fmt.Errorf("git checkout error: %w", err)
return sourcev1.GitRepositoryNotReady(repository, sourcev1.GitOperationFailedReason, err.Error()), err
}
} else {
err = fmt.Errorf("no match found for semver: %s", repository.Spec.Reference.SemVer)
return sourcev1.GitRepositoryNotReady(repository, sourcev1.GitOperationFailedReason, err.Error()), err
}
}
}
// read commit hash
ref, err := repo.Head()
if err != nil {
err = fmt.Errorf("git resolve HEAD error: %w", err)
return sourcev1.GitRepositoryNotReady(repository, sourcev1.GitOperationFailedReason, err.Error()), err
}
commit, err := repo.CommitObject(ref.Hash())
if err != nil {
err = fmt.Errorf("git resolve HEAD error: %w", err)
return sourcev1.GitRepositoryNotReady(repository, sourcev1.GitOperationFailedReason, err.Error()), err return sourcev1.GitRepositoryNotReady(repository, sourcev1.GitOperationFailedReason, err.Error()), err
} }
@ -296,15 +167,8 @@ func (r *GitRepositoryReconciler) sync(ctx context.Context, repository sourcev1.
} }
} }
if revision == "" {
revision = fmt.Sprintf("%s/%s", branch, ref.Hash().String())
if repository.Spec.Reference != nil && repository.Spec.Reference.Tag != "" {
revision = fmt.Sprintf("%s/%s", repository.Spec.Reference.Tag, ref.Hash().String())
}
}
artifact := r.Storage.ArtifactFor(repository.Kind, repository.ObjectMeta.GetObjectMeta(), artifact := r.Storage.ArtifactFor(repository.Kind, repository.ObjectMeta.GetObjectMeta(),
fmt.Sprintf("%s.tar.gz", ref.Hash().String()), revision) fmt.Sprintf("%s.tar.gz", commit.Hash.String()), revision)
// create artifact dir // create artifact dir
err = r.Storage.MkdirAll(artifact) err = r.Storage.MkdirAll(artifact)

View File

@ -23,21 +23,47 @@ import (
"github.com/blang/semver" "github.com/blang/semver"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/plumbing/transport"
sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1"
) )
const (
defaultOrigin = "origin"
defaultBranch = "master"
)
func CheckoutStrategyForRef(ref *sourcev1.GitRepositoryRef) CheckoutStrategy {
switch {
case ref == nil:
return &CheckoutBranch{branch: defaultBranch}
case ref.SemVer != "":
return &CheckoutSemVer{semVer: ref.SemVer}
case ref.Tag != "":
return &CheckoutTag{tag: ref.Tag}
case ref.Commit != "":
return &CheckoutCommit{branch: ref.Branch, commit: ref.Commit}
case ref.Branch != "":
return &CheckoutBranch{branch: ref.Branch}
default:
return &CheckoutBranch{branch: defaultBranch}
}
}
type CheckoutStrategy interface { type CheckoutStrategy interface {
Checkout(ctx context.Context, path string) error Checkout(ctx context.Context, path, url string, auth transport.AuthMethod) (*object.Commit, string, error)
} }
type CheckoutBranch struct { type CheckoutBranch struct {
url string
branch string branch string
} }
func (c *CheckoutBranch) Checkout(ctx context.Context, path string) (string, error) { func (c *CheckoutBranch) Checkout(ctx context.Context, path, url string, auth transport.AuthMethod) (*object.Commit, string, error) {
repo, err := git.PlainCloneContext(ctx, path, false, &git.CloneOptions{ repo, err := git.PlainCloneContext(ctx, path, false, &git.CloneOptions{
URL: c.url, URL: url,
RemoteName: "origin", Auth: auth,
RemoteName: defaultOrigin,
ReferenceName: plumbing.NewBranchReferenceName(c.branch), ReferenceName: plumbing.NewBranchReferenceName(c.branch),
SingleBranch: true, SingleBranch: true,
NoCheckout: false, NoCheckout: false,
@ -47,24 +73,28 @@ func (c *CheckoutBranch) Checkout(ctx context.Context, path string) (string, err
Tags: git.NoTags, Tags: git.NoTags,
}) })
if err != nil { if err != nil {
return "", fmt.Errorf("git clone error: %w", err) return nil, "", fmt.Errorf("git clone error: %w", err)
} }
head, err := repo.Head() head, err := repo.Head()
if err != nil { if err != nil {
return "", fmt.Errorf(" git resolve HEAD error: %w", err) return nil, "", fmt.Errorf("git resolve HEAD error: %w", err)
} }
return fmt.Sprintf("%s/%s", c.branch, head.Hash().String()), nil commit, err := repo.CommitObject(head.Hash())
if err != nil {
return nil, "", fmt.Errorf("git commit not found: %w", err)
}
return commit, fmt.Sprintf("%s/%s", c.branch, head.Hash().String()), nil
} }
type CheckoutTag struct { type CheckoutTag struct {
url string
tag string tag string
} }
func (c *CheckoutTag) Checkout(ctx context.Context, path string) (string, error) { func (c *CheckoutTag) Checkout(ctx context.Context, path, url string, auth transport.AuthMethod) (*object.Commit, string, error) {
repo, err := git.PlainCloneContext(ctx, path, false, &git.CloneOptions{ repo, err := git.PlainCloneContext(ctx, path, false, &git.CloneOptions{
URL: c.url, URL: url,
RemoteName: "origin", Auth: auth,
RemoteName: defaultOrigin,
ReferenceName: plumbing.NewTagReferenceName(c.tag), ReferenceName: plumbing.NewTagReferenceName(c.tag),
SingleBranch: true, SingleBranch: true,
NoCheckout: false, NoCheckout: false,
@ -74,25 +104,29 @@ func (c *CheckoutTag) Checkout(ctx context.Context, path string) (string, error)
Tags: git.NoTags, Tags: git.NoTags,
}) })
if err != nil { if err != nil {
return "", fmt.Errorf("git clone error: %w", err) return nil, "", fmt.Errorf("git clone error: %w", err)
} }
head, err := repo.Head() head, err := repo.Head()
if err != nil { if err != nil {
return "", fmt.Errorf(" git resolve HEAD error: %w", err) return nil, "", fmt.Errorf("git resolve HEAD error: %w", err)
} }
return fmt.Sprintf("%s/%s", c.tag, head.Hash().String()), nil commit, err := repo.CommitObject(head.Hash())
if err != nil {
return nil, "", fmt.Errorf("git commit not found: %w", err)
}
return commit, fmt.Sprintf("%s/%s", c.tag, head.Hash().String()), nil
} }
type CheckoutCommit struct { type CheckoutCommit struct {
url string
branch string branch string
commit string commit string
} }
func (c *CheckoutCommit) Checkout(ctx context.Context, path string) (string, error) { func (c *CheckoutCommit) Checkout(ctx context.Context, path, url string, auth transport.AuthMethod) (*object.Commit, string, error) {
repo, err := git.PlainCloneContext(ctx, path, false, &git.CloneOptions{ repo, err := git.PlainCloneContext(ctx, path, false, &git.CloneOptions{
URL: c.url, URL: url,
RemoteName: "origin", Auth: auth,
RemoteName: defaultOrigin,
ReferenceName: plumbing.NewBranchReferenceName(c.branch), ReferenceName: plumbing.NewBranchReferenceName(c.branch),
SingleBranch: true, SingleBranch: true,
NoCheckout: false, NoCheckout: false,
@ -101,40 +135,40 @@ func (c *CheckoutCommit) Checkout(ctx context.Context, path string) (string, err
Tags: git.NoTags, Tags: git.NoTags,
}) })
if err != nil { if err != nil {
return "", fmt.Errorf("git clone error: %w", err) return nil, "", fmt.Errorf("git clone error: %w", err)
} }
w, err := repo.Worktree() w, err := repo.Worktree()
if err != nil { if err != nil {
return "", fmt.Errorf("git worktree error: %w", err) return nil, "", fmt.Errorf("git worktree error: %w", err)
} }
commit, err := repo.CommitObject(plumbing.NewHash(c.commit)) commit, err := repo.CommitObject(plumbing.NewHash(c.commit))
if err != nil { if err != nil {
return "", fmt.Errorf("git commit not found: %w", err) return nil, "", fmt.Errorf("git commit not found: %w", err)
} }
err = w.Checkout(&git.CheckoutOptions{ err = w.Checkout(&git.CheckoutOptions{
Hash: commit.Hash, Hash: commit.Hash,
Force: true, Force: true,
}) })
if err != nil { if err != nil {
return "", fmt.Errorf("git checkout error: %w", err) return nil, "", fmt.Errorf("git checkout error: %w", err)
} }
return fmt.Sprintf("%s/%s", c.branch, commit.Hash.String()), nil return commit, fmt.Sprintf("%s/%s", c.branch, commit.Hash.String()), nil
} }
type CheckoutSemVer struct { type CheckoutSemVer struct {
url string semVer string
semver string
} }
func (c *CheckoutSemVer) Checkout(ctx context.Context, path string) (string, error) { func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, auth transport.AuthMethod) (*object.Commit, string, error) {
rng, err := semver.ParseRange(c.semver) rng, err := semver.ParseRange(c.semVer)
if err != nil { if err != nil {
return "", fmt.Errorf("semver parse range error: %w", err) return nil, "", fmt.Errorf("semver parse range error: %w", err)
} }
repo, err := git.PlainCloneContext(ctx, path, false, &git.CloneOptions{ repo, err := git.PlainCloneContext(ctx, path, false, &git.CloneOptions{
URL: c.url, URL: url,
RemoteName: "origin", Auth: auth,
RemoteName: defaultOrigin,
SingleBranch: true, SingleBranch: true,
NoCheckout: false, NoCheckout: false,
Depth: 1, Depth: 1,
@ -143,12 +177,12 @@ func (c *CheckoutSemVer) Checkout(ctx context.Context, path string) (string, err
Tags: git.AllTags, Tags: git.AllTags,
}) })
if err != nil { if err != nil {
return "", fmt.Errorf("git clone error: %w", err) return nil, "", fmt.Errorf("git clone error: %w", err)
} }
repoTags, err := repo.Tags() repoTags, err := repo.Tags()
if err != nil { if err != nil {
return "", fmt.Errorf("git list tags error: %w", err) return nil, "", fmt.Errorf("git list tags error: %w", err)
} }
tags := make(map[string]string) tags := make(map[string]string)
@ -168,25 +202,29 @@ func (c *CheckoutSemVer) Checkout(ctx context.Context, path string) (string, err
} }
if len(svers) == 0 { if len(svers) == 0 {
return "", fmt.Errorf("no match found for semver: %s", c.semver) return nil, "", fmt.Errorf("no match found for semver: %s", c.semVer)
} }
semver.Sort(svers) semver.Sort(svers)
v := svers[len(svers)-1] v := svers[len(svers)-1]
t := svTags[v.String()] t := svTags[v.String()]
commit := tags[t] commitRef := tags[t]
w, err := repo.Worktree() w, err := repo.Worktree()
if err != nil { if err != nil {
return "", fmt.Errorf("git worktree error: %w", err) return nil, "", fmt.Errorf("git worktree error: %w", err)
} }
commit, err := repo.CommitObject(plumbing.NewHash(commitRef))
if err != nil {
return nil, "", fmt.Errorf("git commit not found: %w", err)
}
err = w.Checkout(&git.CheckoutOptions{ err = w.Checkout(&git.CheckoutOptions{
Hash: plumbing.NewHash(commit), Hash: commit.Hash,
}) })
if err != nil { if err != nil {
return "", fmt.Errorf("git checkout error: %w", err) return nil, "", fmt.Errorf("git checkout error: %w", err)
} }
return fmt.Sprintf("%s/%s", t, commit), nil return commit, fmt.Sprintf("%s/%s", t, commitRef), nil
} }