Skip to content

Commit

Permalink
Polish UX for sandbox commands (#4)
Browse files Browse the repository at this point in the history
* sandbox create: Add wait timeout and spinner.

* sandbox delete: Wait for sandbox to fully terminate.

* Move print logic to separate package.

* sandbox create: Print info on accessing the sandbox.

* sandbox get: Print details instead of a summary table.

* spinner: Truncate message to fit on one line.

* Keep command-specific printers with their command implementations.

* Update to go-sdk v0.1.3

* Add 'sandbox get-status' command.

* sandbox create/delete: Use new /status API.

* Clarify error message
  • Loading branch information
enisoc authored May 21, 2022
1 parent 4cbf27d commit 424bbad
Show file tree
Hide file tree
Showing 18 changed files with 516 additions and 129 deletions.
9 changes: 8 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@ module github.com/signadot/cli
go 1.18

require (
github.com/docker/go-units v0.4.0
github.com/go-openapi/runtime v0.24.1
github.com/go-openapi/strfmt v0.21.2
github.com/signadot/go-sdk v0.1.2
github.com/signadot/go-sdk v0.1.3
github.com/spf13/cobra v1.4.0
github.com/spf13/viper v1.11.0
github.com/theckman/yacspin v0.13.12
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171
sigs.k8s.io/yaml v1.3.0
)

require (
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/go-openapi/analysis v0.21.3 // indirect
github.com/go-openapi/errors v0.20.2 // indirect
Expand All @@ -29,11 +32,15 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pelletier/go-toml/v2 v2.0.0-beta.8 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
Expand Down
21 changes: 19 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,16 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
Expand Down Expand Up @@ -217,6 +220,14 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
Expand All @@ -241,12 +252,14 @@ github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qR
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/signadot/go-sdk v0.1.2 h1:ucdmm2YfFu8hOY9QZ/kFHT8gN2ObXwYUtIcDBKdXop0=
github.com/signadot/go-sdk v0.1.2/go.mod h1:rdSPPZzJmv4JpdSTXm2Dm1BIwM0esNi9cViCmjDHZVU=
github.com/signadot/go-sdk v0.1.3 h1:0pE+iT80HIVTjyWtrzkz3YyjbvNMy57U3GCdVCXIjVw=
github.com/signadot/go-sdk v0.1.3/go.mod h1:rdSPPZzJmv4JpdSTXm2Dm1BIwM0esNi9cViCmjDHZVU=
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
Expand Down Expand Up @@ -276,6 +289,8 @@ github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMT
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/theckman/yacspin v0.13.12 h1:CdZ57+n0U6JMuh2xqjnjRq5Haj6v1ner2djtLQRzJr4=
github.com/theckman/yacspin v0.13.12/go.mod h1:Rd2+oG2LmQi5f3zC3yeZAOl245z8QOvrH4OPOJNZxLg=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
Expand Down Expand Up @@ -413,6 +428,7 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand All @@ -436,6 +452,7 @@ golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand Down
40 changes: 4 additions & 36 deletions internal/command/cluster/list.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package cluster

import (
"encoding/json"
"fmt"
"io"

"github.com/signadot/cli/internal/config"
"github.com/signadot/cli/internal/sdtab"
"github.com/signadot/cli/internal/print"
"github.com/signadot/go-sdk/client/cluster"
"github.com/spf13/cobra"
"sigs.k8s.io/yaml"
)

func newList(cluster *config.Cluster) *cobra.Command {
Expand All @@ -27,12 +25,6 @@ func newList(cluster *config.Cluster) *cobra.Command {
return cmd
}

type tableRow struct {
Name string `sdtab:"NAME"`
Created string `sdtab:"CREATED"`
Version string `sdtab:"OPERATOR VERSION"`
}

func list(cfg *config.ClusterList, out io.Writer) error {
if err := cfg.InitAPIConfig(); err != nil {
return err
Expand All @@ -45,36 +37,12 @@ func list(cfg *config.ClusterList, out io.Writer) error {

switch cfg.OutputFormat {
case config.OutputFormatDefault:
t := sdtab.New[tableRow](out)
t.AddHeader()
for _, cluster := range clusters {
row := tableRow{
Name: cluster.Name,
Created: cluster.CreatedAt,
Version: cluster.OperatorVersion,
}
t.AddRow(row)
}
if err := t.Flush(); err != nil {
return err
}
return printClusterTable(out, clusters)
case config.OutputFormatJSON:
enc := json.NewEncoder(out)
enc.SetIndent("", " ")
if err := enc.Encode(clusters); err != nil {
return err
}
return print.RawJSON(out, clusters)
case config.OutputFormatYAML:
data, err := yaml.Marshal(clusters)
if err != nil {
return err
}
if _, err := out.Write(data); err != nil {
return err
}
return print.RawYAML(out, clusters)
default:
return fmt.Errorf("unsupported output format: %q", cfg.OutputFormat)
}

return nil
}
27 changes: 27 additions & 0 deletions internal/command/cluster/printers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package cluster

import (
"io"

"github.com/signadot/cli/internal/sdtab"
"github.com/signadot/go-sdk/models"
)

type clusterRow struct {
Name string `sdtab:"NAME"`
Created string `sdtab:"CREATED"`
Version string `sdtab:"OPERATOR VERSION"`
}

func printClusterTable(out io.Writer, clusters []*models.Cluster) error {
t := sdtab.New[clusterRow](out)
t.AddHeader()
for _, cluster := range clusters {
t.AddRow(clusterRow{
Name: cluster.Name,
Created: cluster.CreatedAt,
Version: cluster.OperatorVersion,
})
}
return t.Flush()
}
1 change: 1 addition & 0 deletions internal/command/sandbox/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func New(api *config.Api) *cobra.Command {
newList(cfg),
newCreate(cfg),
newDelete(cfg),
newGetStatus(cfg),
)

return cmd
Expand Down
60 changes: 45 additions & 15 deletions internal/command/sandbox/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (

"github.com/signadot/cli/internal/clio"
"github.com/signadot/cli/internal/config"
"github.com/signadot/cli/internal/poll"
"github.com/signadot/cli/internal/spinner"
"github.com/signadot/go-sdk/client/sandboxes"
"github.com/signadot/go-sdk/models"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -59,24 +61,52 @@ func create(cfg *config.SandboxCreate, out io.Writer) error {

if cfg.Wait {
// Wait for the sandbox to be ready.
fmt.Fprintln(out, "Waiting for sandbox to be ready...")

params := sandboxes.NewGetSandboxReadyParams().WithOrgName(cfg.Org).WithSandboxID(resp.SandboxID)

// We use a hot loop because the server implements rate-limiting for us.
for {
result, err := cfg.Client.Sandboxes.GetSandboxReady(params, nil)
if err != nil {
return err
}
if result.Payload.Ready {
break
}
// TODO: Show status message when it's added to the API response.
if err := waitForReady(cfg, out, resp.SandboxID); err != nil {
fmt.Fprintf(out, "\nThe sandbox was created, but it may not be ready yet. To check status, run:\n\n")
fmt.Fprintf(out, " signadot sandbox get-status %v\n\n", req.Name)
return err
}
}

// Print info on how to access the sandbox.
sbURL := cfg.SandboxDashboardURL(resp.SandboxID)
fmt.Fprintf(out, "\nDashboard page: %v\n\n", sbURL)

fmt.Fprintln(out, "Sandbox is ready.")
if len(resp.PreviewEndpoints) > 0 {
if err := printEndpointTable(out, resp.PreviewEndpoints); err != nil {
return err
}
}

return nil
}

func waitForReady(cfg *config.SandboxCreate, out io.Writer, sandboxID string) error {
fmt.Fprintf(out, "Waiting (up to --wait-timeout=%v) for sandbox to be ready...\n", cfg.WaitTimeout)

params := sandboxes.NewGetSandboxStatusByIDParams().WithOrgName(cfg.Org).WithSandboxID(sandboxID)

spin := spinner.Start(out, "Sandbox status")
defer spin.Stop()

err := poll.Until(cfg.WaitTimeout, func() bool {
result, err := cfg.Client.Sandboxes.GetSandboxStatusByID(params, nil)
if err != nil {
// Keep retrying in case it's a transient error.
spin.Messagef("error: %v", err)
return false
}
status := result.Payload.Status
if !status.Ready {
spin.Messagef("Not Ready: %s", status.Message)
return false
}
spin.StopMessagef("Ready: %s", status.Message)
return true
})
if err != nil {
spin.StopFail()
return err
}
return nil
}
51 changes: 50 additions & 1 deletion internal/command/sandbox/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import (
"errors"
"fmt"
"io"
"strings"

"github.com/signadot/cli/internal/clio"
"github.com/signadot/cli/internal/config"
"github.com/signadot/cli/internal/poll"
"github.com/signadot/cli/internal/spinner"
"github.com/signadot/go-sdk/client/sandboxes"
"github.com/signadot/go-sdk/models"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -79,7 +82,53 @@ func delete(cfg *config.SandboxDelete, out io.Writer, args []string) error {
return err
}

fmt.Fprintf(out, "Deleted sandbox %q.\n", name)
fmt.Fprintf(out, "Deleted sandbox %q.\n\n", name)

if cfg.Wait {
// Wait for the API server to completely reflect deletion.
if err := waitForDeleted(cfg, out, id); err != nil {
fmt.Fprintf(out, "\nDeletion was initiated, but the sandbox may still exist in a terminating state. To check status, run:\n\n")
fmt.Fprintf(out, " signadot sandbox get-status %v\n\n", name)
return err
}
}

return nil
}

func waitForDeleted(cfg *config.SandboxDelete, out io.Writer, sandboxID string) error {
fmt.Fprintf(out, "Waiting (up to --wait-timeout=%v) for sandbox to finish terminating...\n", cfg.WaitTimeout)

params := sandboxes.NewGetSandboxStatusByIDParams().WithOrgName(cfg.Org).WithSandboxID(sandboxID)

spin := spinner.Start(out, "Sandbox status")
defer spin.Stop()

err := poll.Until(cfg.WaitTimeout, func() bool {
result, err := cfg.Client.Sandboxes.GetSandboxStatusByID(params, nil)
if err != nil {
// If it's a "not found" error, that's what we wanted.
// TODO: Pass through an error code so we don't have to rely on the error message.
if strings.Contains(err.Error(), "can't get sandbox status: not found") {
spin.StopMessage("Terminated")
return true
}

// Otherwise, keep retrying in case it's a transient error.
spin.Messagef("error: %v", err)
return false
}
status := result.Payload.Status
if status.Ready {
spin.Message("Waiting for sandbox to terminate")
return false
}
spin.Messagef("%s: %s", status.Reason, status.Message)
return false
})
if err != nil {
spin.StopFail()
return err
}
return nil
}
Loading

0 comments on commit 424bbad

Please sign in to comment.