From 52f90eb3cbc3f4313812712679301531a8c41e8d Mon Sep 17 00:00:00 2001 From: Josh Berry Date: Wed, 27 Mar 2024 16:32:04 -0700 Subject: [PATCH] Make argument and flag use consistent across commands Closes #343. See the issue for the full discussion and rationale behind the specific changes made. --- temporalcli/commands.env.go | 103 +++++++++++++----- temporalcli/commands.env_test.go | 42 +++++-- temporalcli/commands.gen.go | 70 +++++++----- temporalcli/commands.operator_namespace.go | 62 ++++++++--- .../commands.operator_namespace_test.go | 43 ++++++-- temporalcli/commandsmd/commands.md | 93 ++++++++++------ 6 files changed, 294 insertions(+), 119 deletions(-) diff --git a/temporalcli/commands.env.go b/temporalcli/commands.env.go index b9377778..00622aaa 100644 --- a/temporalcli/commands.env.go +++ b/temporalcli/commands.env.go @@ -8,36 +8,66 @@ import ( "github.com/temporalio/cli/temporalcli/internal/printer" ) +func (c *TemporalEnvCommand) envNameAndKey(cctx *CommandContext, args[] string, keyFlag string) (string, string, error) { + if len(args) > 0 { + cctx.Logger.Warn("Arguments to env commands are deprecated; please use -E and/or -k instead.") + + if (c.Parent.Env != "default" || keyFlag != "") { + return "", "", fmt.Errorf("cannot specify both an argument and -E/-k; please use -e and/or -k") + } + + keyPieces := strings.Split(args[0], ".") + switch len(keyPieces) { + case 0: + return "", "", fmt.Errorf("no env or property name specified") + case 1: + return keyPieces[0], "", nil + case 2: + return keyPieces[0], keyPieces[1], nil + default: + return "", "", fmt.Errorf("property name may not contain dots") + } + } + + if strings.Contains(keyFlag, ".") { + return "", "", fmt.Errorf("property name may not contain dots") + } + + return c.Parent.Env, keyFlag, nil +} + func (c *TemporalEnvDeleteCommand) run(cctx *CommandContext, args []string) error { - keyPieces := strings.Split(args[0], ".") - if len(keyPieces) > 2 { - return fmt.Errorf("env property key to delete cannot have more than one dot") + envName, key, err := c.Parent.envNameAndKey(cctx, args, c.Key) + if err != nil { + return err } - // Env must be present (but flag itself doesn't have to be) - env, ok := cctx.EnvConfigValues[keyPieces[0]] + + // Env must be present + env, ok := cctx.EnvConfigValues[envName] if !ok { - return fmt.Errorf("env %q not found", keyPieces[0]) + return fmt.Errorf("env %q not found", envName) } // User can remove single flag or all in env - if len(keyPieces) > 1 { - cctx.Logger.Info("Deleting env property", "env", keyPieces[0], "property", keyPieces[1]) - delete(env, keyPieces[1]) + if key != "" { + cctx.Logger.Info("Deleting env property", "env", envName, "property", key) + delete(env, key) } else { - cctx.Logger.Info("Deleting env", "env", keyPieces[0]) - delete(cctx.EnvConfigValues, keyPieces[0]) + cctx.Logger.Info("Deleting env", "env", env) + delete(cctx.EnvConfigValues, envName) } return cctx.WriteEnvConfigToFile() } func (c *TemporalEnvGetCommand) run(cctx *CommandContext, args []string) error { - keyPieces := strings.Split(args[0], ".") - if len(keyPieces) > 2 { - return fmt.Errorf("env key to get cannot have more than one dot") + envName, key, err := c.Parent.envNameAndKey(cctx, args, c.Key) + if err != nil { + return err } - // Env must be present (but flag itself doesn't have to me) - env, ok := cctx.EnvConfigValues[keyPieces[0]] + + // Env must be present + env, ok := cctx.EnvConfigValues[envName] if !ok { - return fmt.Errorf("env %q not found", keyPieces[0]) + return fmt.Errorf("env %q not found", envName) } type prop struct { Property string `json:"property"` @@ -45,8 +75,8 @@ func (c *TemporalEnvGetCommand) run(cctx *CommandContext, args []string) error { } var props []prop // User can ask for single flag or all in env - if len(keyPieces) > 1 { - props = []prop{{Property: keyPieces[1], Value: env[keyPieces[1]]}} + if key != "" { + props = []prop{{Property: key, Value: env[key]}} } else { props = make([]prop, 0, len(env)) for k, v := range env { @@ -71,17 +101,38 @@ func (c *TemporalEnvListCommand) run(cctx *CommandContext, args []string) error } func (c *TemporalEnvSetCommand) run(cctx *CommandContext, args []string) error { - keyPieces := strings.Split(args[0], ".") - if len(keyPieces) != 2 { - return fmt.Errorf("env property key to set must have single dot separating env and value") + envName, key, err := c.Parent.envNameAndKey(cctx, args, c.Key) + if err != nil { + return err + } + + if key == "" { + return fmt.Errorf("property name must be specified with -k") + } + + value := c.Value + switch len(args) { + case 0: + // Use what's in the flag + case 1: + // We got an "env.name" argument passed above, but no "value" argument + return fmt.Errorf("no value provided; see --help") + case 2: + // Old-style syntax; pull the value out of the args + value = args[1] + default: + // Cobra should catch this / we should never get here; included for + // completeness anyway. + return fmt.Errorf("too many arguments provided; see --help") } + if cctx.EnvConfigValues == nil { cctx.EnvConfigValues = map[string]map[string]string{} } - if cctx.EnvConfigValues[keyPieces[0]] == nil { - cctx.EnvConfigValues[keyPieces[0]] = map[string]string{} + if cctx.EnvConfigValues[envName] == nil { + cctx.EnvConfigValues[envName] = map[string]string{} } - cctx.Logger.Info("Setting env property", "env", keyPieces[0], "property", keyPieces[1], "value", args[1]) - cctx.EnvConfigValues[keyPieces[0]][keyPieces[1]] = args[1] + cctx.Logger.Info("Setting env property", "env", envName, "property", key, "value", value) + cctx.EnvConfigValues[envName][key] = value return cctx.WriteEnvConfigToFile() } diff --git a/temporalcli/commands.env_test.go b/temporalcli/commands.env_test.go index 9bb09fe6..82f187ac 100644 --- a/temporalcli/commands.env_test.go +++ b/temporalcli/commands.env_test.go @@ -13,7 +13,7 @@ func TestEnv_Simple(t *testing.T) { // Non-existent file, no env found for get h.Options.EnvConfigFile = "does-not-exist" - res := h.Execute("env", "get", "myenv1") + res := h.Execute("env", "get", "-E", "myenv1") h.ErrorContains(res.Err, `env "myenv1" not found`) // Temp file for env @@ -23,7 +23,7 @@ func TestEnv_Simple(t *testing.T) { defer os.Remove(h.Options.EnvConfigFile) // Store a key - res = h.Execute("env", "set", "myenv1.foo", "bar") + res = h.Execute("env", "set", "-E", "myenv1", "-k", "foo", "-v", "bar") h.NoError(res.Err) // Confirm file is YAML with expected values b, err := os.ReadFile(h.Options.EnvConfigFile) @@ -33,19 +33,19 @@ func TestEnv_Simple(t *testing.T) { h.Equal("bar", yamlVals["env"]["myenv1"]["foo"]) // Store another key and another env - res = h.Execute("env", "set", "myenv1.baz", "qux") + res = h.Execute("env", "set", "-E", "myenv1", "-k", "baz", "-v", "qux") h.NoError(res.Err) - res = h.Execute("env", "set", "myenv2.foo", "baz") + res = h.Execute("env", "set", "-E", "myenv2", "-k", "foo", "-v", "baz") h.NoError(res.Err) // Get single prop - res = h.Execute("env", "get", "myenv1.baz") + res = h.Execute("env", "get", "-E", "myenv1", "-k", "baz") h.NoError(res.Err) h.ContainsOnSameLine(res.Stdout.String(), "baz", "qux") h.NotContains(res.Stdout.String(), "foo") // Get all props for env - res = h.Execute("env", "get", "myenv1") + res = h.Execute("env", "get", "-E", "myenv1") h.NoError(res.Err) h.ContainsOnSameLine(res.Stdout.String(), "foo", "bar") h.ContainsOnSameLine(res.Stdout.String(), "baz", "qux") @@ -57,7 +57,7 @@ func TestEnv_Simple(t *testing.T) { h.Contains(res.Stdout.String(), "myenv2") // Delete single env value - res = h.Execute("env", "delete", "myenv1.foo") + res = h.Execute("env", "delete", "-E", "myenv1", "-k", "foo") h.NoError(res.Err) res = h.Execute("env", "get", "myenv1") h.NoError(res.Err) @@ -71,10 +71,36 @@ func TestEnv_Simple(t *testing.T) { h.NotContains(res.Stdout.String(), "myenv2") // Ensure env var overrides env file - res = h.Execute("env", "set", "myenv1.address", "something:1234") + res = h.Execute("env", "set", "-E", "myenv1", "-k", "address", "-v", "something:1234") h.NoError(res.Err) h.NoError(os.Setenv("TEMPORAL_ADDRESS", "overridden:1235")) defer os.Unsetenv("TEMPORAL_ADDRESS") res = h.Execute("workflow", "list", "--env", "myenv1") h.Contains(res.Stderr.String(), "Env var overrode --env setting") } + +func TestEnv_InputValidation(t *testing.T) { + h := NewCommandHarness(t) + defer h.Close() + + res := h.Execute("env", "get", "-E", "myenv1", "foo.bar") + h.ErrorContains(res.Err, `cannot specify both`) + + res = h.Execute("env", "get", "-k", "key", "foo.bar") + h.ErrorContains(res.Err, `cannot specify both`) + + res = h.Execute("env", "get", "-E", "myenv1", "-k", "foo.bar") + h.ErrorContains(res.Err, `property name may not contain dots`) + + res = h.Execute("env", "set", "-E", "myenv1", "-k", "foo.bar", "-v", "") + h.ErrorContains(res.Err, `property name may not contain dots`) + + res = h.Execute("env", "set", "-E", "myenv1", "-k", "", "-v", "") + h.ErrorContains(res.Err, `property name must be specified`) + + res = h.Execute("env", "set", "myenv1") + h.ErrorContains(res.Err, `property name must be specified`) + + res = h.Execute("env", "set", "myenv1.foo") + h.ErrorContains(res.Err, `no value provided`) +} diff --git a/temporalcli/commands.gen.go b/temporalcli/commands.gen.go index fe4ffc2a..d08eb2b1 100644 --- a/temporalcli/commands.gen.go +++ b/temporalcli/commands.gen.go @@ -41,7 +41,7 @@ func NewTemporalCommand(cctx *CommandContext) *TemporalCommand { s.Command.AddCommand(&NewTemporalServerCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalTaskQueueCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalWorkflowCommand(cctx, &s).Command) - s.Command.PersistentFlags().StringVar(&s.Env, "env", "default", "Environment to read environment-specific flags from.") + s.Command.PersistentFlags().StringVarP(&s.Env, "env", "E", "default", "Environment to read environment-specific flags from.") cctx.BindFlagEnvVar(s.Command.PersistentFlags().Lookup("env"), "TEMPORAL_ENV") s.Command.PersistentFlags().StringVar(&s.EnvFile, "env-file", "", "File to read all environments (defaults to `$HOME/.config/temporalio/temporal.yaml`).") s.LogLevel = NewStringEnum([]string{"debug", "info", "warn", "error", "never"}, "info") @@ -265,7 +265,11 @@ func NewTemporalEnvCommand(cctx *CommandContext, parent *TemporalCommand) *Tempo s.Parent = parent s.Command.Use = "env" s.Command.Short = "Manage environments." - s.Command.Long = "Use the '--env ' option with other commands to point the CLI at a different Temporal Server instance. If --env\nis not passed, the 'default' environment is used." + if hasHighlighting { + s.Command.Long = "Use the '--env ' option with other commands to point the CLI at a different Temporal Server instance. If --env\nis not passed, the 'default' environment is used. (You can also use \x1b[1m-E\x1b[0m as a shorthand for \x1b[1m--env\x1b[0m.)" + } else { + s.Command.Long = "Use the '--env ' option with other commands to point the CLI at a different Temporal Server instance. If --env\nis not passed, the 'default' environment is used. (You can also use `-E` as a shorthand for `--env`.)" + } s.Command.Args = cobra.NoArgs s.Command.AddCommand(&NewTemporalEnvDeleteCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalEnvGetCommand(cctx, &s).Command) @@ -277,20 +281,22 @@ func NewTemporalEnvCommand(cctx *CommandContext, parent *TemporalCommand) *Tempo type TemporalEnvDeleteCommand struct { Parent *TemporalEnvCommand Command cobra.Command + Key string } func NewTemporalEnvDeleteCommand(cctx *CommandContext, parent *TemporalEnvCommand) *TemporalEnvDeleteCommand { var s TemporalEnvDeleteCommand s.Parent = parent s.Command.DisableFlagsInUseLine = true - s.Command.Use = "delete [flags] [environment or property]" + s.Command.Use = "delete [flags]" s.Command.Short = "Delete an environment or environment property." if hasHighlighting { - s.Command.Long = "\x1b[1mtemporal env delete [environment or property]\x1b[0m\n\nDelete an environment or just a single property:\n\n\x1b[1mtemporal env delete prod\x1b[0m\n\x1b[1mtemporal env delete prod.tls-cert-path\x1b[0m" + s.Command.Long = "\x1b[1mtemporal env delete -E environment [-k property]\x1b[0m\n\nDelete an environment or just a single property:\n\n\x1b[1mtemporal env delete -E prod\x1b[0m\n\x1b[1mtemporal env delete -E prod -k tls-cert-path\x1b[0m\n\nIf the environment is not specified, the \x1b[1mdefault\x1b[0m environment is deleted:\n\n\x1b[1mtemporal env delete -k tls-cert-path\x1b[0m" } else { - s.Command.Long = "`temporal env delete [environment or property]`\n\nDelete an environment or just a single property:\n\n`temporal env delete prod`\n`temporal env delete prod.tls-cert-path`" + s.Command.Long = "`temporal env delete -E environment [-k property]`\n\nDelete an environment or just a single property:\n\n`temporal env delete -E prod`\n`temporal env delete -E prod -k tls-cert-path`\n\nIf the environment is not specified, the `default` environment is deleted:\n\n`temporal env delete -k tls-cert-path`" } - s.Command.Args = cobra.ExactArgs(1) + s.Command.Args = cobra.MaximumNArgs(1) + s.Command.Flags().StringVarP(&s.Key, "key", "k", "", "The name of the property.") s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { cctx.Options.Fail(err) @@ -302,20 +308,22 @@ func NewTemporalEnvDeleteCommand(cctx *CommandContext, parent *TemporalEnvComman type TemporalEnvGetCommand struct { Parent *TemporalEnvCommand Command cobra.Command + Key string } func NewTemporalEnvGetCommand(cctx *CommandContext, parent *TemporalEnvCommand) *TemporalEnvGetCommand { var s TemporalEnvGetCommand s.Parent = parent s.Command.DisableFlagsInUseLine = true - s.Command.Use = "get [flags] [environment or property]" + s.Command.Use = "get [flags]" s.Command.Short = "Print environment properties." if hasHighlighting { - s.Command.Long = "\x1b[1mtemporal env get [environment or property]\x1b[0m\n\nPrint all properties of the 'prod' environment:\n\n\x1b[1mtemporal env get prod\x1b[0m\n\ntls-cert-path /home/my-user/certs/client.cert\ntls-key-path /home/my-user/certs/client.key\naddress temporal.example.com:7233\nnamespace someNamespace\n\nPrint a single property:\n\n\x1b[1mtemporal env get prod.tls-key-path\x1b[0m\n\ntls-key-path /home/my-user/certs/cluster.key" + s.Command.Long = "\x1b[1mtemporal env get -E environment\x1b[0m\n\nPrint all properties of the 'prod' environment:\n\n\x1b[1mtemporal env get prod\x1b[0m\n\n\x1b[1mtls-cert-path /home/my-user/certs/client.cert\ntls-key-path /home/my-user/certs/client.key\naddress temporal.example.com:7233\nnamespace someNamespace\x1b[0m\n\nPrint a single property:\n\n\x1b[1mtemporal env get -E prod -k tls-key-path\x1b[0m\n\n\x1b[1mtls-key-path /home/my-user/certs/cluster.key\x1b[0m\n\nIf the environment is not specified, the \x1b[1mdefault\x1b[0m environment is used." } else { - s.Command.Long = "`temporal env get [environment or property]`\n\nPrint all properties of the 'prod' environment:\n\n`temporal env get prod`\n\ntls-cert-path /home/my-user/certs/client.cert\ntls-key-path /home/my-user/certs/client.key\naddress temporal.example.com:7233\nnamespace someNamespace\n\nPrint a single property:\n\n`temporal env get prod.tls-key-path`\n\ntls-key-path /home/my-user/certs/cluster.key" + s.Command.Long = "`temporal env get -E environment`\n\nPrint all properties of the 'prod' environment:\n\n`temporal env get prod`\n\n```\ntls-cert-path /home/my-user/certs/client.cert\ntls-key-path /home/my-user/certs/client.key\naddress temporal.example.com:7233\nnamespace someNamespace\n```\n\nPrint a single property:\n\n`temporal env get -E prod -k tls-key-path`\n\n```\ntls-key-path /home/my-user/certs/cluster.key\n```\n\nIf the environment is not specified, the `default` environment is used." } - s.Command.Args = cobra.ExactArgs(1) + s.Command.Args = cobra.MaximumNArgs(1) + s.Command.Flags().StringVarP(&s.Key, "key", "k", "", "The name of the property.") s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { cctx.Options.Fail(err) @@ -348,20 +356,24 @@ func NewTemporalEnvListCommand(cctx *CommandContext, parent *TemporalEnvCommand) type TemporalEnvSetCommand struct { Parent *TemporalEnvCommand Command cobra.Command + Key string + Value string } func NewTemporalEnvSetCommand(cctx *CommandContext, parent *TemporalEnvCommand) *TemporalEnvSetCommand { var s TemporalEnvSetCommand s.Parent = parent s.Command.DisableFlagsInUseLine = true - s.Command.Use = "set [flags] [environment.property name] [property value]" + s.Command.Use = "set [flags]" s.Command.Short = "Set environment properties." if hasHighlighting { - s.Command.Long = "\x1b[1mtemporal env set [environment.property name] [property value]\x1b[0m\n\nProperty names match CLI option names, for example '--address' and '--tls-cert-path':\n\n\x1b[1mtemporal env set prod.address 127.0.0.1:7233\x1b[0m\n\x1b[1mtemporal env set prod.tls-cert-path /home/my-user/certs/cluster.cert\x1b[0m" + s.Command.Long = "\x1b[1mtemporal env set -E environment -k property -v value\x1b[0m\n\nProperty names match CLI option names, for example '--address' and '--tls-cert-path':\n\n\x1b[1mtemporal env set -E prod -k address -v 127.0.0.1:7233\x1b[0m\n\x1b[1mtemporal env set -E prod -k tls-cert-path -v /home/my-user/certs/cluster.cert\x1b[0m\n\nIf the environment is not specified, the \x1b[1mdefault\x1b[0m environment is used." } else { - s.Command.Long = "`temporal env set [environment.property name] [property value]`\n\nProperty names match CLI option names, for example '--address' and '--tls-cert-path':\n\n`temporal env set prod.address 127.0.0.1:7233`\n`temporal env set prod.tls-cert-path /home/my-user/certs/cluster.cert`" + s.Command.Long = "`temporal env set -E environment -k property -v value`\n\nProperty names match CLI option names, for example '--address' and '--tls-cert-path':\n\n`temporal env set -E prod -k address -v 127.0.0.1:7233`\n`temporal env set -E prod -k tls-cert-path -v /home/my-user/certs/cluster.cert`\n\nIf the environment is not specified, the `default` environment is used." } - s.Command.Args = cobra.ExactArgs(2) + s.Command.Args = cobra.MaximumNArgs(2) + s.Command.Flags().StringVarP(&s.Key, "key", "k", "", "The name of the property.") + s.Command.Flags().StringVarP(&s.Value, "value", "v", "", "The value to set the property to.") s.Command.Run = func(c *cobra.Command, args []string) { if err := s.run(cctx, args); err != nil { cctx.Options.Fail(err) @@ -625,14 +637,14 @@ func NewTemporalOperatorNamespaceCreateCommand(cctx *CommandContext, parent *Tem var s TemporalOperatorNamespaceCreateCommand s.Parent = parent s.Command.DisableFlagsInUseLine = true - s.Command.Use = "create [flags] [namespace]" + s.Command.Use = "create [flags]" s.Command.Short = "Registers a new Namespace." if hasHighlighting { - s.Command.Long = "The temporal operator namespace create command creates a new Namespace on the Server.\nNamespaces can be created on the active Cluster, or any named Cluster.\n\x1b[1mtemporal operator namespace create --cluster=MyCluster example-1\x1b[0m\n\nGlobal Namespaces can also be created.\n\x1b[1mtemporal operator namespace create --global example-2\x1b[0m\n\nOther settings, such as retention and Visibility Archival State, can be configured as needed.\nFor example, the Visibility Archive can be set on a separate URI.\n\x1b[1mtemporal operator namespace create --retention=5 --visibility-archival-state=enabled --visibility-uri=some-uri example-3\x1b[0m" + s.Command.Long = "The temporal operator namespace create command creates a new Namespace on the Server.\nNamespaces can be created on the active Cluster, or any named Cluster.\n\x1b[1mtemporal operator namespace create --cluster=MyCluster -n example-1\x1b[0m\n\nGlobal Namespaces can also be created.\n\x1b[1mtemporal operator namespace create --global -n example-2\x1b[0m\n\nOther settings, such as retention and Visibility Archival State, can be configured as needed.\nFor example, the Visibility Archive can be set on a separate URI.\n\x1b[1mtemporal operator namespace create --retention=5 --visibility-archival-state=enabled --visibility-uri=some-uri -n example-3\x1b[0m" } else { - s.Command.Long = "The temporal operator namespace create command creates a new Namespace on the Server.\nNamespaces can be created on the active Cluster, or any named Cluster.\n`temporal operator namespace create --cluster=MyCluster example-1`\n\nGlobal Namespaces can also be created.\n`temporal operator namespace create --global example-2`\n\nOther settings, such as retention and Visibility Archival State, can be configured as needed.\nFor example, the Visibility Archive can be set on a separate URI.\n`temporal operator namespace create --retention=5 --visibility-archival-state=enabled --visibility-uri=some-uri example-3`" + s.Command.Long = "The temporal operator namespace create command creates a new Namespace on the Server.\nNamespaces can be created on the active Cluster, or any named Cluster.\n`temporal operator namespace create --cluster=MyCluster -n example-1`\n\nGlobal Namespaces can also be created.\n`temporal operator namespace create --global -n example-2`\n\nOther settings, such as retention and Visibility Archival State, can be configured as needed.\nFor example, the Visibility Archive can be set on a separate URI.\n`temporal operator namespace create --retention=5 --visibility-archival-state=enabled --visibility-uri=some-uri -n example-3`" } - s.Command.Args = cobra.ExactArgs(1) + s.Command.Args = cobra.MaximumNArgs(1) s.Command.Flags().StringVar(&s.ActiveCluster, "active-cluster", "", "Active cluster name.") s.Command.Flags().StringArrayVar(&s.Cluster, "cluster", nil, "Cluster names.") s.Command.Flags().StringVar(&s.Data, "data", "", "Namespace data in key=value format. Use JSON for values.") @@ -667,7 +679,7 @@ func NewTemporalOperatorNamespaceDeleteCommand(cctx *CommandContext, parent *Tem s.Command.Use = "delete [flags] [namespace]" s.Command.Short = "Deletes an existing Namespace." s.Command.Long = "The temporal operator namespace delete command deletes a given Namespace from the system." - s.Command.Args = cobra.ExactArgs(1) + s.Command.Args = cobra.MaximumNArgs(1) 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 { @@ -690,9 +702,9 @@ func NewTemporalOperatorNamespaceDescribeCommand(cctx *CommandContext, parent *T s.Command.Use = "describe [flags] [namespace]" s.Command.Short = "Describe a Namespace by its name or ID." if hasHighlighting { - s.Command.Long = "The temporal operator namespace describe command provides Namespace information.\nNamespaces are identified by Namespace ID.\n\n\x1b[1mtemporal operator namespace describe --namespace-id=some-namespace-id\x1b[0m\n\x1b[1mtemporal operator namespace describe example-namespace-name\x1b[0m" + s.Command.Long = "The temporal operator namespace describe command provides Namespace information.\nNamespaces are identified either by Namespace ID or by name.\n\n\x1b[1mtemporal operator namespace describe --namespace-id=some-namespace-id\x1b[0m\n\x1b[1mtemporal operator namespace describe -n example-namespace-name\x1b[0m" } else { - s.Command.Long = "The temporal operator namespace describe command provides Namespace information.\nNamespaces are identified by Namespace ID.\n\n`temporal operator namespace describe --namespace-id=some-namespace-id`\n`temporal operator namespace describe example-namespace-name`" + s.Command.Long = "The temporal operator namespace describe command provides Namespace information.\nNamespaces are identified either by Namespace ID or by name.\n\n`temporal operator namespace describe --namespace-id=some-namespace-id`\n`temporal operator namespace describe -n example-namespace-name`" } s.Command.Args = cobra.MaximumNArgs(1) s.Command.Flags().StringVar(&s.NamespaceId, "namespace-id", "", "Namespace ID.") @@ -745,14 +757,14 @@ func NewTemporalOperatorNamespaceUpdateCommand(cctx *CommandContext, parent *Tem var s TemporalOperatorNamespaceUpdateCommand s.Parent = parent s.Command.DisableFlagsInUseLine = true - s.Command.Use = "update [flags] [namespace]" + s.Command.Use = "update [flags]" s.Command.Short = "Updates a Namespace." if hasHighlighting { - s.Command.Long = "The temporal operator namespace update command updates a Namespace.\n\nNamespaces can be assigned a different active Cluster.\n\x1b[1mtemporal operator namespace update --active-cluster=NewActiveCluster\x1b[0m\n\nNamespaces can also be promoted to global Namespaces.\n\x1b[1mtemporal operator namespace update --promote-global\x1b[0m\n\nAny Archives that were previously enabled or disabled can be changed through this command.\nHowever, URI values for archival states cannot be changed after the states are enabled.\n\x1b[1mtemporal operator namespace update --history-archival-state=enabled --visibility-archival-state=disabled\x1b[0m" + s.Command.Long = "The temporal operator namespace update command updates a Namespace.\n\nNamespaces can be assigned a different active Cluster.\n\x1b[1mtemporal operator namespace update -n namespace --active-cluster=NewActiveCluster\x1b[0m\n\nNamespaces can also be promoted to global Namespaces.\n\x1b[1mtemporal operator namespace update -n namespace --promote-global\x1b[0m\n\nAny Archives that were previously enabled or disabled can be changed through this command.\nHowever, URI values for archival states cannot be changed after the states are enabled.\n\x1b[1mtemporal operator namespace update -n namespace --history-archival-state=enabled --visibility-archival-state=disabled\x1b[0m" } else { - s.Command.Long = "The temporal operator namespace update command updates a Namespace.\n\nNamespaces can be assigned a different active Cluster.\n`temporal operator namespace update --active-cluster=NewActiveCluster`\n\nNamespaces can also be promoted to global Namespaces.\n`temporal operator namespace update --promote-global`\n\nAny Archives that were previously enabled or disabled can be changed through this command.\nHowever, URI values for archival states cannot be changed after the states are enabled.\n`temporal operator namespace update --history-archival-state=enabled --visibility-archival-state=disabled`" + s.Command.Long = "The temporal operator namespace update command updates a Namespace.\n\nNamespaces can be assigned a different active Cluster.\n`temporal operator namespace update -n namespace --active-cluster=NewActiveCluster`\n\nNamespaces can also be promoted to global Namespaces.\n`temporal operator namespace update -n namespace --promote-global`\n\nAny Archives that were previously enabled or disabled can be changed through this command.\nHowever, URI values for archival states cannot be changed after the states are enabled.\n`temporal operator namespace update -n namespace --history-archival-state=enabled --visibility-archival-state=disabled`" } - s.Command.Args = cobra.ExactArgs(1) + s.Command.Args = cobra.MaximumNArgs(1) s.Command.Flags().StringVar(&s.ActiveCluster, "active-cluster", "", "Active cluster name.") s.Command.Flags().StringArrayVar(&s.Cluster, "cluster", nil, "Cluster names.") s.Command.Flags().StringArrayVar(&s.Data, "data", nil, "Namespace data in key=value format. Use JSON for values.") @@ -1544,9 +1556,9 @@ func NewTemporalWorkflowResetCommand(cctx *CommandContext, parent *TemporalWorkf s.Command.Use = "reset [flags]" s.Command.Short = "Resets a Workflow Execution by Event ID or reset type." if hasHighlighting { - s.Command.Long = "The temporal workflow reset command resets a Workflow Execution.\nA reset allows the Workflow to resume from a certain point without losing its parameters or Event History.\n\nThe Workflow Execution can be set to a given Event Type:\n\x1b[1mtemporal workflow reset --workflow-id=meaningful-business-id --type=LastContinuedAsNew\x1b[0m\n\n...or a specific any Event after \x1b[1mWorkflowTaskStarted\x1b[0m.\n\x1b[1mtemporal workflow reset --workflow-id=meaningful-business-id --event-id=MyLastEvent\x1b[0m\nFor batch reset only FirstWorkflowTask, LastWorkflowTask or BuildId can be used. Workflow Id, run Id and event Id \nshould not be set.\nUse the options listed below to change reset behavior." + s.Command.Long = "The temporal workflow reset command resets a Workflow Execution.\nA reset allows the Workflow to resume from a certain point without losing its parameters or Event History.\n\nThe Workflow Execution can be set to a given Event Type:\n\x1b[1mtemporal workflow reset --workflow-id=meaningful-business-id --type=LastContinuedAsNew\x1b[0m\n\n...or a specific any Event after \x1b[1mWorkflowTaskStarted\x1b[0m.\n\x1b[1mtemporal workflow reset --workflow-id=meaningful-business-id --event-id=MyLastEvent\x1b[0m\nFor batch reset only FirstWorkflowTask, LastWorkflowTask or BuildId can be used. Workflow Id, run Id and event Id\nshould not be set.\nUse the options listed below to change reset behavior." } else { - s.Command.Long = "The temporal workflow reset command resets a Workflow Execution.\nA reset allows the Workflow to resume from a certain point without losing its parameters or Event History.\n\nThe Workflow Execution can be set to a given Event Type:\n```\ntemporal workflow reset --workflow-id=meaningful-business-id --type=LastContinuedAsNew\n```\n\n...or a specific any Event after `WorkflowTaskStarted`.\n```\ntemporal workflow reset --workflow-id=meaningful-business-id --event-id=MyLastEvent\n```\nFor batch reset only FirstWorkflowTask, LastWorkflowTask or BuildId can be used. Workflow Id, run Id and event Id \nshould not be set.\nUse the options listed below to change reset behavior." + s.Command.Long = "The temporal workflow reset command resets a Workflow Execution.\nA reset allows the Workflow to resume from a certain point without losing its parameters or Event History.\n\nThe Workflow Execution can be set to a given Event Type:\n```\ntemporal workflow reset --workflow-id=meaningful-business-id --type=LastContinuedAsNew\n```\n\n...or a specific any Event after `WorkflowTaskStarted`.\n```\ntemporal workflow reset --workflow-id=meaningful-business-id --event-id=MyLastEvent\n```\nFor batch reset only FirstWorkflowTask, LastWorkflowTask or BuildId can be used. Workflow Id, run Id and event Id\nshould not be set.\nUse the options listed below to change reset behavior." } s.Command.Args = cobra.NoArgs s.Command.Flags().StringVarP(&s.WorkflowId, "workflow-id", "w", "", "Workflow Id. Required for non-batch reset operations.") @@ -1824,9 +1836,9 @@ func NewTemporalWorkflowUpdateCommand(cctx *CommandContext, parent *TemporalWork s.Command.Use = "update [flags]" s.Command.Short = "Updates a running workflow synchronously." if hasHighlighting { - s.Command.Long = "The \x1b[1mtemporal workflow update\x1b[0m command is used to synchronously Update a \nWorkflowExecution by ID.\n\n\x1b[1mtemporal workflow update \\\n\t\t--workflow-id MyWorkflowId \\\n\t\t--name MyUpdate \\\n\t\t--input '{\"Input\": \"As-JSON\"}'\x1b[0m\n\nUse the options listed below to change the command's behavior." + s.Command.Long = "The \x1b[1mtemporal workflow update\x1b[0m command is used to synchronously Update a\nWorkflowExecution by ID.\n\n\x1b[1mtemporal workflow update \\\n\t\t--workflow-id MyWorkflowId \\\n\t\t--name MyUpdate \\\n\t\t--input '{\"Input\": \"As-JSON\"}'\x1b[0m\n\nUse the options listed below to change the command's behavior." } else { - s.Command.Long = "The `temporal workflow update` command is used to synchronously Update a \nWorkflowExecution by ID.\n\n```\ntemporal workflow update \\\n\t\t--workflow-id MyWorkflowId \\\n\t\t--name MyUpdate \\\n\t\t--input '{\"Input\": \"As-JSON\"}'\n```\n\nUse the options listed below to change the command's behavior." + s.Command.Long = "The `temporal workflow update` command is used to synchronously Update a\nWorkflowExecution by ID.\n\n```\ntemporal workflow update \\\n\t\t--workflow-id MyWorkflowId \\\n\t\t--name MyUpdate \\\n\t\t--input '{\"Input\": \"As-JSON\"}'\n```\n\nUse the options listed below to change the command's behavior." } s.Command.Args = cobra.NoArgs s.PayloadInputOptions.buildFlags(cctx, s.Command.Flags()) diff --git a/temporalcli/commands.operator_namespace.go b/temporalcli/commands.operator_namespace.go index 5c766bf1..5aaef860 100644 --- a/temporalcli/commands.operator_namespace.go +++ b/temporalcli/commands.operator_namespace.go @@ -14,8 +14,25 @@ import ( "google.golang.org/protobuf/types/known/durationpb" ) +func (c *TemporalOperatorCommand) getNSFromFlagOrArg0(cctx *CommandContext, args []string) (string, error) { + if len(args) > 0 && c.Namespace != "default" { + return "", fmt.Errorf("namespace was provided as both an argument (%s) and a flag (-n %s); please specify namespace only with -n", args[0], c.Namespace) + } + + if len(args) > 0 { + cctx.Logger.Warn("Passing the namespace as an argument is now deprecated; please switch to using -n instead.\n") + return args[0], nil + } else { + return c.Namespace, nil + } +} + func (c *TemporalOperatorNamespaceCreateCommand) run(cctx *CommandContext, args []string) error { - nsName := args[0] + nsName, err := c.Parent.Parent.getNSFromFlagOrArg0(cctx, args) + if err != nil { + return err + } + cl, err := c.Parent.Parent.ClientOptions.dialClient(cctx) if err != nil { return err @@ -52,14 +69,18 @@ func (c *TemporalOperatorNamespaceCreateCommand) run(cctx *CommandContext, args VisibilityArchivalUri: c.VisibilityUri, }) if err != nil { - return fmt.Errorf("unable to create namespace: %w", err) + return fmt.Errorf("unable to create namespace %s: %w", nsName, err) } cctx.Printer.Println(color.GreenString("Namespace %s successfully registered.", nsName)) return nil } func (c *TemporalOperatorNamespaceDeleteCommand) run(cctx *CommandContext, args []string) error { - nsName := args[0] + nsName, err := c.Parent.Parent.getNSFromFlagOrArg0(cctx, args) + if err != nil { + return err + } + yes, err := cctx.promptString( color.RedString("Are you sure you want to delete namespace %s? Type namespace name to confirm:", nsName), nsName, @@ -82,7 +103,7 @@ func (c *TemporalOperatorNamespaceDeleteCommand) run(cctx *CommandContext, args Namespace: nsName, }) if err != nil { - return fmt.Errorf("unable to delete namespace: %w", err) + return fmt.Errorf("unable to delete namespace %s: %w", nsName, err) } if cctx.JSONOutput { @@ -94,15 +115,22 @@ func (c *TemporalOperatorNamespaceDeleteCommand) run(cctx *CommandContext, args func (c *TemporalOperatorNamespaceDescribeCommand) run(cctx *CommandContext, args []string) error { nsID := c.NamespaceId - var nsName string - if nsID == "" && len(args) == 0 { - return fmt.Errorf("provide either namespace-id flag or namespace as an argument") + + nsName, err := c.Parent.Parent.getNSFromFlagOrArg0(cctx, args) + if err != nil { + return err } - if nsID != "" && len(args) > 0 { - cctx.Printer.Println(color.YellowString("Both namespace-id flag and namespace are provided. Will use namespace Id to describe namespace")) + + // Bleh, special case: if the nsName is "default", it may not have been + // supplied at all, and nsID should take precedence. This won't catch the + // user explicitly specifying "default" for the name AND a UUID, but we + // can't help that without some way to know why nsName is "default". + if nsName == "default" && nsID != "" { + nsName = "" } - if nsID == "" { - nsName = args[0] + + if (nsID == "" && nsName == "") || (nsID != "" && nsName != "") { + return fmt.Errorf("provide one of --namespace-id= or -n name, but not both") } cl, err := c.Parent.Parent.ClientOptions.dialClient(cctx) @@ -116,7 +144,11 @@ func (c *TemporalOperatorNamespaceDescribeCommand) run(cctx *CommandContext, arg Id: nsID, }) if err != nil { - return fmt.Errorf("unable to describe namespace: %w", err) + nsNameOrID := nsName + if nsNameOrID == "" { + nsNameOrID = nsID + } + return fmt.Errorf("unable to describe namespace %s: %w", nsNameOrID, err) } if cctx.JSONOutput { @@ -163,7 +195,11 @@ func (c *TemporalOperatorNamespaceListCommand) run(cctx *CommandContext, args [] } func (c *TemporalOperatorNamespaceUpdateCommand) run(cctx *CommandContext, args []string) error { - nsName := args[0] + nsName, err := c.Parent.Parent.getNSFromFlagOrArg0(cctx, args) + if err != nil { + return err + } + cl, err := c.Parent.Parent.ClientOptions.dialClient(cctx) if err != nil { return err diff --git a/temporalcli/commands.operator_namespace_test.go b/temporalcli/commands.operator_namespace_test.go index 1f56563e..4a7444fb 100644 --- a/temporalcli/commands.operator_namespace_test.go +++ b/temporalcli/commands.operator_namespace_test.go @@ -39,7 +39,7 @@ func (s *SharedServerSuite) TestOperator_NamespaceCreateListAndDescribe() { "operator", "namespace", "describe", "--address", s.Address(), "--output", "json", - nsName, + "-n", nsName, ) s.NoError(res.Err) var describeResp workflowservice.DescribeNamespaceResponse @@ -69,7 +69,7 @@ func (s *SharedServerSuite) TestNamespaceUpdate() { "--description", "description before", "--email", "email@before", "--retention", "24h", - nsName, + "-n", nsName, ) s.NoError(res.Err) @@ -82,7 +82,7 @@ func (s *SharedServerSuite) TestNamespaceUpdate() { "--data", "k1=v1", "--data", "k2=v2", "--output", "json", - nsName, + "-n", nsName, ) s.NoError(res.Err) @@ -90,7 +90,7 @@ func (s *SharedServerSuite) TestNamespaceUpdate() { "operator", "namespace", "describe", "--address", s.Address(), "--output", "json", - nsName, + "-n", nsName, ) s.NoError(res.Err) var describeResp workflowservice.DescribeNamespaceResponse @@ -109,7 +109,7 @@ func (s *SharedServerSuite) TestNamespaceUpdate_NamespaceDontExist() { "operator", "namespace", "update", "--email", "email@after", "--address", s.Address(), - nsName, + "-n", nsName, ) s.Error(res.Err) s.Contains(res.Err.Error(), "Namespace missing-namespace is not found") @@ -128,7 +128,7 @@ func (s *SharedServerSuite) TestDeleteNamespace() { "operator", "namespace", "describe", "--address", s.Address(), "--output", "json", - nsName, + "-n", nsName, ) s.NoError(res.Err) var describeResp workflowservice.DescribeNamespaceResponse @@ -139,7 +139,7 @@ func (s *SharedServerSuite) TestDeleteNamespace() { "operator", "namespace", "delete", "--address", s.Address(), "--yes", - nsName, + "-n", nsName, ) s.NoError(res.Err) @@ -147,7 +147,7 @@ func (s *SharedServerSuite) TestDeleteNamespace() { "operator", "namespace", "describe", "--address", s.Address(), "--output", "json", - nsName, + "-n", nsName, ) s.Error(res.Err) s.Contains(res.Err.Error(), "Namespace test-namespace is not found") @@ -158,7 +158,7 @@ func (s *SharedServerSuite) TestDescribeWithID() { "operator", "namespace", "describe", "--address", s.Address(), "--output", "json", - "default", + "-n", "default", ) s.NoError(res.Err) @@ -178,3 +178,28 @@ func (s *SharedServerSuite) TestDescribeWithID() { s.NoError(temporalcli.UnmarshalProtoJSONWithOptions(res.Stdout.Bytes(), &describeResp2, true)) s.Equal(describeResp1.NamespaceInfo, describeResp2.NamespaceInfo) } + +func (s *SharedServerSuite) TestDescribeBothNameAndID() { + res := s.Execute( + "operator", "namespace", "describe", + "--address", s.Address(), + "--output", "json", + "-n", "asdf", + "--namespace-id=ad7ef0ce-7139-4333-b8ee-60a79c8fda1d", + ) + s.Error(res.Err) + s.ContainsOnSameLine(res.Err.Error(), "provide one of", "but not both") +} + +func (s *SharedServerSuite) TestUpdateOldAndNewNSArgs() { + res := s.Execute( + "operator", "namespace", "update", + "--address", s.Address(), + "--output", "json", + "--email", "foo@bar", + "-n", "asdf", + "asdf", + ) + s.Error(res.Err) + s.ContainsOnSameLine(res.Err.Error(), "namespace was provided as both an argument", "and a flag") +} diff --git a/temporalcli/commandsmd/commands.md b/temporalcli/commandsmd/commands.md index f72d9e9a..d948225b 100644 --- a/temporalcli/commandsmd/commands.md +++ b/temporalcli/commandsmd/commands.md @@ -49,7 +49,7 @@ This document has a specific structure used by a parser. Here are the rules: #### Options -* `--env` (string) - Environment to read environment-specific flags from. Default: default. Env: TEMPORAL_ENV. +* `--env`, `-E` (string) - Environment to read environment-specific flags from. Default: default. Env: TEMPORAL_ENV. * `--env-file` (string) - File to read all environments (defaults to `$HOME/.config/temporalio/temporal.yaml`). * `--log-level` (string-enum) - Log level. Options: debug, info, warn, error, never. Default: info. * `--log-format` (string-enum) - Log format. Options: text, json. Default: text. @@ -139,61 +139,86 @@ For future reference, provide a reason for terminating the Batch Job. ### temporal env: Manage environments. Use the '--env ' option with other commands to point the CLI at a different Temporal Server instance. If --env -is not passed, the 'default' environment is used. +is not passed, the 'default' environment is used. (You can also use `-E` as a shorthand for `--env`.) -### temporal env delete [environment or property]: Delete an environment or environment property. +### temporal env delete: Delete an environment or environment property. -`temporal env delete [environment or property]` +`temporal env delete -E environment [-k property]` Delete an environment or just a single property: -`temporal env delete prod` -`temporal env delete prod.tls-cert-path` +`temporal env delete -E prod` +`temporal env delete -E prod -k tls-cert-path` + +If the environment is not specified, the `default` environment is deleted: + +`temporal env delete -k tls-cert-path` -### temporal env get [environment or property]: Print environment properties. +#### Options + +* `--key`, `-k` (string) - The name of the property. + +### temporal env get: Print environment properties. -`temporal env get [environment or property]` +`temporal env get -E environment` Print all properties of the 'prod' environment: `temporal env get prod` +``` tls-cert-path /home/my-user/certs/client.cert tls-key-path /home/my-user/certs/client.key address temporal.example.com:7233 namespace someNamespace +``` Print a single property: -`temporal env get prod.tls-key-path` +`temporal env get -E prod -k tls-key-path` +``` tls-key-path /home/my-user/certs/cluster.key +``` + +If the environment is not specified, the `default` environment is used. +#### Options + +* `--key`, `-k` (string) - The name of the property. + ### temporal env list: Print all environments. List all environments. -### temporal env set [environment.property name] [property value]: Set environment properties. +### temporal env set: Set environment properties. -`temporal env set [environment.property name] [property value]` +`temporal env set -E environment -k property -v value` Property names match CLI option names, for example '--address' and '--tls-cert-path': -`temporal env set prod.address 127.0.0.1:7233` -`temporal env set prod.tls-cert-path /home/my-user/certs/cluster.cert` +`temporal env set -E prod -k address -v 127.0.0.1:7233` +`temporal env set -E prod -k tls-cert-path -v /home/my-user/certs/cluster.cert` + +If the environment is not specified, the `default` environment is used. +#### Options + +* `--key`, `-k` (string) - The name of the property. +* `--value`, `-v` (string) - The value to set the property to. + ### temporal operator: Manage a Temporal deployment. Operator commands enable actions on Namespaces, Search Attributes, and Temporal Clusters. These actions are performed through subcommands. @@ -212,7 +237,7 @@ Cluster commands follow this syntax: `temporal operator cluster [command] [comma ### temporal operator cluster describe: Describe a cluster -`temporal operator cluster describe` command shows information about the Cluster. +`temporal operator cluster describe` command shows information about the Cluster. #### Options @@ -244,7 +269,7 @@ Cluster commands follow this syntax: `temporal operator cluster [command] [comma ### temporal operator cluster upsert: Add a remote -`temporal operator cluster upsert` command allows the user to add or update a remote Cluster. +`temporal operator cluster upsert` command allows the user to add or update a remote Cluster. #### Options @@ -257,21 +282,21 @@ Namespace commands perform operations on Namespaces contained in the Temporal Cl Cluster commands follow this syntax: `temporal operator namespace [command] [command options]` -### temporal operator namespace create [namespace]: Registers a new Namespace. +### temporal operator namespace create: Registers a new Namespace. The temporal operator namespace create command creates a new Namespace on the Server. Namespaces can be created on the active Cluster, or any named Cluster. -`temporal operator namespace create --cluster=MyCluster example-1` +`temporal operator namespace create --cluster=MyCluster -n example-1` Global Namespaces can also be created. -`temporal operator namespace create --global example-2` +`temporal operator namespace create --global -n example-2` Other settings, such as retention and Visibility Archival State, can be configured as needed. For example, the Visibility Archive can be set on a separate URI. -`temporal operator namespace create --retention=5 --visibility-archival-state=enabled --visibility-uri=some-uri example-3` +`temporal operator namespace create --retention=5 --visibility-archival-state=enabled --visibility-uri=some-uri -n example-3` #### Options @@ -293,7 +318,7 @@ For example, the Visibility Archive can be set on a separate URI. The temporal operator namespace delete command deletes a given Namespace from the system. #### Options @@ -303,10 +328,10 @@ The temporal operator namespace delete command deletes a given Namespace from th ### temporal operator namespace describe [namespace]: Describe a Namespace by its name or ID. The temporal operator namespace describe command provides Namespace information. -Namespaces are identified by Namespace ID. +Namespaces are identified either by Namespace ID or by name. `temporal operator namespace describe --namespace-id=some-namespace-id` -`temporal operator namespace describe example-namespace-name` +`temporal operator namespace describe -n example-namespace-name` #### Options @@ -693,7 +718,7 @@ temporal workflow reset --workflow-id=meaningful-business-id --type=LastContinue ``` temporal workflow reset --workflow-id=meaningful-business-id --event-id=MyLastEvent ``` -For batch reset only FirstWorkflowTask, LastWorkflowTask or BuildId can be used. Workflow Id, run Id and event Id +For batch reset only FirstWorkflowTask, LastWorkflowTask or BuildId can be used. Workflow Id, run Id and event Id should not be set. Use the options listed below to change reset behavior. @@ -849,7 +874,7 @@ TODO ### temporal workflow update: Updates a running workflow synchronously. -The `temporal workflow update` command is used to synchronously [Update](/concepts/what-is-an-update) a +The `temporal workflow update` command is used to synchronously [Update](/concepts/what-is-an-update) a [WorkflowExecution](/concepts/what-is-a-workflow-execution) by [ID](/concepts/what-is-a-workflow-id). ``` @@ -866,7 +891,7 @@ Use the options listed below to change the command's behavior. * `--name` (string) - Update Name. Required. * `--workflow-id`, `-w` (string) - Workflow Id. Required. * `--run-id`, `-r` (string) - Run Id. If unset, the currently running Workflow Execution receives the Update. -* `--first-execution-run-id` (string) - Send the Update to the last Workflow Execution in the chain that started +* `--first-execution-run-id` (string) - Send the Update to the last Workflow Execution in the chain that started with this Run Id. Includes options set for [payload input](#options-set-for-payload-input).