diff --git a/cli/catalogue/generate.go b/cli/catalogue/generate.go index 74dedbbb..37c9bbde 100644 --- a/cli/catalogue/generate.go +++ b/cli/catalogue/generate.go @@ -2,6 +2,7 @@ package catalogue import ( "encoding/json" + "fmt" "io/ioutil" "path" @@ -45,11 +46,22 @@ var CatalogueSkipList = map[string]bool{ } var catalogueGenerateCommand = &cli.Command{ - Name: "generate", - Aliases: []string{"g"}, - Usage: "Generate a new copy of the catalogue", - ArgsUsage: "[]", - BashComplete: func(c *cli.Context) {}, + Name: "generate", + Aliases: []string{"g"}, + Usage: "Generate a new copy of the catalogue", + ArgsUsage: "[]", + BashComplete: func(c *cli.Context) { + catl, err := catalogue.ReadRecipeCatalogue() + if err != nil { + logrus.Warn(err) + } + if c.NArg() > 0 { + return + } + for name := range catl { + fmt.Println(name) + } + }, Action: func(c *cli.Context) error { recipeName := c.Args().First() @@ -60,18 +72,18 @@ var catalogueGenerateCommand = &cli.Command{ logrus.Debugf("ensuring '%v' recipe(s) are locally present and up-to-date", len(repos)) - bar := formatter.CreateProgressbar(len(repos), "retrieving recipes...") + retrieveBar := formatter.CreateProgressbar(len(repos), "retrieving recipes...") ch := make(chan string, len(repos)) for _, repoMeta := range repos { go func(rm catalogue.RepoMeta) { if recipeName != "" && recipeName != rm.Name { ch <- rm.Name - bar.Add(1) + retrieveBar.Add(1) return } if _, exists := CatalogueSkipList[rm.Name]; exists { ch <- rm.Name - bar.Add(1) + retrieveBar.Add(1) return } @@ -86,7 +98,7 @@ var catalogueGenerateCommand = &cli.Command{ } ch <- rm.Name - bar.Add(1) + retrieveBar.Add(1) }(repoMeta) } @@ -95,14 +107,23 @@ var catalogueGenerateCommand = &cli.Command{ } catl := make(catalogue.RecipeCatalogue) + catlBar := formatter.CreateProgressbar(len(repos), "generating catalogue...") for _, recipeMeta := range repos { if recipeName != "" && recipeName != recipeMeta.Name { + catlBar.Add(1) continue } + if _, exists := CatalogueSkipList[recipeMeta.Name]; exists { + catlBar.Add(1) continue } + versions, err := catalogue.GetRecipeVersions(recipeMeta.Name) + if err != nil { + logrus.Fatal(err) + } + catl[recipeMeta.Name] = catalogue.RecipeMeta{ Name: recipeMeta.Name, Repository: recipeMeta.CloneURL, @@ -110,10 +131,11 @@ var catalogueGenerateCommand = &cli.Command{ DefaultBranch: recipeMeta.DefaultBranch, Description: recipeMeta.Description, Website: recipeMeta.Website, - // Versions: ..., // FIXME: once the new versions work goes down + Versions: versions, // Category: ..., // FIXME: once we sort out the machine-readable catalogue interface // Features: ..., // FIXME: once we figure out the machine-readable catalogue interface } + catlBar.Add(1) } recipesJSON, err := json.MarshalIndent(catl, "", " ") @@ -125,7 +147,7 @@ var catalogueGenerateCommand = &cli.Command{ logrus.Fatal(err) } - logrus.Debugf("generated new recipe catalogue in '%s'", config.APPS_JSON) + logrus.Infof("generated new recipe catalogue in '%s'", config.APPS_JSON) return nil }, diff --git a/pkg/catalogue/catalogue.go b/pkg/catalogue/catalogue.go index ccd1b071..1b379327 100644 --- a/pkg/catalogue/catalogue.go +++ b/pkg/catalogue/catalogue.go @@ -9,12 +9,17 @@ import ( "io/ioutil" "net/http" "os" + "path" "strings" "time" + "coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/web" + "github.com/docker/distribution/reference" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" "github.com/sirupsen/logrus" ) @@ -56,17 +61,20 @@ type serviceMeta struct { Tag string `json:"tag"` } +// RecipeVersions are the versions associated with a recipe. +type RecipeVersions []map[tag]map[service]serviceMeta + // RecipeMeta represents metadata for a recipe in the abra catalogue. type RecipeMeta struct { - Category string `json:"category"` - DefaultBranch string `json:"default_branch"` - Description string `json:"description"` - Features features `json:"features"` - Icon string `json:"icon"` - Name string `json:"name"` - Repository string `json:"repository"` - Versions []map[tag]map[service]serviceMeta `json:"versions"` - Website string `json:"website"` + Category string `json:"category"` + DefaultBranch string `json:"default_branch"` + Description string `json:"description"` + Features features `json:"features"` + Icon string `json:"icon"` + Name string `json:"name"` + Repository string `json:"repository"` + Versions RecipeVersions `json:"versions"` + Website string `json:"website"` } // LatestVersion returns the latest version of a recipe. @@ -365,3 +373,110 @@ func ReadReposMetadata() (RepoCatalogue, error) { return reposMeta, nil } + +// GetRecipeVersions retrieves all recipe versions. +func GetRecipeVersions(recipeName string) (RecipeVersions, error) { + versions := RecipeVersions{} + + recipeDir := path.Join(config.ABRA_DIR, "apps", recipeName) + + logrus.Debugf("attempting to open git repository in '%s'", recipeDir) + + repo, err := git.PlainOpen(recipeDir) + if err != nil { + return versions, err + } + + worktree, err := repo.Worktree() + if err != nil { + logrus.Fatal(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/") + + logrus.Debugf("processing '%s' for '%s'", tag, recipeName) + + checkOutOpts := &git.CheckoutOptions{ + Create: false, + Force: true, + Keep: false, + Branch: plumbing.ReferenceName(ref.Name()), + } + if err := worktree.Checkout(checkOutOpts); err != nil { + logrus.Debugf("failed to check out '%s' in '%s'", tag, recipeDir) + return err + } + + logrus.Debugf("successfully checked out '%s' in '%s'", ref.Name(), recipeDir) + + recipe, err := recipe.Get(recipeName) + if err != nil { + return err + } + + versionMeta := make(map[string]serviceMeta) + for _, service := range recipe.Config.Services { + + img, err := reference.ParseNormalizedNamed(service.Image) + if err != nil { + return err + } + + path := reference.Path(img) + if strings.Contains(path, "library") { + path = strings.Split(path, "/")[1] + } + + digest, err := client.GetTagDigest(img) + if err != nil { + return err + } + + versionMeta[service.Name] = serviceMeta{ + Digest: digest, + Image: path, + Tag: img.(reference.NamedTagged).Tag(), + } + + logrus.Debugf("collecting digest: '%s', image: '%s', tag: '%s'", digest, path, tag) + } + + versions = append(versions, map[string]map[string]serviceMeta{tag: versionMeta}) + + return nil + }); err != nil { + return versions, 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'", recipeDir) + logrus.Fatal(err) + } + branch = "main" + } + + refName := fmt.Sprintf("refs/heads/%s", branch) + checkOutOpts := &git.CheckoutOptions{ + Create: false, + Force: true, + Keep: false, + Branch: plumbing.ReferenceName(refName), + } + if err := worktree.Checkout(checkOutOpts); err != nil { + logrus.Debugf("failed to check out '%s' in '%s'", branch, recipeDir) + logrus.Fatal(err) + } + + logrus.Debugf("switched back to '%s' in '%s'", branch, recipeDir) + logrus.Debugf("collected '%s' for '%s'", versions, recipeName) + + return versions, nil +}