From 2d4b947a49ea15e696011295d7ef49f7dc7721aa Mon Sep 17 00:00:00 2001 From: loicalleyne Date: Mon, 22 Apr 2024 20:02:15 -0400 Subject: [PATCH] Non-interactive mode https://github.com/danielgtaylor/restish/issues/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. --- cli/auth.go | 31 ++++++++++ cli/cli.go | 145 +++++++++++++++++++++++++++++++++++++++++++++++ embedded/prep.go | 50 ++++++++++++++++ 3 files changed, 226 insertions(+) create mode 100644 embedded/prep.go diff --git a/cli/auth.go b/cli/auth.go index cee743f..d554c59 100644 --- a/cli/auth.go +++ b/cli/auth.go @@ -11,6 +11,7 @@ import ( "strings" "syscall" + "github.com/spf13/viper" "golang.org/x/term" ) @@ -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 +} diff --git a/cli/cli.go b/cli/cli.go index 38402a0..759db98 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -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") { diff --git a/embedded/prep.go b/embedded/prep.go new file mode 100644 index 0000000..e374184 --- /dev/null +++ b/embedded/prep.go @@ -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 +}