package app import ( "encoding/json" "fmt" "sort" "strconv" "strings" "coopcloud.tech/abra/cli/internal" appPkg "coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/log" "coopcloud.tech/tagcmp" "github.com/spf13/cobra" ) type appStatus struct { Server string `json:"server"` Recipe string `json:"recipe"` AppName string `json:"appName"` Domain string `json:"domain"` Status string `json:"status"` Chaos string `json:"chaos"` ChaosVersion string `json:"chaosVersion"` AutoUpdate string `json:"autoUpdate"` Version string `json:"version"` Upgrade string `json:"upgrade"` } type serverStatus struct { Apps []appStatus `json:"apps"` AppCount int `json:"appCount"` VersionCount int `json:"versionCount"` UnversionedCount int `json:"unversionedCount"` LatestCount int `json:"latestCount"` UpgradeCount int `json:"upgradeCount"` } var AppListCommand = &cobra.Command{ Use: "list [flags]", Aliases: []string{"ls"}, Short: "List all managed apps", Long: `Generate a report of all managed apps. Use "--status/-S" flag to query all servers for the live deployment status.`, Example: ` # list apps of all servers without live status abra app ls # list apps of a specific server with live status abra app ls -s 1312.net -S # list apps of all servers which match a specific recipe abra app ls -r gitea`, Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { appFiles, err := appPkg.LoadAppFiles(listAppServer) if err != nil { log.Fatal(err) } apps, err := appPkg.GetApps(appFiles, recipeFilter) if err != nil { log.Fatal(err) } sort.Sort(appPkg.ByServerAndRecipe(apps)) statuses := make(map[string]map[string]string) if status { alreadySeen := make(map[string]bool) for _, app := range apps { if _, ok := alreadySeen[app.Server]; !ok { alreadySeen[app.Server] = true } } statuses, err = appPkg.GetAppStatuses(apps, internal.MachineReadable) if err != nil { log.Fatal(err) } } var totalServersCount int var totalAppsCount int allStats := make(map[string]serverStatus) for _, app := range apps { var stats serverStatus var ok bool if stats, ok = allStats[app.Server]; !ok { stats = serverStatus{} if recipeFilter == "" { // count server, no filtering totalServersCount++ } } if app.Recipe.Name == recipeFilter || recipeFilter == "" { if recipeFilter != "" { // only count server if matches filter totalServersCount++ } appStats := appStatus{} stats.AppCount++ totalAppsCount++ if status { status := "unknown" version := "unknown" chaos := "unknown" chaosVersion := "unknown" autoUpdate := "unknown" if statusMeta, ok := statuses[app.StackName()]; ok { if currentVersion, exists := statusMeta["version"]; exists { if currentVersion != "" { version = currentVersion } } if chaosDeploy, exists := statusMeta["chaos"]; exists { chaos = chaosDeploy } if chaosDeployVersion, exists := statusMeta["chaosVersion"]; exists { chaosVersion = chaosDeployVersion } if autoUpdateState, exists := statusMeta["autoUpdate"]; exists { autoUpdate = autoUpdateState } if statusMeta["status"] != "" { status = statusMeta["status"] } stats.VersionCount++ } else { stats.UnversionedCount++ } appStats.Status = status appStats.Chaos = chaos appStats.ChaosVersion = chaosVersion appStats.Version = version appStats.AutoUpdate = autoUpdate var newUpdates []string if version != "unknown" { if err := app.Recipe.EnsureExists(); err != nil { log.Fatalf("unable to clone %s: %s", app.Name, err) } updates, err := app.Recipe.Tags() if err != nil { log.Fatalf("unable to retrieve tags for %s: %s", app.Name, err) } parsedVersion, err := tagcmp.Parse(version) if err != nil { log.Fatal(err) } for _, update := range updates { parsedUpdate, err := tagcmp.Parse(update) if err != nil { log.Fatal(err) } if update != version && parsedUpdate.IsGreaterThan(parsedVersion) { newUpdates = append(newUpdates, update) } } } if len(newUpdates) == 0 { if version == "unknown" { appStats.Upgrade = "unknown" } else { appStats.Upgrade = "latest" stats.LatestCount++ } } else { newUpdates = internal.SortVersionsDesc(newUpdates) appStats.Upgrade = strings.Join(newUpdates, "\n") stats.UpgradeCount++ } } appStats.Server = app.Server appStats.Recipe = app.Recipe.Name appStats.AppName = app.Name appStats.Domain = app.Domain stats.Apps = append(stats.Apps, appStats) } allStats[app.Server] = stats } if internal.MachineReadable { jsonstring, err := json.Marshal(allStats) if err != nil { log.Fatal(err) } else { fmt.Println(string(jsonstring)) } return } alreadySeen := make(map[string]bool) for _, app := range apps { if _, ok := alreadySeen[app.Server]; ok { continue } serverStat := allStats[app.Server] headers := []string{"RECIPE", "DOMAIN", "SERVER"} if status { headers = append(headers, []string{ "STATUS", "CHAOS", "VERSION", "UPGRADE", "AUTOUPDATE"}..., ) } table, err := formatter.CreateTable() if err != nil { log.Fatal(err) } table.Headers(headers...) var rows [][]string for _, appStat := range serverStat.Apps { row := []string{appStat.Recipe, appStat.Domain, appStat.Server} if status { chaosStatus := appStat.Chaos if chaosStatus != "unknown" { chaosEnabled, err := strconv.ParseBool(chaosStatus) if err != nil { log.Fatal(err) } if chaosEnabled && appStat.ChaosVersion != "unknown" { chaosStatus = appStat.ChaosVersion } } row = append(row, []string{ appStat.Status, chaosStatus, appStat.Version, appStat.Upgrade, appStat.AutoUpdate}..., ) } rows = append(rows, row) } table.Rows(rows...) if len(rows) > 0 { if err := formatter.PrintTable(table); err != nil { log.Fatal(err) } if len(allStats) > 1 && len(rows) > 0 { fmt.Println() // newline separator for multiple servers } } alreadySeen[app.Server] = true } }, } var ( status bool recipeFilter string listAppServer string ) func init() { AppListCommand.Flags().BoolVarP( &status, "status", "S", false, "show app deployment status", ) AppListCommand.Flags().StringVarP( &recipeFilter, "recipe", "r", "", "show apps of a specific recipe", ) AppListCommand.RegisterFlagCompletionFunc( "recipe", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return autocomplete.RecipeNameComplete() }, ) AppListCommand.Flags().BoolVarP( &internal.MachineReadable, "machine", "m", false, "print machine-readable output", ) AppListCommand.Flags().StringVarP( &listAppServer, "server", "s", "", "show apps of a specific server", ) AppListCommand.RegisterFlagCompletionFunc( "server", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return autocomplete.ServerNameComplete() }, ) }