diff --git a/README.md b/README.md index 4d57da0..e984d71 100644 --- a/README.md +++ b/README.md @@ -229,11 +229,6 @@ this is based on [GoJQ which has a number of differences](https://github.com/itc epcc runbooks run misc get-store-info ``` -### Retries vs Ignoring Errors - -Retries in epcc-cli will retry the _exact_ rendered request so if you are using templated parameters i.e., `auto-fill` and the failure is deterministic (say a unique constraint), -a retry will just get stuck in a loop. In this case if you want to create many different records, you want to `--ignore-errors`. - ## Development Tips ### Fast rebuilds diff --git a/cmd/create.go b/cmd/create.go index 8226cbb..3d404fa 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -185,11 +185,12 @@ func NewCreateCommand(parentCmd *cobra.Command) func() { }) } else { // This is an attribute value return completion.Complete(completion.Request{ - Type: completion.CompleteAttributeValue, - Resource: resource, - Verb: completion.Create, - Attribute: args[len(args)-1], - ToComplete: toComplete, + Type: completion.CompleteAttributeValue, + Resource: resource, + Verb: completion.Create, + Attribute: args[len(args)-1], + ToComplete: toComplete, + AllowTemplates: true, }) } } else { diff --git a/cmd/delete.go b/cmd/delete.go index 5f04fd5..eaafd5e 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -161,11 +161,12 @@ func NewDeleteCommand(parentCmd *cobra.Command) func() { }) } else { // This is an attribute value return completion.Complete(completion.Request{ - Type: completion.CompleteAttributeValue, - Resource: resource, - Verb: completion.Delete, - Attribute: args[len(args)-1], - ToComplete: toComplete, + Type: completion.CompleteAttributeValue, + Resource: resource, + Verb: completion.Delete, + Attribute: args[len(args)-1], + ToComplete: toComplete, + AllowTemplates: true, }) } } diff --git a/cmd/login.go b/cmd/login.go index 52742ce..4dd30f4 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -428,11 +428,12 @@ var loginAccountManagement = &cobra.Command{ }) } else { return completion.Complete(completion.Request{ - Type: completion.CompleteAttributeValue, - Verb: completion.Create, - Resource: res, - Attributes: usedAttributes, - ToComplete: toComplete, + Type: completion.CompleteAttributeValue, + Verb: completion.Create, + Resource: res, + Attributes: usedAttributes, + ToComplete: toComplete, + AllowTemplates: true, }) } diff --git a/cmd/repeater.go b/cmd/repeater.go index 499c056..9005075 100644 --- a/cmd/repeater.go +++ b/cmd/repeater.go @@ -16,9 +16,6 @@ func repeater(c func(*cobra.Command, []string) error, repeat, repeatDelay uint32 if ignoreErrors { log.Debugf("Ignored error %v", ignoreErrors) } else { - if repeat > 1 && !ignoreErrors { - log.Infof("if you want to continue in the face of errors use the --ignore-errors") - } return err } } diff --git a/cmd/root.go b/cmd/root.go index 174b29d..e029948 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -123,7 +123,6 @@ func InitializeCmd() { RootCmd.PersistentFlags().StringVarP(&profileNameFromCommandLine, "profile", "P", "", "overrides the current EPCC_PROFILE var to run the command with the chosen profile.") RootCmd.PersistentFlags().Uint16VarP(&rateLimit, "rate-limit", "", 10, "Request limit per second") RootCmd.PersistentFlags().BoolVarP(&httpclient.Retry5xx, "retry-5xx", "", false, "Whether we should retry requests with HTTP 5xx response code") - RootCmd.PersistentFlags().BoolVarP(&httpclient.Retry4xx, "retry-4xx", "", false, "Whether we should retry requests with HTTP 4xx response code") RootCmd.PersistentFlags().BoolVarP(&httpclient.Retry429, "retry-429", "", false, "Whether we should retry requests with HTTP 429 response code") RootCmd.PersistentFlags().BoolVarP(&httpclient.RetryConnectionErrors, "retry-connection-errors", "", false, "Whether we should retry requests with connection errors") RootCmd.PersistentFlags().UintVarP(&httpclient.RetryDelay, "retry-delay", "", 500, "When retrying how long should we delay") @@ -287,13 +286,11 @@ func Execute() { <-shutdownHandlerDone if err != nil { - log.Errorf("Error occurred while processing command: %s", err) os.Exit(1) } else { - - //os.Exit(0) + os.Exit(0) } } diff --git a/cmd/runbooks.go b/cmd/runbooks.go index 52bc033..2a57908 100644 --- a/cmd/runbooks.go +++ b/cmd/runbooks.go @@ -148,9 +148,7 @@ func initRunbookShowCommands() *cobra.Command { for _, line := range rawCmdLines { if len(strings.Trim(line, " \n")) > 0 { - //if i <= 10 { println(line) - //} } } @@ -422,9 +420,7 @@ func processRunbookVariablesOnCommand(runbookActionRunActionCommand *cobra.Comma log.Errorf("Could not set flag as required, this is a bug of some kind %s: %v", key, err) } } else { - description := "" - if variable.Description != nil { description = variable.Description.Short } diff --git a/cmd/update.go b/cmd/update.go index 2c77fde..f09e6ca 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -161,11 +161,12 @@ func NewUpdateCommand(parentCmd *cobra.Command) func() { }) } else { // This is an attribute value return completion.Complete(completion.Request{ - Type: completion.CompleteAttributeValue, - Resource: resource, - Verb: completion.Update, - Attribute: args[len(args)-1], - ToComplete: toComplete, + Type: completion.CompleteAttributeValue, + Resource: resource, + Verb: completion.Update, + Attribute: args[len(args)-1], + ToComplete: toComplete, + AllowTemplates: true, }) } } else { diff --git a/external/completion/completion.go b/external/completion/completion.go index f8ff911..aa47bb3 100644 --- a/external/completion/completion.go +++ b/external/completion/completion.go @@ -1,9 +1,11 @@ package completion import ( + "fmt" "github.com/elasticpath/epcc-cli/external/aliases" "github.com/elasticpath/epcc-cli/external/resources" "github.com/spf13/cobra" + "os" "regexp" "strconv" "strings" @@ -48,8 +50,9 @@ type Request struct { QueryParam string Header string // The current string argument being completed - ToComplete string - NoAliases bool + ToComplete string + NoAliases bool + AllowTemplates bool } func Complete(c Request) ([]string, cobra.ShellCompDirective) { @@ -294,7 +297,79 @@ func Complete(c Request) ([]string, cobra.ShellCompDirective) { } results = append(results, supportedFileTypes...) } + } + + if c.AllowTemplates { + lastPipe := strings.LastIndex(c.ToComplete, "|") + prefix := "" + if lastPipe == -1 { + prefix = "{{ " + } else { + prefix = c.ToComplete[0:lastPipe+1] + " " + } + + myResults := []string{} + myResults = append(myResults, + prefix+"date", + prefix+"now", + prefix+"randAlphaNum", + prefix+"randAlpha", + prefix+"randAscii", + prefix+"randNumeric", + prefix+"randAlphaNum", + prefix+"randAlpha", + prefix+"randAscii", + prefix+"randNumeric", + prefix+"pseudoRandAlphaNum", + prefix+"pseudoRandAlpha", + prefix+"pseudoRandNumeric", + prefix+"pseudoRandString", + prefix+"pseudoRandInt", + prefix+"uuidv4", + prefix+"duration", + ) + + if prefix != "{{ " { + // Functions that make sense as continuations + myResults = append(myResults, + prefix+"trim", + prefix+"trimAll", + prefix+"trimSuffix", + prefix+"trimPrefix", + prefix+"upper", + prefix+"lower", + prefix+"title", + prefix+"repeat", + prefix+"substr", + prefix+"nospace", + prefix+"trunc", + prefix+"abbrev", + prefix+"initials", + prefix+"wrap", + prefix+"cat", + prefix+"replace", + prefix+"snakecase", + prefix+"camelcase", + prefix+"kebabcase", + prefix+"swapcase", + prefix+"shufflecase", + ) + } + re := regexp.MustCompile(`env\s+[A-Za-z]*\s*$`) + if re.MatchString(c.ToComplete) { + for _, v := range os.Environ() { + myResults = append(myResults, + fmt.Sprintf("%venv \"%v\"", prefix, strings.Split(v, "=")[0]), + ) + } + } else { + myResults = append(myResults, prefix+"env") + } + //myResults = append(myResults, strings.TrimSuffix(c.ToComplete, " ")+" }}", strings.TrimSuffix(c.ToComplete, " ")+" |") + for _, r := range myResults { + results = append(results, r+" |", r+" }}") + } } } } diff --git a/external/httpclient/httpclient.go b/external/httpclient/httpclient.go index 8febd29..aa8d85e 100644 --- a/external/httpclient/httpclient.go +++ b/external/httpclient/httpclient.go @@ -118,7 +118,6 @@ func Initialize(rateLimit uint16, requestTimeout float32, statisticsFrequency in var Retry429 = false var Retry5xx = false -var Retry4xx = false var RetryConnectionErrors = false diff --git a/external/json/to_json.go b/external/json/to_json.go index c271343..9101858 100644 --- a/external/json/to_json.go +++ b/external/json/to_json.go @@ -269,6 +269,11 @@ func formatValue(v string) string { } else if match, _ := regexp.MatchString("^\\[\\]$", v); match { return v } else { - return fmt.Sprintf("\"%s\"", strings.ReplaceAll(v, `"`, `\"`)) + v = strings.ReplaceAll(v, "\\", "\\\\") + v = strings.ReplaceAll(v, "\n", "\\n") + v = strings.ReplaceAll(v, `"`, `\"`) + + return fmt.Sprintf("\"%s\"", v) + //return fmt.Sprintf("\"%s\"", v) } } diff --git a/external/json/to_json_test.go b/external/json/to_json_test.go index af1772a..de96389 100644 --- a/external/json/to_json_test.go +++ b/external/json/to_json_test.go @@ -54,6 +54,51 @@ func TestToJsonLegacyFormatSimpleKeyStringValue(t *testing.T) { } } +func TestToJsonLegacyFormatSimpleKeyStringValueWithQuotes(t *testing.T) { + // Fixture Setup + input := []string{"key", "val\"ue"} + expected := `{"data":{"key":"val\"ue"}}` + + // Execute SUT + actual, err := ToJson(input, false, false, map[string]*resources.CrudEntityAttribute{}, true) + + // Verification + require.NoError(t, err) + if actual != expected { + t.Fatalf("Testing json conversion of empty value %s did not match expected %s, actually: %s", input, expected, actual) + } +} + +func TestToJsonLegacyFormatSimpleKeyStringValueWithNewLines(t *testing.T) { + // Fixture Setup + input := []string{"key", "val\nue"} + expected := `{"data":{"key":"val\nue"}}` + + // Execute SUT + actual, err := ToJson(input, false, false, map[string]*resources.CrudEntityAttribute{}, true) + + // Verification + require.NoError(t, err) + if actual != expected { + t.Fatalf("Testing json conversion of empty value %s did not match expected %s, actually: %s", input, expected, actual) + } +} + +func TestToJsonLegacyFormatSimpleKeyStringValueWithBackslashes(t *testing.T) { + // Fixture Setup + input := []string{"key", "val\\nue"} + expected := `{"data":{"key":"val\\nue"}}` + + // Execute SUT + actual, err := ToJson(input, false, false, map[string]*resources.CrudEntityAttribute{}, true) + + // Verification + require.NoError(t, err) + if actual != expected { + t.Fatalf("Testing json conversion of empty value %s did not match expected %s, actually: %s", input, expected, actual) + } +} + func TestToJsonLegacyFormatSimpleNestedKeyValue(t *testing.T) { // Fixture Setup input := []string{"foo.bar", "val"} diff --git a/external/resources/yaml/resources.yaml b/external/resources/yaml/resources.yaml index 4ff6fa5..aae1151 100644 --- a/external/resources/yaml/resources.yaml +++ b/external/resources/yaml/resources.yaml @@ -18,8 +18,7 @@ account-authentication-settings: auto_create_account_for_account_members: type: BOOL account_member_self_management: - type: ENUM:disabled,update_only - + type: BOOL account-management-authentication-tokens: singular-name: "account-management-authentication-token" json-api-type: "account_management_authentication_token" @@ -31,7 +30,7 @@ account-management-authentication-tokens: content-type: application/json attributes: authentication_mechanism: - type: ENUM:oidc,password,passwordless,self_signup + type: STRING oauth_authorization_code: type: STRING oauth_redirect_uri: @@ -44,13 +43,6 @@ account-management-authentication-tokens: type: STRING password: type: STRING - name: - type: STRING - autofill: FUNC:Name - email: - type: string - autofill: FUNC:Email - suppress-reset-warning: true account-members: singular-name: "account-member" @@ -191,25 +183,6 @@ account-addresses: country: type: STRING autofill: FUNC:CountryAbr -account-cart-associations: - singular-name: account-cart-association - json-api-type: account-cart-relationship - json-api-format: "legacy" - no-wrapping: true - docs: "https://elasticpath.dev/docs/commerce-cloud/carts/account-cart-associations/account-cart-associations-overview" - delete-entity: - docs: "https://elasticpath.dev/docs/commerce-cloud/carts/account-cart-associations/delete-an-association" - url: "/v2/carts/{carts}/relationships/accounts" - create-entity: - docs: "https://elasticpath.dev/docs/commerce-cloud/carts/account-cart-associations/create-an-association" - url: "/v2/carts/{carts}/relationships/accounts" - content-type: application/json - attributes: - data[n].id: - type: RESOURCE_ID:account - data[n].type: - type: ENUM:account - autofill: VALUE:account application-keys: singular-name: "application-key" json-api-type: "application_key" @@ -1583,15 +1556,9 @@ pcm-nodes: get-entity: docs: "https://documentation.elasticpath.com/commerce-cloud/docs/api/pcm/hierarchies/nodes/get-a-hierarchy-node.html" url: "/pcm/hierarchies/{pcm_hierarchies}/nodes/{pcm_nodes}" - get-collection: - docs: "https://documentation.elasticpath.com/commerce-cloud/docs/api/pcm/hierarchies/nodes/get-a-hierarchy-node.html" - url: "/pcm/hierarchies/{pcm_hierarchies}/nodes" update-entity: docs: "https://documentation.elasticpath.com/commerce-cloud/docs/api/pcm/hierarchies/nodes/update-a-hierarchy-node.html" url: "/pcm/hierarchies/{pcm_hierarchies}/nodes/{pcm_nodes}" - get-collection: - docs: "https://documentation.elasticpath.com/commerce-cloud/docs/api/pcm/hierarchies/nodes/get-a-hierarchy-node.html" - url: "/pcm/hierarchies/{pcm_hierarchies}/nodes" delete-entity: docs: "https://documentation.elasticpath.com/commerce-cloud/docs/api/pcm/hierarchies/nodes/delete-a-hierarchy-node.html" url: "/pcm/hierarchies/{pcm_hierarchies}/nodes/{pcm_nodes}" @@ -1791,10 +1758,8 @@ pcm-pricebooks: attributes: name: type: STRING - autofill: FUNC:Company description: type: STRING - autofill: FUNC:Phrase pcm-product-prices: singular-name: "pcm-product-price" json-api-type: "product-price" @@ -1832,41 +1797,6 @@ pcm-product-prices: type: INT currencies.CAD.includes_tax: type: BOOL -pcm-pricebook-modifiers: - singular-name: "pcm-pricebook-modifier" - json-api-format: "compliant" - json-api-type: "price-modifier" - docs: "https://elasticpath.dev/docs/pxm/pricebooks/pxm-pricebooks-modifiers/get-a-price-modifier" - get-collection: - docs: "https://elasticpath.dev/docs/pxm/pricebooks/pxm-pricebooks-modifiers/get-all-price-modifiers" - url: "/pcm/pricebooks/{pcm_pricebooks}/modifiers/" - get-entity: - docs: "https://elasticpath.dev/docs/pxm/pricebooks/pxm-pricebooks-modifiers/get-a-price-modifier" - url: "/pcm/pricebooks/{pcm_pricebooks}/modifiers/{pcm_pricebook_modifiers}" - create-entity: - docs: "https://elasticpath.dev/docs/pxm/pricebooks/pxm-pricebooks-modifiers/create-a-price-modifier" - url: "/pcm/pricebooks/{pcm_pricebooks}/modifiers" - update-entity: - docs: "https://elasticpath.dev/docs/pxm/pricebooks/pxm-pricebooks-modifiers/update-a-price-modifier" - url: "/pcm/pricebooks/{pcm_pricebooks}/modifiers/{pcm_pricebook_modifiers}" - delete-entity: - docs: "https://elasticpath.dev/docs/pxm/pricebooks/pxm-pricebooks-modifiers/delete-a-price-modifier" - url: "/pcm/pricebooks/{pcm_pricebooks}/modifiers/{pcm_pricebook_modifiers}" - attributes: - name: - type: STRING - autofill: FUNC:BuzzWord - modifier_type: - type: ENUM:price_increment,price_decrement,price_equals - autofill: VALUE:price_equals - currencies.USD.amount: - type: INT - currencies.USD.includes_tax: - type: BOOL - ^currencies\.USD\.tiers\..+\.minimum_quantity$: - type: INT - ^currencies\.USD\.tiers\..+\.amount$: - type: INT pcm-variations: singular-name: "pcm-variation" json-api-type: "product-variation" diff --git a/external/runbooks/account-management.epcc.yml b/external/runbooks/account-management.epcc.yml index 820f8b6..bfbc2df 100644 --- a/external/runbooks/account-management.epcc.yml +++ b/external/runbooks/account-management.epcc.yml @@ -10,15 +10,6 @@ actions: # Initialize alias for Authentication Realm - epcc get account-authentication-settings - epcc create password-profile related_authentication_realm_for_account_authentication_settings_last_read=entity name "Username and Password Authentication" - enable-self-signup-and-management: - description: - short: "Enable password authentication" - commands: - # Initialize alias for Authentication Realm - - epcc get account-authentication-settings - - | - epcc create password-profile related_authentication_realm_for_account_authentication_settings_last_read=entity name "Username and Password Authentication" - epcc update account-authentication-setting enable_self_signup true auto_create_account_for_account_members true account_member_self_management "update_only" create-deep-hierarchy: description: short: "Create a hierarchy" diff --git a/external/runbooks/runbook_rendering.go b/external/runbooks/runbook_rendering.go index da5d389..0e78306 100644 --- a/external/runbooks/runbook_rendering.go +++ b/external/runbooks/runbook_rendering.go @@ -4,8 +4,8 @@ import ( "bytes" "fmt" "github.com/Masterminds/sprig/v3" + "github.com/elasticpath/epcc-cli/external/templates" "math" - "math/rand" "strconv" "strings" "text/template" @@ -25,11 +25,11 @@ func RenderTemplates(templateName string, rawCmd string, stringVars map[string]* tpl, err := template.New(templateName).Funcs(sprig.FuncMap()).Funcs( map[string]any{ "pow": func(a, b int) int { return int(math.Pow(float64(a), float64(b))) }, - "pseudoRandAlphaNum": randAlphaNum, - "pseudoRandAlpha": randAlpha, - "pseudoRandNumeric": randNumeric, - "pseudoRandString": randString, - "pseudoRandInt": randInt, + "pseudoRandAlphaNum": templates.RandAlphaNum, + "pseudoRandAlpha": templates.RandAlpha, + "pseudoRandNumeric": templates.RandNumeric, + "pseudoRandString": templates.RandString, + "pseudoRandInt": templates.RandInt, }).Parse(rawCmd) if err != nil { @@ -76,35 +76,3 @@ func RenderTemplates(templateName string, rawCmd string, stringVars map[string]* rawCmdLines := strings.Split(renderedTpl.String(), "\n") return rawCmdLines, nil } - -// randString is the internal function that generates a random string. -// It takes the length of the string and a string of allowed characters as parameters. -func randString(letters string, n int) string { - b := make([]byte, n) - for i := range b { - b[i] = letters[rand.Intn(len(letters))] - } - return string(b) -} - -// randAlphaNum generates a string consisting of characters in the range 0-9, a-z, and A-Z. -func randAlphaNum(n int) string { - const letters = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - return randString(letters, n) -} - -// randAlpha generates a string consisting of characters in the range a-z and A-Z. -func randAlpha(n int) string { - const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - return randString(letters, n) -} - -// randNumeric generates a string consisting of characters in the range 0-9. -func randNumeric(n int) string { - const digits = "0123456789" - return randString(digits, n) -} - -func randInt(min, max int) int { - return rand.Intn(max-min) + min -} diff --git a/external/templates/funcs.go b/external/templates/funcs.go new file mode 100644 index 0000000..fb9aac8 --- /dev/null +++ b/external/templates/funcs.go @@ -0,0 +1,35 @@ +package templates + +import "math/rand" + +// randString is the internal function that generates a random string. +// It takes the length of the string and a string of allowed characters as parameters. +func RandString(letters string, n int) string { + b := make([]byte, n) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] + } + return string(b) +} + +// randAlphaNum generates a string consisting of characters in the range 0-9, a-z, and A-Z. +func RandAlphaNum(n int) string { + const letters = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + return RandString(letters, n) +} + +// randAlpha generates a string consisting of characters in the range a-z and A-Z. +func RandAlpha(n int) string { + const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + return RandString(letters, n) +} + +// randNumeric generates a string consisting of characters in the range 0-9. +func RandNumeric(n int) string { + const digits = "0123456789" + return RandString(digits, n) +} + +func RandInt(min, max int) int { + return rand.Intn(max-min) + min +} diff --git a/external/templates/template.go b/external/templates/template.go index fd31ecf..f6554dd 100644 --- a/external/templates/template.go +++ b/external/templates/template.go @@ -17,7 +17,14 @@ func Render(templateString string) string { return templateString } - tpl, err := template.New("templateName").Funcs(sprig.FuncMap()).Parse(templateString) + tpl, err := template.New("templateName").Funcs(sprig.FuncMap()).Funcs( + map[string]any{ + "pseudoRandAlphaNum": RandAlphaNum, + "pseudoRandAlpha": RandAlpha, + "pseudoRandNumeric": RandNumeric, + "pseudoRandString": RandString, + "pseudoRandInt": RandInt, + }).Parse(templateString) if err != nil { log.Warn("Could not process argument template: %s, due to %v", templateString, err)