abra/pkg/recipe/recipe.go

336 lines
7.4 KiB
Go

package recipe
import (
"fmt"
"os"
"path"
"path/filepath"
"strings"
"coopcloud.tech/abra/pkg/compose"
"coopcloud.tech/abra/pkg/config"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/upstream/stack"
loader "coopcloud.tech/abra/pkg/upstream/stack"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/sirupsen/logrus"
)
// Recipe represents a recipe.
type Recipe struct {
Name string
Config *composetypes.Config
}
// Dir retrieves the recipe repository path
func (r Recipe) Dir() string {
return path.Join(config.RECIPES_DIR, r.Name)
}
// UpdateLabel updates a recipe label
func (r Recipe) UpdateLabel(pattern, serviceName, label string) error {
fullPattern := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, r.Name, pattern)
if err := compose.UpdateLabel(fullPattern, serviceName, label, r.Name); err != nil {
return err
}
return nil
}
// UpdateTag updates a recipe tag
func (r Recipe) UpdateTag(image, tag string) error {
pattern := fmt.Sprintf("%s/%s/compose**yml", config.RECIPES_DIR, r.Name)
if err := compose.UpdateTag(pattern, image, tag, r.Name); 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
}
logrus.Debugf("detected %s as tags for recipe %s", strings.Join(tags, ", "), r.Name)
return tags, nil
}
// Get retrieves a recipe.
func Get(recipeName string) (Recipe, error) {
if err := EnsureExists(recipeName); err != nil {
return Recipe{}, err
}
pattern := fmt.Sprintf("%s/%s/compose**yml", config.RECIPES_DIR, recipeName)
composeFiles, err := filepath.Glob(pattern)
if err != nil {
return Recipe{}, err
}
if len(composeFiles) == 0 {
return Recipe{}, fmt.Errorf("%s is missing a compose.yml or compose.*.yml file?", recipeName)
}
envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample")
sampleEnv, err := config.ReadEnv(envSamplePath)
if err != nil {
return Recipe{}, err
}
opts := stack.Deploy{Composefiles: composeFiles}
config, err := loader.LoadComposefile(opts, sampleEnv)
if err != nil {
return Recipe{}, err
}
return Recipe{Name: recipeName, Config: config}, nil
}
// EnsureExists ensures that a recipe is locally cloned
func EnsureExists(recipeName string) error {
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
if _, err := os.Stat(recipeDir); os.IsNotExist(err) {
logrus.Debugf("%s does not exist, attemmpting to clone", recipeDir)
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, recipeName)
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 EnsureVersion(recipeName, version string) error {
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
isClean, err := gitPkg.IsClean(recipeName)
if err != nil {
return err
}
if !isClean {
return fmt.Errorf("%s has locally unstaged changes", recipeName)
}
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
}
logrus.Debugf("read %s as tags for recipe %s", strings.Join(parsedTags, ", "), recipeName)
if tagRef.String() == "" {
logrus.Warnf("no git tag discovered for %s, assuming unreleased recipe", recipeName)
return nil
}
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
}
logrus.Debugf("successfully checked %s out to %s in %s", recipeName, tagRef.Short(), recipeDir)
return nil
}
// EnsureLatest makes sure the latest commit is checked out for a local recipe repository
func EnsureLatest(recipeName string) error {
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
isClean, err := gitPkg.IsClean(recipeName)
if err != nil {
return err
}
if !isClean {
return fmt.Errorf("%s has locally unstaged changes", recipeName)
}
if err := gitPkg.EnsureGitRepo(recipeDir); err != nil {
return err
}
logrus.Debugf("attempting to open git repository in %s", recipeDir)
repo, err := git.PlainOpen(recipeDir)
if err != nil {
return err
}
worktree, err := repo.Worktree()
if err != nil {
return err
}
branch, err := gitPkg.GetCurrentBranch(repo)
if err != nil {
return err
}
checkOutOpts := &git.CheckoutOptions{
Create: false,
Force: true,
Branch: plumbing.ReferenceName(branch),
}
if err := worktree.Checkout(checkOutOpts); err != nil {
logrus.Debugf("failed to check out %s in %s", branch, recipeDir)
return err
}
return nil
}
// ChaosVersion constructs a chaos mode recipe version.
func ChaosVersion(recipeName string) (string, error) {
var version string
head, err := gitPkg.GetRecipeHead(recipeName)
if err != nil {
return version, err
}
version = head.String()[:8]
isClean, err := gitPkg.IsClean(recipeName)
if err != nil {
return version, err
}
if !isClean {
version = fmt.Sprintf("%s + unstaged changes", version)
}
return version, nil
}
// GetRecipesLocal retrieves all local recipe directories
func GetRecipesLocal() ([]string, error) {
var recipes []string
recipes, err := config.GetAllFoldersInDirectory(config.RECIPES_DIR)
if err != nil {
return recipes, err
}
return recipes, nil
}
// GetVersionLabelLocal retrieves the version label on the local recipe config
func GetVersionLabelLocal(recipe Recipe) (string, error) {
var label string
for _, service := range recipe.Config.Services {
for label, value := range service.Deploy.Labels {
if strings.HasPrefix(label, "coop-cloud") {
return value, nil
}
}
}
if label == "" {
return label, fmt.Errorf("%s has no version label? try running \"abra recipe sync %s\" first?", recipe.Name, recipe.Name)
}
return label, nil
}
// EnsureUpToDate ensures that the local repo is synced to the remote
func EnsureUpToDate(recipeName string) error {
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
repo, err := git.PlainOpen(recipeDir)
if err != nil {
return err
}
worktree, err := repo.Worktree()
if err != nil {
return err
}
branch, err := gitPkg.GetCurrentBranch(repo)
if err != nil {
return err
}
remotes, err := repo.Remotes()
if err != nil {
return err
}
if len(remotes) == 0 {
logrus.Debugf("cannot ensure %s is up-to-date, no git remotes configured", recipeName)
return nil
}
opts := &git.PullOptions{
ReferenceName: plumbing.ReferenceName(branch),
}
if err := worktree.Pull(opts); err != nil {
if !strings.Contains(err.Error(), "already up-to-date") {
return err
}
}
logrus.Debugf("fetched latest git changes for %s", recipeName)
return nil
}