config: several refactorings and interactive fixes

This also enables log in attempt and reuse of http client once a user
successfully logs in. This speeds up the CLI experience when username
and password credentials are used instead of api/secret keys.

Signed-off-by: Rohit Yadav <rohit@apache.org>
diff --git a/cli/completer.go b/cli/completer.go
index bd2822a..9c02eff 100644
--- a/cli/completer.go
+++ b/cli/completer.go
@@ -97,7 +97,6 @@
 }
 
 func (t *autoCompleter) Do(line []rune, pos int) (options [][]rune, offset int) {
-
 	apiMap := buildAPICacheMap(t.Config.GetAPIVerbMap())
 
 	var verbs []string
@@ -206,14 +205,16 @@
 				return nil, 0
 			}
 
-			r := cmd.NewRequest(nil, completer.Config, nil, nil)
+			r := cmd.NewRequest(nil, completer.Config, nil)
 			autocompleteAPIArgs := []string{"listall=true"}
 			if autocompleteAPI.Noun == "templates" {
-				autocompleteAPIArgs = append(autocompleteAPIArgs, "templatefilter=all")
+				autocompleteAPIArgs = append(autocompleteAPIArgs, "templatefilter=executable")
 			}
-			fmt.Printf("\nFetching options, please wait...")
+
+			fmt.Println("")
+			spinner := t.Config.StartSpinner("fetching options, please wait...")
 			response, _ := cmd.NewAPIRequest(r, autocompleteAPI.Name, autocompleteAPIArgs, false)
-			fmt.Printf("\r")
+			t.Config.StopSpinner(spinner)
 
 			var autocompleteOptions []selectOption
 			for _, v := range response {
diff --git a/cli/exec.go b/cli/exec.go
index d64daa1..17d5829 100644
--- a/cli/exec.go
+++ b/cli/exec.go
@@ -20,20 +20,19 @@
 import (
 	"cloudmonkey/cmd"
 	"cloudmonkey/config"
-	"github.com/chzyer/readline"
 )
 
 // ExecCmd executes a single provided command
-func ExecCmd(cfg *config.Config, args []string, shell *readline.Instance) error {
+func ExecCmd(cfg *config.Config, args []string) error {
 	if len(args) < 1 {
 		return nil
 	}
 
 	command := cmd.FindCommand(args[0])
 	if command != nil {
-		return command.Handle(cmd.NewRequest(command, cfg, shell, args[1:]))
+		return command.Handle(cmd.NewRequest(command, cfg, args[1:]))
 	}
 
 	catchAllHandler := cmd.GetAPIHandler()
-	return catchAllHandler.Handle(cmd.NewRequest(catchAllHandler, cfg, shell, args))
+	return catchAllHandler.Handle(cmd.NewRequest(catchAllHandler, cfg, args))
 }
diff --git a/cli/shell.go b/cli/shell.go
index 884150c..d9ffea1 100644
--- a/cli/shell.go
+++ b/cli/shell.go
@@ -31,6 +31,7 @@
 )
 
 var completer *autoCompleter
+var shell *readline.Instance
 
 // ExecShell starts a shell
 func ExecShell(sysArgs []string) {
@@ -39,6 +40,15 @@
 		Config: cfg,
 	}
 
+	if len(sysArgs) > 0 {
+		err := ExecCmd(cfg, sysArgs)
+		if err != nil {
+			fmt.Println("🙈 Error:", err)
+			os.Exit(1)
+		}
+		os.Exit(0)
+	}
+
 	shell, err := readline.NewEx(&readline.Config{
 		Prompt:            cfg.GetPrompt(),
 		HistoryFile:       cfg.HistoryFile,
@@ -61,15 +71,7 @@
 	}
 	defer shell.Close()
 
-	if len(sysArgs) > 0 {
-		err := ExecCmd(cfg, sysArgs, nil)
-		if err != nil {
-			fmt.Println("🙈 Error:", err)
-			os.Exit(1)
-		}
-		os.Exit(0)
-	}
-
+	cfg.HasShell = true
 	cfg.PrintHeader()
 
 	for {
@@ -99,7 +101,7 @@
 			args = strings.Split(line, " ")
 		}
 
-		err = ExecCmd(cfg, args, shell)
+		err = ExecCmd(cfg, args)
 		if err != nil {
 			fmt.Println("🙈 Error:", err)
 		}
diff --git a/cmd/login.go b/cmd/login.go
index 3a67103..bb1655d 100644
--- a/cmd/login.go
+++ b/cmd/login.go
@@ -80,8 +80,7 @@
 			r.Config.ActiveProfile.Password = password
 			r.Config.ActiveProfile.Domain = domain
 
-			client, _, err := Login(r)
-			if client == nil || err != nil {
+			if sessionKey, err := Login(r); err != nil || sessionKey == "" {
 				fmt.Println("Failed to login, check credentials and try again.")
 			} else {
 				fmt.Println("Successfully logged in and saved credentials to the server profile.")
diff --git a/cmd/network.go b/cmd/network.go
index c3e5b9f..67d08e6 100644
--- a/cmd/network.go
+++ b/cmd/network.go
@@ -21,28 +21,60 @@
 	"bytes"
 	"crypto/hmac"
 	"crypto/sha1"
-	"crypto/tls"
 	"encoding/base64"
 	"encoding/json"
 	"errors"
 	"fmt"
-	"github.com/briandowns/spinner"
 	"io/ioutil"
 	"net/http"
-	"net/http/cookiejar"
 	"net/url"
-	"runtime"
 	"sort"
 	"strings"
 	"time"
 )
 
-var cursor = []string{"\r⣷ 😸", "\r⣯ 😹", "\r⣟ 😺", "\r⡿ 😻", "\r⢿ 😼", "\r⣻ 😽", "\r⣽ 😾", "\r⣾ 😻"}
-
-func init() {
-	if runtime.GOOS == "windows" {
-		cursor = []string{"|", "/", "-", "\\"}
+func findSessionCookie(cookies []*http.Cookie) string {
+	var sessionKey string = ""
+	for _, cookie := range cookies {
+		if cookie.Name == "sessionkey" {
+			sessionKey = cookie.Value
+			break
+		}
 	}
+	return sessionKey
+}
+
+// Login logs in a user based on provided request and returns http client and session key
+func Login(r *Request) (string, error) {
+	params := make(url.Values)
+	params.Add("command", "login")
+	params.Add("username", r.Config.ActiveProfile.Username)
+	params.Add("password", r.Config.ActiveProfile.Password)
+	params.Add("domain", r.Config.ActiveProfile.Domain)
+	params.Add("response", "json")
+
+	url, _ := url.Parse(r.Config.ActiveProfile.URL)
+	if sessionKey := findSessionCookie(r.Client().Jar.Cookies(url)); sessionKey != "" {
+		return sessionKey, nil
+	}
+
+	spinner := r.Config.StartSpinner("trying to log in...")
+	resp, err := r.Client().PostForm(url.String(), params)
+	r.Config.StopSpinner(spinner)
+
+	if err != nil {
+		return "", errors.New("failed to authenticate with the CloudStack server, please check the settings: " + err.Error())
+	}
+
+	if resp.StatusCode != http.StatusOK {
+		e := errors.New("failed to authenticate, please check the credentials")
+		if err != nil {
+			e = errors.New("failed to authenticate due to " + err.Error())
+		}
+		return "", e
+	}
+
+	return findSessionCookie(resp.Cookies()), nil
 }
 
 func encodeRequestParams(params url.Values) string {
@@ -69,45 +101,6 @@
 	return buf.String()
 }
 
-// Login logs in a user based on provided request and returns http client and session key
-func Login(r *Request) (*http.Client, string, error) {
-	params := make(url.Values)
-	params.Add("command", "login")
-	params.Add("username", r.Config.ActiveProfile.Username)
-	params.Add("password", r.Config.ActiveProfile.Password)
-	params.Add("domain", r.Config.ActiveProfile.Domain)
-	params.Add("response", "json")
-
-	jar, _ := cookiejar.New(nil)
-	client := &http.Client{
-		Jar: jar,
-		Transport: &http.Transport{
-			TLSClientConfig: &tls.Config{InsecureSkipVerify: !r.Config.Core.VerifyCert},
-		},
-	}
-
-	sessionKey := ""
-	resp, err := client.PostForm(r.Config.ActiveProfile.URL, params)
-	if err != nil {
-		return client, sessionKey, errors.New("failed to authenticate with the CloudStack server, please check the settings: " + err.Error())
-	}
-	if resp.StatusCode != http.StatusOK {
-		e := errors.New("failed to authenticate, please check the credentials")
-		if err != nil {
-			e = errors.New("failed to authenticate due to " + err.Error())
-		}
-		return client, sessionKey, e
-	}
-
-	for _, cookie := range resp.Cookies() {
-		if cookie.Name == "sessionkey" {
-			sessionKey = cookie.Value
-			break
-		}
-	}
-	return client, sessionKey, nil
-}
-
 func getResponseData(data map[string]interface{}) map[string]interface{} {
 	for k := range data {
 		if strings.HasSuffix(k, "response") {
@@ -120,17 +113,14 @@
 func pollAsyncJob(r *Request, jobID string) (map[string]interface{}, error) {
 	for timeout := float64(r.Config.Core.Timeout); timeout > 0.0; {
 		startTime := time.Now()
-		s := spinner.New(cursor, 200*time.Millisecond)
-		s.Color("blue", "bold")
-		s.Suffix = " polling for async API job result"
-		s.Start()
+		spinner := r.Config.StartSpinner("polling for async API result")
 		queryResult, queryError := NewAPIRequest(r, "queryAsyncJobResult", []string{"jobid=" + jobID}, false)
 		diff := time.Duration(1*time.Second).Nanoseconds() - time.Now().Sub(startTime).Nanoseconds()
 		if diff > 0 {
 			time.Sleep(time.Duration(diff) * time.Nanosecond)
 		}
-		s.Stop()
 		timeout = timeout - time.Now().Sub(startTime).Seconds()
+		r.Config.StopSpinner(spinner)
 		if queryError != nil {
 			return queryResult, queryError
 		}
@@ -161,7 +151,6 @@
 	}
 	params.Add("response", "json")
 
-	var client *http.Client
 	var encodedParams string
 	var err error
 
@@ -172,12 +161,6 @@
 		if len(apiKey) > 0 {
 			params.Add("apiKey", apiKey)
 		}
-
-		client = &http.Client{
-			Transport: &http.Transport{
-				TLSClientConfig: &tls.Config{InsecureSkipVerify: !r.Config.Core.VerifyCert},
-			},
-		}
 		encodedParams = encodeRequestParams(params)
 
 		mac := hmac.New(sha1.New, []byte(secretKey))
@@ -185,8 +168,7 @@
 		signature := base64.StdEncoding.EncodeToString(mac.Sum(nil))
 		encodedParams = encodedParams + fmt.Sprintf("&signature=%s", url.QueryEscape(signature))
 	} else if len(r.Config.ActiveProfile.Username) > 0 && len(r.Config.ActiveProfile.Password) > 0 {
-		var sessionKey string
-		client, sessionKey, err = Login(r)
+		sessionKey, err := Login(r)
 		if err != nil {
 			return nil, err
 		}
@@ -197,8 +179,7 @@
 		return nil, errors.New("failed to authenticate to make API call")
 	}
 
-	client.Timeout = time.Duration(time.Duration(r.Config.Core.Timeout) * time.Second)
-	response, err := client.Get(fmt.Sprintf("%s?%s", r.Config.ActiveProfile.URL, encodedParams))
+	response, err := r.Client().Get(fmt.Sprintf("%s?%s", r.Config.ActiveProfile.URL, encodedParams))
 	if err != nil {
 		return nil, err
 	}
diff --git a/cmd/request.go b/cmd/request.go
index 45b006a..179080d 100644
--- a/cmd/request.go
+++ b/cmd/request.go
@@ -19,23 +19,25 @@
 
 import (
 	"cloudmonkey/config"
-	"github.com/chzyer/readline"
+	"net/http"
 )
 
 // Request describes a command request
 type Request struct {
 	Command *Command
 	Config  *config.Config
-	Shell   *readline.Instance
 	Args    []string
 }
 
+func (r *Request) Client() *http.Client {
+	return r.Config.ActiveProfile.Client
+}
+
 // NewRequest creates a new request from a command
-func NewRequest(cmd *Command, cfg *config.Config, shell *readline.Instance, args []string) *Request {
+func NewRequest(cmd *Command, cfg *config.Config, args []string) *Request {
 	return &Request{
 		Command: cmd,
 		Config:  cfg,
-		Shell:   shell,
 		Args:    args,
 	}
 }
diff --git a/cmd/set.go b/cmd/set.go
index a43c104..a2eeb9a 100644
--- a/cmd/set.go
+++ b/cmd/set.go
@@ -49,15 +49,13 @@
 			value := strings.Join(r.Args[1:], " ")
 			r.Config.UpdateConfig(subCommand, value)
 
-			if subCommand == "profile" && r.Shell != nil {
+			if subCommand == "profile" && r.Config.HasShell {
 				fmt.Println("Loaded server profile:", r.Config.Core.ProfileName)
 				fmt.Println("Url:        ", r.Config.ActiveProfile.URL)
 				fmt.Println("Username:   ", r.Config.ActiveProfile.Username)
 				fmt.Println("Domain:     ", r.Config.ActiveProfile.Domain)
 				fmt.Println("API Key:    ", r.Config.ActiveProfile.APIKey)
 				fmt.Println()
-
-				r.Shell.SetPrompt(r.Config.GetPrompt())
 			}
 			return nil
 		},
diff --git a/cmd/sync.go b/cmd/sync.go
index 160aa3f..a6d0289 100644
--- a/cmd/sync.go
+++ b/cmd/sync.go
@@ -26,7 +26,9 @@
 		Name: "sync",
 		Help: "Discovers and updates APIs",
 		Handle: func(r *Request) error {
+			spinner := r.Config.StartSpinner("discovering APIs, please wait...")
 			response, err := NewAPIRequest(r, "listApis", []string{"listall=true"}, false)
+			r.Config.StopSpinner(spinner)
 			if err != nil {
 				return err
 			}
diff --git a/config/cache.go b/config/cache.go
index 88c2480..216cf02 100644
--- a/config/cache.go
+++ b/config/cache.go
@@ -107,7 +107,7 @@
 	for _, node := range apiList {
 		api, valid := node.(map[string]interface{})
 		if !valid {
-			fmt.Println("Errro, moving on 🍌")
+			fmt.Println("Error, moving on...")
 			continue
 		}
 		apiName := api["name"].(string)
diff --git a/config/config.go b/config/config.go
index 5d203c0..0a87dd5 100644
--- a/config/config.go
+++ b/config/config.go
@@ -18,12 +18,16 @@
 package config
 
 import (
+	"crypto/tls"
 	"fmt"
 	"github.com/mitchellh/go-homedir"
 	"gopkg.in/ini.v1"
+	"net/http"
+	"net/http/cookiejar"
 	"os"
 	"path"
 	"strconv"
+	"time"
 )
 
 // Output formats
@@ -46,6 +50,7 @@
 	Domain    string `ini:"domain"`
 	APIKey    string `ini:"apikey"`
 	SecretKey string `ini:"secretkey"`
+	Client    *http.Client
 }
 
 // Core block describes common options for the CLI
@@ -65,6 +70,7 @@
 	HistoryFile   string
 	CacheFile     string
 	LogFile       string
+	HasShell      bool
 	Core          *Core
 	ActiveProfile *ServerProfile
 }
@@ -110,6 +116,7 @@
 		CacheFile:     path.Join(configDir, "cache"),
 		HistoryFile:   path.Join(configDir, "history"),
 		LogFile:       path.Join(configDir, "log"),
+		HasShell:      false,
 		Core:          &defaultCoreConfig,
 		ActiveProfile: &defaultProfile,
 	}
@@ -122,8 +129,19 @@
 	return profiles
 }
 
-func reloadConfig(cfg *Config) *Config {
+func newHttpClient(cfg *Config) *http.Client {
+	jar, _ := cookiejar.New(nil)
+	client := &http.Client{
+		Jar: jar,
+		Transport: &http.Transport{
+			TLSClientConfig: &tls.Config{InsecureSkipVerify: !cfg.Core.VerifyCert},
+		},
+	}
+	client.Timeout = time.Duration(time.Duration(cfg.Core.Timeout) * time.Second)
+	return client
+}
 
+func reloadConfig(cfg *Config) *Config {
 	if _, err := os.Stat(cfg.Dir); err != nil {
 		os.Mkdir(cfg.Dir, 0700)
 	}
@@ -193,6 +211,7 @@
 		profiles = append(profiles, profile.Name())
 	}
 
+	cfg.ActiveProfile.Client = newHttpClient(cfg)
 	return cfg
 }
 
@@ -203,8 +222,9 @@
 		c.Core.Prompt = value
 	case "asyncblock":
 		c.Core.AsyncBlock = value == "true"
-	case "output":
 	case "display":
+		fallthrough
+	case "output":
 		c.Core.Output = value
 	case "timeout":
 		intValue, _ := strconv.Atoi(value)
diff --git a/config/spinner.go b/config/spinner.go
new file mode 100644
index 0000000..732032c
--- /dev/null
+++ b/config/spinner.go
@@ -0,0 +1,50 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package config
+
+import (
+	"github.com/briandowns/spinner"
+	"runtime"
+	"time"
+)
+
+var cursor = []string{"\r⣷ 😸", "\r⣯ 😹", "\r⣟ 😺", "\r⡿ 😻", "\r⢿ 😼", "\r⣻ 😽", "\r⣽ 😾", "\r⣾ 😻"}
+
+func init() {
+	if runtime.GOOS == "windows" {
+		cursor = []string{"|", "/", "-", "\\"}
+	}
+}
+
+// StartSpinner starts and returns a waiting cursor that the CLI can use
+func (c *Config) StartSpinner(suffix string) *spinner.Spinner {
+	if !c.HasShell {
+		return nil
+	}
+	waiter := spinner.New(cursor, 200*time.Millisecond)
+	waiter.Color("blue", "bold")
+	waiter.Suffix = " " + suffix
+	waiter.Start()
+	return waiter
+}
+
+func (c *Config) StopSpinner(waiter *spinner.Spinner) {
+	if waiter != nil {
+		waiter.Stop()
+	}
+}