Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add operator search-attribute commands #470

Merged
merged 1 commit into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Binaries for programs and plugins
/temporal
/temporal.exe

# Used by IDE
/.idea
/.vscode
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ Uses normal `go test`, e.g.:

See other tests for how to leverage things like the command harness and dev server suite.

Example to run a single test case:

go test ./... -run TestSharedServerSuite/TestOperator_SearchAttribute

## Adding/updating commands

First, update [commands.md](temporalcli/commandsmd/commands.md) following the rules in that file. Then to regenerate the
Expand Down
105 changes: 105 additions & 0 deletions temporalcli/commands.gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ func NewTemporalOperatorCommand(cctx *CommandContext, parent *TemporalCommand) *
}
s.Command.Args = cobra.NoArgs
s.Command.AddCommand(&NewTemporalOperatorClusterCommand(cctx, &s).Command)
s.Command.AddCommand(&NewTemporalOperatorSearchAttributeCommand(cctx, &s).Command)
s.ClientOptions.buildFlags(cctx, s.Command.PersistentFlags())
return &s
}
Expand Down Expand Up @@ -472,6 +473,110 @@ func NewTemporalOperatorClusterUpsertCommand(cctx *CommandContext, parent *Tempo
return &s
}

type TemporalOperatorSearchAttributeCommand struct {
Parent *TemporalOperatorCommand
Command cobra.Command
}

func NewTemporalOperatorSearchAttributeCommand(cctx *CommandContext, parent *TemporalOperatorCommand) *TemporalOperatorSearchAttributeCommand {
var s TemporalOperatorSearchAttributeCommand
s.Parent = parent
s.Command.Use = "search-attribute"
s.Command.Short = "Operations applying to Search Attributes"
s.Command.Long = "Search Attribute commands enable operations for the creation, listing, and removal of Search Attributes."
s.Command.Args = cobra.NoArgs
s.Command.AddCommand(&NewTemporalOperatorSearchAttributeCreateCommand(cctx, &s).Command)
s.Command.AddCommand(&NewTemporalOperatorSearchAttributeListCommand(cctx, &s).Command)
s.Command.AddCommand(&NewTemporalOperatorSearchAttributeRemoveCommand(cctx, &s).Command)
return &s
}

type TemporalOperatorSearchAttributeCreateCommand struct {
Parent *TemporalOperatorSearchAttributeCommand
Command cobra.Command
Name []string
Type []string
}

func NewTemporalOperatorSearchAttributeCreateCommand(cctx *CommandContext, parent *TemporalOperatorSearchAttributeCommand) *TemporalOperatorSearchAttributeCreateCommand {
var s TemporalOperatorSearchAttributeCreateCommand
s.Parent = parent
s.Command.DisableFlagsInUseLine = true
s.Command.Use = "create [flags]"
s.Command.Short = "Adds one or more custom Search Attributes"
if hasHighlighting {
s.Command.Long = "\x1b[1mtemporal operator search-attribute create\x1b[0m command adds one or more custom Search Attributes."
} else {
s.Command.Long = "`temporal operator search-attribute create` command adds one or more custom Search Attributes."
}
s.Command.Args = cobra.NoArgs
s.Command.Flags().StringArrayVar(&s.Name, "name", nil, "Search Attribute name.")
_ = cobra.MarkFlagRequired(s.Command.Flags(), "name")
s.Command.Flags().StringArrayVar(&s.Type, "type", nil, "Search Attribute type. Accepted values: Text, Keyword, Int, Double, Bool, Datetime, KeywordList.")
_ = cobra.MarkFlagRequired(s.Command.Flags(), "type")
s.Command.Run = func(c *cobra.Command, args []string) {
if err := s.run(cctx, args); err != nil {
cctx.Options.Fail(err)
}
}
return &s
}

type TemporalOperatorSearchAttributeListCommand struct {
Parent *TemporalOperatorSearchAttributeCommand
Command cobra.Command
}

func NewTemporalOperatorSearchAttributeListCommand(cctx *CommandContext, parent *TemporalOperatorSearchAttributeCommand) *TemporalOperatorSearchAttributeListCommand {
var s TemporalOperatorSearchAttributeListCommand
s.Parent = parent
s.Command.DisableFlagsInUseLine = true
s.Command.Use = "list [flags]"
s.Command.Short = "Lists all Search Attributes that can be used in list Workflow Queries"
if hasHighlighting {
s.Command.Long = "\x1b[1mtemporal operator search-attribute list\x1b[0m displays a list of all Search Attributes."
} else {
s.Command.Long = "`temporal operator search-attribute list` displays a list of all Search Attributes."
}
s.Command.Args = cobra.NoArgs
s.Command.Run = func(c *cobra.Command, args []string) {
if err := s.run(cctx, args); err != nil {
cctx.Options.Fail(err)
}
}
return &s
}

type TemporalOperatorSearchAttributeRemoveCommand struct {
Parent *TemporalOperatorSearchAttributeCommand
Command cobra.Command
Name []string
Yes bool
}

func NewTemporalOperatorSearchAttributeRemoveCommand(cctx *CommandContext, parent *TemporalOperatorSearchAttributeCommand) *TemporalOperatorSearchAttributeRemoveCommand {
var s TemporalOperatorSearchAttributeRemoveCommand
s.Parent = parent
s.Command.DisableFlagsInUseLine = true
s.Command.Use = "remove [flags]"
s.Command.Short = "Removes custom search attribute metadata only"
if hasHighlighting {
s.Command.Long = "\x1b[1mtemporal operator search-attribute remove\x1b[0m command removes custom Search Attribute metadata."
} else {
s.Command.Long = "`temporal operator search-attribute remove` command removes custom Search Attribute metadata."
}
s.Command.Args = cobra.NoArgs
s.Command.Flags().StringArrayVar(&s.Name, "name", nil, "Search Attribute name.")
_ = cobra.MarkFlagRequired(s.Command.Flags(), "name")
s.Command.Flags().BoolVarP(&s.Yes, "yes", "y", false, "Confirm prompt to perform deletion.")
s.Command.Run = func(c *cobra.Command, args []string) {
if err := s.run(cctx, args); err != nil {
cctx.Options.Fail(err)
}
}
return &s
}

type TemporalServerCommand struct {
Parent *TemporalCommand
Command cobra.Command
Expand Down
141 changes: 141 additions & 0 deletions temporalcli/commands.operator_search_attribute.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package temporalcli

import (
"fmt"
"strings"

"github.com/fatih/color"
"github.com/temporalio/cli/temporalcli/internal/printer"
"go.temporal.io/api/enums/v1"
"go.temporal.io/api/operatorservice/v1"
)

func (c *TemporalOperatorSearchAttributeCreateCommand) run(cctx *CommandContext, args []string) error {
cl, err := c.Parent.Parent.ClientOptions.dialClient(cctx)
if err != nil {
return err
}
defer cl.Close()

// Name and Type are required parameters, and there must be the same number of them.

listReq := &operatorservice.ListSearchAttributesRequest{
Namespace: c.Parent.Parent.Namespace,
}
existingSearchAttributes, err := cl.OperatorService().ListSearchAttributes(cctx, listReq)
if err != nil {
return fmt.Errorf("unable to get existing search attributes: %w", err)
}

searchAttributes := make(map[string]enums.IndexedValueType, len(c.Type))
for i, saType := range c.Type {
saName := c.Name[i]
typeInt, err := searchAttributeTypeStringToEnum(saType)
if err != nil {
return fmt.Errorf("unable to parse search attribute type %s: %w", saType, err)
}
existingSearchAttributeType, searchAttributeExists := existingSearchAttributes.CustomAttributes[saName]
if searchAttributeExists && existingSearchAttributeType != typeInt {
return fmt.Errorf("search attribute %s already exists and has different type %s", saName, existingSearchAttributeType.String())
}
searchAttributes[saName] = typeInt
}

request := &operatorservice.AddSearchAttributesRequest{
SearchAttributes: searchAttributes,
Namespace: c.Parent.Parent.Namespace,
}

_, err = cl.OperatorService().AddSearchAttributes(cctx, request)
if err != nil {
return fmt.Errorf("unable to add search attributes: %w", err)
}
cctx.Printer.Println(color.GreenString("Search attributes have been added"))
return nil
}

func searchAttributeTypeStringToEnum(search string) (enums.IndexedValueType, error) {
for k, v := range enums.IndexedValueType_shorthandValue {
if strings.EqualFold(search, k) {
return enums.IndexedValueType(v), nil
}
}
return enums.INDEXED_VALUE_TYPE_UNSPECIFIED, fmt.Errorf("unsupported search attribute type: %v", search)
}

func (c *TemporalOperatorSearchAttributeRemoveCommand) run(cctx *CommandContext, args []string) error {
cl, err := c.Parent.Parent.ClientOptions.dialClient(cctx)
if err != nil {
return err
}
defer cl.Close()

yes, err := cctx.promptYes(
fmt.Sprintf("You are about to remove search attribute %q? y/N", c.Name), c.Yes)
if err != nil {
return err
} else if !yes {
// We consider this a command failure
return fmt.Errorf("user denied confirmation, operation aborted")
}

names := c.Name
namespace := c.Parent.Parent.Namespace
if err != nil {
return err
}

request := &operatorservice.RemoveSearchAttributesRequest{
SearchAttributes: names,
Namespace: namespace,
}

_, err = cl.OperatorService().RemoveSearchAttributes(cctx, request)
if err != nil {
return fmt.Errorf("unable to remove search attributes: %w", err)
}

// response contains nothing
yiminc marked this conversation as resolved.
Show resolved Hide resolved
cctx.Printer.Println(color.GreenString("Search attributes have been removed"))
return nil
}

func (c *TemporalOperatorSearchAttributeListCommand) run(cctx *CommandContext, args []string) error {
cl, err := c.Parent.Parent.ClientOptions.dialClient(cctx)
if err != nil {
return err
}
defer cl.Close()

request := &operatorservice.ListSearchAttributesRequest{
Namespace: c.Parent.Parent.Namespace,
}
resp, err := cl.OperatorService().ListSearchAttributes(cctx, request)
if err != nil {
return fmt.Errorf("unable to list search attributes: %w", err)
}
if cctx.JSONOutput {
return cctx.Printer.PrintStructured(resp, printer.StructuredOptions{})
}

type saNameType struct {
Name string `json:"name"`
Type string `json:"type"`
}

var sas []saNameType
for saName, saType := range resp.SystemAttributes {
sas = append(sas, saNameType{
Name: saName,
Type: saType.String(),
})
}
for saName, saType := range resp.CustomAttributes {
sas = append(sas, saNameType{
Name: saName,
Type: saType.String(),
})
}

return cctx.Printer.PrintStructured(sas, printer.StructuredOptions{Table: &printer.TableOptions{}})
}
100 changes: 100 additions & 0 deletions temporalcli/commands.operator_search_attribute_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package temporalcli_test

import (
"github.com/temporalio/cli/temporalcli"
"go.temporal.io/api/enums/v1"
"go.temporal.io/api/operatorservice/v1"
)

func (s *SharedServerSuite) TestOperator_SearchAttribute_Create_Already_Exists() {
/*
This test try to create:
1, a system search attribute, that should fail.
2, a custom search attribute with different type, that should fail.
3, a custom search attribute with same type, that should pass.
*/
// Create System search attribute (WorkflowId)
res := s.Execute(
"operator", "search-attribute", "create",
"--address", s.Address(),
"--name", "WorkflowId",
"--type", "Keyword",
)
s.Error(res.Err)
s.Contains(res.Err.Error(), "unable to add search attributes: Search attribute WorkflowId is reserved by system.")

// Create Custom search attribute with different type
res2 := s.Execute(
"operator", "search-attribute", "create",
"--address", s.Address(),
"--name", "CustomKeywordField",
"--type", "Int",
)
s.Error(res2.Err)
s.Contains(res2.Err.Error(), "search attribute CustomKeywordField already exists and has different type Keyword")

// Create Custom search attribute with same type
res3 := s.Execute(
"operator", "search-attribute", "create",
"--address", s.Address(),
"--name", "CustomKeywordField",
"--type", "Keyword",
)
s.NoError(res3.Err)
}

func (s *SharedServerSuite) TestOperator_SearchAttribute() {
/*
This test case first create 2 new custom search attributes, and verify the creation by list them.
Then we delete one of the newly created SA, and verify the deletion by list again.
*/
res := s.Execute(
"operator", "search-attribute", "create",
"--address", s.Address(),
"--name", "MySearchAttributeForTestCreateKeyword",
"--type", "Keyword",
"--name", "MySearchAttributeForTestCreateInt",
"--type", "Int",
)
s.NoError(res.Err)

// verify the creation succeed
res2 := s.Execute(
"operator", "search-attribute", "list",
"--address", s.Address(),
"-o", "json",
)
s.NoError(res2.Err)
var jsonOut operatorservice.ListSearchAttributesResponse
s.NoError(temporalcli.UnmarshalProtoJSONWithOptions(res2.Stdout.Bytes(), &jsonOut, true))
s.Equal(enums.INDEXED_VALUE_TYPE_KEYWORD, jsonOut.CustomAttributes["MySearchAttributeForTestCreateKeyword"])
s.Equal(enums.INDEXED_VALUE_TYPE_INT, jsonOut.CustomAttributes["MySearchAttributeForTestCreateInt"])

// Remove it
res3 := s.Execute(
"operator", "search-attribute", "remove",
"--address", s.Address(),
"--name", "MySearchAttributeForTestCreateKeyword",
"--yes",
)
s.NoError(res3.Err)

// verify deletion
res4 := s.Execute(
"operator", "search-attribute", "list",
"--address", s.Address(),
"-o", "json",
)
s.NoError(res2.Err)
var jsonOut2 operatorservice.ListSearchAttributesResponse
s.NoError(temporalcli.UnmarshalProtoJSONWithOptions(res4.Stdout.Bytes(), &jsonOut2, true))
// Int SA still exists
s.Equal(enums.INDEXED_VALUE_TYPE_INT, jsonOut2.CustomAttributes["MySearchAttributeForTestCreateInt"])
// Keyword SA no longer exists
_, exists := jsonOut2.CustomAttributes["MySearchAttributeForTestCreateKeyword"]
s.False(exists)

// also verify some system attributes from list result
s.Equal(enums.INDEXED_VALUE_TYPE_DATETIME, jsonOut.SystemAttributes["StartTime"])
s.Equal(enums.INDEXED_VALUE_TYPE_KEYWORD, jsonOut.SystemAttributes["WorkflowId"])
}
Loading
Loading