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/deploy.go b/cli/app/deploy.go index d589c694..60cf765d 100644 --- a/cli/app/deploy.go +++ b/cli/app/deploy.go @@ -2,6 +2,7 @@ package app import ( "context" + "fmt" "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/autocomplete" @@ -47,6 +48,8 @@ EXAMPLE: abra app deploy foo.example.com 1e83340e`, BashComplete: autocomplete.AppNameComplete, Action: func(c *cli.Context) error { + var warnMessages []string + app := internal.ValidateApp(c) stackName := app.StackName() @@ -115,7 +118,7 @@ EXAMPLE: if deployMeta.IsDeployed { if internal.Force || internal.Chaos { - log.Warnf("%s is already deployed but continuing (--force/--chaos)", app.Name) + warnMessages = append(warnMessages, fmt.Sprintf("%s is already deployed", app.Name)) } else { log.Fatalf("%s is already deployed", app.Name) } @@ -139,13 +142,13 @@ EXAMPLE: log.Fatal(err) } version = formatter.SmallSHA(head.String()) - log.Warn("no versions detected, using latest commit") + warnMessages = append(warnMessages, fmt.Sprintf("no versions detected, using latest commit")) } } chaosVersion := "false" if internal.Chaos { - log.Warnf("chaos mode engaged") + warnMessages = append(warnMessages, "chaos mode engaged") if isChaosCommit { chaosVersion = specificVersion @@ -201,14 +204,12 @@ EXAMPLE: for _, envVar := range envVars { if !envVar.Present { - log.Warnf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain) + warnMessages = append(warnMessages, + fmt.Sprintf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain), + ) } } - if err := internal.DeployOverview(app, version, chaosVersion, "continue with deployment?"); err != nil { - log.Fatal(err) - } - if !internal.NoDomainChecks { domainName, ok := app.Env["DOMAIN"] if ok { @@ -216,10 +217,14 @@ EXAMPLE: log.Fatal(err) } } else { - log.Warn("skipping domain checks as no DOMAIN=... configured for app") + warnMessages = append(warnMessages, "skipping domain checks as no DOMAIN=... configured for app") } } else { - log.Warn("skipping domain checks as requested") + warnMessages = append(warnMessages, "skipping domain checks as requested") + } + + if err := internal.DeployOverview(app, warnMessages, version, chaosVersion); err != nil { + log.Fatal(err) } stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName) 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/rollback.go b/cli/app/rollback.go index 79a35359..29b2710e 100644 --- a/cli/app/rollback.go +++ b/cli/app/rollback.go @@ -47,6 +47,8 @@ EXAMPLE: abra app rollback foo.example.com 1.2.3+3.2.1`, BashComplete: autocomplete.AppNameComplete, Action: func(c *cli.Context) error { + var warnMessages []string + app := internal.ValidateApp(c) stackName := app.StackName() @@ -82,7 +84,7 @@ EXAMPLE: var availableDowngrades []string if deployMeta.Version == "unknown" { availableDowngrades = versions - log.Warnf("failed to determine deployed version of %s", app.Name) + warnMessages = append(warnMessages, fmt.Sprintf("failed to determine deployed version of %s", app.Name)) } specificVersion := c.Args().Get(1) @@ -113,7 +115,7 @@ EXAMPLE: if deployMeta.Version != "unknown" && specificVersion == "" { if deployMeta.IsChaos { - log.Warn("attempting to rollback a chaos deployment") + warnMessages = append(warnMessages, fmt.Sprintf("attempting to rollback a chaos deployment")) } for _, version := range versions { @@ -203,7 +205,14 @@ EXAMPLE: } // NOTE(d1): no release notes implemeneted for rolling back - if err := internal.NewVersionOverview(app, deployMeta.Version, chaosVersion, chosenDowngrade, ""); err != nil { + if err := internal.NewVersionOverview( + app, + warnMessages, + "rollback", + deployMeta.Version, + chaosVersion, + chosenDowngrade, + ""); err != nil { log.Fatal(err) } 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/undeploy.go b/cli/app/undeploy.go index 5e63d62c..3adffd06 100644 --- a/cli/app/undeploy.go +++ b/cli/app/undeploy.go @@ -107,7 +107,7 @@ Passing "-p/--prune" does not remove those volumes.`, chaosVersion = deployMeta.ChaosVersion } - if err := internal.DeployOverview(app, deployMeta.Version, chaosVersion, "continue with undeploy?"); err != nil { + if err := internal.DeployOverview(app, []string{}, deployMeta.Version, chaosVersion); err != nil { log.Fatal(err) } diff --git a/cli/app/upgrade.go b/cli/app/upgrade.go index 26366aef..38849856 100644 --- a/cli/app/upgrade.go +++ b/cli/app/upgrade.go @@ -47,6 +47,8 @@ EXAMPLE: abra app upgrade foo.example.com 1.2.3+3.2.1`, BashComplete: autocomplete.AppNameComplete, Action: func(c *cli.Context) error { + var warnMessages []string + app := internal.ValidateApp(c) stackName := app.StackName() @@ -82,7 +84,7 @@ EXAMPLE: var availableUpgrades []string if deployMeta.Version == "unknown" { availableUpgrades = versions - log.Warnf("failed to determine deployed version of %s", app.Name) + warnMessages = append(warnMessages, fmt.Sprintf("failed to determine deployed version of %s", app.Name)) } specificVersion := c.Args().Get(1) @@ -114,7 +116,7 @@ EXAMPLE: if deployMeta.Version != "unknown" && specificVersion == "" { if deployMeta.IsChaos { - log.Warn("attempting to upgrade a chaos deployment") + warnMessages = append(warnMessages, fmt.Sprintf("attempting to upgrade a chaos deployment")) } for _, version := range versions { @@ -156,7 +158,7 @@ EXAMPLE: } if internal.Force && chosenUpgrade == "" { - log.Warnf("%s is already upgraded to latest but continuing (--force)", app.Name) + warnMessages = append(warnMessages, fmt.Sprintf("%s is already upgraded to latest", app.Name)) chosenUpgrade = deployMeta.Version } @@ -230,7 +232,9 @@ EXAMPLE: for _, envVar := range envVars { if !envVar.Present { - log.Warnf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain) + warnMessages = append(warnMessages, + fmt.Sprintf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain), + ) } } @@ -245,7 +249,14 @@ EXAMPLE: chaosVersion = deployMeta.ChaosVersion } - if err := internal.NewVersionOverview(app, deployMeta.Version, chaosVersion, chosenUpgrade, releaseNotes); err != nil { + if err := internal.NewVersionOverview( + app, + warnMessages, + "upgrade", + deployMeta.Version, + chaosVersion, + chosenUpgrade, + releaseNotes); err != nil { log.Fatal(err) } 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/cli.go b/cli/cli.go index cea0b9ef..e3ed5728 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -190,7 +190,9 @@ func newAbraApp(version, commit string) *cli.App { } } + log.Logger.SetStyles(log.Styles()) charmLog.SetDefault(log.Logger) + log.Debugf("abra version %s, commit %s", version, commit) return nil diff --git a/cli/internal/deploy.go b/cli/internal/deploy.go index 3147d66c..5f761a65 100644 --- a/cli/internal/deploy.go +++ b/cli/internal/deploy.go @@ -6,17 +6,41 @@ import ( "strings" appPkg "coopcloud.tech/abra/pkg/app" - "coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/log" "github.com/AlecAivazis/survey/v2" + "github.com/charmbracelet/lipgloss" dockerClient "github.com/docker/docker/client" ) -// NewVersionOverview shows an upgrade or downgrade overview -func NewVersionOverview(app appPkg.App, currentVersion, chaosVersion, newVersion, releaseNotes string) error { - tableCol := []string{"server", "recipe", "config", "domain", "version", "chaos", "to deploy"} - table := formatter.CreateTable(tableCol) +var borderStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.ThickBorder()). + Padding(0, 1, 0, 1). + MaxWidth(79). + BorderForeground(lipgloss.Color("63")) +var headerStyle = lipgloss.NewStyle(). + Underline(true). + Bold(true) + +var leftStyle = lipgloss.NewStyle(). + Bold(true) + +var rightStyle = lipgloss.NewStyle() + +// horizontal is a JoinHorizontal helper function. +func horizontal(left, mid, right string) string { + return lipgloss.JoinHorizontal(lipgloss.Left, left, mid, right) +} + +// NewVersionOverview shows an upgrade or downgrade overview +func NewVersionOverview( + app appPkg.App, + warnMessages []string, + kind, + currentVersion, + chaosVersion, + newVersion, + releaseNotes string) error { deployConfig := "compose.yml" if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n") @@ -27,22 +51,36 @@ func NewVersionOverview(app appPkg.App, currentVersion, chaosVersion, newVersion server = "local" } - table.Append([]string{ - server, - app.Recipe.Name, - deployConfig, - app.Domain, - currentVersion, - chaosVersion, - newVersion, - }) - table.Render() + body := strings.Builder{} + body.WriteString( + borderStyle.Render( + lipgloss.JoinVertical( + lipgloss.Center, + headerStyle.Render(fmt.Sprintf("%s OVERVIEW", strings.ToUpper(kind))), + lipgloss.JoinVertical( + lipgloss.Left, + horizontal(leftStyle.Render("SERVER"), " ", rightStyle.Render(server)), + horizontal(leftStyle.Render("DOMAIN"), " ", rightStyle.Render(app.Domain)), + horizontal(leftStyle.Render("RECIPE"), " ", rightStyle.Render(app.Recipe.Name)), + horizontal(leftStyle.Render("CONFIG"), " ", rightStyle.Render(deployConfig)), + horizontal(leftStyle.Render("VERSION"), " ", rightStyle.Render(currentVersion)), + horizontal(leftStyle.Render("CHAOS"), " ", rightStyle.Render(chaosVersion)), + horizontal(leftStyle.Render("DEPLOY"), " ", rightStyle.Padding(0).Render(newVersion)), + ), + ), + ), + ) + fmt.Println(body.String()) if releaseNotes != "" && newVersion != "" { fmt.Println() fmt.Print(releaseNotes) } else { - log.Warnf("no release notes available for %s", newVersion) + warnMessages = append(warnMessages, fmt.Sprintf("no release notes available for %s", newVersion)) + } + + for _, msg := range warnMessages { + log.Warn(msg) } if NoInput { @@ -50,16 +88,66 @@ func NewVersionOverview(app appPkg.App, currentVersion, chaosVersion, newVersion } response := false - prompt := &survey.Confirm{ - Message: "continue with deployment?", - } - + prompt := &survey.Confirm{Message: "proceed?"} if err := survey.AskOne(prompt, &response); err != nil { return err } if !response { - log.Fatal("exiting as requested") + log.Fatal("deployment cancelled") + } + + return nil +} + +// DeployOverview shows a deployment overview +func DeployOverview(app appPkg.App, warnMessages []string, version, chaosVersion string) error { + deployConfig := "compose.yml" + if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { + deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n") + } + + server := app.Server + if app.Server == "default" { + server = "local" + } + + body := strings.Builder{} + body.WriteString( + borderStyle.Render( + lipgloss.JoinVertical( + lipgloss.Center, + headerStyle.Render("DEPLOY OVERVIEW"), + lipgloss.JoinVertical( + lipgloss.Left, + horizontal(leftStyle.Render("SERVER"), " ", rightStyle.Render(server)), + horizontal(leftStyle.Render("DOMAIN"), " ", rightStyle.Render(app.Domain)), + horizontal(leftStyle.Render("RECIPE"), " ", rightStyle.Render(app.Recipe.Name)), + horizontal(leftStyle.Render("CONFIG"), " ", rightStyle.Render(deployConfig)), + horizontal(leftStyle.Render("VERSION"), " ", rightStyle.Render(version)), + horizontal(leftStyle.Render("CHAOS"), " ", rightStyle.Padding(0).Render(chaosVersion)), + ), + ), + ), + ) + fmt.Println(body.String()) + + for _, msg := range warnMessages { + log.Warn(msg) + } + + if NoInput { + return nil + } + + response := false + prompt := &survey.Confirm{Message: "proceed?"} + if err := survey.AskOne(prompt, &response); err != nil { + return err + } + + if !response { + log.Fatal("deployment cancelled") } return nil @@ -118,48 +206,3 @@ func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error { } return nil } - -// DeployOverview shows a deployment overview -func DeployOverview(app appPkg.App, version, chaosVersion, message string) error { - tableCol := []string{"server", "recipe", "config", "domain", "version", "chaos"} - table := formatter.CreateTable(tableCol) - - deployConfig := "compose.yml" - if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { - deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n") - } - - server := app.Server - if app.Server == "default" { - server = "local" - } - - table.Append([]string{ - server, - app.Recipe.Name, - deployConfig, - app.Domain, - version, - chaosVersion, - }) - table.Render() - - if NoInput { - return nil - } - - response := false - prompt := &survey.Confirm{ - Message: message, - } - - if err := survey.AskOne(prompt, &response); err != nil { - return err - } - - if !response { - log.Fatal("exiting as requested") - } - - 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/cli/updater/updater.go b/cli/updater/updater.go index ca2b8ff2..e35b1867 100644 --- a/cli/updater/updater.go +++ b/cli/updater/updater.go @@ -487,8 +487,11 @@ func newAbraApp(version, commit string) *cli.App { } app.Before = func(c *cli.Context) error { + log.Logger.SetStyles(log.Styles()) charmLog.SetDefault(log.Logger) + log.Debugf("kadabra version %s, commit %s", version, commit) + return nil } diff --git a/go.mod b/go.mod index 7ff87ad7..b49de930 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355 github.com/AlecAivazis/survey/v2 v2.3.7 + github.com/charmbracelet/lipgloss v0.11.1 github.com/charmbracelet/log v0.4.0 github.com/distribution/reference v0.6.0 github.com/docker/cli v27.0.3+incompatible @@ -15,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 ) @@ -32,8 +33,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/charmbracelet/lipgloss v0.11.0 // indirect - github.com/charmbracelet/x/ansi v0.1.2 // indirect + github.com/charmbracelet/x/ansi v0.1.3 // indirect github.com/cloudflare/circl v1.3.9 // indirect github.com/containerd/log v0.1.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect @@ -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 86018965..9ec6b4c3 100644 --- a/go.sum +++ b/go.sum @@ -135,12 +135,12 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/charmbracelet/lipgloss v0.11.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g= -github.com/charmbracelet/lipgloss v0.11.0/go.mod h1:1UdRTH9gYgpcdNN5oBtjbu/IzNKtzVtb7sqN1t9LNn8= +github.com/charmbracelet/lipgloss v0.11.1 h1:a8KgVPHa7kOoP95vm2tQQrjD2AKhbWmfr4uJ2RW6kNk= +github.com/charmbracelet/lipgloss v0.11.1/go.mod h1:beLlcmkF7MWA+5UrKKIRo/VJ21xGXr7YJ9miWfdMRIU= github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= -github.com/charmbracelet/x/ansi v0.1.2 h1:6+LR39uG8DE6zAmbu023YlqjJHkYXDF1z36ZwzO4xZY= -github.com/charmbracelet/x/ansi v0.1.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/ansi v0.1.3 h1:RBh/eleNWML5R524mjUF0yVRePTwqN9tPtV+DPgO5Lw= +github.com/charmbracelet/x/ansi v0.1.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= @@ -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/log/log.go b/pkg/log/log.go index 9e19f45d..e2a30e3c 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -3,7 +3,9 @@ package log import ( "os" + "strings" + "github.com/charmbracelet/lipgloss" charmLog "github.com/charmbracelet/log" ) @@ -32,3 +34,42 @@ var SetLevel = Logger.SetLevel var DebugLevel = charmLog.DebugLevel var SetOutput = charmLog.SetOutput var SetReportCaller = charmLog.SetReportCaller + +func Styles() *charmLog.Styles { + styles := charmLog.DefaultStyles() + + styles.Levels = map[charmLog.Level]lipgloss.Style{ + charmLog.DebugLevel: lipgloss.NewStyle(). + SetString(strings.ToUpper(DebugLevel.String())). + Bold(true). + Padding(0, 1, 0, 1). + Background(lipgloss.Color("63")). + Foreground(lipgloss.Color("15")), + charmLog.InfoLevel: lipgloss.NewStyle(). + SetString(strings.ToUpper(charmLog.InfoLevel.String())). + Bold(true). + Padding(0, 1, 0, 1). + Background(lipgloss.Color("86")). + Foreground(lipgloss.Color("16")), + charmLog.WarnLevel: lipgloss.NewStyle(). + SetString(strings.ToUpper(charmLog.WarnLevel.String())). + Bold(true). + Padding(0, 1, 0, 1). + Background(lipgloss.Color("192")). + Foreground(lipgloss.Color("16")), + charmLog.ErrorLevel: lipgloss.NewStyle(). + SetString(strings.ToUpper(charmLog.ErrorLevel.String())). + Bold(true). + Padding(0, 1, 0, 1). + Background(lipgloss.Color("204")). + Foreground(lipgloss.Color("15")), + charmLog.FatalLevel: lipgloss.NewStyle(). + SetString(strings.ToUpper(charmLog.FatalLevel.String())). + Bold(true). + Padding(0, 1, 0, 1). + Background(lipgloss.Color("134")). + Foreground(lipgloss.Color("15")), + } + + return styles +} 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