diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4c8405b..eefd856 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -55,7 +55,7 @@ jobs: go test -v -cover ./cmd/ ./external/... - name: Runbook Smoke Test - timeout-minutes: 15 + timeout-minutes: 5 env: EPCC_CLIENT_ID: ${{ secrets.EPCC_CLIENT_ID }} EPCC_CLIENT_SECRET: ${{ secrets.EPCC_CLIENT_SECRET }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 156a74b..5884b47 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -38,9 +38,6 @@ nfpms: # ID of the nfpm config, must be unique. # Defaults to "default". package_name: "epcc-cli" - replacements: - 386: i386 - darwin: macOS vendor: Elastic Path Software Inc. homepage: https://github.com/elasticpath/epcc-cli maintainer: Release Engineering @@ -109,4 +106,4 @@ announce: # # Attention: goreleaser doesn't check the full structure of the Slack API: please make sure that # your configuration for advanced message formatting abides by this API. - #attachments: [] \ No newline at end of file + #attachments: [] diff --git a/README.md b/README.md index c7af83b..4926c70 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ The following is a summary of the main commands, in general you can type `epcc h 1. `--execution-timeout` will control how long the `epcc` process can run before timing out. 2. `--rate-limit` will control the number of requests per second to EPCC. 3. `--max-concurrency` will control the maximum number of concurrent commands that can run simultaneously. - * This differs from the rate limit in that if a request takes 2 seconds, a rate limit of 3 will allow 6 requests in flight at a time, where as `--max-concurrency` would limit you to 3. + * This differs from the rate limit in that if a request takes 2 seconds, a rate limit of 3 will allow 6 requests in flight at a time, whereas `--max-concurrency` would limit you to 3. A higher value will slow down initial start time. ### Configuration diff --git a/cmd/create.go b/cmd/create.go index fa3e4e7..06e5628 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -20,7 +20,7 @@ import ( "strings" ) -func NewCreateCommand(parentCmd *cobra.Command) { +func NewCreateCommand(parentCmd *cobra.Command) func() { var createCmd = &cobra.Command{ Use: "create", @@ -34,6 +34,12 @@ func NewCreateCommand(parentCmd *cobra.Command) { }, } + overrides := &httpclient.HttpParameterOverrides{ + QueryParameters: nil, + OverrideUrlPath: "", + } + + // Ensure that any new options here are added to the resetFunc var autoFillOnCreate = false var noBodyPrint = false var outputJq = "" @@ -41,9 +47,15 @@ func NewCreateCommand(parentCmd *cobra.Command) { var ifAliasExists = "" var ifAliasDoesNotExist = "" - overrides := &httpclient.HttpParameterOverrides{ - QueryParameters: nil, - OverrideUrlPath: "", + resetFunc := func() { + autoFillOnCreate = false + noBodyPrint = false + outputJq = "" + setAlias = "" + ifAliasExists = "" + ifAliasDoesNotExist = "" + overrides.OverrideUrlPath = "" + overrides.QueryParameters = nil } for _, resource := range resources.GetPluralResources() { @@ -198,6 +210,8 @@ func NewCreateCommand(parentCmd *cobra.Command) { createCmd.PersistentFlags().StringVarP(&ifAliasDoesNotExist, "if-alias-does-not-exist", "", "", "If the alias does not exist we will run this command, otherwise exit with no error") createCmd.MarkFlagsMutuallyExclusive("if-alias-exists", "if-alias-does-not-exist") _ = createCmd.RegisterFlagCompletionFunc("output-jq", jqCompletionFunc) + + return resetFunc } func createInternal(ctx context.Context, overrides *httpclient.HttpParameterOverrides, args []string, autoFillOnCreate bool, aliasName string) (string, error) { @@ -313,6 +327,7 @@ func createInternal(ctx context.Context, overrides *httpclient.HttpParameterOver if aliasName != "" { aliases.SetAliasForResource(string(resBody), aliasName) } + return string(resBody), nil } else { return "", nil diff --git a/cmd/delete-all.go b/cmd/delete-all.go index c6c8071..d3a0aaf 100644 --- a/cmd/delete-all.go +++ b/cmd/delete-all.go @@ -17,7 +17,7 @@ import ( "sync" ) -func NewDeleteAllCommand(parentCmd *cobra.Command) { +func NewDeleteAllCommand(parentCmd *cobra.Command) func() { var deleteAll = &cobra.Command{ Use: "delete-all", @@ -53,6 +53,7 @@ func NewDeleteAllCommand(parentCmd *cobra.Command) { deleteAll.AddCommand(deleteAllResourceCmd) } parentCmd.AddCommand(deleteAll) + return func() {} } func deleteAllInternal(ctx context.Context, args []string) error { diff --git a/cmd/delete.go b/cmd/delete.go index 963dc88..49b9656 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -17,7 +17,7 @@ import ( "strings" ) -func NewDeleteCommand(parentCmd *cobra.Command) { +func NewDeleteCommand(parentCmd *cobra.Command) func() { var deleteCmd = &cobra.Command{ Use: "delete", @@ -37,11 +37,19 @@ func NewDeleteCommand(parentCmd *cobra.Command) { OverrideUrlPath: "", } + // Ensure that any new options here are added to the resetFunc var allow404 = false - var ifAliasExists = "" var ifAliasDoesNotExist = "" + resetFunc := func() { + overrides.QueryParameters = nil + overrides.OverrideUrlPath = "" + allow404 = false + ifAliasExists = "" + ifAliasDoesNotExist = "" + } + for _, resource := range resources.GetPluralResources() { if resource.DeleteEntityInfo == nil { continue @@ -159,6 +167,8 @@ func NewDeleteCommand(parentCmd *cobra.Command) { deleteCmd.PersistentFlags().StringVarP(&ifAliasDoesNotExist, "if-alias-does-not-exist", "", "", "If the alias does not exist we will run this command, otherwise exit with no error") deleteCmd.MarkFlagsMutuallyExclusive("if-alias-exists", "if-alias-does-not-exist") parentCmd.AddCommand(deleteCmd) + + return resetFunc } func deleteInternal(ctx context.Context, overrides *httpclient.HttpParameterOverrides, allow404 bool, args []string) (string, error) { crud.OutstandingRequestCounter.Add(1) diff --git a/cmd/get.go b/cmd/get.go index 5bf76d0..6092aee 100644 --- a/cmd/get.go +++ b/cmd/get.go @@ -22,22 +22,31 @@ import ( const singularResourceRequest = 0 const collectionResourceRequest = 1 -func NewGetCommand(parentCmd *cobra.Command) { +func NewGetCommand(parentCmd *cobra.Command) func() { overrides := &httpclient.HttpParameterOverrides{ QueryParameters: nil, OverrideUrlPath: "", } + // Ensure that any new options here are added to the resetFunc var outputJq = "" var noBodyPrint = false - var retryWhileJQ = "" - var retryWhileJQMaxAttempts = uint16(1200) - var ifAliasExists = "" var ifAliasDoesNotExist = "" + resetFunc := func() { + overrides.QueryParameters = nil + overrides.OverrideUrlPath = "" + outputJq = "" + noBodyPrint = false + retryWhileJQ = "" + retryWhileJQMaxAttempts = uint16(1200) + ifAliasExists = "" + ifAliasDoesNotExist = "" + } + var getCmd = &cobra.Command{ Use: "get", Short: "Retrieves either a single or all resources", @@ -263,6 +272,8 @@ func NewGetCommand(parentCmd *cobra.Command) { _ = getCmd.RegisterFlagCompletionFunc("output-jq", jqCompletionFunc) parentCmd.AddCommand(getCmd) + + return resetFunc } func getInternal(ctx context.Context, overrides *httpclient.HttpParameterOverrides, args []string) (string, error) { diff --git a/cmd/root.go b/cmd/root.go index 0065edd..1e2976a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -249,8 +249,10 @@ func Execute() { if err != nil { log.Errorf("Error occurred while processing command: %s", err) + os.Exit(1) } else { + os.Exit(0) } } diff --git a/cmd/runbooks.go b/cmd/runbooks.go index 7ca6f30..d087300 100644 --- a/cmd/runbooks.go +++ b/cmd/runbooks.go @@ -9,6 +9,7 @@ import ( "github.com/elasticpath/epcc-cli/external/runbooks" _ "github.com/elasticpath/epcc-cli/external/runbooks" "github.com/elasticpath/epcc-cli/external/shutdown" + "github.com/jolestar/go-commons-pool/v2" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "golang.org/x/sync/semaphore" @@ -113,7 +114,7 @@ func initRunbookRunCommands() *cobra.Command { } execTimeoutInSeconds := runbookRunCommand.PersistentFlags().Int64("execution-timeout", 900, "How long should the script take to execute before timing out") - maxConcurrency := runbookRunCommand.PersistentFlags().Int64("max-concurrency", 2048, "Maximum number of commands at once") + maxConcurrency := runbookRunCommand.PersistentFlags().Int("max-concurrency", 20, "Maximum number of commands that can run simultaneously") for _, runbook := range runbooks.GetRunbooks() { // Create a copy of runbook scoped to the loop @@ -146,8 +147,16 @@ func initRunbookRunCommands() *cobra.Command { ctx, cancelFunc := context.WithCancel(parentCtx) - concurrentRunSemaphore := semaphore.NewWeighted(*maxConcurrency) + concurrentRunSemaphore := semaphore.NewWeighted(int64(*maxConcurrency)) + factory := pool.NewPooledObjectFactorySimple( + func(ctx2 context.Context) (interface{}, error) { + return generateRunbookCmd(), nil + }) + objectPool := pool.NewObjectPool(ctx, factory, &pool.ObjectPoolConfig{ + MaxTotal: *maxConcurrency, + MaxIdle: *maxConcurrency, + }) for stepIdx, rawCmd := range runbookAction.RawCommands { // Create a copy of loop variables @@ -191,11 +200,22 @@ func initRunbookRunCommands() *cobra.Command { log.Tracef("(Step %d/%d Command %d/%d) Building Commmand", stepIdx+1, numSteps, commandIdx+1, len(funcs)) - stepCmd := generateRunbookCmd() - stepCmd.SetArgs(rawCmdArguments[1:]) - log.Tracef("(Step %d/%d Command %d/%d) Starting Command", stepIdx+1, numSteps, commandIdx+1, len(funcs)) - err := stepCmd.ExecuteContext(ctx) - log.Tracef("(Step %d/%d Command %d/%d) Complete Command", stepIdx+1, numSteps, commandIdx+1, len(funcs)) + stepCmdObject, err := objectPool.BorrowObject(ctx) + defer objectPool.ReturnObject(ctx, stepCmdObject) + + if err == nil { + commandAndResetFunc := stepCmdObject.(*CommandAndReset) + commandAndResetFunc.reset() + stepCmd := commandAndResetFunc.cmd + + stepCmd.SetArgs(rawCmdArguments[1:]) + log.Tracef("(Step %d/%d Command %d/%d) Starting Command", stepIdx+1, numSteps, commandIdx+1, len(funcs)) + + stepCmd.ResetFlags() + err = stepCmd.ExecuteContext(ctx) + log.Tracef("(Step %d/%d Command %d/%d) Complete Command", stepIdx+1, numSteps, commandIdx+1, len(funcs)) + } + commandResult := &commandResult{ stepIdx: stepIdx, commandIdx: commandIdx, @@ -314,20 +334,36 @@ func processRunbookVariablesOnCommand(runbookActionRunActionCommand *cobra.Comma // Creates a new instance of a cobra.Command // We use a new instance for each step so that we can benefit from flags in runbooks -func generateRunbookCmd() *cobra.Command { + +type CommandAndReset struct { + cmd *cobra.Command + reset func() +} + +func generateRunbookCmd() *CommandAndReset { root := &cobra.Command{ Use: "epcc", SilenceUsage: true, } - NewCreateCommand(root) - NewUpdateCommand(root) - NewDeleteCommand(root) - NewGetCommand(root) - NewDeleteAllCommand(root) + resetCreateCmd := NewCreateCommand(root) + resetUpdateCmd := NewUpdateCommand(root) + resetDeleteCmd := NewDeleteCommand(root) + resetGetCmd := NewGetCommand(root) + resetDeleteAllCmd := NewDeleteAllCommand(root) getDevCommands(root) - return root + return &CommandAndReset{ + root, + func() { + // We need to reset the state of all commands since we are reusing the objects + resetCreateCmd() + resetUpdateCmd() + resetDeleteCmd() + resetGetCmd() + resetDeleteAllCmd() + }, + } } func initRunbookDevCommands() *cobra.Command { diff --git a/cmd/update.go b/cmd/update.go index 93a8abc..7a2c0d3 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -17,19 +17,27 @@ import ( "strings" ) -func NewUpdateCommand(parentCmd *cobra.Command) { +func NewUpdateCommand(parentCmd *cobra.Command) func() { overrides := &httpclient.HttpParameterOverrides{ QueryParameters: nil, OverrideUrlPath: "", } + // Ensure that any new options here are added to the resetFunc var outputJq = "" - var noBodyPrint = false - var ifAliasExists = "" var ifAliasDoesNotExist = "" + resetFunc := func() { + overrides.QueryParameters = nil + overrides.OverrideUrlPath = "" + outputJq = "" + noBodyPrint = false + ifAliasExists = "" + ifAliasDoesNotExist = "" + } + var updateCmd = &cobra.Command{ Use: "update", Short: "Updates a resource", @@ -175,6 +183,8 @@ func NewUpdateCommand(parentCmd *cobra.Command) { _ = updateCmd.RegisterFlagCompletionFunc("output-jq", jqCompletionFunc) parentCmd.AddCommand(updateCmd) + return resetFunc + } func updateInternal(ctx context.Context, overrides *httpclient.HttpParameterOverrides, args []string) (string, error) { diff --git a/external/httpclient/httpclient.go b/external/httpclient/httpclient.go index 62a74a2..1b44933 100644 --- a/external/httpclient/httpclient.go +++ b/external/httpclient/httpclient.go @@ -112,7 +112,11 @@ func LogStats() { counts := "" for _, k := range keys { - counts += fmt.Sprintf("%d:%d, ", k, stats.respCodes[k]) + if k == 0 { + counts += fmt.Sprintf("%d:%d, ", k, stats.respCodes[k]) + } else { + counts += fmt.Sprintf("CONN_ERROR:%d, ", stats.respCodes[k]) + } } if stats.totalRequests > 3 { @@ -225,7 +229,7 @@ func doRequestInternal(ctx context.Context, method string, contentType string, p log.Tracef("Waiting for rate limiter") if err := Limit.Wait(ctx); err != nil { - return nil, fmt.Errorf("Rate limiter returned error %v, %w", err, err) + return nil, fmt.Errorf("rate limiter returned error %v, %w", err, err) } rateLimitTime := time.Since(start) @@ -237,13 +241,15 @@ func doRequestInternal(ctx context.Context, method string, contentType string, p stats.totalRequests += 1 if rateLimitTime.Milliseconds() > 50 { // Only count rate limit time if it took us longer than 50 ms to get here. - stats.totalRateLimitedTimeInMs += int64(rateLimitTime.Milliseconds()) + stats.totalRateLimitedTimeInMs += rateLimitTime.Milliseconds() } - stats.totalHttpRequestProcessingTime += int64(requestTime.Milliseconds()) - int64(rateLimitTime.Milliseconds()) + stats.totalHttpRequestProcessingTime += requestTime.Milliseconds() - rateLimitTime.Milliseconds() if resp != nil { stats.respCodes[resp.StatusCode] = stats.respCodes[resp.StatusCode] + 1 + } else { + stats.respCodes[0] = stats.respCodes[0] + 1 } requestNumber := stats.totalRequests diff --git a/go.mod b/go.mod index 15d89e3..22036e0 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,10 @@ require ( require github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 -require github.com/iancoleman/strcase v0.2.0 +require ( + github.com/iancoleman/strcase v0.2.0 + github.com/jolestar/go-commons-pool/v2 v2.1.2 +) require ( github.com/Masterminds/goutils v1.1.1 // indirect diff --git a/go.sum b/go.sum index 508a879..a721938 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -72,6 +74,8 @@ github.com/itchyny/gojq v0.12.13 h1:IxyYlHYIlspQHHTE0f3cJF0NKDMfajxViuhBLnHd/QU= github.com/itchyny/gojq v0.12.13/go.mod h1:JzwzAqenfhrPUuwbmEz3nu3JQmFLlQTQMUcOdnu/Sf4= github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE= github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8= +github.com/jolestar/go-commons-pool/v2 v2.1.2 h1:E+XGo58F23t7HtZiC/W6jzO2Ux2IccSH/yx4nD+J1CM= +github.com/jolestar/go-commons-pool/v2 v2.1.2/go.mod h1:r4NYccrkS5UqP1YQI1COyTZ9UjPJAAGTUxzcsK1kqhY= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= @@ -148,6 +152,7 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/thediveo/enumflag v0.10.1 h1:DB3Ag69VZ7BCv6jzKECrZ0ebZrHLzFRMIFYt96s4OxM=