Skip to content

Commit

Permalink
support for context and quiet mode in platform calls (#335)
Browse files Browse the repository at this point in the history
* support for context and quiet in calls
  • Loading branch information
pnickolov committed Apr 2, 2024
1 parent 02b90a3 commit e37078a
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 29 deletions.
42 changes: 28 additions & 14 deletions platform/api/call.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,22 @@ var FlagCurlifyRequests bool

// --- Public Interface -----------------------------------------------------

// Options contains extra, optional parameteres that modify the API call behavior
type Options struct {
Headers map[string]string
ResponseHeaders map[string][]string // headers as returned by the call
ExpectedErrors []int // log expected error status codes as Info rather than Error
// Headers contains additional headers to be provided in the request
Headers map[string]string

// ResponseHeaders will be populated with the headers returned by the call
ResponseHeaders map[string][]string

// ExpectedErrors is a list of status codes that are expected and should be logged as Info rather than Error
ExpectedErrors []int

// Quiet suppresses the interactive spinner and display of request
Quiet bool

// Context provides a Go context for the API call (nil is accepted and will be replaced with a default context)
Context context.Context
}

// JSONGet performs a GET request and parses the response as JSON
Expand Down Expand Up @@ -88,14 +100,16 @@ func JSONRequest(method string, path string, body any, out any, options *Options

// --- Internal methods -----------------------------------------------------

func prepareHTTPRequest(cfg *config.Context, client *http.Client, method string, path string, body any, headers map[string]string) (*http.Request, error) {
func prepareHTTPRequest(ctx *callContext, client *http.Client, method string, path string, body any, headers map[string]string) (*http.Request, error) {
// body will be JSONified if a body is given but no Content-Type is provided
// (if a content type is provided, we assume the body is in the desired format)
jsonify := body != nil && (headers == nil || headers["Content-Type"] == "")
// Due to issues with encoding the special characters used for ids generated with identifyingProperties, we
// need to create the fullPath for the request ourselves instead of calling uri.String()
var fullPath string

cfg := ctx.cfg // quick access

// prepare a body reader
var bodyReader io.Reader = nil
if jsonify {
Expand Down Expand Up @@ -132,7 +146,7 @@ func prepareHTTPRequest(cfg *config.Context, client *http.Client, method string,
fullPath = fmt.Sprintf("%s?%s", joinedPath, query)
}

req, err := http.NewRequest(method, fullPath, bodyReader)
req, err := http.NewRequestWithContext(ctx.goContext, method, fullPath, bodyReader)
if err != nil {
return nil, fmt.Errorf("failed to create a request for %q: %w", uri.String(), err)
}
Expand Down Expand Up @@ -195,22 +209,21 @@ func getCurlCommandOfRequest(req *http.Request) (string, error) {
func httpRequest(method string, path string, body any, out any, options *Options) error {
log.WithFields(log.Fields{"method": method, "path": path}).Info("Calling the observability platform API")

callCtx := newCallContext()
cfg := callCtx.cfg // quick access
defer callCtx.stopSpinner(false) // ensure the spinner is not running when returning (belt & suspenders)

// create a default options to avoid nil-checking
if options == nil {
options = &Options{}
}

callCtx := newCallContext(options.Context, options.Quiet)
defer callCtx.stopSpinner(false) // ensure the spinner is not running when returning (belt & suspenders)

// force login if no token
if cfg.Token == "" {
if callCtx.cfg.Token == "" {
log.Info("No auth token available, trying to log in")
if err := login(callCtx); err != nil {
return err
}
cfg = callCtx.cfg // may have changed across login
// note: callCtx.cfg has been updated by login()
}

// create http client for the request
Expand All @@ -221,7 +234,7 @@ func httpRequest(method string, path string, body any, out any, options *Options
}

// build HTTP request
req, err := prepareHTTPRequest(cfg, client, method, path, body, options.Headers)
req, err := prepareHTTPRequest(callCtx, client, method, path, body, options.Headers)
if err != nil {
return err // assume error messages provide sufficient info
}
Expand Down Expand Up @@ -250,11 +263,12 @@ func httpRequest(method string, path string, body any, out any, options *Options
if err != nil {
return fmt.Errorf("failed to login: %w", err)
}
cfg = callCtx.cfg // may have changed across login

// note: callCtx.cfg has been updated by login()

// retry the request
log.Info("Retrying the request with the refreshed token")
req, err = prepareHTTPRequest(cfg, client, method, path, body, options.Headers)
req, err = prepareHTTPRequest(callCtx, client, method, path, body, options.Headers)
if err != nil {
return err // error should have enough context
}
Expand Down
15 changes: 5 additions & 10 deletions platform/api/call_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package api

import (
"context"
"net/http"
"testing"

Expand All @@ -14,17 +15,11 @@ func TestPrepareHTTPRequest(t *testing.T) {
cfg := &config.Context{
URL: "http://localhost:8080",
}
req, err := prepareHTTPRequest(cfg, client, "POST", "/test/path/1", nil, nil)
assert.Nil(t, err)
assert.Equal(t, "http://localhost:8080/test/path/1", req.URL.String())
}

func TestPrepareJSONRequest(t *testing.T) {
client := &http.Client{}
cfg := &config.Context{
URL: "http://localhost:8080",
callCtx := &callContext{
goContext: context.Background(),
cfg: cfg,
}
req, err := prepareHTTPRequest(cfg, client, "POST", "/test/path/1", nil, nil)
req, err := prepareHTTPRequest(callCtx, client, "POST", "/test/path/1", nil, nil)
assert.Nil(t, err)
assert.Equal(t, "http://localhost:8080/test/path/1", req.URL.String())
}
18 changes: 15 additions & 3 deletions platform/api/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/apex/log"
"github.com/briandowns/spinner"
"github.com/fatih/color"
"golang.org/x/term"

"github.com/cisco-open/fsoc/config"
)
Expand All @@ -38,7 +39,7 @@ var statusChar = map[bool]string{
true: color.GreenString("\u2713"), // checkmark
}

func newCallContext() *callContext {
func newCallContext(goContext context.Context, quiet bool) *callContext {
// get current config context
cfg := config.GetCurrentContext()
if cfg == nil {
Expand All @@ -47,11 +48,22 @@ func newCallContext() *callContext {
}
log.WithFields(log.Fields{"context": cfg.Name, "url": cfg.URL, "tenant": cfg.Tenant}).Info("Using context")

// use background if no context was provided
if goContext == nil {
goContext = context.Background()
}

// create spinner if needed
var spinnerObj *spinner.Spinner
if !quiet && term.IsTerminal(int(os.Stderr.Fd())) {
spinnerObj = spinner.New(spinner.CharSets[21], 50*time.Millisecond, spinner.WithWriterFile(os.Stderr))
}

// prepare call context
callCtx := callContext{
context.Background(),
goContext,
cfg,
spinner.New(spinner.CharSets[21], 50*time.Millisecond, spinner.WithWriterFile(os.Stderr)),
spinnerObj,
}

return &callCtx
Expand Down
3 changes: 2 additions & 1 deletion platform/api/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package api

import (
"context"
"fmt"
"reflect"
"strings"
Expand Down Expand Up @@ -51,7 +52,7 @@ var fieldToFlag = map[string]string{
// Login respects different access profile types (when supported) to provide the correct
// login mechanism for each.
func Login() error {
callCtx := newCallContext()
callCtx := newCallContext(context.Background(), false)
defer callCtx.stopSpinner(false) // ensure not running when returning

return login(callCtx)
Expand Down
2 changes: 1 addition & 1 deletion platform/api/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func RunProxyServer(port int, command []string, statusPrinter func(string), exit
}

// Create a new call context
callCtx := newCallContext()
callCtx := newCallContext(context.Background(), false)
cfg := callCtx.cfg // quick access to config

// force login if no token
Expand Down

0 comments on commit e37078a

Please sign in to comment.