Files
abra/cli/catalogue/catalogue.go
decentral1se 4bfbc53b94
All checks were successful
continuous-integration/drone/push Build is passing
feat: support alias translation
See #627
2025-08-30 11:39:49 +02:00

323 lines
8.3 KiB
Go

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/<my-ssh-private-key-for-git-coopcloud-tech>`),
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"),
)
}