diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a1edf65..95b916ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ _Date_ - More precise errors are returned to users when invalid usernames, org, project, team, or policy names are submitted to a ui prompt. - The experimental and hidden `policies test` command has been removed. +- Added spinners to represent progress. This means fewer lasting print-outs + for certain commands. ## v0.29.0 diff --git a/Gopkg.lock b/Gopkg.lock index 5b9cf0c0..58cf5d4d 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -48,6 +48,12 @@ revision = "2f1ce7a837dcb8da3ec595b1dac9d0632f0f99e8" version = "v1.3.1" +[[projects]] + name = "github.com/briandowns/spinner" + packages = ["."] + revision = "48dbb65d7bd5c74ab50d53d04c949f20e3d14944" + version = "1.0" + [[projects]] name = "github.com/chzyer/readline" packages = ["."] @@ -89,6 +95,12 @@ packages = ["."] revision = "1b76add642e42c6ffba7211ad7b3939ce654526e" +[[projects]] + name = "github.com/fatih/color" + packages = ["."] + revision = "570b54cabe6b8eb0bc2dfce68d964677d63b5260" + version = "v1.5.0" + [[projects]] branch = "master" name = "github.com/fullsailor/pkcs7" @@ -317,6 +329,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "3f9c345203976dbfe03e5d1a336d3c3e9832a521ce94ddb58bd570eecaa99824" + inputs-digest = "a7676fcc388b53a60a57470c500c0c678bfbf8eddb93d072b9e908ab21dfcc98" solver-name = "gps-cdcl" solver-version = 1 diff --git a/api/credentials.go b/api/credentials.go index 23d7c166..19c8bea9 100644 --- a/api/credentials.go +++ b/api/credentials.go @@ -16,25 +16,25 @@ type CredentialsClient struct { } // Search returns all credentials at the given pathexp in an undecrypted state -func (c *CredentialsClient) Search(ctx context.Context, pathexp string) ([]apitypes.CredentialEnvelope, error) { +func (c *CredentialsClient) Search(ctx context.Context, pathexp string, p ProgressFunc) ([]apitypes.CredentialEnvelope, error) { v := &url.Values{} v.Set("pathexp", pathexp) v.Set("skip-decryption", "true") - return c.listWorker(ctx, v) + return c.listWorker(ctx, v, p) } // Get returns all credentials at the given path. -func (c *CredentialsClient) Get(ctx context.Context, path string) ([]apitypes.CredentialEnvelope, error) { +func (c *CredentialsClient) Get(ctx context.Context, path string, p ProgressFunc) ([]apitypes.CredentialEnvelope, error) { v := &url.Values{} v.Set("path", path) - return c.listWorker(ctx, v) + return c.listWorker(ctx, v, p) } -func (c *CredentialsClient) listWorker(ctx context.Context, v *url.Values) ([]apitypes.CredentialEnvelope, error) { +func (c *CredentialsClient) listWorker(ctx context.Context, v *url.Values, p ProgressFunc) ([]apitypes.CredentialEnvelope, error) { var resp []apitypes.CredentialResp - err := c.client.DaemonRoundTrip(ctx, "GET", "/credentials", v, nil, &resp, nil) + err := c.client.DaemonRoundTrip(ctx, "GET", "/credentials", v, nil, &resp, p) if err != nil { return nil, err } diff --git a/cmd/cmd.go b/cmd/cmd.go index c4f6cd98..a338c871 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -20,6 +20,16 @@ var progress api.ProgressFunc = func(evt *api.Event, err error) { } } +func spinner(text string) (*ui.Spinner, api.ProgressFunc) { + s := ui.NewSpinner(text) + + return s, func(evt *api.Event, err error) { + if evt != nil { + s.Update(evt.Message) + } + } +} + // NewAPIClient loads config and creates a new api client func NewAPIClient(ctx *context.Context, client *api.Client) (context.Context, *api.Client, error) { if client == nil { diff --git a/cmd/import.go b/cmd/import.go index 04fe2dd3..028aece9 100644 --- a/cmd/import.go +++ b/cmd/import.go @@ -8,10 +8,11 @@ import ( "strings" "github.com/google/shlex" + "github.com/urfave/cli" + "github.com/manifoldco/torus-cli/apitypes" "github.com/manifoldco/torus-cli/errs" "github.com/manifoldco/torus-cli/hints" - "github.com/urfave/cli" ) type secretPair struct { @@ -57,7 +58,10 @@ func importCmd(ctx *cli.Context) error { }(secret.value) } - creds, err := setCredentials(ctx, path, makers) + s, p := spinner("Attempting to set credentials") + s.Start() + creds, err := setCredentials(ctx, path, makers, p) + s.Stop() if err != nil { return errs.NewErrorExitError("Could not set credentials.", err) } diff --git a/cmd/invites_approve.go b/cmd/invites_approve.go index b591d7ae..a4d3c2bb 100644 --- a/cmd/invites_approve.go +++ b/cmd/invites_approve.go @@ -53,7 +53,10 @@ func invitesApprove(ctx *cli.Context) error { return errs.NewExitError("Invite not found.") } - err = client.OrgInvites.Approve(context.Background(), *targetInvite, progress) + s, p := spinner(fmt.Sprintf("Attempting to approve invite for %s", email)) + s.Start() + err = client.OrgInvites.Approve(context.Background(), *targetInvite, p) + s.Stop() if err != nil { return err } diff --git a/cmd/keypairs.go b/cmd/keypairs.go index b260074e..394e6892 100644 --- a/cmd/keypairs.go +++ b/cmd/keypairs.go @@ -188,21 +188,27 @@ func generateKeypairs(ctx *cli.Context) error { var rErr error + s, p := spinner("Attempting to generate keypairs") + s.Start() for orgID, name := range regenOrgs { - fmt.Println("Generating signing and encryption keypairs for org: " + name) - err := client.KeyPairs.Create(c, orgID, progress) + s.Update("Generating signing and encryption keypairs for org: " + name) + err := client.KeyPairs.Create(c, orgID, p) if err != nil && rErr == nil { + s.Stop() rErr = err break } } + s.Stop() if rErr != nil { return errs.NewExitError("Error while regenerating keypairs.") } if len(regenOrgs) > 0 { - fmt.Println("Keypair generation successful.") + for _, name := range regenOrgs { + fmt.Printf("Successfully generated keypairs for %s org\n", name) + } } else { fmt.Println("No keypairs missing.") } @@ -228,7 +234,10 @@ func generateKeypairsForOrg(c context.Context, ctx *cli.Context, client *api.Cli orgID = org.ID } - err = client.KeyPairs.Create(c, orgID, progress) + s, p := spinner("Attempting to generate keypairs") + s.Start() + err = client.KeyPairs.Create(c, orgID, p) + s.Stop() if err != nil { return outputErr } @@ -276,7 +285,10 @@ func revokeKeypairs(ctx *cli.Context) error { return nil } - err = client.KeyPairs.Revoke(c, org.ID, progress) + s, p := spinner("Attempting to revoke keypairs") + s.Start() + err = client.KeyPairs.Revoke(c, org.ID, p) + s.Stop() if err != nil { return errs.NewErrorExitError("Error while revoking keypairs.", err) } diff --git a/cmd/list.go b/cmd/list.go index 9d7a9096..b75b1c9c 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -125,7 +125,7 @@ func listCmd(ctx *cli.Context) error { go func() { // Get credentials - credentials, cErr = client.Credentials.Search(c, filterPathExp.String()) + credentials, cErr = client.Credentials.Search(c, filterPathExp.String(), nil) getEnvsServicesCreds.Done() }() @@ -217,8 +217,8 @@ func listCmd(ctx *cli.Context) error { if(verbose){ fmt.Println("") projW := ansiterm.NewTabWriter(os.Stdout, 0, 0, 4, ' ', 0) - fmt.Fprintf(projW, "Org:\t" + ui.Bold(org.Body.Name) + "\t\n") - fmt.Fprintf(projW, "Project:\t" + ui.Bold(project.Body.Name) + "\t\n") + fmt.Fprintf(projW, ui.Bold("Org") + ":\t" + org.Body.Name + "\t\n") + fmt.Fprintf(projW, ui.Bold("Project") + ":\t" + project.Body.Name + "\t\n") projW.Flush() } @@ -247,7 +247,7 @@ func listCmd(ctx *cli.Context) error { } w.Flush() - fmt.Printf("\n(%s) secrets found\n.", ui.Faint(strconv.Itoa(credCount))) + fmt.Printf("\n(%s) secrets found\n", ui.Faint(strconv.Itoa(credCount))) return nil } diff --git a/cmd/set.go b/cmd/set.go index ab6ae376..527d76bc 100644 --- a/cmd/set.go +++ b/cmd/set.go @@ -68,11 +68,13 @@ func setCmd(ctx *cli.Context) error { return apitypes.NewStringCredentialValue(value) } - _, err = setCredentials(ctx, path, makers) + s, p := spinner(fmt.Sprintf("Attempting to set credential %s", name)) + s.Start() + _, err = setCredentials(ctx, path, makers, p) + s.Stop() if err != nil { return errs.NewErrorExitError("Could not set credential.", err) } - fmt.Printf("\nCredential %s has been set at %s/%s\n", name, path, name) hints.Display(hints.View, hints.Run, hints.Unset, hints.Import, hints.Export) @@ -158,7 +160,7 @@ func determinePathFromFlags(ctx *cli.Context) (*pathexp.PathExp, error) { type valueMaker func() *apitypes.CredentialValue type valueMakers map[string]valueMaker -func setCredentials(ctx *cli.Context, pe *pathexp.PathExp, makers valueMakers) ([]apitypes.CredentialEnvelope, error) { +func setCredentials(ctx *cli.Context, pe *pathexp.PathExp, makers valueMakers, p api.ProgressFunc) ([]apitypes.CredentialEnvelope, error) { cfg, err := config.LoadConfig() if err != nil { return nil, err @@ -207,5 +209,5 @@ func setCredentials(ctx *cli.Context, pe *pathexp.PathExp, makers valueMakers) ( }) } - return client.Credentials.Create(c, creds, progress) + return client.Credentials.Create(c, creds, p) } diff --git a/cmd/unset.go b/cmd/unset.go index 8c75c678..38df4d14 100644 --- a/cmd/unset.go +++ b/cmd/unset.go @@ -57,10 +57,13 @@ func unsetCmd(ctx *cli.Context) error { return apitypes.NewUnsetCredentialValue() } - _, err = setCredentials(ctx, pe, makers) + s, p := spinner(fmt.Sprintf("Attempting to unset credential %s", name)) + s.Start() + _, err = setCredentials(ctx, pe, makers, p) if err != nil { return errs.NewErrorExitError("Could not unset credential", err) } + s.Stop() output := fmt.Sprintf("\nCredential %s has been unset at %s/%s.", name, pe, name) fmt.Println(output) diff --git a/cmd/view.go b/cmd/view.go index e4021617..c5fa753a 100644 --- a/cmd/view.go +++ b/cmd/view.go @@ -77,11 +77,9 @@ func viewCmd(ctx *cli.Context) error { } } - return tw.Flush() - hints.Display(hints.Link, hints.Run, hints.Export) - return err + return tw.Flush() } func getSecrets(ctx *cli.Context) ([]apitypes.CredentialEnvelope, string, error) { @@ -120,7 +118,10 @@ func getSecrets(ctx *cli.Context) ([]apitypes.CredentialEnvelope, string, error) path := strings.Join(parts, "/") - secrets, err := client.Credentials.Get(c, path) + s, p := spinner("Decrypting credentials") + s.Start() + secrets, err := client.Credentials.Get(c, path, p) + s.Stop() if err != nil { return nil, "", errs.NewErrorExitError("Error fetching secrets", err) } diff --git a/docs/commands/secrets.md b/docs/commands/secrets.md index 844e4a33..97011884 100644 --- a/docs/commands/secrets.md +++ b/docs/commands/secrets.md @@ -35,12 +35,6 @@ This is how all secrets are stored in Torus. # Setting the port for the production auth service inside myorg's api project. $ torus set -o myorg -p api -e production -s auth PORT 3000 -Credentials retrieved -Keypairs retrieved -Encrypting key retrieved -Credential encrypted -Completed Operation - Credential PORT has been set at /myorg/api/production/auth/*/*/PORT ``` @@ -100,6 +94,16 @@ Credential PORT has been set at /myorg/api/[production|staging]/auth/*/*/PORT `torus unset ` unsets the value for the specified name (or [path](../concepts/path.md)). +**Example** + +```bash +$ torus unset port +You are about to unset "/myorg/myproject/dev-matt/default/*/*/port". This cannot be undone. +✔ Do you wish to continue? [y/N] y + +Credential port has been unset at /myorg/myproject/dev-matt/default/*/*/port. +``` + ## import ###### Added [v0.25.0](https://github.com/manifoldco/torus-cli/blob/v0.25.0/CHANGELOG.md) @@ -109,21 +113,13 @@ Credential PORT has been set at /myorg/api/[production|staging]/auth/*/*/PORT ```bash $ cat prod.env -PORT=4000 -DOMAIN=mydomain.co -MYSQL_URL=mysql://user:pass@host.com:4321/mydb - -$ torus import -e production test.env +DOMAIN="mydomain.co" +PORT="4000" -Credentials retrieved -Keypairs retrieved -Encrypting key retrieved -Credential encrypted -Credential encrypted -Completed Operation +$ torus import -e production prod.env -Credential port has been set at /myorg/myproject/production/default/*/*/PORT -Credential mysql_url has been set at /myorg/myproject/production/default/*/*/MYSQL_URL +Credential domain has been set at /myorg/myproject/production/default/*/*/domain +Credential port has been set at /myorg/myproject/production/default/*/*/port ``` ## export diff --git a/ui/spinner.go b/ui/spinner.go new file mode 100644 index 00000000..a0158690 --- /dev/null +++ b/ui/spinner.go @@ -0,0 +1,38 @@ +package ui + +import ( + "time" + + "github.com/briandowns/spinner" +) + +// Spinner struct contains the spinner struct and display text +type Spinner struct{ + spinner *spinner.Spinner + text string +} + +// NewSpinner creates a new Spinner struct +func NewSpinner(text string) *Spinner { + s := spinner.New(spinner.CharSets[9], 100 * time.Millisecond) + s.Suffix = " " + text + return &Spinner { + s, + text, + } +} + +// Start displays the Spinner and starts movement +func (s *Spinner) Start() { + s.spinner.Start() +} + +// Stop halts the spiner movement and removes it from display +func (s *Spinner) Stop() { + s.spinner.Stop() +} + +// Update changes the Spinner's suffix to 'text' +func (s *Spinner) Update(text string) { + s.spinner.Suffix = " " + text +} diff --git a/ui/ui.go b/ui/ui.go index 14737614..d6462905 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -79,6 +79,31 @@ type UI struct { EnableColors bool } +// NewSpinner creates a new ui.Spinner struct (spinner.go) +func (u *UI) NewSpinner(text string) *Spinner { + return NewSpinner(text) +} + +// StartSpinner checks to ensure progress is enabled and the output is a +// terminal. After that, it starts the ui.Spinner spinning. +func (u *UI) StartSpinner(s *Spinner) { + if !u.EnableProgress || !readline.IsTerminal(int(os.Stdout.Fd())) { + return + } + + s.Start() +} + +// StartSpinner checks to ensure progress is enabled and the output is a +// terminal. After that, it stops the ui.Spinner spinning. +func (u *UI) StopSpinner(s *Spinner) { + if !u.EnableProgress || !readline.IsTerminal(int(os.Stdout.Fd())) { + return + } + + s.Stop() +} + // Progress calls Progress on the default UI func Progress(str string) { defUI.Progress(str) }