Skip to content

Commit

Permalink
Non-interactive mode
Browse files Browse the repository at this point in the history
danielgtaylor#238

Pass in []string which replaces the os.Args[], io.Writers to optionally redirect Stdout and Stderr, and optionally pass in an authentication token override.
  • Loading branch information
loic-alleyne committed Apr 23, 2024
1 parent d16bdd7 commit 2d4b947
Show file tree
Hide file tree
Showing 3 changed files with 226 additions and 0 deletions.
31 changes: 31 additions & 0 deletions cli/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"strings"
"syscall"

"github.com/spf13/viper"
"golang.org/x/term"
)

Expand Down Expand Up @@ -164,3 +165,33 @@ func (a *ExternalToolAuth) OnRequest(req *http.Request, key string, params map[s
}
return nil
}

// ExternalOverrideAuth implements External Override Auth where
// an HTTP Authorization token is passed in as an argument in non-interactive
// mode.
type ExternalOverrideAuth struct{}

// Parameters define the External Override Auth parameter names.
func (a *ExternalOverrideAuth) Parameters() []AuthParam {
return []AuthParam{
{Name: "prefix", Required: true},
{Name: "token", Required: true},
}
}

// OnRequest gets run before the request goes out on the wire.
func (a *ExternalOverrideAuth) OnRequest(req *http.Request, key string, params map[string]string) error {
prefix := viper.GetString("ni-override-auth-prefix")
token := viper.GetString("ni-override-auth-token")

if token == "" {
return fmt.Errorf("no token provided")
}
switch len(prefix) > 0 {
case true:
req.Header.Add("Authorization", fmt.Sprintf("%s %s", prefix, token))
default:
req.Header.Add("Authorization", token)
}
return nil
}
145 changes: 145 additions & 0 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,151 @@ func Run() (returnErr error) {
return returnErr
}

// Run the CLI! Parse arguments, make requests, print responses.
func RunEmbedded(args []string, overrideAuthPrefix, overrideAuthToken string, newOut, newErr io.Writer) (returnErr error) {

Root.SetArgs(args)
Root.Use = filepath.Base(args[0])
if os.Getenv("COLOR") != "" {
viper.Set("color", true)
}
if os.Getenv("NOCOLOR") != "" {
viper.Set("nocolor", true)
}
viper.Set("ni-override-auth-prefix", overrideAuthPrefix)
viper.Set("ni-override-auth-token", overrideAuthToken)
// Because we may be doing HTTP calls before cobra has parsed the flags
// we parse the GlobalFlags here and already set some config values
// to ensure they are available
if err := GlobalFlags.Parse(args[1:]); err != nil {
if err != pflag.ErrHelp {
panic(err)
}
}
if newOut != nil {
Root.SetOut(newOut)
Stdout = newOut
}
if newErr != nil {
Root.SetErr(newErr)
Stderr = newErr
}
if noCache, _ := GlobalFlags.GetBool("rsh-no-cache"); noCache {
viper.Set("rsh-no-cache", true)
}
if verbose, _ := GlobalFlags.GetBool("rsh-verbose"); verbose {
viper.Set("rsh-verbose", true)
}
if insecure, _ := GlobalFlags.GetBool("rsh-insecure"); insecure {
viper.Set("rsh-insecure", true)
}
if cert, _ := GlobalFlags.GetString("rsh-client-cert"); cert != "" {
viper.Set("rsh-client-cert", cert)
}
if key, _ := GlobalFlags.GetString("rsh-client-key"); key != "" {
viper.Set("rsh-client-key", key)
}
if caCert, _ := GlobalFlags.GetString("rsh-ca-cert"); caCert != "" {
viper.Set("rsh-ca-cert", caCert)
}
if query, _ := GlobalFlags.GetStringArray("rsh-query"); len(query) > 0 {
viper.Set("rsh-query", query)
}
if headers, _ := GlobalFlags.GetStringArray("rsh-header"); len(headers) > 0 {
viper.Set("rsh-header", headers)
}
profile, _ := GlobalFlags.GetString("rsh-profile")
viper.Set("rsh-profile", profile)
if retries, _ := GlobalFlags.GetInt("rsh-retry"); retries > 0 {
viper.Set("rsh-retry", retries)
}
if timeout, _ := GlobalFlags.GetDuration("rsh-timeout"); timeout > 0 {
viper.Set("rsh-timeout", timeout)
}

// Now that global flags are parsed we can enable verbose mode if requested.
if viper.GetBool("rsh-verbose") {
enableVerbose = true
}

// Load the API commands if we can.
if len(args) > 1 {
apiName := args[1]

if apiName == "help" && len(args) > 2 {
// The explicit `help` command is followed by the actual commands
// you want help with. The first one is the API name.
apiName = args[2]
}

loaded := false
if apiName != "help" && apiName != "head" && apiName != "options" && apiName != "get" && apiName != "post" && apiName != "put" && apiName != "patch" && apiName != "delete" && apiName != "api" && apiName != "links" && apiName != "edit" && apiName != "auth-header" {
// Try to find the registered config for this API. If not found,
// there is no need to do anything since the normal flow will catch
// the command being missing and print help.
if cfg, ok := configs[apiName]; ok {

// This is used to give context to findApi
// Smallest fix for https://github.com/danielgtaylor/restish/issues/128
viper.Set("api-name", apiName)

currentConfig = cfg
for _, cmd := range Root.Commands() {
if cmd.Use == apiName {
currentBase := cfg.Base
currentProfile := cfg.Profiles[profile]
if currentProfile == nil {
if profile != "default" {
return fmt.Errorf("invalid profile " + profile)
}
}
if currentProfile != nil && currentProfile.Base != "" {
currentBase = currentProfile.Base
}
if _, err := Load(currentBase, cmd); err != nil {
return err
}
loaded = true
break
}
}
}
}

if !loaded {
// This could be a URL or short-name as part of a URL for generic
// commands. We should load the config for shell completion.
if (apiName == "head" || apiName == "options" || apiName == "get" || apiName == "post" || apiName == "put" || apiName == "patch" || apiName == "delete") && len(args) > 2 {
apiName = args[2]
}
apiName = fixAddress(apiName)
if name, _ := findAPI(apiName); name != "" {
currentConfig = configs[name]
}
}
}

// Phew, we made it. Execute the command now that everything is loaded
// and all the relevant sub-commands are registered.
defer func() {
if err := recover(); err != nil {
LogError("Caught error: %v", err)
LogDebug("%s", string(debug.Stack()))
if e, ok := err.(error); ok {
returnErr = e
} else {
returnErr = fmt.Errorf("%v", err)
}
}
}()
if err := Root.Execute(); err != nil {
LogError("Error: %v", err)
returnErr = err
}

return returnErr
}

// GetExitCode returns the exit code to use based on the last HTTP status code.
func GetExitCode() int {
if s := GetLastStatus() / 100; s > 2 && !viper.GetBool("rsh-ignore-status-code") {
Expand Down
50 changes: 50 additions & 0 deletions embedded/prep.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package embedded

import (
"fmt"
"io"
"strings"

"github.com/danielgtaylor/restish/bulk"
"github.com/danielgtaylor/restish/cli"
"github.com/danielgtaylor/restish/oauth"
"github.com/danielgtaylor/restish/openapi"
)

var version string = "embedded"

func Restish(args []string, overrideAuthPrefix, overrideAuthToken string, newOut, newErr io.Writer) error {

cli.Init("restish", version)

// Register default encodings, content type handlers, and link parsers.
cli.Defaults()

bulk.Init(cli.Root)

// Register format loaders to auto-discover API descriptions
cli.AddLoader(openapi.New())

// Register auth schemes
cli.AddAuth("oauth-client-credentials", &oauth.ClientCredentialsHandler{})
cli.AddAuth("oauth-authorization-code", &oauth.AuthorizationCodeHandler{})
if overrideAuthToken != "" {
cli.AddAuth("override", &cli.ExternalOverrideAuth{})
}
// We need to register new commands at runtime based on the selected API
// so that we don't have to potentially refresh and parse every single
// registered API just to run. So this is a little hacky, but we hijack
// the input args to find non-option arguments, get the first arg, and
// if it isn't from a well-known set try to load that API.
runArgs := []string{}
for _, arg := range args {
if !strings.HasPrefix(arg, "-") && !strings.HasPrefix(arg, "__") {
runArgs = append(runArgs, arg)
}
}
// Run the CLI, parsing arguments, making requests, and printing responses.
if err := cli.RunEmbedded(runArgs, overrideAuthPrefix, overrideAuthToken, newOut, newErr); err != nil {
return fmt.Errorf("%w %v", err, cli.GetExitCode())
}
return nil
}

0 comments on commit 2d4b947

Please sign in to comment.