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 } // UpdateLabel updates a recipe label func (r Recipe) UpdateLabel(serviceName, label string) error { pattern := fmt.Sprintf("%s/%s/compose**yml", config.APPS_DIR, r.Name) if err := compose.UpdateLabel(pattern, 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.APPS_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 recipeDir := path.Join(config.ABRA_DIR, "apps", r.Name) repo, err := git.PlainOpen(recipeDir) 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.APPS_DIR, recipeName) composeFiles, err := filepath.Glob(pattern) if err != nil { return Recipe{}, err } envSamplePath := path.Join(config.ABRA_DIR, "apps", recipeName, ".env.sample") sampleEnv, err := config.ReadEnv(envSamplePath) if err != nil { logrus.Fatal(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(recipe string) error { recipeDir := path.Join(config.ABRA_DIR, "apps", strings.ToLower(recipe)) 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, recipe) if err := gitPkg.Clone(recipeDir, url); 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.ABRA_DIR, "apps", recipeName) isClean, err := gitPkg.IsClean(recipeName) if err != nil { return err } if !isClean { return fmt.Errorf("'%s' has locally unstaged changes", recipeName) } 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() == "" { return fmt.Errorf("%s is not available?", 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 } logrus.Debugf("successfully checked '%s' out to '%s' in '%s'", recipeName, tagRef.Short(), recipeDir) return nil } // EnsureLatest makes sure the latest commit is checkout on for a local recipe repository. func EnsureLatest(recipeName string) error { recipeDir := path.Join(config.ABRA_DIR, "apps", recipeName) isClean, err := gitPkg.IsClean(recipeName) if err != nil { return err } if !isClean { return fmt.Errorf("'%s' has locally unstaged changes", recipeName) } 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 := "master" if _, err := repo.Branch("master"); err != nil { if _, err := repo.Branch("main"); err != nil { logrus.Debugf("failed to select branch in '%s'", path.Join(config.APPS_DIR, recipeName)) return err } branch = "main" } refName := fmt.Sprintf("refs/heads/%s", branch) checkOutOpts := &git.CheckoutOptions{ Create: false, Force: true, Branch: plumbing.ReferenceName(refName), } 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 }