diff --git a/controllers/gitrepository_controller.go b/controllers/gitrepository_controller.go index 4dea3e4c..b9d00625 100644 --- a/controllers/gitrepository_controller.go +++ b/controllers/gitrepository_controller.go @@ -22,9 +22,6 @@ import ( "io/ioutil" "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/transport" "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) { - // set defaults: master branch, no tags fetching, max two commits - branch := "master" - revision := "" - tagMode := git.NoTags - depth := 2 - - // 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 - } - } + // create tmp dir for the Git clone + tmpGit, 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(tmpGit) // determine auth method - strategy := intgit.AuthSecretStrategyForURL(repository.Spec.URL) 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{ Namespace: repository.GetNamespace(), 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 } - auth, err = strategy.Method(secret) + auth, err = authStrategy.Method(secret) if err != nil { err = fmt.Errorf("auth error: %w", err) return sourcev1.GitRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err } } - // create tmp dir for the Git clone - tmpGit, err := ioutil.TempDir("", repository.Name) + checkoutStrategy := intgit.CheckoutStrategyForRef(repository.Spec.Reference) + commit, revision, err := checkoutStrategy.Checkout(ctx, tmpGit, repository.Spec.URL, auth) 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 } @@ -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(), - fmt.Sprintf("%s.tar.gz", ref.Hash().String()), revision) + fmt.Sprintf("%s.tar.gz", commit.Hash.String()), revision) // create artifact dir err = r.Storage.MkdirAll(artifact) diff --git a/internal/git/checkout.go b/internal/git/checkout.go index e8ee2f48..a53cb620 100644 --- a/internal/git/checkout.go +++ b/internal/git/checkout.go @@ -23,21 +23,47 @@ import ( "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/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 { - Checkout(ctx context.Context, path string) error + Checkout(ctx context.Context, path, url string, auth transport.AuthMethod) (*object.Commit, string, error) } type CheckoutBranch struct { - url 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{ - URL: c.url, - RemoteName: "origin", + URL: url, + Auth: auth, + RemoteName: defaultOrigin, ReferenceName: plumbing.NewBranchReferenceName(c.branch), SingleBranch: true, NoCheckout: false, @@ -47,24 +73,28 @@ func (c *CheckoutBranch) Checkout(ctx context.Context, path string) (string, err Tags: git.NoTags, }) if err != nil { - return "", fmt.Errorf("git clone error: %w", err) + return nil, "", fmt.Errorf("git clone error: %w", err) } head, err := repo.Head() 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 { - url 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{ - URL: c.url, - RemoteName: "origin", + URL: url, + Auth: auth, + RemoteName: defaultOrigin, ReferenceName: plumbing.NewTagReferenceName(c.tag), SingleBranch: true, NoCheckout: false, @@ -74,25 +104,29 @@ func (c *CheckoutTag) Checkout(ctx context.Context, path string) (string, error) Tags: git.NoTags, }) if err != nil { - return "", fmt.Errorf("git clone error: %w", err) + return nil, "", fmt.Errorf("git clone error: %w", err) } head, err := repo.Head() 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 { - url string branch 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{ - URL: c.url, - RemoteName: "origin", + URL: url, + Auth: auth, + RemoteName: defaultOrigin, ReferenceName: plumbing.NewBranchReferenceName(c.branch), SingleBranch: true, NoCheckout: false, @@ -101,40 +135,40 @@ func (c *CheckoutCommit) Checkout(ctx context.Context, path string) (string, err Tags: git.NoTags, }) if err != nil { - return "", fmt.Errorf("git clone error: %w", err) + return nil, "", fmt.Errorf("git clone error: %w", err) } w, err := repo.Worktree() 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)) 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{ Hash: commit.Hash, Force: true, }) 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 { - url string - semver string + semVer string } -func (c *CheckoutSemVer) Checkout(ctx context.Context, path string) (string, error) { - rng, err := semver.ParseRange(c.semver) +func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, auth transport.AuthMethod) (*object.Commit, string, error) { + rng, err := semver.ParseRange(c.semVer) 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{ - URL: c.url, - RemoteName: "origin", + URL: url, + Auth: auth, + RemoteName: defaultOrigin, SingleBranch: true, NoCheckout: false, Depth: 1, @@ -143,12 +177,12 @@ func (c *CheckoutSemVer) Checkout(ctx context.Context, path string) (string, err Tags: git.AllTags, }) if err != nil { - return "", fmt.Errorf("git clone error: %w", err) + return nil, "", fmt.Errorf("git clone error: %w", err) } repoTags, err := repo.Tags() 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) @@ -168,25 +202,29 @@ func (c *CheckoutSemVer) Checkout(ctx context.Context, path string) (string, err } 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) v := svers[len(svers)-1] t := svTags[v.String()] - commit := tags[t] + commitRef := tags[t] w, err := repo.Worktree() 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{ - Hash: plumbing.NewHash(commit), + Hash: commit.Hash, }) 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 }