Skip to content

Commit

Permalink
Make search attributes and memo follow key=value format (#303)
Browse files Browse the repository at this point in the history
  • Loading branch information
feedmeapples authored Sep 28, 2022
1 parent a919b8f commit b26b24b
Show file tree
Hide file tree
Showing 7 changed files with 63 additions and 88 deletions.
20 changes: 5 additions & 15 deletions cli/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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",
},
}

Expand Down
4 changes: 2 additions & 2 deletions cli/namespace_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
20 changes: 6 additions & 14 deletions cli/schedule.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
}

Expand All @@ -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{
Expand Down
6 changes: 3 additions & 3 deletions cli/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
2 changes: 1 addition & 1 deletion cli/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
79 changes: 28 additions & 51 deletions cli/workflow_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
20 changes: 18 additions & 2 deletions cli/workflow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand All @@ -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() {
Expand Down

0 comments on commit b26b24b

Please sign in to comment.