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/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 Recipe2) 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 Recipe2) 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 Recipe2) EnsureVersion(version string) 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 } tags, err := repo.Tags() if err != nil { return nil } 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 err } joinedTags := strings.Join(parsedTags, ", ") if joinedTags != "" { log.Debugf("read %s as tags for recipe %s", joinedTags, r.Name) } if tagRef.String() == "" { return fmt.Errorf("the local copy of %s doesn't seem to have version %s available?", r.Name, version) } worktree, err := repo.Worktree() if err != nil { return err } opts := &git.CheckoutOptions{ Branch: tagRef, Create: false, Force: true, } if err := worktree.Checkout(opts); err != nil { return err } log.Debugf("successfully checked %s out to %s in %s", r.Name, tagRef.Short(), recipeDir) return nil } // EnsureIsClean makes sure that the recipe repository has no unstaged changes. func (r Recipe2) 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 Recipe2) 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 Recipe2) 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 Recipe2) 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 Recipe2) 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 }