package catalogue import ( "encoding/json" "fmt" "io/ioutil" "path" "slices" "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/catalogue" "coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/formatter" gitPkg "coopcloud.tech/abra/pkg/git" "coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/recipe" "github.com/go-git/go-git/v5" "github.com/spf13/cobra" ) var CatalogueGenerateCommand = &cobra.Command{ Use: "generate [recipe] [flags]", Aliases: []string{"g"}, Short: "Generate the recipe catalogue", Long: `Generate a new copy of the recipe catalogue. N.B. this command **will** wipe local unstaged changes from your local recipes if present. "--chaos/-C" on this command refers to the catalogue repository ("$ABRA_DIR/catalogue") and not the recipes. Please take care not to lose your changes. It is possible to generate new metadata for a single recipe by passing [recipe]. The existing local catalogue will be updated, not overwritten. It is quite easy to get rate limited by Docker Hub when running this command. If you have a Hub account you can "docker login" and Abra will automatically use those details. Push your new release to git.coopcloud.tech with "--publish/-p". This requires that you have permission to git push to these repositories and have your SSH keys configured on your account.`, Args: cobra.RangeArgs(0, 1), ValidArgsFunction: func( cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return autocomplete.RecipeNameComplete() }, Run: func(cmd *cobra.Command, args []string) { var recipeName string if len(args) > 0 { recipeName = args[0] } if recipeName != "" { internal.ValidateRecipe(args, cmd.Name()) } if err := catalogue.EnsureCatalogue(); err != nil { log.Fatal(err) } if !internal.Chaos { if err := catalogue.EnsureIsClean(); err != nil { log.Fatal(err) } } repos, err := recipe.ReadReposMetadata(internal.Debug) if err != nil { log.Fatal(err) } barLength := len(repos) if recipeName != "" { barLength = 1 } if !skipUpdates { if err := recipe.UpdateRepositories(repos, recipeName, internal.Debug); err != nil { log.Fatal(err) } } var warnings []string catl := make(recipe.RecipeCatalogue) catlBar := formatter.CreateProgressbar(barLength, "collecting catalogue metadata") for _, recipeMeta := range repos { if recipeName != "" && recipeName != recipeMeta.Name { if !internal.Debug { catlBar.Add(1) } continue } r := recipe.Get(recipeMeta.Name) versions, warnMsgs, err := r.GetRecipeVersions() if err != nil { warnings = append(warnings, err.Error()) } if len(warnMsgs) > 0 { warnings = append(warnings, warnMsgs...) } features, category, warnMsgs, err := recipe.GetRecipeFeaturesAndCategory(r) if err != nil { warnings = append(warnings, err.Error()) } if len(warnMsgs) > 0 { warnings = append(warnings, warnMsgs...) } catl[recipeMeta.Name] = recipe.RecipeMeta{ Name: recipeMeta.Name, Repository: recipeMeta.CloneURL, SSHURL: recipeMeta.SSHURL, Icon: recipeMeta.AvatarURL, DefaultBranch: recipeMeta.DefaultBranch, Description: recipeMeta.Description, Website: recipeMeta.Website, Versions: versions, Category: category, Features: features, } if !internal.Debug { catlBar.Add(1) } } if err := catlBar.Close(); err != nil { log.Fatal(err) } var uniqueWarnings []string for _, w := range warnings { if !slices.Contains(uniqueWarnings, w) { uniqueWarnings = append(uniqueWarnings, w) } } for _, warnMsg := range uniqueWarnings { log.Warn(warnMsg) } recipesJSON, err := json.MarshalIndent(catl, "", " ") if err != nil { log.Fatal(err) } if recipeName == "" { if err := ioutil.WriteFile(config.RECIPES_JSON, recipesJSON, 0764); err != nil { log.Fatal(err) } } else { catlFS, err := recipe.ReadRecipeCatalogue(internal.Offline) if err != nil { log.Fatal(err) } catlFS[recipeName] = catl[recipeName] updatedRecipesJSON, err := json.MarshalIndent(catlFS, "", " ") if err != nil { log.Fatal(err) } if err := ioutil.WriteFile(config.RECIPES_JSON, updatedRecipesJSON, 0764); err != nil { log.Fatal(err) } } log.Infof("generated recipe catalogue: %s", config.RECIPES_JSON) cataloguePath := path.Join(config.ABRA_DIR, "catalogue") if publishChanges { isClean, err := gitPkg.IsClean(cataloguePath) if err != nil { log.Fatal(err) } if isClean { if !internal.Dry { log.Fatalf("no changes discovered in %s, nothing to publish?", cataloguePath) } } msg := "chore: publish new catalogue release changes" if err := gitPkg.Commit(cataloguePath, msg, internal.Dry); err != nil { log.Fatal(err) } repo, err := git.PlainOpen(cataloguePath) if err != nil { log.Fatal(err) } sshURL := fmt.Sprintf(config.TOOLSHED_SSH_URL_TEMPLATE, config.CATALOGUE_JSON_REPO_NAME) if err := gitPkg.CreateRemote(repo, "origin-ssh", sshURL, internal.Dry); err != nil { log.Fatal(err) } if err := gitPkg.Push(cataloguePath, "origin-ssh", false, internal.Dry); err != nil { log.Fatal(err) } } repo, err := git.PlainOpen(cataloguePath) if err != nil { log.Fatal(err) } head, err := repo.Head() if err != nil { log.Fatal(err) } if !internal.Dry && publishChanges { url := fmt.Sprintf("%s/%s/commit/%s", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME, head.Hash()) log.Infof("new changes published: %s", url) } if internal.Dry { log.Info("dry run: no changes published") } }, } // CatalogueCommand defines the `abra catalogue` command and sub-commands. var CatalogueCommand = &cobra.Command{ Use: "catalogue [cmd] [args] [flags]", Short: "Manage the recipe catalogue", Aliases: []string{"c"}, } var ( publishChanges bool skipUpdates bool ) func init() { CatalogueGenerateCommand.Flags().BoolVarP( &publishChanges, "publish", "p", false, "publish changes to git.coopcloud.tech", ) CatalogueGenerateCommand.Flags().BoolVarP( &internal.Dry, "dry-run", "r", false, "report changes that would be made", ) CatalogueGenerateCommand.Flags().BoolVarP( &skipUpdates, "skip-updates", "s", false, "skip updating recipe repositories", ) CatalogueGenerateCommand.Flags().BoolVarP( &internal.Chaos, "chaos", "C", false, "ignore uncommitted recipes changes", ) }