package catalogue import ( "encoding/json" "fmt" "io/ioutil" "os" "path" "slices" "strings" "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/i18n" "coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/recipe" "github.com/go-git/go-git/v5" "github.com/spf13/cobra" ) // translators: `abra catalogue sync` aliases. use a comma separated list of aliases with // no spaces in between var appCatalogueSyncAliases = i18n.G("s") var CatalogueSyncCommand = &cobra.Command{ // translators: `catalogue sync` command Use: i18n.G("sync [flags]"), Aliases: strings.Split(appCatalogueSyncAliases, ","), // translators: Short description for `catalogue sync` command Short: i18n.G("Sync recipe catalogue for latest changes"), Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { if err := catalogue.EnsureCatalogue(); err != nil { log.Fatal(err) } if err := catalogue.EnsureUpToDate(); err != nil { log.Fatal(err) } log.Info(i18n.G("catalogue successfully synced")) }, } // translators: `abra catalogue` aliases. use a comma separated list of aliases with // no spaces in between var appCatalogueAliases = i18n.G("g") var CatalogueGenerateCommand = &cobra.Command{ // translators: `catalogue generate` command Use: i18n.G("generate [recipe] [flags]"), Aliases: strings.Split(appCatalogueAliases, ","), // translators: Short description for `catalogue generate` command Short: i18n.G("Generate the recipe catalogue"), Long: i18n.G(`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. Publish 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. Enable ssh-agent and make sure to add your private key and enter your passphrase beforehand. eval ` + "`ssh-agent`" + ` ssh-add ~/.ssh/`), Example: ` # publish catalogue eval ` + "`ssh-agent`" + ` ssh-add ~/.ssh/id_ed25519 abra catalogue generate -p`, 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 os.Getenv("SSH_AUTH_SOCK") == "" { log.Warn(i18n.G("ssh: SSH_AUTH_SOCK missing, --publish/-p will fail. see \"abra catalogue generate --help\"")) } 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, i18n.G("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.Info(i18n.G("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.Fatal(i18n.G("no changes discovered in %s, nothing to publish?", cataloguePath)) } } msg := i18n.G("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.Info(i18n.G("new changes published: %s", url)) } if internal.Dry { log.Info(i18n.G("dry run: no changes published")) } }, } // CatalogueCommand defines the `abra catalogue` command and sub-commands. var CatalogueCommand = &cobra.Command{ // translators: `catalogue` command group Use: i18n.G("catalogue [cmd] [args] [flags]"), // translators: Short description for `catalogue` command group Short: i18n.G("Manage the recipe catalogue"), Aliases: []string{"c"}, } var ( publishChanges bool skipUpdates bool ) func init() { CatalogueGenerateCommand.Flags().BoolVarP( &publishChanges, i18n.G("publish"), i18n.G("p"), false, i18n.G("publish changes to git.coopcloud.tech"), ) CatalogueGenerateCommand.Flags().BoolVarP( &internal.Dry, i18n.G("dry-run"), i18n.G("r"), false, i18n.G("report changes that would be made"), ) CatalogueGenerateCommand.Flags().BoolVarP( &skipUpdates, i18n.G("skip-updates"), i18n.G("s"), false, i18n.G("skip updating recipe repositories"), ) CatalogueGenerateCommand.Flags().BoolVarP( &internal.Chaos, i18n.G("chaos"), i18n.G("C"), false, i18n.G("ignore uncommitted recipes changes"), ) }