388 lines
8.9 KiB
Go
388 lines
8.9 KiB
Go
package recipe
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
|
|
"coopcloud.tech/abra/pkg/config"
|
|
"coopcloud.tech/abra/pkg/formatter"
|
|
gitPkg "coopcloud.tech/abra/pkg/git"
|
|
"coopcloud.tech/abra/pkg/log"
|
|
"github.com/distribution/reference"
|
|
"github.com/go-git/go-git/v5"
|
|
"github.com/go-git/go-git/v5/plumbing"
|
|
)
|
|
|
|
// Ensure makes sure the recipe exists, is up to date and has the latest version checked out.
|
|
func (r Recipe) Ensure(chaos bool, offline bool) error {
|
|
if err := r.EnsureExists(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !chaos {
|
|
if err := r.EnsureIsClean(); err != nil {
|
|
return err
|
|
}
|
|
if !offline {
|
|
if err := r.EnsureUpToDate(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
if err := r.EnsureLatest(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EnsureExists ensures that the recipe is locally cloned
|
|
func (r Recipe) EnsureExists() error {
|
|
recipeDir := path.Join(config.RECIPES_DIR, r.Name)
|
|
|
|
if _, err := os.Stat(recipeDir); os.IsNotExist(err) {
|
|
log.Debugf("%s does not exist, attemmpting to clone", recipeDir)
|
|
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, r.Name)
|
|
if err := gitPkg.Clone(recipeDir, url); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := gitPkg.EnsureGitRepo(recipeDir); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// EnsureVersion checks whether a specific version exists for a recipe.
|
|
func (r Recipe) EnsureVersion(version string) (bool, error) {
|
|
isChaosCommit := false
|
|
|
|
recipeDir := path.Join(config.RECIPES_DIR, r.Name)
|
|
|
|
if err := gitPkg.EnsureGitRepo(recipeDir); err != nil {
|
|
return isChaosCommit, err
|
|
}
|
|
|
|
repo, err := git.PlainOpen(recipeDir)
|
|
if err != nil {
|
|
return isChaosCommit, err
|
|
}
|
|
|
|
tags, err := repo.Tags()
|
|
if err != nil {
|
|
return isChaosCommit, err
|
|
}
|
|
|
|
var parsedTags []string
|
|
var tagRef plumbing.ReferenceName
|
|
if err := tags.ForEach(func(ref *plumbing.Reference) (err error) {
|
|
parsedTags = append(parsedTags, ref.Name().Short())
|
|
if ref.Name().Short() == version {
|
|
tagRef = ref.Name()
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return isChaosCommit, err
|
|
}
|
|
|
|
joinedTags := strings.Join(parsedTags, ", ")
|
|
if joinedTags != "" {
|
|
log.Debugf("read %s as tags for recipe %s", joinedTags, r.Name)
|
|
}
|
|
|
|
var opts *git.CheckoutOptions
|
|
if tagRef.String() == "" {
|
|
log.Debugf("attempting to checkout '%s' as chaos commit", version)
|
|
|
|
hash, err := repo.ResolveRevision(plumbing.Revision(version))
|
|
if err != nil {
|
|
log.Fatalf("unable to resolve '%s': %s", version, err)
|
|
}
|
|
|
|
opts = &git.CheckoutOptions{Hash: *hash, Create: false, Force: true}
|
|
isChaosCommit = true
|
|
} else {
|
|
opts = &git.CheckoutOptions{Branch: tagRef, Create: false, Force: true}
|
|
}
|
|
|
|
worktree, err := repo.Worktree()
|
|
if err != nil {
|
|
return isChaosCommit, nil
|
|
}
|
|
|
|
if err := worktree.Checkout(opts); err != nil {
|
|
return isChaosCommit, nil
|
|
}
|
|
|
|
log.Debugf("successfully checked %s out to %s in %s", r.Name, tagRef.Short(), recipeDir)
|
|
|
|
return isChaosCommit, nil
|
|
}
|
|
|
|
// EnsureIsClean makes sure that the recipe repository has no unstaged changes.
|
|
func (r Recipe) EnsureIsClean() error {
|
|
recipeDir := path.Join(config.RECIPES_DIR, r.Name)
|
|
|
|
isClean, err := gitPkg.IsClean(recipeDir)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to check git clean status in %s: %s", recipeDir, err)
|
|
}
|
|
|
|
if !isClean {
|
|
msg := "%s (%s) has locally unstaged changes? please commit/remove your changes before proceeding"
|
|
return fmt.Errorf(msg, r.Name, recipeDir)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// EnsureLatest makes sure the latest commit is checked out for the local recipe repository
|
|
func (r Recipe) EnsureLatest() error {
|
|
recipeDir := path.Join(config.RECIPES_DIR, r.Name)
|
|
|
|
if err := gitPkg.EnsureGitRepo(recipeDir); err != nil {
|
|
return err
|
|
}
|
|
|
|
repo, err := git.PlainOpen(recipeDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
worktree, err := repo.Worktree()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
branch, err := gitPkg.GetDefaultBranch(repo, recipeDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
checkOutOpts := &git.CheckoutOptions{
|
|
Create: false,
|
|
Force: true,
|
|
Branch: plumbing.ReferenceName(branch),
|
|
}
|
|
|
|
if err := worktree.Checkout(checkOutOpts); err != nil {
|
|
log.Debugf("failed to check out %s in %s", branch, recipeDir)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// EnsureUpToDate ensures that the local repo is synced to the remote
|
|
func (r Recipe) EnsureUpToDate() error {
|
|
recipeDir := path.Join(config.RECIPES_DIR, r.Name)
|
|
|
|
repo, err := git.PlainOpen(recipeDir)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to open %s: %s", recipeDir, err)
|
|
}
|
|
|
|
remotes, err := repo.Remotes()
|
|
if err != nil {
|
|
return fmt.Errorf("unable to read remotes in %s: %s", recipeDir, err)
|
|
}
|
|
|
|
if len(remotes) == 0 {
|
|
log.Debugf("cannot ensure %s is up-to-date, no git remotes configured", r.Name)
|
|
return nil
|
|
}
|
|
|
|
worktree, err := repo.Worktree()
|
|
if err != nil {
|
|
return fmt.Errorf("unable to open git work tree in %s: %s", recipeDir, err)
|
|
}
|
|
|
|
branch, err := gitPkg.CheckoutDefaultBranch(repo, recipeDir)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to check out default branch in %s: %s", recipeDir, err)
|
|
}
|
|
|
|
fetchOpts := &git.FetchOptions{Tags: git.AllTags}
|
|
if err := repo.Fetch(fetchOpts); err != nil {
|
|
if !strings.Contains(err.Error(), "already up-to-date") {
|
|
return fmt.Errorf("unable to fetch tags in %s: %s", recipeDir, err)
|
|
}
|
|
}
|
|
|
|
opts := &git.PullOptions{
|
|
Force: true,
|
|
ReferenceName: branch,
|
|
SingleBranch: true,
|
|
}
|
|
|
|
if err := worktree.Pull(opts); err != nil {
|
|
if !strings.Contains(err.Error(), "already up-to-date") {
|
|
return fmt.Errorf("unable to git pull in %s: %s", recipeDir, err)
|
|
}
|
|
}
|
|
|
|
log.Debugf("fetched latest git changes for %s", r.Name)
|
|
|
|
return nil
|
|
}
|
|
|
|
// ChaosVersion constructs a chaos mode recipe version.
|
|
func (r Recipe) ChaosVersion() (string, error) {
|
|
var version string
|
|
|
|
head, err := gitPkg.GetRecipeHead(r.Name)
|
|
if err != nil {
|
|
return version, err
|
|
}
|
|
|
|
version = formatter.SmallSHA(head.String())
|
|
|
|
recipeDir := path.Join(config.RECIPES_DIR, r.Name)
|
|
isClean, err := gitPkg.IsClean(recipeDir)
|
|
if err != nil {
|
|
return version, err
|
|
}
|
|
|
|
if !isClean {
|
|
version = fmt.Sprintf("%s + unstaged changes", version)
|
|
}
|
|
|
|
return version, nil
|
|
}
|
|
|
|
// Push pushes the latest changes to a SSH URL remote. You need to have your
|
|
// local SSH configuration for git.coopcloud.tech working for this to work
|
|
func (r Recipe) Push(dryRun bool) error {
|
|
repo, err := git.PlainOpen(r.Dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := gitPkg.CreateRemote(repo, "origin-ssh", r.SSHURL, dryRun); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := gitPkg.Push(r.Dir, "origin-ssh", true, dryRun); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Tags list the recipe tags
|
|
func (r Recipe) Tags() ([]string, error) {
|
|
var tags []string
|
|
|
|
repo, err := git.PlainOpen(r.Dir)
|
|
if err != nil {
|
|
return tags, err
|
|
}
|
|
|
|
gitTags, err := repo.Tags()
|
|
if err != nil {
|
|
return tags, err
|
|
}
|
|
|
|
if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) {
|
|
tags = append(tags, strings.TrimPrefix(string(ref.Name()), "refs/tags/"))
|
|
return nil
|
|
}); err != nil {
|
|
return tags, err
|
|
}
|
|
|
|
log.Debugf("detected %s as tags for recipe %s", strings.Join(tags, ", "), r.Name)
|
|
|
|
return tags, nil
|
|
}
|
|
|
|
// GetRecipeVersions retrieves all recipe versions.
|
|
func (r Recipe) GetRecipeVersions(offline bool) (RecipeVersions, error) {
|
|
versions := RecipeVersions{}
|
|
log.Debugf("attempting to open git repository in %s", r.Dir)
|
|
|
|
repo, err := git.PlainOpen(r.Dir)
|
|
if err != nil {
|
|
return versions, err
|
|
}
|
|
|
|
worktree, err := repo.Worktree()
|
|
if err != nil {
|
|
return versions, err
|
|
}
|
|
|
|
gitTags, err := repo.Tags()
|
|
if err != nil {
|
|
return versions, err
|
|
}
|
|
|
|
if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) {
|
|
tag := strings.TrimPrefix(string(ref.Name()), "refs/tags/")
|
|
|
|
log.Debugf("processing %s for %s", tag, r.Name)
|
|
|
|
checkOutOpts := &git.CheckoutOptions{
|
|
Create: false,
|
|
Force: true,
|
|
Branch: plumbing.ReferenceName(ref.Name()),
|
|
}
|
|
if err := worktree.Checkout(checkOutOpts); err != nil {
|
|
log.Debugf("failed to check out %s in %s", tag, r.Dir)
|
|
return err
|
|
}
|
|
|
|
log.Debugf("successfully checked out %s in %s", ref.Name(), r.Dir)
|
|
|
|
config, err := r.GetComposeConfig(nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
versionMeta := make(map[string]ServiceMeta)
|
|
for _, service := range config.Services {
|
|
|
|
img, err := reference.ParseNormalizedNamed(service.Image)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
path := reference.Path(img)
|
|
|
|
path = formatter.StripTagMeta(path)
|
|
|
|
var tag string
|
|
switch img.(type) {
|
|
case reference.NamedTagged:
|
|
tag = img.(reference.NamedTagged).Tag()
|
|
case reference.Named:
|
|
log.Warnf("%s service is missing image tag?", path)
|
|
continue
|
|
}
|
|
|
|
versionMeta[service.Name] = ServiceMeta{
|
|
Image: path,
|
|
Tag: tag,
|
|
}
|
|
}
|
|
|
|
versions = append(versions, map[string]map[string]ServiceMeta{tag: versionMeta})
|
|
|
|
return nil
|
|
}); err != nil {
|
|
return versions, err
|
|
}
|
|
|
|
_, err = gitPkg.CheckoutDefaultBranch(repo, r.Dir)
|
|
if err != nil {
|
|
return versions, err
|
|
}
|
|
|
|
sortRecipeVersions(versions)
|
|
|
|
log.Debugf("collected %s for %s", versions, r.Dir)
|
|
|
|
return versions, nil
|
|
}
|