diff --git a/cli/app/check.go b/cli/app/check.go index b5eca951..e1df776e 100644 --- a/cli/app/check.go +++ b/cli/app/check.go @@ -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:} 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:} 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 }, diff --git a/cli/app/list.go b/cli/app/list.go index 20fa2acc..cc471855 100644 --- a/cli/app/list.go +++ b/cli/app/list.go @@ -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 diff --git a/cli/app/new.go b/cli/app/new.go index 27bda8f4..2fb912f4 100644 --- a/cli/app/new.go +++ b/cli/app/new.go @@ -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 diff --git a/cli/app/ps.go b/cli/app/ps.go index 008d59ce..1891c23f 100644 --- a/cli/app/ps.go +++ b/cli/app/ps.go @@ -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) } diff --git a/cli/app/remove.go b/cli/app/remove.go index b6476f20..e0c35235 100644 --- a/cli/app/remove.go +++ b/cli/app/remove.go @@ -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") } diff --git a/cli/app/secret.go b/cli/app/secret.go index a8fe9486..c869b346 100644 --- a/cli/app/secret.go +++ b/cli/app/secret.go @@ -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 }, } diff --git a/cli/app/services.go b/cli/app/services.go index 97dfda61..98c729d7 100644 --- a/cli/app/services.go +++ b/cli/app/services.go @@ -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 }, diff --git a/cli/app/volume.go b/cli/app/volume.go index 31569394..4be9d6ef 100644 --- a/cli/app/volume.go +++ b/cli/app/volume.go @@ -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 }, } diff --git a/cli/recipe/lint.go b/cli/recipe/lint.go index 4de8be2d..8bfbc9f0 100644 --- a/cli/recipe/lint.go +++ b/cli/recipe/lint.go @@ -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 diff --git a/cli/recipe/list.go b/cli/recipe/list.go index 8cd13627..2f686766 100644 --- a/cli/recipe/list.go +++ b/cli/recipe/list.go @@ -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 diff --git a/cli/recipe/version.go b/cli/recipe/version.go index a0069bc0..b9d4328f 100644 --- a/cli/recipe/version.go +++ b/cli/recipe/version.go @@ -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 diff --git a/cli/server/list.go b/cli/server/list.go index 8fea3e0b..9b9ad6e5 100644 --- a/cli/server/list.go +++ b/cli/server/list.go @@ -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 }, diff --git a/go.mod b/go.mod index 0951caf8..b49de930 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 6cf86238..9ec6b4c3 100644 --- a/go.sum +++ b/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= diff --git a/pkg/formatter/formatter.go b/pkg/formatter/formatter.go index 1983bade..8e672686 100644 --- a/pkg/formatter/formatter.go +++ b/pkg/formatter/formatter.go @@ -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 diff --git a/pkg/jsontable/jsontable.go b/pkg/jsontable/jsontable.go deleted file mode 100644 index 51dd6020..00000000 --- a/pkg/jsontable/jsontable.go +++ /dev/null @@ -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 -} diff --git a/pkg/jsontable/jsontable_test.go b/pkg/jsontable/jsontable_test.go deleted file mode 100644 index e25c9a2b..00000000 --- a/pkg/jsontable/jsontable_test.go +++ /dev/null @@ -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 diff --git a/pkg/secret/secret.go b/pkg/secret/secret.go index 1f17169c..298eb1ff 100644 --- a/pkg/secret/secret.go +++ b/pkg/secret/secret.go @@ -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