summaryrefslogtreecommitdiff
path: root/internal/cli
diff options
context:
space:
mode:
authorValery Piashchynski <[email protected]>2022-01-15 12:08:20 +0300
committerValery Piashchynski <[email protected]>2022-01-15 12:08:20 +0300
commit5254c8eb27311e2a8a53a4c90c3829cf1238c563 (patch)
treeb51c9a4c1dd4c25adc511498ce0380a7078c5572 /internal/cli
parent13609dd03dd0d2fa85b9fb850be787bf4e2ea67f (diff)
Repository content update
Signed-off-by: Valery Piashchynski <[email protected]>
Diffstat (limited to 'internal/cli')
-rw-r--r--internal/cli/reset/command.go104
-rw-r--r--internal/cli/reset/command_test.go21
-rw-r--r--internal/cli/root.go98
-rw-r--r--internal/cli/root_test.go85
-rw-r--r--internal/cli/serve/command.go104
-rw-r--r--internal/cli/serve/command_test.go21
-rw-r--r--internal/cli/workers/command.go143
-rw-r--r--internal/cli/workers/command_test.go49
-rw-r--r--internal/cli/workers/render.go135
9 files changed, 760 insertions, 0 deletions
diff --git a/internal/cli/reset/command.go b/internal/cli/reset/command.go
new file mode 100644
index 00000000..d6cf7087
--- /dev/null
+++ b/internal/cli/reset/command.go
@@ -0,0 +1,104 @@
+package reset
+
+import (
+ "fmt"
+ "sync"
+
+ internalRpc "github.com/spiral/roadrunner-binary/v2/internal/rpc"
+
+ "github.com/fatih/color"
+ "github.com/mattn/go-runewidth"
+ "github.com/spf13/cobra"
+ "github.com/spiral/errors"
+ "github.com/spiral/roadrunner-plugins/v2/config"
+ "github.com/vbauerster/mpb/v5"
+ "github.com/vbauerster/mpb/v5/decor"
+)
+
+var spinnerStyle = []string{"∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙"} //nolint:gochecknoglobals
+
+// NewCommand creates `reset` command.
+func NewCommand(cfgPlugin *config.Plugin) *cobra.Command { //nolint:funlen
+ return &cobra.Command{
+ Use: "reset",
+ Short: "Reset workers of all or specific RoadRunner service",
+ RunE: func(_ *cobra.Command, args []string) error {
+ const (
+ op = errors.Op("reset_handler")
+ resetterList = "resetter.List"
+ resetterReset = "resetter.Reset"
+ )
+
+ client, err := internalRpc.NewClient(cfgPlugin)
+ if err != nil {
+ return err
+ }
+
+ defer func() { _ = client.Close() }()
+
+ services := args // by default we expect services list from user
+ if len(services) == 0 { // but if nothing was passed - request all services list
+ if err = client.Call(resetterList, true, &services); err != nil {
+ return err
+ }
+ }
+
+ var wg sync.WaitGroup
+ wg.Add(len(services))
+
+ pr := mpb.New(mpb.WithWaitGroup(&wg), mpb.WithWidth(6)) //nolint:gomnd
+
+ for _, service := range services {
+ var (
+ bar *mpb.Bar
+ name = runewidth.FillRight(fmt.Sprintf("Resetting plugin: [%s]", color.HiYellowString(service)), 27)
+ result = make(chan interface{})
+ )
+
+ bar = pr.AddSpinner(
+ 1,
+ mpb.SpinnerOnMiddle,
+ mpb.SpinnerStyle(spinnerStyle),
+ mpb.PrependDecorators(decor.Name(name)),
+ mpb.AppendDecorators(onComplete(result)),
+ )
+
+ // simulating some work
+ go func(service string, result chan interface{}) {
+ defer wg.Done()
+ defer bar.Increment()
+
+ var done bool
+ <-client.Go(resetterReset, service, &done, nil).Done
+
+ if err != nil {
+ result <- errors.E(op, err)
+
+ return
+ }
+
+ result <- nil
+ }(service, result)
+ }
+
+ pr.Wait()
+
+ return nil
+ },
+ }
+}
+
+func onComplete(result chan interface{}) decor.Decorator {
+ return decor.Any(func(s decor.Statistics) string {
+ select {
+ case r := <-result:
+ if err, ok := r.(error); ok {
+ return color.HiRedString(err.Error())
+ }
+
+ return color.HiGreenString("done")
+ default:
+ return ""
+ }
+ })
+}
diff --git a/internal/cli/reset/command_test.go b/internal/cli/reset/command_test.go
new file mode 100644
index 00000000..00cd046e
--- /dev/null
+++ b/internal/cli/reset/command_test.go
@@ -0,0 +1,21 @@
+package reset_test
+
+import (
+ "testing"
+
+ "github.com/spiral/roadrunner-binary/v2/internal/cli/reset"
+
+ "github.com/spiral/roadrunner-plugins/v2/config"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCommandProperties(t *testing.T) {
+ cmd := reset.NewCommand(&config.Plugin{})
+
+ assert.Equal(t, "reset", cmd.Use)
+ assert.NotNil(t, cmd.RunE)
+}
+
+func TestExecution(t *testing.T) {
+ t.Skip("Command execution is not implemented yet")
+}
diff --git a/internal/cli/root.go b/internal/cli/root.go
new file mode 100644
index 00000000..8572bdc6
--- /dev/null
+++ b/internal/cli/root.go
@@ -0,0 +1,98 @@
+package cli
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "runtime"
+
+ "github.com/spiral/roadrunner-binary/v2/internal/cli/reset"
+ "github.com/spiral/roadrunner-binary/v2/internal/cli/serve"
+ "github.com/spiral/roadrunner-binary/v2/internal/cli/workers"
+ dbg "github.com/spiral/roadrunner-binary/v2/internal/debug"
+ "github.com/spiral/roadrunner-binary/v2/internal/meta"
+
+ "github.com/joho/godotenv"
+ "github.com/spf13/cobra"
+ "github.com/spiral/roadrunner-plugins/v2/config"
+)
+
+// NewCommand creates root command.
+func NewCommand(cmdName string) *cobra.Command { //nolint:funlen
+ const envDotenv = "DOTENV_PATH" // env var name: path to the .env file
+
+ var ( // flag values
+ cfgFile string // path to the .rr.yaml
+ workDir string // working directory
+ dotenv string // path to the .env file
+ debug bool // debug mode
+ override []string // override config values
+ )
+
+ var configPlugin = &config.Plugin{} // will be overwritten on pre-run action
+
+ cmd := &cobra.Command{
+ Use: cmdName,
+ Short: "High-performance PHP application server, load-balancer and process manager",
+ SilenceErrors: true,
+ SilenceUsage: true,
+ Version: fmt.Sprintf("%s (build time: %s, %s)", meta.Version(), meta.BuildTime(), runtime.Version()),
+ PersistentPreRunE: func(*cobra.Command, []string) error {
+ if cfgFile != "" {
+ if absPath, err := filepath.Abs(cfgFile); err == nil {
+ cfgFile = absPath // switch config path to the absolute
+
+ // force working absPath related to config file
+ if err = os.Chdir(filepath.Dir(absPath)); err != nil {
+ return err
+ }
+ }
+ }
+
+ if workDir != "" {
+ if err := os.Chdir(workDir); err != nil {
+ return err
+ }
+ }
+
+ if v, ok := os.LookupEnv(envDotenv); ok { // read path to the dotenv file from environment variable
+ dotenv = v
+ }
+
+ if dotenv != "" {
+ _ = godotenv.Load(dotenv) // error ignored because dotenv is optional feature
+ }
+
+ cfg := &config.Plugin{Path: cfgFile, Prefix: "rr", Flags: override}
+ if err := cfg.Init(); err != nil {
+ return err
+ }
+
+ if debug {
+ srv := dbg.NewServer()
+ go func() { _ = srv.Start(":6061") }() // TODO implement graceful server stopping
+ }
+
+ // overwrite
+ *configPlugin = *cfg
+
+ return nil
+ },
+ }
+
+ f := cmd.PersistentFlags()
+
+ f.StringVarP(&cfgFile, "config", "c", ".rr.yaml", "config file")
+ f.StringVarP(&workDir, "WorkDir", "w", "", "working directory") // TODO change to `workDir`?
+ f.StringVarP(&dotenv, "dotenv", "", "", fmt.Sprintf("dotenv file [$%s]", envDotenv))
+ f.BoolVarP(&debug, "debug", "d", false, "debug mode")
+ f.StringArrayVarP(&override, "override", "o", nil, "override config value (dot.notation=value)")
+
+ cmd.AddCommand(
+ workers.NewCommand(configPlugin),
+ reset.NewCommand(configPlugin),
+ serve.NewCommand(configPlugin),
+ )
+
+ return cmd
+}
diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go
new file mode 100644
index 00000000..59af9294
--- /dev/null
+++ b/internal/cli/root_test.go
@@ -0,0 +1,85 @@
+package cli_test
+
+import (
+ "testing"
+
+ "github.com/spiral/roadrunner-binary/v2/internal/cli"
+
+ "github.com/spf13/cobra"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCommandSubcommands(t *testing.T) {
+ cmd := cli.NewCommand("unit test")
+
+ cases := []struct {
+ giveName string
+ }{
+ {giveName: "workers"},
+ {giveName: "reset"},
+ {giveName: "serve"},
+ }
+
+ // get all existing subcommands and put into the map
+ subcommands := make(map[string]*cobra.Command)
+ for _, sub := range cmd.Commands() {
+ subcommands[sub.Name()] = sub
+ }
+
+ for _, tt := range cases {
+ tt := tt
+ t.Run(tt.giveName, func(t *testing.T) {
+ if _, exists := subcommands[tt.giveName]; !exists {
+ assert.Failf(t, "command not found", "command [%s] was not found", tt.giveName)
+ }
+ })
+ }
+}
+
+func TestCommandFlags(t *testing.T) {
+ cmd := cli.NewCommand("unit test")
+
+ cases := []struct {
+ giveName string
+ wantShorthand string
+ wantDefault string
+ }{
+ {giveName: "config", wantShorthand: "c", wantDefault: ".rr.yaml"},
+ {giveName: "WorkDir", wantShorthand: "w", wantDefault: ""},
+ {giveName: "dotenv", wantShorthand: "", wantDefault: ""},
+ {giveName: "debug", wantShorthand: "d", wantDefault: "false"},
+ {giveName: "override", wantShorthand: "o", wantDefault: "[]"},
+ }
+
+ for _, tt := range cases {
+ tt := tt
+ t.Run(tt.giveName, func(t *testing.T) {
+ flag := cmd.Flag(tt.giveName)
+
+ if flag == nil {
+ assert.Failf(t, "flag not found", "flag [%s] was not found", tt.giveName)
+
+ return
+ }
+
+ assert.Equal(t, tt.wantShorthand, flag.Shorthand)
+ assert.Equal(t, tt.wantDefault, flag.DefValue)
+ })
+ }
+}
+
+func TestCommandSimpleExecuting(t *testing.T) {
+ cmd := cli.NewCommand("unit test")
+ cmd.SetArgs([]string{"-c", "./../../.rr.yaml"})
+
+ var executed bool
+
+ if cmd.Run == nil { // override "Run" property for test (if it was not set)
+ cmd.Run = func(cmd *cobra.Command, args []string) {
+ executed = true
+ }
+ }
+
+ assert.NoError(t, cmd.Execute())
+ assert.True(t, executed)
+}
diff --git a/internal/cli/serve/command.go b/internal/cli/serve/command.go
new file mode 100644
index 00000000..6679d795
--- /dev/null
+++ b/internal/cli/serve/command.go
@@ -0,0 +1,104 @@
+package serve
+
+import (
+ "fmt"
+ "os"
+ "os/signal"
+ "syscall"
+
+ "github.com/spiral/roadrunner-binary/v2/internal/container"
+ "github.com/spiral/roadrunner-binary/v2/internal/meta"
+
+ "github.com/spf13/cobra"
+ "github.com/spiral/errors"
+ configImpl "github.com/spiral/roadrunner-plugins/v2/config"
+)
+
+// NewCommand creates `serve` command.
+func NewCommand(cfgPlugin *configImpl.Plugin) *cobra.Command { //nolint:funlen
+ return &cobra.Command{
+ Use: "serve",
+ Short: "Start RoadRunner server",
+ RunE: func(*cobra.Command, []string) error {
+ const op = errors.Op("handle_serve_command")
+
+ // create endure container config
+ containerCfg, err := container.NewConfig(cfgPlugin)
+ if err != nil {
+ return errors.E(op, err)
+ }
+
+ // set the grace period which would be same for all the plugins
+ cfgPlugin.Timeout = containerCfg.GracePeriod
+ cfgPlugin.Version = meta.Version()
+
+ // create endure container
+ endureContainer, err := container.NewContainer(*containerCfg)
+ if err != nil {
+ return errors.E(op, err)
+ }
+
+ // register config plugin
+ if err = endureContainer.Register(cfgPlugin); err != nil {
+ return errors.E(op, err)
+ }
+
+ // register another container plugins
+ for i, plugins := 0, container.Plugins(); i < len(plugins); i++ {
+ if err = endureContainer.Register(plugins[i]); err != nil {
+ return errors.E(op, err)
+ }
+ }
+
+ // init container and all services
+ if err = endureContainer.Init(); err != nil {
+ return errors.E(op, err)
+ }
+
+ // start serving the graph
+ errCh, err := endureContainer.Serve()
+ if err != nil {
+ return errors.E(op, err)
+ }
+
+ oss, stop := make(chan os.Signal, 2), make(chan struct{}, 1) //nolint:gomnd
+ signal.Notify(oss, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
+
+ go func() {
+ // first catch - stop the container
+ <-oss
+ // send signal to stop execution
+ stop <- struct{}{}
+
+ // after first hit we are waiting for the second
+ // second catch - exit from the process
+ <-oss
+ fmt.Println("exit forced")
+ os.Exit(1)
+ }()
+
+ fmt.Printf("[INFO] RoadRunner server started; version: %s, buildtime: %s\n", meta.Version(), meta.BuildTime())
+
+ for {
+ select {
+ case e := <-errCh:
+ fmt.Printf("error occurred: %v, plugin: %s\n", e.Error, e.VertexID)
+
+ // return error, container already stopped internally
+ if !containerCfg.RetryOnFail {
+ return errors.E(op, e.Error)
+ }
+
+ case <-stop: // stop the container after first signal
+ fmt.Printf("stop signal received, grace timeout is: %d seconds\n", uint64(containerCfg.GracePeriod.Seconds()))
+
+ if err = endureContainer.Stop(); err != nil {
+ fmt.Printf("error occurred during the stopping container: %v\n", err)
+ }
+
+ return nil
+ }
+ }
+ },
+ }
+}
diff --git a/internal/cli/serve/command_test.go b/internal/cli/serve/command_test.go
new file mode 100644
index 00000000..0e61ce83
--- /dev/null
+++ b/internal/cli/serve/command_test.go
@@ -0,0 +1,21 @@
+package serve_test
+
+import (
+ "testing"
+
+ "github.com/spiral/roadrunner-binary/v2/internal/cli/serve"
+
+ "github.com/spiral/roadrunner-plugins/v2/config"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCommandProperties(t *testing.T) {
+ cmd := serve.NewCommand(&config.Plugin{})
+
+ assert.Equal(t, "serve", cmd.Use)
+ assert.NotNil(t, cmd.RunE)
+}
+
+func TestExecution(t *testing.T) {
+ t.Skip("Command execution is not implemented yet")
+}
diff --git a/internal/cli/workers/command.go b/internal/cli/workers/command.go
new file mode 100644
index 00000000..283887e4
--- /dev/null
+++ b/internal/cli/workers/command.go
@@ -0,0 +1,143 @@
+package workers
+
+import (
+ "fmt"
+ "net/rpc"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+
+ "github.com/roadrunner-server/api/v2/plugins/jobs"
+ internalRpc "github.com/spiral/roadrunner-binary/v2/internal/rpc"
+
+ tm "github.com/buger/goterm"
+ "github.com/fatih/color"
+ "github.com/spf13/cobra"
+ "github.com/spiral/errors"
+ "github.com/spiral/roadrunner-plugins/v2/config"
+ "github.com/spiral/roadrunner-plugins/v2/informer"
+)
+
+// NewCommand creates `workers` command.
+func NewCommand(cfgPlugin *config.Plugin) *cobra.Command { //nolint:funlen
+ var (
+ // interactive workers updates
+ interactive bool
+ )
+
+ cmd := &cobra.Command{
+ Use: "workers",
+ Short: "Show information about active RoadRunner workers",
+ RunE: func(_ *cobra.Command, args []string) error {
+ const (
+ op = errors.Op("handle_workers_command")
+ informerList = "informer.List"
+ )
+
+ client, err := internalRpc.NewClient(cfgPlugin)
+ if err != nil {
+ return err
+ }
+
+ defer func() { _ = client.Close() }()
+
+ plugins := args // by default we expect plugins list from user
+ if len(plugins) == 0 { // but if nothing was passed - request all informers list
+ if err = client.Call(informerList, true, &plugins); err != nil {
+ return err
+ }
+ }
+
+ if !interactive {
+ return showWorkers(plugins, client)
+ }
+
+ oss := make(chan os.Signal, 1)
+ signal.Notify(oss, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
+
+ tm.Clear()
+
+ tt := time.NewTicker(time.Second)
+ defer tt.Stop()
+
+ for {
+ select {
+ case <-oss:
+ return nil
+
+ case <-tt.C:
+ tm.MoveCursor(1, 1)
+ tm.Flush()
+
+ if err = showWorkers(plugins, client); err != nil {
+ return errors.E(op, err)
+ }
+ }
+ }
+ },
+ }
+
+ cmd.Flags().BoolVarP(
+ &interactive,
+ "interactive",
+ "i",
+ false,
+ "render interactive workers table",
+ )
+
+ return cmd
+}
+
+func showWorkers(plugins []string, client *rpc.Client) error {
+ const (
+ op = errors.Op("show_workers")
+ informerWorkers = "informer.Workers"
+ informerJobs = "informer.Jobs"
+ // this is only one exception to Render the workers, service plugin has the same workers as other plugins,
+ // but they are RAW processes and needs to be handled in a different way. We don't need a special RPC call, but
+ // need a special render method.
+ servicePluginName = "service"
+ )
+
+ for _, plugin := range plugins {
+ list := &informer.WorkerList{}
+
+ if err := client.Call(informerWorkers, plugin, &list); err != nil {
+ return errors.E(op, err)
+ }
+
+ if len(list.Workers) == 0 {
+ continue
+ }
+
+ if plugin == servicePluginName {
+ fmt.Printf("Workers of [%s]:\n", color.HiYellowString(plugin))
+ ServiceWorkerTable(os.Stdout, list.Workers).Render()
+
+ continue
+ }
+
+ fmt.Printf("Workers of [%s]:\n", color.HiYellowString(plugin))
+
+ WorkerTable(os.Stdout, list.Workers).Render()
+ }
+
+ for _, plugin := range plugins {
+ var jst []*jobs.State
+
+ if err := client.Call(informerJobs, plugin, &jst); err != nil {
+ return errors.E(op, err)
+ }
+
+ // eq to nil
+ if len(jst) == 0 {
+ continue
+ }
+
+ fmt.Printf("Jobs of [%s]:\n", color.HiYellowString(plugin))
+ JobsTable(os.Stdout, jst).Render()
+ }
+
+ return nil
+}
diff --git a/internal/cli/workers/command_test.go b/internal/cli/workers/command_test.go
new file mode 100644
index 00000000..e593686d
--- /dev/null
+++ b/internal/cli/workers/command_test.go
@@ -0,0 +1,49 @@
+package workers_test
+
+import (
+ "testing"
+
+ "github.com/spiral/roadrunner-binary/v2/internal/cli/workers"
+
+ "github.com/spiral/roadrunner-plugins/v2/config"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCommandProperties(t *testing.T) {
+ cmd := workers.NewCommand(&config.Plugin{})
+
+ assert.Equal(t, "workers", cmd.Use)
+ assert.NotNil(t, cmd.RunE)
+}
+
+func TestCommandFlags(t *testing.T) {
+ cmd := workers.NewCommand(&config.Plugin{})
+
+ cases := []struct {
+ giveName string
+ wantShorthand string
+ wantDefault string
+ }{
+ {giveName: "interactive", wantShorthand: "i", wantDefault: "false"},
+ }
+
+ for _, tt := range cases {
+ tt := tt
+ t.Run(tt.giveName, func(t *testing.T) {
+ flag := cmd.Flag(tt.giveName)
+
+ if flag == nil {
+ assert.Failf(t, "flag not found", "flag [%s] was not found", tt.giveName)
+
+ return
+ }
+
+ assert.Equal(t, tt.wantShorthand, flag.Shorthand)
+ assert.Equal(t, tt.wantDefault, flag.DefValue)
+ })
+ }
+}
+
+func TestExecution(t *testing.T) {
+ t.Skip("Command execution is not implemented yet")
+}
diff --git a/internal/cli/workers/render.go b/internal/cli/workers/render.go
new file mode 100644
index 00000000..0bdf09b6
--- /dev/null
+++ b/internal/cli/workers/render.go
@@ -0,0 +1,135 @@
+package workers
+
+import (
+ "io"
+ "strconv"
+ "time"
+
+ "github.com/dustin/go-humanize"
+ "github.com/fatih/color"
+ "github.com/olekukonko/tablewriter"
+ "github.com/roadrunner-server/api/v2/plugins/jobs"
+ "github.com/spiral/roadrunner/v2/state/process"
+)
+
+const (
+ Ready string = "READY"
+ Paused string = "PAUSED/STOPPED"
+)
+
+// WorkerTable renders table with information about rr server workers.
+func WorkerTable(writer io.Writer, workers []*process.State) *tablewriter.Table {
+ tw := tablewriter.NewWriter(writer)
+ tw.SetHeader([]string{"PID", "Status", "Execs", "Memory", "CPU%", "Created"})
+ tw.SetColMinWidth(0, 7)
+ tw.SetColMinWidth(1, 9)
+ tw.SetColMinWidth(2, 7)
+ tw.SetColMinWidth(3, 7)
+ tw.SetColMinWidth(4, 7)
+ tw.SetColMinWidth(5, 18)
+
+ for i := 0; i < len(workers); i++ {
+ tw.Append([]string{
+ strconv.Itoa(workers[i].Pid),
+ renderStatus(workers[i].Status),
+ renderJobs(workers[i].NumJobs),
+ humanize.Bytes(workers[i].MemoryUsage),
+ renderCPU(workers[i].CPUPercent),
+ renderAlive(time.Unix(0, workers[i].Created)),
+ })
+ }
+
+ return tw
+}
+
+// ServiceWorkerTable renders table with information about rr server workers.
+func ServiceWorkerTable(writer io.Writer, workers []*process.State) *tablewriter.Table {
+ tw := tablewriter.NewWriter(writer)
+ tw.SetAutoWrapText(false)
+ tw.SetHeader([]string{"PID", "Memory", "CPU%", "Command"})
+ tw.SetColMinWidth(0, 7)
+ tw.SetColMinWidth(1, 7)
+ tw.SetColMinWidth(2, 7)
+ tw.SetColMinWidth(3, 18)
+ tw.SetAlignment(tablewriter.ALIGN_LEFT)
+
+ for i := 0; i < len(workers); i++ {
+ tw.Append([]string{
+ strconv.Itoa(workers[i].Pid),
+ humanize.Bytes(workers[i].MemoryUsage),
+ renderCPU(workers[i].CPUPercent),
+ workers[i].Command,
+ })
+ }
+
+ return tw
+}
+
+// JobsTable renders table with information about rr server jobs.
+func JobsTable(writer io.Writer, jobs []*jobs.State) *tablewriter.Table {
+ tw := tablewriter.NewWriter(writer)
+ tw.SetAutoWrapText(false)
+ tw.SetHeader([]string{"Status", "Pipeline", "Driver", "Queue", "Active", "Delayed", "Reserved"})
+ tw.SetColWidth(10)
+ tw.SetColWidth(10)
+ tw.SetColWidth(7)
+ tw.SetColWidth(15)
+ tw.SetColWidth(10)
+ tw.SetColWidth(10)
+ tw.SetColWidth(10)
+ tw.SetAlignment(tablewriter.ALIGN_LEFT)
+
+ for i := 0; i < len(jobs); i++ {
+ tw.Append([]string{
+ renderReady(jobs[i].Ready),
+ jobs[i].Pipeline,
+ jobs[i].Driver,
+ jobs[i].Queue,
+ strconv.Itoa(int(jobs[i].Active)),
+ strconv.Itoa(int(jobs[i].Delayed)),
+ strconv.Itoa(int(jobs[i].Reserved)),
+ })
+ }
+
+ return tw
+}
+
+func renderReady(ready bool) string {
+ if ready {
+ return Ready
+ }
+
+ return Paused
+}
+
+//go:inline
+func renderCPU(cpu float64) string {
+ return strconv.FormatFloat(cpu, 'f', 2, 64)
+}
+
+func renderStatus(status string) string {
+ switch status {
+ case "inactive":
+ return color.YellowString("inactive")
+ case "ready":
+ return color.CyanString("ready")
+ case "working":
+ return color.GreenString("working")
+ case "invalid":
+ return color.YellowString("invalid")
+ case "stopped":
+ return color.RedString("stopped")
+ case "errored":
+ return color.RedString("errored")
+ default:
+ return status
+ }
+}
+
+func renderJobs(number uint64) string {
+ return humanize.Comma(int64(number))
+}
+
+func renderAlive(t time.Time) string {
+ return humanize.RelTime(t, time.Now(), "ago", "")
+}