diff --git a/cli/flags.go b/cli/flags.go index 14749e4..4cafa43 100644 --- a/cli/flags.go +++ b/cli/flags.go @@ -84,11 +84,9 @@ var ( FlagEventID = "event-id" FlagActivityID = "activity-id" FlagMaxFieldLength = "max-field-length" - FlagMemoKey = "memo-key" FlagMemo = "memo" FlagMemoFile = "memo-file" - FlagSearchAttributeKey = "search-attribute-key" - FlagSearchAttributeValue = "search-attribute-value" + FlagSearchAttribute = "search-attribute" FlagAddBadBinary = "add-bad-binary" FlagRemoveBadBinary = "remove-bad-binary" FlagResetReapplyType = "reapply-type" @@ -245,24 +243,16 @@ var flagsForStartWorkflowT = []cli.Flag{ Usage: "Maximum length for each attribute field", }, &cli.StringSliceFlag{ - Name: FlagMemoKey, - Usage: "Pass a key for an optional memo", + Name: FlagSearchAttribute, + Usage: "Pass Search Attribute in a format key=value. Use valid JSON formats for value", }, &cli.StringSliceFlag{ Name: FlagMemo, - Usage: "Pass a memo value. A memo is information in JSON format that can be shown when the Workflow is listed", + Usage: "Pass a memo in a format key=value. Use valid JSON formats for value", }, &cli.StringFlag{ Name: FlagMemoFile, - Usage: "Pass information for a memo from a JSON file. If there are multiple values, separate them by newline.", - }, - &cli.StringSliceFlag{ - Name: FlagSearchAttributeKey, - Usage: "Specify a Search Attribute key. See https://docs.temporal.io/docs/concepts/what-is-a-search-attribute/", - }, - &cli.StringSliceFlag{ - Name: FlagSearchAttributeValue, - Usage: "Specify a Search Attribute value. If value is an array, use JSON format, such as [\"a\",\"b\"] or [1,2], [\"true\",\"false\"]", + Usage: "Pass a memo from a file, where each line follows the format key=value. Use valid JSON formats for value", }, } diff --git a/cli/namespace_commands.go b/cli/namespace_commands.go index fa78d93..0a6e611 100644 --- a/cli/namespace_commands.go +++ b/cli/namespace_commands.go @@ -72,7 +72,7 @@ func RegisterNamespace(c *cli.Context) error { data := map[string]string{} if c.IsSet(FlagNamespaceData) { datas := c.StringSlice(FlagNamespaceData) - data, err = ParseKeyValuePairs(datas) + data, err = SplitKeyValuePairs(datas) if err != nil { return err } @@ -195,7 +195,7 @@ func UpdateNamespace(c *cli.Context) error { data := map[string]string{} if c.IsSet(FlagNamespaceData) { datas := c.StringSlice(FlagNamespaceData) - data, err = ParseKeyValuePairs(datas) + data, err = SplitKeyValuePairs(datas) if err != nil { return err } diff --git a/cli/schedule.go b/cli/schedule.go index aeabe96..50feb3c 100644 --- a/cli/schedule.go +++ b/cli/schedule.go @@ -104,24 +104,16 @@ func newScheduleCommands() []*cli.Command { // These are the same flags as for start workflow, but we need to change the Usage to talk about schedules instead of workflows. scheduleVisibilityFlags := []cli.Flag{ &cli.StringSliceFlag{ - Name: FlagMemoKey, - Usage: "Key for an optional memo", + Name: FlagSearchAttribute, + Usage: "Set Search Attribute on a schedule. Format: key=value. Use valid JSON formats for value", }, &cli.StringSliceFlag{ Name: FlagMemo, - Usage: "Memo value to be set on the schedule", + Usage: "Set a memo on a schedule. Format: key=value. Use valid JSON formats for value", }, &cli.StringFlag{ Name: FlagMemoFile, - Usage: "Information for a memo from a JSON file. If there are multiple values, separate them by newline", - }, - &cli.StringSliceFlag{ - Name: FlagSearchAttributeKey, - Usage: "Search Attribute key to be set on the schedule", - }, - &cli.StringSliceFlag{ - Name: FlagSearchAttributeValue, - Usage: "Search Attribute value. If value is an array, use JSON format, such as [\"a\",\"b\"] or [1,2], [\"true\",\"false\"]", + Usage: "Set a memo from a file. Each line should follow the format key=value. Use valid JSON formats for value", }, } @@ -132,8 +124,8 @@ func newScheduleCommands() []*cli.Command { createFlags = append(createFlags, scheduleVisibilityFlags...) createFlags = append(createFlags, removeFlags(flagsForStartWorkflowLong, FlagCronSchedule, FlagWorkflowIDReusePolicy, - FlagMemoKey, FlagMemo, FlagMemoFile, - FlagSearchAttributeKey, FlagSearchAttributeValue, + FlagMemo, FlagMemoFile, + FlagSearchAttribute, )...) return []*cli.Command{ diff --git a/cli/util.go b/cli/util.go index e46affd..bc55a9d 100644 --- a/cli/util.go +++ b/cli/util.go @@ -776,13 +776,13 @@ func parseFoldStatusList(flagValue string) ([]enumspb.WorkflowExecutionStatus, e return statusList, nil } -// ParseKeyValuePairs parses key=value pairs -func ParseKeyValuePairs(kvs []string) (map[string]string, error) { +// SplitKeyValuePairs parses key=value pairs +func SplitKeyValuePairs(kvs []string) (map[string]string, error) { pairs := make(map[string]string, len(kvs)) for _, v := range kvs { parts := strings.SplitN(v, "=", 2) if len(parts) != 2 { - return nil, fmt.Errorf("unable to parse key=value pair: %v", v) + return nil, fmt.Errorf("unable to split key=value pair: %v", v) } key := strings.TrimSpace(parts[0]) value := strings.TrimSpace(parts[1]) diff --git a/cli/util_test.go b/cli/util_test.go index 0178b9a..cc53e69 100644 --- a/cli/util_test.go +++ b/cli/util_test.go @@ -195,7 +195,7 @@ func (s *utilSuite) TestParseKeyValuePairs() { for name, tt := range tests { s.Run(name, func() { - got, err := ParseKeyValuePairs(tt.input) + got, err := SplitKeyValuePairs(tt.input) if tt.wantErr { s.Error(err) } else { diff --git a/cli/workflow_commands.go b/cli/workflow_commands.go index 1e97e95..69e4949 100644 --- a/cli/workflow_commands.go +++ b/cli/workflow_commands.go @@ -187,83 +187,60 @@ func formatInputsForDisplay(inputs []interface{}) string { } func unmarshalSearchAttrFromCLI(c *cli.Context) (map[string]interface{}, error) { - sanitize := func(val []string) []string { - result := make([]string, len(val)) - for i, v := range val { - result[i] = strings.TrimSpace(v) - } - return result - } - - searchAttrKeys := sanitize(c.StringSlice(FlagSearchAttributeKey)) - if len(searchAttrKeys) == 0 { - return nil, nil - } - rawSearchAttrVals := sanitize(c.StringSlice(FlagSearchAttributeValue)) - if len(rawSearchAttrVals) == 0 { - return nil, nil - } - - if len(searchAttrKeys) != len(rawSearchAttrVals) { - return nil, fmt.Errorf("uneven number of search attributes keys (%d): %v and values (%d): %v", len(searchAttrKeys), searchAttrKeys, len(rawSearchAttrVals), rawSearchAttrVals) + raw := c.StringSlice(FlagSearchAttribute) + parsed, err := SplitKeyValuePairs(raw) + if err != nil { + return nil, err } - fields := make(map[string]interface{}, len(searchAttrKeys)) - - for i, v := range rawSearchAttrVals { + attributes := make(map[string]interface{}, len(parsed)) + for k, v := range parsed { var j interface{} if err := json.Unmarshal([]byte(v), &j); err != nil { - return nil, fmt.Errorf("unable to parse search attribute JSON: %w", err) + return nil, fmt.Errorf("unable to parse Search Attribute JSON: %w", err) } - fields[searchAttrKeys[i]] = j + attributes[k] = j } - return fields, nil + return attributes, nil } func unmarshalMemoFromCLI(c *cli.Context) (map[string]interface{}, error) { - // Memo flags were not passed => Memo is not provided. - if !c.IsSet(FlagMemoKey) && !c.IsSet(FlagMemo) && !c.IsSet(FlagMemoFile) { - return nil, nil - } - - if !c.IsSet(FlagMemoKey) { - return nil, fmt.Errorf("memo keys must be provided using %s", FlagMemoKey) - } - - if c.IsSet(FlagMemo) && c.IsSet(FlagMemoFile) { - return nil, fmt.Errorf("provide only one of %s or %s", FlagMemo, FlagMemoFile) - } - if !c.IsSet(FlagMemo) && !c.IsSet(FlagMemoFile) { - return nil, fmt.Errorf("memo values must be provided using %s or %s", FlagMemo, FlagMemoFile) + return nil, nil } - memoKeys := c.StringSlice(FlagMemoKey) + raw := c.StringSlice(FlagMemo) - var memoValues []string + var rawFromFile []string if c.IsSet(FlagMemoFile) { inputFile := c.String(FlagMemoFile) - // This method is purely used to parse input from the CLI. The input comes from a trusted user + // The input comes from a trusted user // #nosec data, err := os.ReadFile(inputFile) if err != nil { return nil, fmt.Errorf("unable to read memo file %s", inputFile) } - memoValues = strings.Split(string(data), "\n") - } else if c.IsSet(FlagMemo) { - memoValues = c.StringSlice(FlagMemo) + rawFromFile = strings.Split(string(data), "\n") } - if len(memoKeys) != len(memoValues) { - return nil, fmt.Errorf("number of memo keys %d and values %d are not equal", len(memoKeys), len(memoValues)) + raw = append(raw, rawFromFile...) + + parsed, err := SplitKeyValuePairs(raw) + if err != nil { + return nil, err } - fields := make(map[string]interface{}, len(memoKeys)) - for i, key := range memoKeys { - fields[key] = memoValues[i] + memo := make(map[string]interface{}, len(parsed)) + for k, v := range parsed { + var j interface{} + if err := json.Unmarshal([]byte(v), &j); err != nil { + return nil, fmt.Errorf("unable to parse Search Attribute JSON: %w", err) + } + memo[k] = j } - return fields, nil + + return memo, nil } type historyIterator struct { diff --git a/cli/workflow_test.go b/cli/workflow_test.go index 8ddb6cc..6ba4ff4 100644 --- a/cli/workflow_test.go +++ b/cli/workflow_test.go @@ -87,7 +87,7 @@ func (s *cliAppSuite) TestStartWorkflow_SearchAttributes() { s.sdkClient.On("ExecuteWorkflow", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(workflowRun(), nil) // start with basic search attributes err := s.app.Run([]string{"", "--namespace", cliTestNamespace, "workflow", "start", "--task-queue", "testTaskQueue", "--type", "testWorkflowType", - "--search-attribute-key", "k1", "--search-attribute-value", "\"v1\"", "--search-attribute-key", "k2", "--search-attribute-value", "\"v2\""}) + "--search-attribute", "k1=\"v1\"", "--search-attribute", "k2=\"v2\""}) s.Nil(err) s.sdkClient.AssertExpectations(s.T()) @@ -99,8 +99,24 @@ func (s *cliAppSuite) TestStartWorkflow_SearchAttributes() { s.sdkClient.AssertCalled(s.T(), "ExecuteWorkflow", mock.Anything, hasCorrectSearchAttributes, mock.Anything, mock.Anything) s.sdkClient.On("ExecuteWorkflow", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(workflowRun(), nil) +} + +func (s *cliAppSuite) TestStartWorkflow_Memo() { + s.sdkClient.On("ExecuteWorkflow", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(workflowRun(), nil) + // start with basic search attributes + err := s.app.Run([]string{"", "--namespace", cliTestNamespace, "workflow", "start", "--task-queue", "testTaskQueue", "--type", "testWorkflowType", + "--memo", "k1=\"v1\"", "--memo", "k2=\"v2\""}) + s.Nil(err) + s.sdkClient.AssertExpectations(s.T()) - // TODO: test json search attributes once we know how to they'll be specified (--search-attribute-json?) + hasCorrectMemo := mock.MatchedBy(func(options sdkclient.StartWorkflowOptions) bool { + return len(options.Memo) == 2 && + options.Memo["k1"] == "v1" && + options.Memo["k2"] == "v2" + }) + s.sdkClient.AssertCalled(s.T(), "ExecuteWorkflow", mock.Anything, hasCorrectMemo, mock.Anything, mock.Anything) + + s.sdkClient.On("ExecuteWorkflow", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(workflowRun(), nil) } func (s *cliAppSuite) TestStartWorkflow_Failed() {