Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 19 additions & 16 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package cmd

import (
"errors"
"fmt"
"os"

Expand All @@ -13,6 +14,7 @@ import (
gleanClient "github.com/gleanwork/glean-cli/internal/client"
"github.com/gleanwork/glean-cli/internal/config"
"github.com/gleanwork/glean-cli/internal/debug"
clierrors "github.com/gleanwork/glean-cli/internal/errors"
"github.com/gleanwork/glean-cli/internal/tui"
"github.com/gleanwork/glean-cli/internal/update"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -176,30 +178,31 @@ func NewCmdRoot() *cobra.Command {
return cmd
}

// errSilent is returned when we've already printed a user-friendly error
// and don't want Cobra to print anything additional.
var errSilent = fmt.Errorf("")

// authError formats an authentication failure as a user-friendly message
// written to stderr, then returns errSilent so Cobra prints nothing further.
func authError(err error) error {
fmt.Fprintf(os.Stderr, "\nYou're not signed in to Glean.\n\n")
fmt.Fprintf(os.Stderr, " Run: glean auth login\n\n")
fmt.Fprintf(os.Stderr, "Or set environment variables:\n")
fmt.Fprintf(os.Stderr, " export GLEAN_HOST=your-company-be.glean.com\n")
fmt.Fprintf(os.Stderr, " export GLEAN_API_TOKEN=your-token\n\n")
authErrLog.Log("underlying auth error: %v", err)

suggestion := "Run: glean auth login\n\n" +
"Or set environment variables:\n" +
" export GLEAN_HOST=your-company-be.glean.com\n" +
" export GLEAN_API_TOKEN=your-token"
if !authErrLog.Enabled() {
fmt.Fprintf(os.Stderr, " Tip: re-run with -v or GLEAN_DEBUG=auth:* for details\n\n")
suggestion += "\n\n Tip: re-run with -v or GLEAN_DEBUG=auth:* for details"
}

return &clierrors.CLIError{
UserMessage: "You're not signed in to Glean.",
Suggestion: suggestion,
ExitCode: clierrors.ExitAuthError,
Cause: err,
}
return errSilent
}

// Execute runs the root command and handles any errors.
// It's the main entry point for the CLI application.
func Execute() error {
if err := rootCmd.Execute(); err != nil {
if err != errSilent {
var cliErr *clierrors.CLIError
if errors.As(err, &cliErr) {
fmt.Fprintf(os.Stderr, "\n%s\n", cliErr.Error())
} else {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
}
return err
Expand Down
30 changes: 30 additions & 0 deletions internal/errors/cli_error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package errors

import "fmt"

const (
ExitSuccess = 0
ExitGeneralError = 1
ExitUsageError = 2
ExitAuthError = 3
ExitNetworkError = 4
ExitAPIError = 5
ExitRateLimited = 6
ExitTimeout = 7
)

type CLIError struct {
UserMessage string
Suggestion string
ExitCode int
Cause error
}

func (e *CLIError) Error() string {
if e.Suggestion != "" {
return fmt.Sprintf("%s\n\n %s", e.UserMessage, e.Suggestion)
}
return e.UserMessage
}

func (e *CLIError) Unwrap() error { return e.Cause }
72 changes: 72 additions & 0 deletions internal/errors/cli_error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package errors_test

import (
"errors"
"fmt"
"testing"

clierrors "github.com/gleanwork/glean-cli/internal/errors"
)

func TestCLIError_ErrorWithoutSuggestion(t *testing.T) {
err := &clierrors.CLIError{
UserMessage: "something went wrong",
ExitCode: clierrors.ExitGeneralError,
}
want := "something went wrong"
if got := err.Error(); got != want {
t.Errorf("Error() = %q, want %q", got, want)
}
}

func TestCLIError_ErrorWithSuggestion(t *testing.T) {
err := &clierrors.CLIError{
UserMessage: "not authenticated",
Suggestion: "Run: glean auth login",
ExitCode: clierrors.ExitAuthError,
}
want := "not authenticated\n\n Run: glean auth login"
if got := err.Error(); got != want {
t.Errorf("Error() = %q, want %q", got, want)
}
}

func TestCLIError_Unwrap(t *testing.T) {
cause := fmt.Errorf("token expired")
err := &clierrors.CLIError{
UserMessage: "auth failed",
ExitCode: clierrors.ExitAuthError,
Cause: cause,
}
if got := err.Unwrap(); got != cause {
t.Errorf("Unwrap() = %v, want %v", got, cause)
}
}

func TestCLIError_UnwrapNilCause(t *testing.T) {
err := &clierrors.CLIError{
UserMessage: "auth failed",
ExitCode: clierrors.ExitAuthError,
}
if got := err.Unwrap(); got != nil {
t.Errorf("Unwrap() = %v, want nil", got)
}
}

func TestCLIError_ErrorsAs(t *testing.T) {
cause := fmt.Errorf("token expired")
inner := &clierrors.CLIError{
UserMessage: "auth failed",
ExitCode: clierrors.ExitAuthError,
Cause: cause,
}
wrapped := fmt.Errorf("command failed: %w", inner)

var cliErr *clierrors.CLIError
if !errors.As(wrapped, &cliErr) {
t.Fatal("errors.As did not find CLIError in chain")
}
if cliErr.ExitCode != clierrors.ExitAuthError {
t.Errorf("ExitCode = %d, want %d", cliErr.ExitCode, clierrors.ExitAuthError)
}
}
6 changes: 6 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
package main

import (
"errors"
"os"

"github.com/gleanwork/glean-cli/cmd"
clierrors "github.com/gleanwork/glean-cli/internal/errors"
"github.com/gleanwork/glean-cli/internal/httputil"
)

Expand All @@ -15,6 +17,10 @@ func main() {
cmd.SetVersion(version)
httputil.SetVersion(version)
if err := cmd.Execute(); err != nil {
var cliErr *clierrors.CLIError
if errors.As(err, &cliErr) {
os.Exit(cliErr.ExitCode)
}
os.Exit(1)
}
}
Loading