refactor: tablewriter -> lipgloss
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing

Also the jsontable impl. is dropped also. Output is unchanged.
This commit is contained in:
decentral1se 2024-07-16 16:22:47 +02:00
parent f28cffe6d8
commit de006782b6
Signed by: decentral1se
GPG Key ID: 03789458B3D0C410
18 changed files with 383 additions and 444 deletions

View File

@ -1,11 +1,14 @@
package app package app
import ( import (
"fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/charmbracelet/lipgloss"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -40,8 +43,21 @@ ${FOO:<default>} syntax). "check" does not confirm or deny this for you.`,
log.Fatal(err) log.Fatal(err)
} }
tableCol := []string{"recipe env sample", "app env"} table, err := formatter.CreateTable2()
table := formatter.CreateTable(tableCol) 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) envVars, err := appPkg.CheckEnv(app)
if err != nil { if err != nil {
@ -50,13 +66,15 @@ ${FOO:<default>} syntax). "check" does not confirm or deny this for you.`,
for _, envVar := range envVars { for _, envVar := range envVars {
if envVar.Present { if envVar.Present {
table.Append([]string{envVar.Name, "✅"}) val := []string{envVar.Name, "✅"}
table.Row(val...)
} else { } else {
table.Append([]string{envVar.Name, "❌"}) val := []string{envVar.Name, "❌"}
table.Row(val...)
} }
} }
table.Render() fmt.Println(table)
return nil return nil
}, },

View File

@ -239,15 +239,27 @@ can take some time.`,
serverStat := allStats[app.Server] serverStat := allStats[app.Server]
tableCol := []string{"recipe", "domain"} headers := []string{"RECIPE", "DOMAIN"}
if status { 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 { for _, appStat := range serverStat.Apps {
tableRow := []string{appStat.Recipe, appStat.Domain} row := []string{appStat.Recipe, appStat.Domain}
if status { if status {
chaosStatus := appStat.Chaos chaosStatus := appStat.Chaos
if chaosStatus != "unknown" { if chaosStatus != "unknown" {
@ -259,17 +271,27 @@ can take some time.`,
chaosStatus = appStat.ChaosVersion 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.Rows(rows...)
table.Render()
if len(rows) > 0 {
fmt.Println(table)
if status { if status {
fmt.Println(fmt.Sprintf( 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, app.Server,
serverStat.AppCount, serverStat.AppCount,
serverStat.VersionCount, serverStat.VersionCount,
@ -278,19 +300,21 @@ can take some time.`,
serverStat.UpgradeCount, serverStat.UpgradeCount,
)) ))
} else { } 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 { if len(allStats) > 1 && len(rows) > 0 {
fmt.Println() // newline separator for multiple servers fmt.Println() // newline separator for multiple servers
}
} }
alreadySeen[app.Server] = true alreadySeen[app.Server] = true
} }
if len(allStats) > 1 { 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 return nil

View File

@ -9,11 +9,11 @@ import (
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/jsontable"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
recipePkg "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/secret" "coopcloud.tech/abra/pkg/secret"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/charmbracelet/lipgloss/table"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -127,7 +127,7 @@ var appNewCommand = cli.Command{
} }
var secrets AppSecrets var secrets AppSecrets
var secretTable *jsontable.JSONTable var secretsTable *table.Table
if internal.Secrets { if internal.Secrets {
sampleEnv, err := recipe.SampleEnv() sampleEnv, err := recipe.SampleEnv()
if err != nil { if err != nil {
@ -158,10 +158,16 @@ var appNewCommand = cli.Command{
log.Fatal(err) log.Fatal(err)
} }
secretCols := []string{"Name", "Value"} secretsTable, err = formatter.CreateTable2()
secretTable = formatter.CreateTable(secretCols) if err != nil {
log.Fatal(err)
}
headers := []string{"NAME", "VALUE"}
secretsTable.Headers(headers...)
for name, val := range secrets { 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" internal.NewAppServer = "local"
} }
tableCol := []string{"server", "recipe", "domain"} table, err := formatter.CreateTable2()
table := formatter.CreateTable(tableCol) if err != nil {
table.Append([]string{internal.NewAppServer, recipe.Name, internal.Domain}) 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) log.Infof("new app '%s' created 🌞", recipe.Name)
fmt.Println("") fmt.Println("")
table.Render() fmt.Println(table)
fmt.Println("") fmt.Println("")
fmt.Println("Configure this app:") fmt.Println("Configure this app:")
@ -190,8 +202,13 @@ var appNewCommand = cli.Command{
fmt.Println("") fmt.Println("")
fmt.Println("Generated secrets:") fmt.Println("Generated secrets:")
fmt.Println("") fmt.Println("")
secretTable.Render() fmt.Println(secretsTable)
log.Warn("generated secrets are not shown again, please take note of them NOW")
log.Warnf(
"generated secrets %s shown again, please take note of them %s",
formatter.BoldStyle.Render("NOT"),
formatter.BoldStyle.Render("NOW"),
)
} }
return nil return nil

View File

@ -94,7 +94,7 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
return return
} }
var tablerows [][]string var rows [][]string
allContainerStats := make(map[string]map[string]string) allContainerStats := make(map[string]map[string]string)
for _, service := range compose.Services { for _, service := range compose.Services {
filters := filters.NewArgs() filters := filters.NewArgs()
@ -109,8 +109,6 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
var containerStats map[string]string var containerStats map[string]string
if len(containers) == 0 { if len(containers) == 0 {
containerStats = map[string]string{ containerStats = map[string]string{
"version": deployedVersion,
"chaos": chaosVersion,
"service": service.Name, "service": service.Name,
"image": "unknown", "image": "unknown",
"created": "unknown", "created": "unknown",
@ -121,8 +119,6 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
} else { } else {
container := containers[0] container := containers[0]
containerStats = map[string]string{ containerStats = map[string]string{
"version": deployedVersion,
"chaos": chaosVersion,
"service": abraService.ContainerToServiceName(container.Names, app.StackName()), "service": abraService.ContainerToServiceName(container.Names, app.StackName()),
"image": formatter.RemoveSha(container.Image), "image": formatter.RemoveSha(container.Image),
"created": formatter.HumanDuration(container.Created), "created": formatter.HumanDuration(container.Created),
@ -134,9 +130,7 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
allContainerStats[containerStats["service"]] = containerStats allContainerStats[containerStats["service"]] = containerStats
tablerow := []string{ row := []string{
deployedVersion,
chaosVersion,
containerStats["service"], containerStats["service"],
containerStats["image"], containerStats["image"],
containerStats["created"], containerStats["created"],
@ -145,25 +139,37 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
containerStats["ports"], containerStats["ports"],
} }
tablerows = append(tablerows, tablerow) rows = append(rows, row)
} }
if internal.MachineReadable { if internal.MachineReadable {
jsonstring, err := json.Marshal(allContainerStats) jsonstring, err := json.Marshal(allContainerStats)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal("unable to convert to JSON: %s", err)
} }
fmt.Println(string(jsonstring)) fmt.Println(string(jsonstring))
return return
} }
tableCol := []string{"version", "chaos", "service", "image", "created", "status", "state", "ports"} table, err := formatter.CreateTable2()
table := formatter.CreateTable(tableCol) if err != nil {
for _, row := range tablerows { log.Fatal(err)
table.Append(row)
} }
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)
} }

View File

@ -49,12 +49,14 @@ flag.`,
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
if !internal.Force && !internal.NoInput { if !internal.Force && !internal.NoInput {
log.Warnf("ALERTA ALERTA: this will completely remove %s data and config locally and remotely", app.Name)
response := false response := false
msg := "ALERTA ALERTA: this will completely remove %s data and configurations locally and remotely, are you sure?" prompt := &survey.Confirm{Message: "are you sure?"}
prompt := &survey.Confirm{Message: fmt.Sprintf(msg, app.Name)}
if err := survey.AskOne(prompt, &response); err != nil { if err := survey.AskOne(prompt, &response); err != nil {
log.Fatal(err) log.Fatal(err)
} }
if !response { if !response {
log.Fatal("aborting as requested") log.Fatal("aborting as requested")
} }

View File

@ -115,18 +115,37 @@ var appSecretGenerateCommand = cli.Command{
os.Exit(1) os.Exit(1)
} }
tableCol := []string{"name", "value"} headers := []string{"NAME", "VALUE"}
table := formatter.CreateTable(tableCol) table, err := formatter.CreateTable2()
if err != nil {
log.Fatal(err)
}
table.Headers(headers...)
var rows [][]string
for name, val := range secretVals { for name, val := range secretVals {
table.Append([]string{name, val}) row := []string{name, val}
rows = append(rows, row)
table.Row(row...)
} }
if internal.MachineReadable { if internal.MachineReadable {
table.JSONRender() out, err := formatter.ToJSON(headers, rows)
} else { if err != nil {
table.Render() 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 return nil
}, },
@ -345,34 +364,48 @@ var appSecretLsCommand = cli.Command{
log.Fatal(err) log.Fatal(err)
} }
tableCol := []string{"Name", "Version", "Generated Name", "Created On Server"} headers := []string{"NAME", "VERSION", "GENERATED NAME", "CREATED ON SERVER"}
table := formatter.CreateTable(tableCol) table, err := formatter.CreateTable2()
if err != nil {
log.Fatal(err)
}
table.Headers(headers...)
secStats, err := secret.PollSecretsStatus(cl, app) secStats, err := secret.PollSecretsStatus(cl, app)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
var rows [][]string
for _, secStat := range secStats { for _, secStat := range secStats {
tableRow := []string{ row := []string{
secStat.LocalName, secStat.LocalName,
secStat.Version, secStat.Version,
secStat.RemoteName, secStat.RemoteName,
strconv.FormatBool(secStat.CreatedOnRemote), 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 { if internal.MachineReadable {
table.JSONRender() out, err := formatter.ToJSON(headers, rows)
} else { if err != nil {
table.Render() 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 return nil
}, },
} }

View File

@ -56,9 +56,15 @@ var appServicesCommand = cli.Command{
log.Fatal(err) log.Fatal(err)
} }
tableCol := []string{"service name", "image"} table, err := formatter.CreateTable2()
table := formatter.CreateTable(tableCol) if err != nil {
log.Fatal(err)
}
headers := []string{"SERVICE (SHORT)", "SERVICE (LONG)", "IMAGE"}
table.Headers(headers...)
var rows [][]string
for _, container := range containers { for _, container := range containers {
var containerNames []string var containerNames []string
for _, containerName := range container.Names { for _, containerName := range container.Names {
@ -69,14 +75,20 @@ var appServicesCommand = cli.Command{
serviceShortName := service.ContainerToServiceName(container.Names, app.StackName()) serviceShortName := service.ContainerToServiceName(container.Names, app.StackName())
serviceLongName := fmt.Sprintf("%s_%s", app.StackName(), serviceShortName) serviceLongName := fmt.Sprintf("%s_%s", app.StackName(), serviceShortName)
tableRow := []string{ row := []string{
serviceShortName,
serviceLongName, serviceLongName,
formatter.RemoveSha(container.Image), 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 return nil
}, },

View File

@ -2,6 +2,7 @@ package app
import ( import (
"context" "context"
"fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
@ -37,26 +38,35 @@ var appVolumeListCommand = cli.Command{
log.Fatal(err) 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 { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
table := formatter.CreateTable([]string{"name", "created", "mounted"}) headers := []string{"name", "created", "mounted"}
var volTable [][]string
for _, volume := range volumeList { table, err := formatter.CreateTable2()
volRow := []string{volume.Name, volume.CreatedAt, volume.Mountpoint} if err != nil {
volTable = append(volTable, volRow) log.Fatal(err)
} }
table.AppendBulk(volTable) table.Headers(headers...)
if table.NumLines() > 0 { var rows [][]string
table.Render() for _, volume := range volumes {
} else { row := []string{volume.Name, volume.CreatedAt, volume.Mountpoint}
log.Warnf("no volumes created for %s", app.Name) 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 return nil
}, },
} }

View File

@ -32,11 +32,25 @@ var recipeLintCommand = cli.Command{
log.Fatal(err) log.Fatal(err)
} }
tableCol := []string{"ref", "rule", "severity", "satisfied", "skipped", "resolve"} headers := []string{
table := formatter.CreateTable(tableCol) "ref",
"rule",
"severity",
"satisfied",
"skipped",
"resolve",
}
table, err := formatter.CreateTable2()
if err != nil {
log.Fatal(err)
}
table.Headers(headers...)
hasError := false hasError := false
bar := formatter.CreateProgressbar(-1, "running recipe lint rules...") var rows [][]string
var warnMessages []string
for level := range lint.LintRules { for level := range lint.LintRules {
for _, rule := range lint.LintRules[level] { for _, rule := range lint.LintRules[level] {
if internal.OnlyErrors && rule.Level != "error" { if internal.OnlyErrors && rule.Level != "error" {
@ -58,7 +72,7 @@ var recipeLintCommand = cli.Command{
if !skipped { if !skipped {
ok, err := rule.Function(recipe) ok, err := rule.Function(recipe)
if err != nil { if err != nil {
log.Warn(err) warnMessages = append(warnMessages, err.Error())
} }
if !ok && rule.Level == "error" { if !ok && rule.Level == "error" {
@ -78,26 +92,30 @@ var recipeLintCommand = cli.Command{
} }
} }
table.Append([]string{ row := []string{
rule.Ref, rule.Ref,
rule.Description, rule.Description,
rule.Level, rule.Level,
satisfiedOutput, satisfiedOutput,
skippedOutput, skippedOutput,
rule.HowToResolve, rule.HowToResolve,
}) }
bar.Add(1) rows = append(rows, row)
table.Row(row...)
} }
} }
if table.NumLines() > 0 { if len(rows) > 0 {
fmt.Println() fmt.Println(table)
table.Render()
}
if hasError { for _, warnMsg := range warnMessages {
log.Warn("watch out, some critical errors are present in your recipe config") log.Warn(warnMsg)
}
if hasError {
log.Warnf("critical errors present in %s config", recipe.Name)
}
} }
return nil return nil

View File

@ -41,12 +41,27 @@ var recipeListCommand = cli.Command{
recipes := catl.Flatten() recipes := catl.Flatten()
sort.Sort(recipe.ByRecipeName(recipes)) sort.Sort(recipe.ByRecipeName(recipes))
tableCol := []string{"name", "category", "status", "healthcheck", "backups", "email", "tests", "SSO"} table, err := formatter.CreateTable2()
table := formatter.CreateTable(tableCol) 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 { for _, recipe := range recipes {
tableRow := []string{ row := []string{
recipe.Name, recipe.Name,
recipe.Category, recipe.Category,
strconv.Itoa(recipe.Features.Status), strconv.Itoa(recipe.Features.Status),
@ -59,23 +74,27 @@ var recipeListCommand = cli.Command{
if pattern != "" { if pattern != "" {
if strings.Contains(recipe.Name, pattern) { if strings.Contains(recipe.Name, pattern) {
table.Append(tableRow) table.Row(row...)
len++ rows = append(rows, row)
} }
} else { } else {
table.Append(tableRow) table.Row(row...)
len++ rows = append(rows, row)
} }
} }
if table.NumLines() > 0 { if len(rows) > 0 {
if internal.MachineReadable { if internal.MachineReadable {
table.SetCaption(false, "") out, err := formatter.ToJSON(headers, rows)
table.JSONRender() if err != nil {
} else { log.Fatal("unable to render to JSON: %s", err)
table.SetCaption(true, fmt.Sprintf("total recipes: %v", len)) }
table.Render() fmt.Println(out)
return nil
} }
fmt.Println(table)
log.Infof("total recipes: %v", len(rows))
} }
return nil return nil

View File

@ -9,7 +9,6 @@ import (
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
recipePkg "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/olekukonko/tablewriter"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -37,6 +36,8 @@ var recipeVersionCommand = cli.Command{
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete, BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
var warnMessages []string
recipe := internal.ValidateRecipe(c) recipe := internal.ValidateRecipe(c)
catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline) catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline)
@ -46,47 +47,63 @@ var recipeVersionCommand = cli.Command{
recipeMeta, ok := catl[recipe.Name] recipeMeta, ok := catl[recipe.Name]
if !ok { 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() recipeVersions, err := recipe.GetRecipeVersions()
if err != nil { if err != nil {
log.Warn(err) warnMessages = append(warnMessages, err.Error())
} }
recipeMeta = recipePkg.RecipeMeta{Versions: recipeVersions} recipeMeta = recipePkg.RecipeMeta{Versions: recipeVersions}
} }
if len(recipeMeta.Versions) == 0 { 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"} var allRows [][]string
aggregated_table := formatter.CreateTable(tableCols)
for i := len(recipeMeta.Versions) - 1; i >= 0; i-- { 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] { for version, meta := range recipeMeta.Versions[i] {
var versions [][]string var rows [][]string
for service, serviceMeta := range meta { 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.Rows(rows...)
table.Append(version)
aggregated_table.Append(version)
}
if !internal.MachineReadable { if !internal.MachineReadable {
table.SetAutoMergeCellsByColumnIndex([]int{0}) fmt.Println(table)
table.SetAlignment(tablewriter.ALIGN_LEFT) log.Infof("VERSION: %s", version)
table.Render()
fmt.Println()
} }
} }
} }
if !internal.MachineReadable {
for _, warnMsg := range warnMessages {
log.Warn(warnMsg)
}
}
if internal.MachineReadable { 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 return nil

View File

@ -1,6 +1,7 @@
package server package server
import ( import (
"fmt"
"strings" "strings"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
@ -29,14 +30,20 @@ var serverListCommand = cli.Command{
log.Fatal(err) log.Fatal(err)
} }
tableColumns := []string{"name", "host"} table, err := formatter.CreateTable2()
table := formatter.CreateTable(tableColumns) if err != nil {
log.Fatal(err)
}
headers := []string{"NAME", "HOST"}
table.Headers(headers...)
serverNames, err := config.ReadServerNames() serverNames, err := config.ReadServerNames()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
var rows [][]string
for _, serverName := range serverNames { for _, serverName := range serverNames {
var row []string var row []string
for _, ctx := range contexts { for _, ctx := range contexts {
@ -57,6 +64,7 @@ var serverListCommand = cli.Command{
} }
row = []string{serverName, sp.Host} row = []string{serverName, sp.Host}
rows = append(rows, row)
} }
} }
@ -66,17 +74,22 @@ var serverListCommand = cli.Command{
} else { } else {
row = []string{serverName, "unknown"} row = []string{serverName, "unknown"}
} }
rows = append(rows, row)
} }
table.Append(row) table.Row(row...)
} }
if internal.MachineReadable { 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 return nil
} }
table.Render() fmt.Println(table)
return nil return nil
}, },

3
go.mod
View File

@ -16,9 +16,9 @@ require (
github.com/google/go-cmp v0.6.0 github.com/google/go-cmp v0.6.0
github.com/moby/sys/signal v0.7.0 github.com/moby/sys/signal v0.7.0
github.com/moby/term v0.5.0 github.com/moby/term v0.5.0
github.com/olekukonko/tablewriter v0.0.5
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/schollz/progressbar/v3 v3.14.4 github.com/schollz/progressbar/v3 v3.14.4
golang.org/x/term v0.22.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.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/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/net v0.27.0 // indirect golang.org/x/net v0.27.0 // indirect
golang.org/x/sync v0.7.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/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect golang.org/x/time v0.5.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect

3
go.sum
View File

@ -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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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.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 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 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= 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/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 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.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-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 v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=

View File

@ -1,18 +1,25 @@
package formatter package formatter
import ( import (
"bytes"
"encoding/json"
"fmt" "fmt"
"os"
"strings" "strings"
"time" "time"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
"github.com/docker/go-units" "github.com/docker/go-units"
// "github.com/olekukonko/tablewriter" "golang.org/x/term"
"coopcloud.tech/abra/pkg/jsontable"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/schollz/progressbar/v3" "github.com/schollz/progressbar/v3"
) )
var BoldStyle = lipgloss.NewStyle().
Bold(true).
Underline(true)
func ShortenID(str string) string { func ShortenID(str string) string {
return str[:12] return str[:12]
} }
@ -33,12 +40,53 @@ func HumanDuration(timestamp int64) string {
return units.HumanDuration(now.Sub(date)) + " ago" return units.HumanDuration(now.Sub(date)) + " ago"
} }
// CreateTable prepares a table layout for output. // CreateTable2 prepares a table layout for output.
func CreateTable(columns []string) *jsontable.JSONTable { func CreateTable2() (*table.Table, error) {
table := jsontable.NewJSONTable(os.Stdout) width, _, err := term.GetSize(0)
table.SetAutoWrapText(false) if err != nil {
table.SetHeader(columns) return nil, err
return table }
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 // CreateProgressbar generates a progress bar

View File

@ -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
}

View File

@ -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

View File

@ -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 err := client.StoreSecret(cl, secret.RemoteName, passwords[0], server); err != nil {
if strings.Contains(err.Error(), "AlreadyExists") { if strings.Contains(err.Error(), "AlreadyExists") {
log.Warnf("%s already exists, moving on...", secret.RemoteName) log.Warnf("%s already exists", secret.RemoteName)
ch <- nil ch <- nil
} else { } else {
ch <- err 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 err := client.StoreSecret(cl, secret.RemoteName, passphrases[0], server); err != nil {
if strings.Contains(err.Error(), "AlreadyExists") { if strings.Contains(err.Error(), "AlreadyExists") {
log.Warnf("%s already exists, moving on...", secret.RemoteName) log.Warnf("%s already exists", secret.RemoteName)
ch <- nil ch <- nil
} else { } else {
ch <- err ch <- err