forked from toolshed/abra
		
	refactor: tablewriter -> lipgloss
Also the jsontable impl. is dropped also. Output is unchanged.
This commit is contained in:
		@ -1,11 +1,14 @@
 | 
			
		||||
package app
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"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"
 | 
			
		||||
	"github.com/charmbracelet/lipgloss"
 | 
			
		||||
	"github.com/urfave/cli"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -40,8 +43,21 @@ ${FOO:<default>} syntax). "check" does not confirm or deny this for you.`,
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tableCol := []string{"recipe env sample", "app env"}
 | 
			
		||||
		table := formatter.CreateTable(tableCol)
 | 
			
		||||
		table, err := formatter.CreateTable2()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		table.
 | 
			
		||||
			Headers("RECIPE ENV SAMPLE", "APP ENV").
 | 
			
		||||
			StyleFunc(func(row, col int) lipgloss.Style {
 | 
			
		||||
				switch {
 | 
			
		||||
				case col == 1:
 | 
			
		||||
					return lipgloss.NewStyle().Padding(0, 1, 0, 1).Align(lipgloss.Center)
 | 
			
		||||
				default:
 | 
			
		||||
					return lipgloss.NewStyle().Padding(0, 1, 0, 1)
 | 
			
		||||
				}
 | 
			
		||||
			})
 | 
			
		||||
 | 
			
		||||
		envVars, err := appPkg.CheckEnv(app)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@ -50,13 +66,15 @@ ${FOO:<default>} syntax). "check" does not confirm or deny this for you.`,
 | 
			
		||||
 | 
			
		||||
		for _, envVar := range envVars {
 | 
			
		||||
			if envVar.Present {
 | 
			
		||||
				table.Append([]string{envVar.Name, "✅"})
 | 
			
		||||
				val := []string{envVar.Name, "✅"}
 | 
			
		||||
				table.Row(val...)
 | 
			
		||||
			} else {
 | 
			
		||||
				table.Append([]string{envVar.Name, "❌"})
 | 
			
		||||
				val := []string{envVar.Name, "❌"}
 | 
			
		||||
				table.Row(val...)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		table.Render()
 | 
			
		||||
		fmt.Println(table)
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
@ -239,15 +239,27 @@ can take some time.`,
 | 
			
		||||
 | 
			
		||||
			serverStat := allStats[app.Server]
 | 
			
		||||
 | 
			
		||||
			tableCol := []string{"recipe", "domain"}
 | 
			
		||||
			headers := []string{"RECIPE", "DOMAIN"}
 | 
			
		||||
			if status {
 | 
			
		||||
				tableCol = append(tableCol, []string{"status", "chaos", "version", "upgrade", "autoupdate"}...)
 | 
			
		||||
				headers = append(headers, []string{
 | 
			
		||||
					"STATUS",
 | 
			
		||||
					"CHAOS",
 | 
			
		||||
					"VERSION",
 | 
			
		||||
					"UPGRADE",
 | 
			
		||||
					"AUTOUPDATE"}...,
 | 
			
		||||
				)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			table := formatter.CreateTable(tableCol)
 | 
			
		||||
			table, err := formatter.CreateTable2()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			table.Headers(headers...)
 | 
			
		||||
 | 
			
		||||
			var rows [][]string
 | 
			
		||||
			for _, appStat := range serverStat.Apps {
 | 
			
		||||
				tableRow := []string{appStat.Recipe, appStat.Domain}
 | 
			
		||||
				row := []string{appStat.Recipe, appStat.Domain}
 | 
			
		||||
				if status {
 | 
			
		||||
					chaosStatus := appStat.Chaos
 | 
			
		||||
					if chaosStatus != "unknown" {
 | 
			
		||||
@ -259,17 +271,27 @@ can take some time.`,
 | 
			
		||||
							chaosStatus = appStat.ChaosVersion
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
					tableRow = append(tableRow, []string{appStat.Status, chaosStatus, appStat.Version, appStat.Upgrade, appStat.AutoUpdate}...)
 | 
			
		||||
 | 
			
		||||
					row = append(row, []string{
 | 
			
		||||
						appStat.Status,
 | 
			
		||||
						chaosStatus,
 | 
			
		||||
						appStat.Version,
 | 
			
		||||
						appStat.Upgrade,
 | 
			
		||||
						appStat.AutoUpdate}...,
 | 
			
		||||
					)
 | 
			
		||||
				}
 | 
			
		||||
				table.Append(tableRow)
 | 
			
		||||
 | 
			
		||||
				rows = append(rows, row)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if table.NumLines() > 0 {
 | 
			
		||||
				table.Render()
 | 
			
		||||
			table.Rows(rows...)
 | 
			
		||||
 | 
			
		||||
			if len(rows) > 0 {
 | 
			
		||||
				fmt.Println(table)
 | 
			
		||||
 | 
			
		||||
				if status {
 | 
			
		||||
					fmt.Println(fmt.Sprintf(
 | 
			
		||||
						"server: %s | total apps: %v | versioned: %v | unversioned: %v | latest: %v | upgrade: %v",
 | 
			
		||||
						"SERVER: %s | TOTAL APPS: %v | VERSIONED: %v | UNVERSIONED: %v | LATEST : %v | UPGRADE: %v",
 | 
			
		||||
						app.Server,
 | 
			
		||||
						serverStat.AppCount,
 | 
			
		||||
						serverStat.VersionCount,
 | 
			
		||||
@ -278,19 +300,21 @@ can take some time.`,
 | 
			
		||||
						serverStat.UpgradeCount,
 | 
			
		||||
					))
 | 
			
		||||
				} else {
 | 
			
		||||
					fmt.Println(fmt.Sprintf("server: %s | total apps: %v", app.Server, serverStat.AppCount))
 | 
			
		||||
					log.Infof("SERVER: %s TOTAL APPS: %v", app.Server, serverStat.AppCount)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if len(allStats) > 1 && table.NumLines() > 0 {
 | 
			
		||||
				fmt.Println() // newline separator for multiple servers
 | 
			
		||||
				if len(allStats) > 1 && len(rows) > 0 {
 | 
			
		||||
					fmt.Println() // newline separator for multiple servers
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			alreadySeen[app.Server] = true
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if len(allStats) > 1 {
 | 
			
		||||
			fmt.Println(fmt.Sprintf("total servers: %v | total apps: %v ", totalServersCount, totalAppsCount))
 | 
			
		||||
			totalServers := formatter.BoldStyle.Render("TOTAL SERVERS")
 | 
			
		||||
			totalApps := formatter.BoldStyle.Render("TOTAL APPS")
 | 
			
		||||
			log.Infof("%s: %v | %s: %v ", totalServers, totalServersCount, totalApps, totalAppsCount)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
 | 
			
		||||
@ -9,11 +9,11 @@ import (
 | 
			
		||||
	"coopcloud.tech/abra/pkg/client"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/config"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/formatter"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/jsontable"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/log"
 | 
			
		||||
	recipePkg "coopcloud.tech/abra/pkg/recipe"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/secret"
 | 
			
		||||
	"github.com/AlecAivazis/survey/v2"
 | 
			
		||||
	"github.com/charmbracelet/lipgloss/table"
 | 
			
		||||
	dockerClient "github.com/docker/docker/client"
 | 
			
		||||
	"github.com/urfave/cli"
 | 
			
		||||
)
 | 
			
		||||
@ -127,7 +127,7 @@ var appNewCommand = cli.Command{
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var secrets AppSecrets
 | 
			
		||||
		var secretTable *jsontable.JSONTable
 | 
			
		||||
		var secretsTable *table.Table
 | 
			
		||||
		if internal.Secrets {
 | 
			
		||||
			sampleEnv, err := recipe.SampleEnv()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
@ -158,10 +158,16 @@ var appNewCommand = cli.Command{
 | 
			
		||||
				log.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			secretCols := []string{"Name", "Value"}
 | 
			
		||||
			secretTable = formatter.CreateTable(secretCols)
 | 
			
		||||
			secretsTable, err = formatter.CreateTable2()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			headers := []string{"NAME", "VALUE"}
 | 
			
		||||
			secretsTable.Headers(headers...)
 | 
			
		||||
 | 
			
		||||
			for name, val := range secrets {
 | 
			
		||||
				secretTable.Append([]string{name, val})
 | 
			
		||||
				secretsTable.Row(name, val)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@ -169,14 +175,20 @@ var appNewCommand = cli.Command{
 | 
			
		||||
			internal.NewAppServer = "local"
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tableCol := []string{"server", "recipe", "domain"}
 | 
			
		||||
		table := formatter.CreateTable(tableCol)
 | 
			
		||||
		table.Append([]string{internal.NewAppServer, recipe.Name, internal.Domain})
 | 
			
		||||
		table, err := formatter.CreateTable2()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		headers := []string{"SERVER", "RECIPE", "DOMAIN"}
 | 
			
		||||
		table.Headers(headers...)
 | 
			
		||||
 | 
			
		||||
		table.Row(internal.NewAppServer, recipe.Name, internal.Domain)
 | 
			
		||||
 | 
			
		||||
		log.Infof("new app '%s' created 🌞", recipe.Name)
 | 
			
		||||
 | 
			
		||||
		fmt.Println("")
 | 
			
		||||
		table.Render()
 | 
			
		||||
		fmt.Println(table)
 | 
			
		||||
		fmt.Println("")
 | 
			
		||||
 | 
			
		||||
		fmt.Println("Configure this app:")
 | 
			
		||||
@ -190,8 +202,13 @@ var appNewCommand = cli.Command{
 | 
			
		||||
			fmt.Println("")
 | 
			
		||||
			fmt.Println("Generated secrets:")
 | 
			
		||||
			fmt.Println("")
 | 
			
		||||
			secretTable.Render()
 | 
			
		||||
			log.Warn("generated secrets are not shown again, please take note of them NOW")
 | 
			
		||||
			fmt.Println(secretsTable)
 | 
			
		||||
 | 
			
		||||
			log.Warnf(
 | 
			
		||||
				"generated secrets %s shown again, please take note of them %s",
 | 
			
		||||
				formatter.BoldStyle.Render("NOT"),
 | 
			
		||||
				formatter.BoldStyle.Render("NOW"),
 | 
			
		||||
			)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
 | 
			
		||||
@ -94,7 +94,7 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var tablerows [][]string
 | 
			
		||||
	var rows [][]string
 | 
			
		||||
	allContainerStats := make(map[string]map[string]string)
 | 
			
		||||
	for _, service := range compose.Services {
 | 
			
		||||
		filters := filters.NewArgs()
 | 
			
		||||
@ -109,8 +109,6 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
 | 
			
		||||
		var containerStats map[string]string
 | 
			
		||||
		if len(containers) == 0 {
 | 
			
		||||
			containerStats = map[string]string{
 | 
			
		||||
				"version": deployedVersion,
 | 
			
		||||
				"chaos":   chaosVersion,
 | 
			
		||||
				"service": service.Name,
 | 
			
		||||
				"image":   "unknown",
 | 
			
		||||
				"created": "unknown",
 | 
			
		||||
@ -121,8 +119,6 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
 | 
			
		||||
		} else {
 | 
			
		||||
			container := containers[0]
 | 
			
		||||
			containerStats = map[string]string{
 | 
			
		||||
				"version": deployedVersion,
 | 
			
		||||
				"chaos":   chaosVersion,
 | 
			
		||||
				"service": abraService.ContainerToServiceName(container.Names, app.StackName()),
 | 
			
		||||
				"image":   formatter.RemoveSha(container.Image),
 | 
			
		||||
				"created": formatter.HumanDuration(container.Created),
 | 
			
		||||
@ -134,9 +130,7 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
 | 
			
		||||
 | 
			
		||||
		allContainerStats[containerStats["service"]] = containerStats
 | 
			
		||||
 | 
			
		||||
		tablerow := []string{
 | 
			
		||||
			deployedVersion,
 | 
			
		||||
			chaosVersion,
 | 
			
		||||
		row := []string{
 | 
			
		||||
			containerStats["service"],
 | 
			
		||||
			containerStats["image"],
 | 
			
		||||
			containerStats["created"],
 | 
			
		||||
@ -145,25 +139,37 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
 | 
			
		||||
			containerStats["ports"],
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tablerows = append(tablerows, tablerow)
 | 
			
		||||
		rows = append(rows, row)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if internal.MachineReadable {
 | 
			
		||||
		jsonstring, err := json.Marshal(allContainerStats)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
			log.Fatal("unable to convert to JSON: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		fmt.Println(string(jsonstring))
 | 
			
		||||
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tableCol := []string{"version", "chaos", "service", "image", "created", "status", "state", "ports"}
 | 
			
		||||
	table := formatter.CreateTable(tableCol)
 | 
			
		||||
	for _, row := range tablerows {
 | 
			
		||||
		table.Append(row)
 | 
			
		||||
	table, err := formatter.CreateTable2()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	table.SetAutoMergeCellsByColumnIndex([]int{0, 1})
 | 
			
		||||
	table.Render()
 | 
			
		||||
 | 
			
		||||
	headers := []string{
 | 
			
		||||
		"SERVICE",
 | 
			
		||||
		"IMAGE",
 | 
			
		||||
		"CREATED",
 | 
			
		||||
		"STATUS",
 | 
			
		||||
		"STATE",
 | 
			
		||||
		"PORTS",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	table.
 | 
			
		||||
		Headers(headers...).
 | 
			
		||||
		Rows(rows...)
 | 
			
		||||
 | 
			
		||||
	fmt.Println(table)
 | 
			
		||||
 | 
			
		||||
	log.Infof("VERSION: %s CHAOS: %s", deployedVersion, chaosVersion)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -49,12 +49,14 @@ flag.`,
 | 
			
		||||
		app := internal.ValidateApp(c)
 | 
			
		||||
 | 
			
		||||
		if !internal.Force && !internal.NoInput {
 | 
			
		||||
			log.Warnf("ALERTA ALERTA: this will completely remove %s data and config locally and remotely", app.Name)
 | 
			
		||||
 | 
			
		||||
			response := false
 | 
			
		||||
			msg := "ALERTA ALERTA: this will completely remove %s data and configurations locally and remotely, are you sure?"
 | 
			
		||||
			prompt := &survey.Confirm{Message: fmt.Sprintf(msg, app.Name)}
 | 
			
		||||
			prompt := &survey.Confirm{Message: "are you sure?"}
 | 
			
		||||
			if err := survey.AskOne(prompt, &response); err != nil {
 | 
			
		||||
				log.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if !response {
 | 
			
		||||
				log.Fatal("aborting as requested")
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
@ -115,18 +115,37 @@ var appSecretGenerateCommand = cli.Command{
 | 
			
		||||
			os.Exit(1)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tableCol := []string{"name", "value"}
 | 
			
		||||
		table := formatter.CreateTable(tableCol)
 | 
			
		||||
		headers := []string{"NAME", "VALUE"}
 | 
			
		||||
		table, err := formatter.CreateTable2()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		table.Headers(headers...)
 | 
			
		||||
 | 
			
		||||
		var rows [][]string
 | 
			
		||||
		for name, val := range secretVals {
 | 
			
		||||
			table.Append([]string{name, val})
 | 
			
		||||
			row := []string{name, val}
 | 
			
		||||
			rows = append(rows, row)
 | 
			
		||||
			table.Row(row...)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if internal.MachineReadable {
 | 
			
		||||
			table.JSONRender()
 | 
			
		||||
		} else {
 | 
			
		||||
			table.Render()
 | 
			
		||||
			out, err := formatter.ToJSON(headers, rows)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Fatal("unable to render to JSON: %s", err)
 | 
			
		||||
			}
 | 
			
		||||
			fmt.Println(out)
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		log.Warn("generated secrets are not shown again, please take note of them NOW")
 | 
			
		||||
 | 
			
		||||
		fmt.Println(table)
 | 
			
		||||
 | 
			
		||||
		log.Warnf(
 | 
			
		||||
			"generated secrets %s shown again, please take note of them %s",
 | 
			
		||||
			formatter.BoldStyle.Render("NOT"),
 | 
			
		||||
			formatter.BoldStyle.Render("NOW"),
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
	},
 | 
			
		||||
@ -345,34 +364,48 @@ var appSecretLsCommand = cli.Command{
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tableCol := []string{"Name", "Version", "Generated Name", "Created On Server"}
 | 
			
		||||
		table := formatter.CreateTable(tableCol)
 | 
			
		||||
		headers := []string{"NAME", "VERSION", "GENERATED NAME", "CREATED ON SERVER"}
 | 
			
		||||
		table, err := formatter.CreateTable2()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		table.Headers(headers...)
 | 
			
		||||
 | 
			
		||||
		secStats, err := secret.PollSecretsStatus(cl, app)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var rows [][]string
 | 
			
		||||
		for _, secStat := range secStats {
 | 
			
		||||
			tableRow := []string{
 | 
			
		||||
			row := []string{
 | 
			
		||||
				secStat.LocalName,
 | 
			
		||||
				secStat.Version,
 | 
			
		||||
				secStat.RemoteName,
 | 
			
		||||
				strconv.FormatBool(secStat.CreatedOnRemote),
 | 
			
		||||
			}
 | 
			
		||||
			table.Append(tableRow)
 | 
			
		||||
 | 
			
		||||
			rows = append(rows, row)
 | 
			
		||||
			table.Row(row...)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if table.NumLines() > 0 {
 | 
			
		||||
		if len(rows) > 0 {
 | 
			
		||||
			if internal.MachineReadable {
 | 
			
		||||
				table.JSONRender()
 | 
			
		||||
			} else {
 | 
			
		||||
				table.Render()
 | 
			
		||||
				out, err := formatter.ToJSON(headers, rows)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					log.Fatal("unable to render to JSON: %s", err)
 | 
			
		||||
				}
 | 
			
		||||
				fmt.Println(out)
 | 
			
		||||
				return nil
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			log.Warnf("no secrets stored for %s", app.Name)
 | 
			
		||||
 | 
			
		||||
			fmt.Println(table)
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		log.Warnf("no secrets stored for %s", app.Name)
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -56,9 +56,15 @@ var appServicesCommand = cli.Command{
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tableCol := []string{"service name", "image"}
 | 
			
		||||
		table := formatter.CreateTable(tableCol)
 | 
			
		||||
		table, err := formatter.CreateTable2()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		headers := []string{"SERVICE (SHORT)", "SERVICE (LONG)", "IMAGE"}
 | 
			
		||||
		table.Headers(headers...)
 | 
			
		||||
 | 
			
		||||
		var rows [][]string
 | 
			
		||||
		for _, container := range containers {
 | 
			
		||||
			var containerNames []string
 | 
			
		||||
			for _, containerName := range container.Names {
 | 
			
		||||
@ -69,14 +75,20 @@ var appServicesCommand = cli.Command{
 | 
			
		||||
			serviceShortName := service.ContainerToServiceName(container.Names, app.StackName())
 | 
			
		||||
			serviceLongName := fmt.Sprintf("%s_%s", app.StackName(), serviceShortName)
 | 
			
		||||
 | 
			
		||||
			tableRow := []string{
 | 
			
		||||
			row := []string{
 | 
			
		||||
				serviceShortName,
 | 
			
		||||
				serviceLongName,
 | 
			
		||||
				formatter.RemoveSha(container.Image),
 | 
			
		||||
			}
 | 
			
		||||
			table.Append(tableRow)
 | 
			
		||||
 | 
			
		||||
			rows = append(rows, row)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		table.Render()
 | 
			
		||||
		table.Rows(rows...)
 | 
			
		||||
 | 
			
		||||
		if len(rows) > 0 {
 | 
			
		||||
			fmt.Println(table)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ package app
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"coopcloud.tech/abra/cli/internal"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/autocomplete"
 | 
			
		||||
@ -37,26 +38,35 @@ var appVolumeListCommand = cli.Command{
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		volumeList, err := client.GetVolumes(cl, context.Background(), app.Server, filters)
 | 
			
		||||
		volumes, err := client.GetVolumes(cl, context.Background(), app.Server, filters)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		table := formatter.CreateTable([]string{"name", "created", "mounted"})
 | 
			
		||||
		var volTable [][]string
 | 
			
		||||
		for _, volume := range volumeList {
 | 
			
		||||
			volRow := []string{volume.Name, volume.CreatedAt, volume.Mountpoint}
 | 
			
		||||
			volTable = append(volTable, volRow)
 | 
			
		||||
		headers := []string{"name", "created", "mounted"}
 | 
			
		||||
 | 
			
		||||
		table, err := formatter.CreateTable2()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		table.AppendBulk(volTable)
 | 
			
		||||
		table.Headers(headers...)
 | 
			
		||||
 | 
			
		||||
		if table.NumLines() > 0 {
 | 
			
		||||
			table.Render()
 | 
			
		||||
		} else {
 | 
			
		||||
			log.Warnf("no volumes created for %s", app.Name)
 | 
			
		||||
		var rows [][]string
 | 
			
		||||
		for _, volume := range volumes {
 | 
			
		||||
			row := []string{volume.Name, volume.CreatedAt, volume.Mountpoint}
 | 
			
		||||
			rows = append(rows, row)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		table.Rows(rows...)
 | 
			
		||||
 | 
			
		||||
		if len(rows) > 0 {
 | 
			
		||||
			fmt.Println(table)
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		log.Warnf("no volumes created for %s", app.Name)
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -32,11 +32,25 @@ var recipeLintCommand = cli.Command{
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tableCol := []string{"ref", "rule", "severity", "satisfied", "skipped", "resolve"}
 | 
			
		||||
		table := formatter.CreateTable(tableCol)
 | 
			
		||||
		headers := []string{
 | 
			
		||||
			"ref",
 | 
			
		||||
			"rule",
 | 
			
		||||
			"severity",
 | 
			
		||||
			"satisfied",
 | 
			
		||||
			"skipped",
 | 
			
		||||
			"resolve",
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		table, err := formatter.CreateTable2()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		table.Headers(headers...)
 | 
			
		||||
 | 
			
		||||
		hasError := false
 | 
			
		||||
		bar := formatter.CreateProgressbar(-1, "running recipe lint rules...")
 | 
			
		||||
		var rows [][]string
 | 
			
		||||
		var warnMessages []string
 | 
			
		||||
		for level := range lint.LintRules {
 | 
			
		||||
			for _, rule := range lint.LintRules[level] {
 | 
			
		||||
				if internal.OnlyErrors && rule.Level != "error" {
 | 
			
		||||
@ -58,7 +72,7 @@ var recipeLintCommand = cli.Command{
 | 
			
		||||
				if !skipped {
 | 
			
		||||
					ok, err := rule.Function(recipe)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						log.Warn(err)
 | 
			
		||||
						warnMessages = append(warnMessages, err.Error())
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					if !ok && rule.Level == "error" {
 | 
			
		||||
@ -78,26 +92,30 @@ var recipeLintCommand = cli.Command{
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				table.Append([]string{
 | 
			
		||||
				row := []string{
 | 
			
		||||
					rule.Ref,
 | 
			
		||||
					rule.Description,
 | 
			
		||||
					rule.Level,
 | 
			
		||||
					satisfiedOutput,
 | 
			
		||||
					skippedOutput,
 | 
			
		||||
					rule.HowToResolve,
 | 
			
		||||
				})
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				bar.Add(1)
 | 
			
		||||
				rows = append(rows, row)
 | 
			
		||||
				table.Row(row...)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if table.NumLines() > 0 {
 | 
			
		||||
			fmt.Println()
 | 
			
		||||
			table.Render()
 | 
			
		||||
		}
 | 
			
		||||
		if len(rows) > 0 {
 | 
			
		||||
			fmt.Println(table)
 | 
			
		||||
 | 
			
		||||
		if hasError {
 | 
			
		||||
			log.Warn("watch out, some critical errors are present in your recipe config")
 | 
			
		||||
			for _, warnMsg := range warnMessages {
 | 
			
		||||
				log.Warn(warnMsg)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if hasError {
 | 
			
		||||
				log.Warnf("critical errors present in %s config", recipe.Name)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
 | 
			
		||||
@ -41,12 +41,27 @@ var recipeListCommand = cli.Command{
 | 
			
		||||
		recipes := catl.Flatten()
 | 
			
		||||
		sort.Sort(recipe.ByRecipeName(recipes))
 | 
			
		||||
 | 
			
		||||
		tableCol := []string{"name", "category", "status", "healthcheck", "backups", "email", "tests", "SSO"}
 | 
			
		||||
		table := formatter.CreateTable(tableCol)
 | 
			
		||||
		table, err := formatter.CreateTable2()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		len := 0
 | 
			
		||||
		headers := []string{
 | 
			
		||||
			"name",
 | 
			
		||||
			"category",
 | 
			
		||||
			"status",
 | 
			
		||||
			"healthcheck",
 | 
			
		||||
			"backups",
 | 
			
		||||
			"email",
 | 
			
		||||
			"tests",
 | 
			
		||||
			"SSO",
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		table.Headers(headers...)
 | 
			
		||||
 | 
			
		||||
		var rows [][]string
 | 
			
		||||
		for _, recipe := range recipes {
 | 
			
		||||
			tableRow := []string{
 | 
			
		||||
			row := []string{
 | 
			
		||||
				recipe.Name,
 | 
			
		||||
				recipe.Category,
 | 
			
		||||
				strconv.Itoa(recipe.Features.Status),
 | 
			
		||||
@ -59,23 +74,27 @@ var recipeListCommand = cli.Command{
 | 
			
		||||
 | 
			
		||||
			if pattern != "" {
 | 
			
		||||
				if strings.Contains(recipe.Name, pattern) {
 | 
			
		||||
					table.Append(tableRow)
 | 
			
		||||
					len++
 | 
			
		||||
					table.Row(row...)
 | 
			
		||||
					rows = append(rows, row)
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				table.Append(tableRow)
 | 
			
		||||
				len++
 | 
			
		||||
				table.Row(row...)
 | 
			
		||||
				rows = append(rows, row)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if table.NumLines() > 0 {
 | 
			
		||||
		if len(rows) > 0 {
 | 
			
		||||
			if internal.MachineReadable {
 | 
			
		||||
				table.SetCaption(false, "")
 | 
			
		||||
				table.JSONRender()
 | 
			
		||||
			} else {
 | 
			
		||||
				table.SetCaption(true, fmt.Sprintf("total recipes: %v", len))
 | 
			
		||||
				table.Render()
 | 
			
		||||
				out, err := formatter.ToJSON(headers, rows)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					log.Fatal("unable to render to JSON: %s", err)
 | 
			
		||||
				}
 | 
			
		||||
				fmt.Println(out)
 | 
			
		||||
				return nil
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			fmt.Println(table)
 | 
			
		||||
			log.Infof("total recipes: %v", len(rows))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,6 @@ import (
 | 
			
		||||
	"coopcloud.tech/abra/pkg/formatter"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/log"
 | 
			
		||||
	recipePkg "coopcloud.tech/abra/pkg/recipe"
 | 
			
		||||
	"github.com/olekukonko/tablewriter"
 | 
			
		||||
	"github.com/urfave/cli"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -37,6 +36,8 @@ var recipeVersionCommand = cli.Command{
 | 
			
		||||
	Before:       internal.SubCommandBefore,
 | 
			
		||||
	BashComplete: autocomplete.RecipeNameComplete,
 | 
			
		||||
	Action: func(c *cli.Context) error {
 | 
			
		||||
		var warnMessages []string
 | 
			
		||||
 | 
			
		||||
		recipe := internal.ValidateRecipe(c)
 | 
			
		||||
 | 
			
		||||
		catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline)
 | 
			
		||||
@ -46,47 +47,63 @@ var recipeVersionCommand = cli.Command{
 | 
			
		||||
 | 
			
		||||
		recipeMeta, ok := catl[recipe.Name]
 | 
			
		||||
		if !ok {
 | 
			
		||||
			log.Warn("no published versions in catalogue, trying local recipe repository")
 | 
			
		||||
			warnMessages = append(warnMessages, "retrieved versions from local recipe repository")
 | 
			
		||||
 | 
			
		||||
			recipeVersions, err := recipe.GetRecipeVersions()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Warn(err)
 | 
			
		||||
				warnMessages = append(warnMessages, err.Error())
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			recipeMeta = recipePkg.RecipeMeta{Versions: recipeVersions}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if len(recipeMeta.Versions) == 0 {
 | 
			
		||||
			log.Fatalf("%s has no catalogue published versions?", recipe.Name)
 | 
			
		||||
			log.Fatalf("%s has no published versions?", recipe.Name)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tableCols := []string{"version", "service", "image", "tag"}
 | 
			
		||||
		aggregated_table := formatter.CreateTable(tableCols)
 | 
			
		||||
		var allRows [][]string
 | 
			
		||||
		for i := len(recipeMeta.Versions) - 1; i >= 0; i-- {
 | 
			
		||||
			table := formatter.CreateTable(tableCols)
 | 
			
		||||
			table, err := formatter.CreateTable2()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			table.Headers("SERVICE", "NAME", "TAG")
 | 
			
		||||
 | 
			
		||||
			for version, meta := range recipeMeta.Versions[i] {
 | 
			
		||||
				var versions [][]string
 | 
			
		||||
				var rows [][]string
 | 
			
		||||
 | 
			
		||||
				for service, serviceMeta := range meta {
 | 
			
		||||
					versions = append(versions, []string{version, service, serviceMeta.Image, serviceMeta.Tag})
 | 
			
		||||
					rows = append(rows, []string{service, serviceMeta.Image, serviceMeta.Tag})
 | 
			
		||||
					allRows = append(allRows, []string{version, service, serviceMeta.Image, serviceMeta.Tag})
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				sort.Slice(versions, sortServiceByName(versions))
 | 
			
		||||
				sort.Slice(rows, sortServiceByName(rows))
 | 
			
		||||
 | 
			
		||||
				for _, version := range versions {
 | 
			
		||||
					table.Append(version)
 | 
			
		||||
					aggregated_table.Append(version)
 | 
			
		||||
				}
 | 
			
		||||
				table.Rows(rows...)
 | 
			
		||||
 | 
			
		||||
				if !internal.MachineReadable {
 | 
			
		||||
					table.SetAutoMergeCellsByColumnIndex([]int{0})
 | 
			
		||||
					table.SetAlignment(tablewriter.ALIGN_LEFT)
 | 
			
		||||
					table.Render()
 | 
			
		||||
					fmt.Println()
 | 
			
		||||
					fmt.Println(table)
 | 
			
		||||
					log.Infof("VERSION: %s", version)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !internal.MachineReadable {
 | 
			
		||||
			for _, warnMsg := range warnMessages {
 | 
			
		||||
				log.Warn(warnMsg)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if internal.MachineReadable {
 | 
			
		||||
			aggregated_table.JSONRender()
 | 
			
		||||
			sort.Slice(allRows, sortServiceByName(allRows))
 | 
			
		||||
			headers := []string{"VERSION", "SERVICE", "NAME", "TAG"}
 | 
			
		||||
			out, err := formatter.ToJSON(headers, allRows)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Fatal("unable to render to JSON: %s", err)
 | 
			
		||||
			}
 | 
			
		||||
			fmt.Println(out)
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
package server
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"coopcloud.tech/abra/cli/internal"
 | 
			
		||||
@ -29,14 +30,20 @@ var serverListCommand = cli.Command{
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tableColumns := []string{"name", "host"}
 | 
			
		||||
		table := formatter.CreateTable(tableColumns)
 | 
			
		||||
		table, err := formatter.CreateTable2()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		headers := []string{"NAME", "HOST"}
 | 
			
		||||
		table.Headers(headers...)
 | 
			
		||||
 | 
			
		||||
		serverNames, err := config.ReadServerNames()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var rows [][]string
 | 
			
		||||
		for _, serverName := range serverNames {
 | 
			
		||||
			var row []string
 | 
			
		||||
			for _, ctx := range contexts {
 | 
			
		||||
@ -57,6 +64,7 @@ var serverListCommand = cli.Command{
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					row = []string{serverName, sp.Host}
 | 
			
		||||
					rows = append(rows, row)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
@ -66,17 +74,22 @@ var serverListCommand = cli.Command{
 | 
			
		||||
				} else {
 | 
			
		||||
					row = []string{serverName, "unknown"}
 | 
			
		||||
				}
 | 
			
		||||
				rows = append(rows, row)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			table.Append(row)
 | 
			
		||||
			table.Row(row...)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if internal.MachineReadable {
 | 
			
		||||
			table.JSONRender()
 | 
			
		||||
			out, err := formatter.ToJSON(headers, rows)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Fatal("unable to render to JSON: %s", err)
 | 
			
		||||
			}
 | 
			
		||||
			fmt.Println(out)
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		table.Render()
 | 
			
		||||
		fmt.Println(table)
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										3
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								go.mod
									
									
									
									
									
								
							@ -16,9 +16,9 @@ require (
 | 
			
		||||
	github.com/google/go-cmp v0.6.0
 | 
			
		||||
	github.com/moby/sys/signal v0.7.0
 | 
			
		||||
	github.com/moby/term v0.5.0
 | 
			
		||||
	github.com/olekukonko/tablewriter v0.0.5
 | 
			
		||||
	github.com/pkg/errors v0.9.1
 | 
			
		||||
	github.com/schollz/progressbar/v3 v3.14.4
 | 
			
		||||
	golang.org/x/term v0.22.0
 | 
			
		||||
	gopkg.in/yaml.v3 v3.0.1
 | 
			
		||||
	gotest.tools/v3 v3.5.1
 | 
			
		||||
)
 | 
			
		||||
@ -105,7 +105,6 @@ require (
 | 
			
		||||
	golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
 | 
			
		||||
	golang.org/x/net v0.27.0 // indirect
 | 
			
		||||
	golang.org/x/sync v0.7.0 // indirect
 | 
			
		||||
	golang.org/x/term v0.22.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.16.0 // indirect
 | 
			
		||||
	golang.org/x/time v0.5.0 // indirect
 | 
			
		||||
	google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										3
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								go.sum
									
									
									
									
									
								
							@ -621,7 +621,6 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
 | 
			
		||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 | 
			
		||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 | 
			
		||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
 | 
			
		||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 | 
			
		||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
 | 
			
		||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 | 
			
		||||
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
 | 
			
		||||
@ -687,8 +686,6 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA
 | 
			
		||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
 | 
			
		||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 | 
			
		||||
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
 | 
			
		||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
 | 
			
		||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
 | 
			
		||||
github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 | 
			
		||||
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 | 
			
		||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 | 
			
		||||
 | 
			
		||||
@ -1,18 +1,25 @@
 | 
			
		||||
package formatter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/charmbracelet/lipgloss"
 | 
			
		||||
	"github.com/charmbracelet/lipgloss/table"
 | 
			
		||||
	"github.com/docker/go-units"
 | 
			
		||||
	// "github.com/olekukonko/tablewriter"
 | 
			
		||||
	"coopcloud.tech/abra/pkg/jsontable"
 | 
			
		||||
	"golang.org/x/term"
 | 
			
		||||
 | 
			
		||||
	"coopcloud.tech/abra/pkg/log"
 | 
			
		||||
	"github.com/schollz/progressbar/v3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var BoldStyle = lipgloss.NewStyle().
 | 
			
		||||
	Bold(true).
 | 
			
		||||
	Underline(true)
 | 
			
		||||
 | 
			
		||||
func ShortenID(str string) string {
 | 
			
		||||
	return str[:12]
 | 
			
		||||
}
 | 
			
		||||
@ -33,12 +40,53 @@ func HumanDuration(timestamp int64) string {
 | 
			
		||||
	return units.HumanDuration(now.Sub(date)) + " ago"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateTable prepares a table layout for output.
 | 
			
		||||
func CreateTable(columns []string) *jsontable.JSONTable {
 | 
			
		||||
	table := jsontable.NewJSONTable(os.Stdout)
 | 
			
		||||
	table.SetAutoWrapText(false)
 | 
			
		||||
	table.SetHeader(columns)
 | 
			
		||||
	return table
 | 
			
		||||
// CreateTable2 prepares a table layout for output.
 | 
			
		||||
func CreateTable2() (*table.Table, error) {
 | 
			
		||||
	width, _, err := term.GetSize(0)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if width-10 < 79 {
 | 
			
		||||
		width = 79
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return table.New().
 | 
			
		||||
		Width(width - 10).
 | 
			
		||||
		Border(lipgloss.ThickBorder()).
 | 
			
		||||
		BorderStyle(
 | 
			
		||||
			lipgloss.NewStyle().
 | 
			
		||||
				Foreground(lipgloss.Color("63")),
 | 
			
		||||
		), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ToJSON converts a lipgloss.Table to JSON representation. It's not a robust
 | 
			
		||||
// implementation and mainly caters for our current use case which is basically
 | 
			
		||||
// a bunch of strings. See https://github.com/charmbracelet/lipgloss/issues/335
 | 
			
		||||
// for the real thing (hopefully).
 | 
			
		||||
func ToJSON(headers []string, rows [][]string) (string, error) {
 | 
			
		||||
	var buff bytes.Buffer
 | 
			
		||||
 | 
			
		||||
	buff.Write([]byte("["))
 | 
			
		||||
 | 
			
		||||
	for _, row := range rows {
 | 
			
		||||
		payload := make(map[string]string)
 | 
			
		||||
 | 
			
		||||
		for idx, header := range headers {
 | 
			
		||||
			payload[strings.ToLower(header)] = row[idx]
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		serialized, err := json.Marshal(payload)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		buff.Write(serialized)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	buff.Write([]byte("]"))
 | 
			
		||||
 | 
			
		||||
	return buff.String(), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateProgressbar generates a progress bar
 | 
			
		||||
 | 
			
		||||
@ -1,211 +0,0 @@
 | 
			
		||||
package jsontable
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/olekukonko/tablewriter"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// A quick-and-dirty proxy/emulator of tablewriter to enable more easy machine readable output
 | 
			
		||||
// - Does not strictly support types, just quoted or unquoted values
 | 
			
		||||
// - Does not support nested values.
 | 
			
		||||
// If a datalabel is set with SetDataLabel(true, "..."), that will be used as the key for teh data of the table,
 | 
			
		||||
// otherwise if the caption is set with SetCaption(true, "..."), the data label will be set to the default of
 | 
			
		||||
// "rows", otherwise the table will output as a JSON list.
 | 
			
		||||
//
 | 
			
		||||
// Proxys all actions through to the tablewriter except addrow and addbatch, which it does at render time
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
type JSONTable struct {
 | 
			
		||||
	out             io.Writer
 | 
			
		||||
	colsize         int
 | 
			
		||||
	rows            [][]string
 | 
			
		||||
	keys            []string
 | 
			
		||||
	quoted          []bool // hack to do output typing, quoted vs. unquoted
 | 
			
		||||
	hasDataLabel    bool
 | 
			
		||||
	dataLabel       string
 | 
			
		||||
	hasCaption      bool
 | 
			
		||||
	caption         string // the actual caption
 | 
			
		||||
	hasCaptionLabel bool
 | 
			
		||||
	captionLabel    string // the key in the dictionary for the caption
 | 
			
		||||
	tbl             *tablewriter.Table
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func writeChar(w io.Writer, c byte) {
 | 
			
		||||
	w.Write([]byte{c})
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewJSONTable(writer io.Writer) *JSONTable {
 | 
			
		||||
	t := &JSONTable{
 | 
			
		||||
		out:             writer,
 | 
			
		||||
		colsize:         0,
 | 
			
		||||
		rows:            [][]string{},
 | 
			
		||||
		keys:            []string{},
 | 
			
		||||
		quoted:          []bool{},
 | 
			
		||||
		hasDataLabel:    false,
 | 
			
		||||
		dataLabel:       "rows",
 | 
			
		||||
		hasCaption:      false,
 | 
			
		||||
		caption:         "",
 | 
			
		||||
		hasCaptionLabel: false,
 | 
			
		||||
		captionLabel:    "caption",
 | 
			
		||||
		tbl:             tablewriter.NewWriter(writer),
 | 
			
		||||
	}
 | 
			
		||||
	return t
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *JSONTable) NumLines() int {
 | 
			
		||||
	// JSON only but reflects a shared state.
 | 
			
		||||
	return len(t.rows)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *JSONTable) SetHeader(keys []string) {
 | 
			
		||||
	// Set the keys value which will assign each column to the keys.
 | 
			
		||||
	// Note that we'll ignore values that are beyond the length of the keys list
 | 
			
		||||
	t.colsize = len(keys)
 | 
			
		||||
	t.keys = []string{}
 | 
			
		||||
	for _, k := range keys {
 | 
			
		||||
		t.keys = append(t.keys, k)
 | 
			
		||||
		t.quoted = append(t.quoted, true)
 | 
			
		||||
	}
 | 
			
		||||
	t.tbl.SetHeader(keys)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *JSONTable) SetColumnQuoting(quoting []bool) {
 | 
			
		||||
	// Specify which columns are quoted or unquoted in output
 | 
			
		||||
	// JSON only
 | 
			
		||||
	for i := 0; i < t.colsize; i++ {
 | 
			
		||||
		t.quoted[i] = quoting[i]
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *JSONTable) Append(row []string) {
 | 
			
		||||
	// We'll just append whatever to the rows list. If they fix the keys after appending rows, it'll work as
 | 
			
		||||
	// expected.
 | 
			
		||||
	// We should detect if the row is narrower than the key list tho.
 | 
			
		||||
	// JSON only (but we use the rows later when rendering a regular table)
 | 
			
		||||
	t.rows = append(t.rows, row)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *JSONTable) Render() {
 | 
			
		||||
	// Load the table with rows and render.
 | 
			
		||||
	// Proxy only
 | 
			
		||||
	for _, row := range t.rows {
 | 
			
		||||
		t.tbl.Append(row)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t.tbl.Render()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *JSONTable) _JSONRenderInner() {
 | 
			
		||||
	// JSON only
 | 
			
		||||
	// Render the list of dictionaries to the writer.
 | 
			
		||||
	//// inner render loop
 | 
			
		||||
	writeChar(t.out, '[')
 | 
			
		||||
	for rowidx, row := range t.rows {
 | 
			
		||||
		if rowidx != 0 {
 | 
			
		||||
			writeChar(t.out, ',')
 | 
			
		||||
		}
 | 
			
		||||
		writeChar(t.out, '{')
 | 
			
		||||
		for keyidx, key := range t.keys {
 | 
			
		||||
			key := strings.ToLower(key)
 | 
			
		||||
			key = strings.ReplaceAll(key, " ", "-")
 | 
			
		||||
 | 
			
		||||
			value := "nil"
 | 
			
		||||
			if keyidx < len(row) {
 | 
			
		||||
				value = row[keyidx]
 | 
			
		||||
			}
 | 
			
		||||
			if keyidx != 0 {
 | 
			
		||||
				writeChar(t.out, ',')
 | 
			
		||||
			}
 | 
			
		||||
			if t.quoted[keyidx] {
 | 
			
		||||
				fmt.Fprintf(t.out, "\"%s\":\"%s\"", key, value)
 | 
			
		||||
			} else {
 | 
			
		||||
				fmt.Fprintf(t.out, "\"%s\":%s", key, value)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		writeChar(t.out, '}')
 | 
			
		||||
	}
 | 
			
		||||
	writeChar(t.out, ']')
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *JSONTable) JSONRender() {
 | 
			
		||||
	// write JSON table to output
 | 
			
		||||
	// JSON only
 | 
			
		||||
 | 
			
		||||
	if t.hasDataLabel || t.hasCaption {
 | 
			
		||||
		// dict mode
 | 
			
		||||
		writeChar(t.out, '{')
 | 
			
		||||
 | 
			
		||||
		if t.hasCaption {
 | 
			
		||||
			fmt.Fprintf(t.out, "\"%s\":\"%s\",", t.captionLabel, t.caption)
 | 
			
		||||
		}
 | 
			
		||||
		fmt.Fprintf(t.out, "\"%s\":", t.dataLabel)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// write list
 | 
			
		||||
	t._JSONRenderInner()
 | 
			
		||||
 | 
			
		||||
	if t.hasDataLabel || t.hasCaption {
 | 
			
		||||
		// dict mode
 | 
			
		||||
		writeChar(t.out, '}')
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *JSONTable) SetCaption(caption bool, captionText ...string) {
 | 
			
		||||
	t.hasCaption = caption
 | 
			
		||||
	if len(captionText) == 1 {
 | 
			
		||||
		t.caption = captionText[0]
 | 
			
		||||
	}
 | 
			
		||||
	t.tbl.SetCaption(caption, captionText...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *JSONTable) SetCaptionLabel(captionLabel bool, captionLabelText ...string) {
 | 
			
		||||
	// JSON only
 | 
			
		||||
	t.hasCaptionLabel = captionLabel
 | 
			
		||||
	if len(captionLabelText) == 1 {
 | 
			
		||||
		t.captionLabel = captionLabelText[0]
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *JSONTable) SetDataLabel(dataLabel bool, dataLabelText ...string) {
 | 
			
		||||
	// JSON only
 | 
			
		||||
	t.hasDataLabel = dataLabel
 | 
			
		||||
	if len(dataLabelText) == 1 {
 | 
			
		||||
		t.dataLabel = dataLabelText[0]
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *JSONTable) AppendBulk(rows [][]string) {
 | 
			
		||||
	// JSON only but reflects shared state
 | 
			
		||||
	for _, row := range rows {
 | 
			
		||||
		t.Append(row)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Stuff we should implement but we just proxy for now.
 | 
			
		||||
func (t *JSONTable) SetAutoMergeCellsByColumnIndex(cols []int) {
 | 
			
		||||
	// FIXME
 | 
			
		||||
	t.tbl.SetAutoMergeCellsByColumnIndex(cols)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Stuff we should implement but we just proxy for now.
 | 
			
		||||
func (t *JSONTable) SetAlignment(align int) {
 | 
			
		||||
	// FIXME
 | 
			
		||||
	t.tbl.SetAlignment(align)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *JSONTable) SetAutoMergeCells(auto bool) {
 | 
			
		||||
	// FIXME
 | 
			
		||||
	t.tbl.SetAutoMergeCells(auto)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Stub functions
 | 
			
		||||
func (t *JSONTable) SetAutoWrapText(auto bool) {
 | 
			
		||||
	t.tbl.SetAutoWrapText(auto)
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
@ -1,83 +0,0 @@
 | 
			
		||||
package jsontable
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
 | 
			
		||||
	"github.com/olekukonko/tablewriter"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var TestLine = []string{"1", "2"}
 | 
			
		||||
var TestGroup = [][]string{{"1", "2", "3"}, {"a", "teohunteohu", "c", "d"}, {"☺", "☹"}}
 | 
			
		||||
var TestKeys = []string{"key0", "key1", "key2"}
 | 
			
		||||
 | 
			
		||||
// test creation
 | 
			
		||||
func TestNewTable(t *testing.T) {
 | 
			
		||||
	var b bytes.Buffer
 | 
			
		||||
	tbl := NewJSONTable(&b)
 | 
			
		||||
	if tbl.NumLines() != 0 {
 | 
			
		||||
		t.Fatalf("Something went weird when making table (should have 0 lines)")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// test adding things
 | 
			
		||||
func TestTableAdd(t *testing.T) {
 | 
			
		||||
	var b bytes.Buffer
 | 
			
		||||
	tbl := NewJSONTable(&b)
 | 
			
		||||
 | 
			
		||||
	tbl.Append(TestLine)
 | 
			
		||||
	if tbl.NumLines() != 1 {
 | 
			
		||||
		t.Fatalf("Appending a line does not result in a length of 1.")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tbl.AppendBulk(TestGroup)
 | 
			
		||||
	numlines := tbl.NumLines()
 | 
			
		||||
	if numlines != (len(TestGroup) + 1) {
 | 
			
		||||
		t.Fatalf("Appending two lines does not result in a length of 4 (length is %d).", numlines)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// test JSON output is parsable
 | 
			
		||||
func TestJsonParsable(t *testing.T) {
 | 
			
		||||
	var b bytes.Buffer
 | 
			
		||||
	tbl := NewJSONTable(&b)
 | 
			
		||||
 | 
			
		||||
	tbl.AppendBulk(TestGroup)
 | 
			
		||||
	tbl.SetHeader(TestKeys)
 | 
			
		||||
 | 
			
		||||
	tbl.JSONRender()
 | 
			
		||||
 | 
			
		||||
	var son []map[string]interface{}
 | 
			
		||||
 | 
			
		||||
	err := json.Unmarshal(b.Bytes(), &son)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Did not produce parsable JSON: %s", err.Error())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// test identical commands to a tablewriter and jsontable produce the same rendered output
 | 
			
		||||
func TestTableWriter(t *testing.T) {
 | 
			
		||||
	var bjson bytes.Buffer
 | 
			
		||||
	var btable bytes.Buffer
 | 
			
		||||
 | 
			
		||||
	tbl := NewJSONTable(&bjson)
 | 
			
		||||
 | 
			
		||||
	tbl.AppendBulk(TestGroup)
 | 
			
		||||
	tbl.SetHeader(TestKeys)
 | 
			
		||||
	tbl.Render()
 | 
			
		||||
 | 
			
		||||
	wtbl := tablewriter.NewWriter(&btable)
 | 
			
		||||
 | 
			
		||||
	wtbl.AppendBulk(TestGroup)
 | 
			
		||||
	wtbl.SetHeader(TestKeys)
 | 
			
		||||
	wtbl.Render()
 | 
			
		||||
 | 
			
		||||
	if bytes.Compare(bjson.Bytes(), btable.Bytes()) != 0 {
 | 
			
		||||
		t.Fatalf("JSON table and TableWriter produce non-identical outputs.\n%s\n%s", bjson.Bytes(), btable.Bytes())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// FIXME test different output formats when captions etc. are added
 | 
			
		||||
@ -181,7 +181,7 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
 | 
			
		||||
 | 
			
		||||
				if err := client.StoreSecret(cl, secret.RemoteName, passwords[0], server); err != nil {
 | 
			
		||||
					if strings.Contains(err.Error(), "AlreadyExists") {
 | 
			
		||||
						log.Warnf("%s already exists, moving on...", secret.RemoteName)
 | 
			
		||||
						log.Warnf("%s already exists", secret.RemoteName)
 | 
			
		||||
						ch <- nil
 | 
			
		||||
					} else {
 | 
			
		||||
						ch <- err
 | 
			
		||||
@ -201,7 +201,7 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
 | 
			
		||||
 | 
			
		||||
				if err := client.StoreSecret(cl, secret.RemoteName, passphrases[0], server); err != nil {
 | 
			
		||||
					if strings.Contains(err.Error(), "AlreadyExists") {
 | 
			
		||||
						log.Warnf("%s already exists, moving on...", secret.RemoteName)
 | 
			
		||||
						log.Warnf("%s already exists", secret.RemoteName)
 | 
			
		||||
						ch <- nil
 | 
			
		||||
					} else {
 | 
			
		||||
						ch <- err
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user