From 06d656ac1670a6c044a9ba3b86956c9557d875a1 Mon Sep 17 00:00:00 2001 From: Priyesh Padmavilasom Date: Thu, 9 Oct 2025 21:43:16 +0000 Subject: [PATCH] refactor main, add gosec in linter options --- .golangci.yaml | 1 + cmd/main.go | 146 +--------------------------------- internal/cmd/http.go | 2 +- internal/cmd/params.go | 13 ++- internal/env/run.go | 153 ++++++++++++++++++++++++++++++++++++ internal/utils/retry_cli.go | 19 +++-- internal/utils/tcp.go | 8 +- 7 files changed, 190 insertions(+), 152 deletions(-) create mode 100644 internal/env/run.go diff --git a/.golangci.yaml b/.golangci.yaml index baf4b79..00bd23c 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -7,3 +7,4 @@ linters: # See the comment on top of this file. enable: - errcheck + - gosec diff --git a/cmd/main.go b/cmd/main.go index 15fed7c..8ffeecd 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -4,151 +4,13 @@ package main import ( - "encoding/json" - "errors" - "io" "os" - "strings" - - "tcli/internal/cmd" - "tcli/internal/common" - "tcli/internal/config" - "tcli/internal/parser" -) - -const ( - cmdModules = 1 - cmdCommands = 2 - cmdCommand = 3 - - // - posModule = 1 - posCommand = 2 - posSubCommand = 3 -) - -type cmdState struct { - state int - module string - cmd string - subCmd string - args []string -} - -type module struct { - Tag parser.Tag -} - -const ( - defaultInput = "data/example.json" - envOpenApiFile = "OPENAPI_FILE" -) - -var ( - modules []module - argc = len(os.Args) - state = getCmdState() + "tcli/internal/env" ) +// main is the entry point for the tcli application. func main() { - var err error - if !config.Load() { - panic("config load failed") - } - // handle command line args - switch state.state { - case cmdModules: - config.ShowModules() - case cmdCommands: - err = config.ShowCommands(state.module) - case cmdCommand: - err = call(config.ShowCommand(state.module, state.cmd, state.subCmd)) - } - handleError(err) -} - -func call(m *parser.Method, err error) error { - if err != nil { - return err - } - - env := cmd.GetExecutionEnv(state.args) - - // read from stdin and feed to commands - if hasStdin() { - input := json.NewDecoder(os.Stdin) - var any common.Input - for err == nil { - err = input.Decode(&any) - if err == io.EOF { - break - } - err = env.Exec(m, &any) - } - } else { - err = env.Exec(m, nil) - } - if err != nil { - return err - } - return env.Wait() -} - -func getCmdState() cmdState { - if argc < 2 { - return cmdState{state: cmdModules} - } else if argc < 3 { - return cmdState{state: cmdCommands, module: os.Args[posModule]} - } else { - subCmd := getSubCmd() - args := os.Args[3:] - if subCmd != "" { - args = os.Args[4:] - } - return cmdState{ - state: cmdCommand, - module: os.Args[posModule], - cmd: os.Args[posCommand], - subCmd: subCmd, - args: args} - } -} - -// subcommands are present when an api has a tagged set of commands -// this will be the final level of supported drill in for commands -func getSubCmd() string { - if argc > posSubCommand { - temp := os.Args[posSubCommand] - if !strings.HasPrefix(temp, "-") { - return temp - } - } - return "" -} - -// Attempt to avoid specifying a flag for stdin -// check if there is something readable at stdin -// this is not tested beyond minimal redirection -// it is tested in windows as well but there could be -// corner cases when it doesnt work. If that is the case -// it's better to specify a flag to indicate input -func hasStdin() bool { - fi, err := os.Stdin.Stat() - if err != nil { - return false - } - return fi.Mode()&os.ModeCharDevice == 0 -} - -func handleError(err error) { - if err == nil { - return - } - if errors.Is(err, common.ErrNoSuchModule) { - config.ShowModules() - } else if errors.Is(err, common.ErrNoSuchCommand) { - err = config.ShowCommands(state.module) - } else if errors.Is(err, common.ErrNoSuchSubCommand) { - config.ShowTaggedCommands(state.cmd) + if err := env.Run(); err != nil { + os.Exit(1) } } diff --git a/internal/cmd/http.go b/internal/cmd/http.go index 60f8c40..f37be0f 100644 --- a/internal/cmd/http.go +++ b/internal/cmd/http.go @@ -58,7 +58,7 @@ func (c *HttpCommand) http() error { utils.AddAuthorizationHeader(req, *p.Values[JwtParam]) } - client := utils.RetriableClient(p.Global.RetryCount) + client := utils.RetriableClient(int64(p.Global.RetryCount)) resp, err := client.Do(req) if err != nil { log.Fatalf("Response error: %v\n", err) diff --git a/internal/cmd/params.go b/internal/cmd/params.go index 06a977a..9b9a1aa 100644 --- a/internal/cmd/params.go +++ b/internal/cmd/params.go @@ -21,7 +21,7 @@ const GlobalFlags = "global" type GlobalResult struct { Count uint - RetryCount uint + RetryCount int64 BasePath string Scheme string Server string @@ -47,6 +47,8 @@ type paramsResult struct { headers http.Header } +// New creates a new ParseResult and initializes the flag set +// with the parameters from the given method and global flags. func New(o *parser.Method, in *common.Input, r *parser.Root) *ParseResult { fs := flag.NewFlagSet(o.OperationId, flag.ExitOnError) @@ -71,6 +73,8 @@ func New(o *parser.Method, in *common.Input, r *parser.Root) *ParseResult { return &p } +// getParamVal returns the value of the parameter from the input map +// if it exists, otherwise it returns the default value of the parameter. func getParamVal(p *parser.Parameter, i *common.Input) string { if i != nil { if input, ok := (*i)[p.Name]; ok { @@ -83,12 +87,13 @@ func getParamVal(p *parser.Parameter, i *common.Input) string { return p.DefaultStr() } +// getGlobal initializes the global flags and returns a GlobalResult. func getGlobal(fs *flag.FlagSet, r *parser.Root) *GlobalResult { g := GlobalResult{} fs.StringVar(&g.BasePath, "base_path", getBasePath(r), "http base path") fs.StringVar(&g.Doc, "doc", "none", "Generate docs (none, shell)") fs.StringVar(&g.Format, "format", "", "json format") - fs.UintVar(&g.RetryCount, "retry_count", 10, "Number of retries on failure") + fs.Int64Var(&g.RetryCount, "retry_count", 10, "Number of retries on failure") fs.StringVar(&g.Scheme, "scheme", getScheme(r), "Scheme") fs.StringVar(&g.Server, "server", getHost(r), "Server") fs.UintVar(&g.Count, "count", 1, "Number of times to repeat command") @@ -124,6 +129,8 @@ func getScheme(r *parser.Root) string { } } +// ValidateParams checks that all required parameters have values +// and sets global configuration options based on the parsed values. func (p *ParseResult) ValidateParams() error { for _, v := range p.Method.Parameters { val, ok := p.Values[v.Name] @@ -143,6 +150,8 @@ func (p *ParseResult) ValidateParams() error { return nil } +// getParamValues processes the parameters and returns a paramsResult +// containing the URL values, path, body, and headers for the HTTP request. func (p *ParseResult) getParamValues() (*paramsResult, error) { result := paramsResult{ urlValues: &url.Values{}, diff --git a/internal/env/run.go b/internal/env/run.go new file mode 100644 index 0000000..07d4433 --- /dev/null +++ b/internal/env/run.go @@ -0,0 +1,153 @@ +// Copyright 2025 HP Development Company, L.P. +// SPDX-License-Identifier: MIT + +package env + +import ( + "encoding/json" + "errors" + "io" + "os" + "strings" + + "tcli/internal/cmd" + "tcli/internal/common" + "tcli/internal/config" + "tcli/internal/parser" +) + +const ( + cmdModules = 1 + cmdCommands = 2 + cmdCommand = 3 + + // + posModule = 1 + posCommand = 2 + posSubCommand = 3 +) + +type cmdState struct { + state int + module string + cmd string + subCmd string + args []string +} + +type module struct { + Tag parser.Tag +} + +var ( + modules []module + argc = len(os.Args) + state = getCmdState() +) + +// Run is the main entry point for executing commands based on command line arguments +func Run() error { + var err error + if !config.Load() { + return errors.New("config load failed") + } + // handle command line args + switch state.state { + case cmdModules: + config.ShowModules() + case cmdCommands: + err = config.ShowCommands(state.module) + case cmdCommand: + err = call(config.ShowCommand(state.module, state.cmd, state.subCmd)) + } + return handleError(err) +} + +// call executes the given method with the provided error handling +func call(m *parser.Method, err error) error { + if err != nil { + return err + } + + env := cmd.GetExecutionEnv(state.args) + + // read from stdin and feed to commands + if hasStdin() { + input := json.NewDecoder(os.Stdin) + var any common.Input + for err == nil { + err = input.Decode(&any) + if err == io.EOF { + break + } + err = env.Exec(m, &any) + } + } else { + err = env.Exec(m, nil) + } + return env.Wait() +} + +// getCmdState determines the current command state based on command line arguments +func getCmdState() cmdState { + if argc < 2 { + return cmdState{state: cmdModules} + } else if argc < 3 { + return cmdState{state: cmdCommands, module: os.Args[posModule]} + } else { + subCmd := getSubCmd() + args := os.Args[3:] + if subCmd != "" { + args = os.Args[4:] + } + return cmdState{ + state: cmdCommand, + module: os.Args[posModule], + cmd: os.Args[posCommand], + subCmd: subCmd, + args: args} + } +} + +// subcommands are present when an api has a tagged set of commands +// this will be the final level of supported drill in for commands +func getSubCmd() string { + if argc > posSubCommand { + temp := os.Args[posSubCommand] + if !strings.HasPrefix(temp, "-") { + return temp + } + } + return "" +} + +// Attempt to avoid specifying a flag for stdin +// check if there is something readable at stdin +// this is not tested beyond minimal redirection +// it is tested in windows as well but there could be +// corner cases when it doesnt work. If that is the case +// it's better to specify a flag to indicate input +func hasStdin() bool { + fi, err := os.Stdin.Stat() + if err != nil { + return false + } + return fi.Mode()&os.ModeCharDevice == 0 +} + +// handleError processes errors and displays appropriate information +func handleError(err error) error { + if err == nil { + return nil + } + if errors.Is(err, common.ErrNoSuchModule) { + config.ShowModules() + } else if errors.Is(err, common.ErrNoSuchCommand) { + err = config.ShowCommands(state.module) + } else if errors.Is(err, common.ErrNoSuchSubCommand) { + config.ShowTaggedCommands(state.cmd) + } else { + return err + } + return nil +} diff --git a/internal/utils/retry_cli.go b/internal/utils/retry_cli.go index 8f54a46..86a22e2 100644 --- a/internal/utils/retry_cli.go +++ b/internal/utils/retry_cli.go @@ -23,7 +23,7 @@ type Do func(req *http.Request) (*http.Response, error) type Client struct { HttpClient *http.Client Do Do - MaxRetry uint + MaxRetry int64 StatusCode int } @@ -36,7 +36,7 @@ func NewClient() *Client { } // retry enabled client -func RetriableClient(retries uint) *Client { +func RetriableClient(retries int64) *Client { c := NewClient() c.Do = c.RetriableDo c.MaxRetry = retries @@ -44,12 +44,15 @@ func RetriableClient(retries uint) *Client { return c } -func RetriableClientWithStatus(retries uint, status int) *Client { +// retry enabled client with specific status code to wait for +// e.g. wait for 200 OK +func RetriableClientWithStatus(retries int64, status int) *Client { c := RetriableClient(retries) c.StatusCode = status return c } +// get with retry func (c *Client) Get(url string) (*http.Response, error) { return c.doRequest(url, "GET", nil) } @@ -70,10 +73,11 @@ func (c *Client) doRequest(url, method string, body io.Reader) (*http.Response, return c.Do(req) } +// retry logic func (c *Client) RetriableDo(req *http.Request) (*http.Response, error) { logger := config.GetLogger() logger.HttpRequest(req) - var i uint + var i int64 for { resp, err := c.HttpClient.Do(req) if c.RetryWithBackoff(logger, i, resp, err) { @@ -85,8 +89,11 @@ func (c *Client) RetriableDo(req *http.Request) (*http.Response, error) { } } +// determine if we should retry, and backoff if so +// returns true if we should retry +// sleeps for backoff duration if retrying func (c *Client) RetryWithBackoff(logger *config.Log, - i uint, resp *http.Response, err error) bool { + i int64, resp *http.Response, err error) bool { doRetry := false retrySeconds := time.Second * time.Duration(i) if err != nil { @@ -113,6 +120,8 @@ func (c *Client) RetryWithBackoff(logger *config.Log, return false } +// get the retry-after header value in seconds +// default to DefaultRetryAfterSeconds if not present or invalid func getRetryAfterHeaderValue(resp *http.Response) time.Duration { retryAfter := DefaultRetryAfterSeconds var err error diff --git a/internal/utils/tcp.go b/internal/utils/tcp.go index 5ee0598..a0dcca3 100644 --- a/internal/utils/tcp.go +++ b/internal/utils/tcp.go @@ -8,8 +8,12 @@ import ( "time" ) -func RetryWait(count uint, fn func() bool) bool { - for i := uint(1); i <= count; i++ { +// RetryWait retries a function up to 'count' times, waiting an increasing amount of time between each attempt. +// The wait time increases linearly (1s, 2s, 3s, ...). +// If the function returns true, it stops retrying and returns true. +// If all attempts fail, it returns false. +func RetryWait(count int64, fn func() bool) bool { + for i := int64(1); i <= count; i++ { if fn() { return true }