diff --git a/temporalcli/commands.gen.go b/temporalcli/commands.gen.go index ff6c537ac..e45623357 100644 --- a/temporalcli/commands.gen.go +++ b/temporalcli/commands.gen.go @@ -278,6 +278,15 @@ func (v *NexusEndpointConfigOptions) buildFlags(cctx *CommandContext, f *pflag.F f.StringVar(&v.TargetUrl, "target-url", "", "An external Nexus Endpoint that receives forwarded Nexus requests. May be used as an alternative to `--target-namespace` and `--target-task-queue`.") } +type QueryModifiersOptions struct { + RejectCondition StringEnum +} + +func (v *QueryModifiersOptions) buildFlags(cctx *CommandContext, f *pflag.FlagSet) { + v.RejectCondition = NewStringEnum([]string{"not_open", "not_completed_cleanly"}, "") + f.Var(&v.RejectCondition, "reject-condition", "Optional flag for rejecting Queries based on Workflow state. Accepted values: not_open, not_completed_cleanly.") +} + type TemporalCommand struct { Command cobra.Command Env string @@ -342,9 +351,9 @@ func NewTemporalActivityCommand(cctx *CommandContext, parent *TemporalCommand) * s.Command.Use = "activity" s.Command.Short = "Complete, update, pause, unpause, reset or fail an Activity" if hasHighlighting { - s.Command.Long = "Update an Activity's options, manage activity lifecycle or update \nan Activity's state to completed or failed.\n\nUpdating activity state marks an Activity as successfully finished or as\nhaving encountered an error.\n\n\x1b[1mtemporal activity complete \\\n --activity-id=YourActivityId \\\n --workflow-id=YourWorkflowId \\\n --result='{\"YourResultKey\": \"YourResultValue\"}'\x1b[0m" + s.Command.Long = "Update an Activity's options, manage activity lifecycle or update\nan Activity's state to completed or failed.\n\nUpdating activity state marks an Activity as successfully finished or as\nhaving encountered an error.\n\n\x1b[1mtemporal activity complete \\\n --activity-id=YourActivityId \\\n --workflow-id=YourWorkflowId \\\n --result='{\"YourResultKey\": \"YourResultValue\"}'\x1b[0m" } else { - s.Command.Long = "Update an Activity's options, manage activity lifecycle or update \nan Activity's state to completed or failed.\n\nUpdating activity state marks an Activity as successfully finished or as\nhaving encountered an error.\n\n```\ntemporal activity complete \\\n --activity-id=YourActivityId \\\n --workflow-id=YourWorkflowId \\\n --result='{\"YourResultKey\": \"YourResultValue\"}'\n```" + s.Command.Long = "Update an Activity's options, manage activity lifecycle or update\nan Activity's state to completed or failed.\n\nUpdating activity state marks an Activity as successfully finished or as\nhaving encountered an error.\n\n```\ntemporal activity complete \\\n --activity-id=YourActivityId \\\n --workflow-id=YourWorkflowId \\\n --result='{\"YourResultKey\": \"YourResultValue\"}'\n```" } s.Command.Args = cobra.NoArgs s.Command.AddCommand(&NewTemporalActivityCompleteCommand(cctx, &s).Command) @@ -477,15 +486,15 @@ func NewTemporalActivityResetCommand(cctx *CommandContext, parent *TemporalActiv s.Command.Use = "reset [flags]" s.Command.Short = "Reset an Activity" if hasHighlighting { - s.Command.Long = "Resetting an activity resets both the number of attempts and the activity \ntimeout. If activity is paused, it will be un-paused. \n\nIf the \x1b[1mno-wait\x1b[0m flag is provided, the activity will be rescheduled \nimmediately. Even if the activity is currently running.\nIf the \x1b[1mno-wait\x1b[0m flag is not provided, the activity will be scheduled \nafter the current instance completes, if needed. \nIf the 'reset_heartbeats' flag is set, the activity heartbeat timer and \nheartbeats will be reset.\n\nSpecify the Activity and Workflow IDs:\n\n\x1b[1mtemporal activity reset \\\n --activity-id YourActivityId \\\n --workflow-id YourWorkflowId\n --no-wait\n --reset-heartbeats\x1b[0m" + s.Command.Long = "Resetting an activity resets both the number of attempts and the activity\ntimeout. If activity is paused, it will be un-paused.\n\nIf the \x1b[1mno-wait\x1b[0m flag is provided, the activity will be rescheduled\nimmediately. Even if the activity is currently running.\nIf the \x1b[1mno-wait\x1b[0m flag is not provided, the activity will be scheduled\nafter the current instance completes, if needed.\nIf the 'reset_heartbeats' flag is set, the activity heartbeat timer and\nheartbeats will be reset.\n\nSpecify the Activity and Workflow IDs:\n\n\x1b[1mtemporal activity reset \\\n --activity-id YourActivityId \\\n --workflow-id YourWorkflowId\n --no-wait\n --reset-heartbeats\x1b[0m" } else { - s.Command.Long = "Resetting an activity resets both the number of attempts and the activity \ntimeout. If activity is paused, it will be un-paused. \n\nIf the `no-wait` flag is provided, the activity will be rescheduled \nimmediately. Even if the activity is currently running.\nIf the `no-wait` flag is not provided, the activity will be scheduled \nafter the current instance completes, if needed. \nIf the 'reset_heartbeats' flag is set, the activity heartbeat timer and \nheartbeats will be reset.\n\nSpecify the Activity and Workflow IDs:\n\n```\ntemporal activity reset \\\n --activity-id YourActivityId \\\n --workflow-id YourWorkflowId\n --no-wait\n --reset-heartbeats\n```" + s.Command.Long = "Resetting an activity resets both the number of attempts and the activity\ntimeout. If activity is paused, it will be un-paused.\n\nIf the `no-wait` flag is provided, the activity will be rescheduled\nimmediately. Even if the activity is currently running.\nIf the `no-wait` flag is not provided, the activity will be scheduled\nafter the current instance completes, if needed.\nIf the 'reset_heartbeats' flag is set, the activity heartbeat timer and\nheartbeats will be reset.\n\nSpecify the Activity and Workflow IDs:\n\n```\ntemporal activity reset \\\n --activity-id YourActivityId \\\n --workflow-id YourWorkflowId\n --no-wait\n --reset-heartbeats\n```" } s.Command.Args = cobra.NoArgs s.Command.Flags().StringVarP(&s.ActivityId, "activity-id", "a", "", "Activity ID to pause. Required.") _ = cobra.MarkFlagRequired(s.Command.Flags(), "activity-id") s.Command.Flags().StringVar(&s.Identity, "identity", "", "Identity of the user submitting this request.") - s.Command.Flags().BoolVar(&s.NoWait, "no-wait", false, "Schedule the Activity immediately, even if its retry timeout has not expired or the activity is currently running.") + s.Command.Flags().BoolVar(&s.NoWait, "no-wait", false, "Schedule the Activity immediately, even if its retry timeout has not expired or the activity is currently running.") s.Command.Flags().BoolVar(&s.ResetHeartbeats, "reset-heartbeats", false, "Reset the Activity's heartbeat.") s.WorkflowReferenceOptions.buildFlags(cctx, s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { @@ -558,28 +567,28 @@ func NewTemporalActivityUpdateOptionsCommand(cctx *CommandContext, parent *Tempo s.Command.Use = "update-options [flags]" s.Command.Short = "Update Activity options" if hasHighlighting { - s.Command.Long = "Update Activity options. Specify the Activity and Workflow IDs, and \noptions you want to update. \nUpdates are incremental, only changing the specified options. \n\n\x1b[1mtemporal activity update-options \\\n --activity-id YourActivityId \\\n --workflow-id YourWorkflowId \\\n --task-queue NewTaskQueueName \\\n --schedule-to-close-timeout DURATION \\\n --schedule-to-start-timeout DURATION \\\n --start-to-close-timeout DURATION \\\n --heartbeat-timeout DURATION \\\n --retry-initial-interval DURATION \\\n --retry-maximum-interval DURATION \\\n --retry-backoff-coefficient NewBackoffCoefficient \\\n --retry-maximum-attempts NewMaximumAttempts\x1b[0m" + s.Command.Long = "Update Activity options. Specify the Activity and Workflow IDs, and\noptions you want to update.\nUpdates are incremental, only changing the specified options.\n\n\x1b[1mtemporal activity update-options \\\n --activity-id YourActivityId \\\n --workflow-id YourWorkflowId \\\n --task-queue NewTaskQueueName \\\n --schedule-to-close-timeout DURATION \\\n --schedule-to-start-timeout DURATION \\\n --start-to-close-timeout DURATION \\\n --heartbeat-timeout DURATION \\\n --retry-initial-interval DURATION \\\n --retry-maximum-interval DURATION \\\n --retry-backoff-coefficient NewBackoffCoefficient \\\n --retry-maximum-attempts NewMaximumAttempts\x1b[0m" } else { - s.Command.Long = "Update Activity options. Specify the Activity and Workflow IDs, and \noptions you want to update. \nUpdates are incremental, only changing the specified options. \n\n```\ntemporal activity update-options \\\n --activity-id YourActivityId \\\n --workflow-id YourWorkflowId \\\n --task-queue NewTaskQueueName \\\n --schedule-to-close-timeout DURATION \\\n --schedule-to-start-timeout DURATION \\\n --start-to-close-timeout DURATION \\\n --heartbeat-timeout DURATION \\\n --retry-initial-interval DURATION \\\n --retry-maximum-interval DURATION \\\n --retry-backoff-coefficient NewBackoffCoefficient \\\n --retry-maximum-attempts NewMaximumAttempts\n\n```" + s.Command.Long = "Update Activity options. Specify the Activity and Workflow IDs, and\noptions you want to update.\nUpdates are incremental, only changing the specified options.\n\n```\ntemporal activity update-options \\\n --activity-id YourActivityId \\\n --workflow-id YourWorkflowId \\\n --task-queue NewTaskQueueName \\\n --schedule-to-close-timeout DURATION \\\n --schedule-to-start-timeout DURATION \\\n --start-to-close-timeout DURATION \\\n --heartbeat-timeout DURATION \\\n --retry-initial-interval DURATION \\\n --retry-maximum-interval DURATION \\\n --retry-backoff-coefficient NewBackoffCoefficient \\\n --retry-maximum-attempts NewMaximumAttempts\n\n```" } s.Command.Args = cobra.NoArgs s.Command.Flags().StringVar(&s.ActivityId, "activity-id", "", "Activity ID. Required.") _ = cobra.MarkFlagRequired(s.Command.Flags(), "activity-id") s.Command.Flags().StringVar(&s.TaskQueue, "task-queue", "", "Name of the task queue for the Activity.") s.ScheduleToCloseTimeout = 0 - s.Command.Flags().Var(&s.ScheduleToCloseTimeout, "schedule-to-close-timeout", "Indicates how long the caller is willing to wait for an activity completion. Limits how long retries will be attempted.") + s.Command.Flags().Var(&s.ScheduleToCloseTimeout, "schedule-to-close-timeout", "Indicates how long the caller is willing to wait for an activity completion. Limits how long retries will be attempted.") s.ScheduleToStartTimeout = 0 - s.Command.Flags().Var(&s.ScheduleToStartTimeout, "schedule-to-start-timeout", "Limits time an activity task can stay in a task queue before a worker picks it up. This timeout is always non retryable, as all a retry would achieve is to put it back into the same queue. Defaults to the schedule-to-close timeout or workflow execution timeout if not specified.") + s.Command.Flags().Var(&s.ScheduleToStartTimeout, "schedule-to-start-timeout", "Limits time an activity task can stay in a task queue before a worker picks it up. This timeout is always non retryable, as all a retry would achieve is to put it back into the same queue. Defaults to the schedule-to-close timeout or workflow execution timeout if not specified.") s.StartToCloseTimeout = 0 - s.Command.Flags().Var(&s.StartToCloseTimeout, "start-to-close-timeout", "Maximum time an activity is allowed to execute after being picked up by a worker. This timeout is always retryable.") + s.Command.Flags().Var(&s.StartToCloseTimeout, "start-to-close-timeout", "Maximum time an activity is allowed to execute after being picked up by a worker. This timeout is always retryable.") s.HeartbeatTimeout = 0 s.Command.Flags().Var(&s.HeartbeatTimeout, "heartbeat-timeout", "Maximum permitted time between successful worker heartbeats.") s.RetryInitialInterval = 0 s.Command.Flags().Var(&s.RetryInitialInterval, "retry-initial-interval", "Interval of the first retry. If retryBackoffCoefficient is 1.0 then it is used for all retries.") s.RetryMaximumInterval = 0 - s.Command.Flags().Var(&s.RetryMaximumInterval, "retry-maximum-interval", "Maximum interval between retries. Exponential backoff leads to interval increase. This value is the cap of the increase.") + s.Command.Flags().Var(&s.RetryMaximumInterval, "retry-maximum-interval", "Maximum interval between retries. Exponential backoff leads to interval increase. This value is the cap of the increase.") s.Command.Flags().Float32Var(&s.RetryBackoffCoefficient, "retry-backoff-coefficient", 0, "Coefficient used to calculate the next retry interval. The next retry interval is previous interval multiplied by the backoff coefficient. Must be 1 or larger.") - s.Command.Flags().IntVar(&s.RetryMaximumAttempts, "retry-maximum-attempts", 0, "Maximum number of attempts. When exceeded the retries stop even if not expired yet. Setting this value to 1 disables retries. Setting this value to 0 means unlimited attempts(up to the timeouts).") + s.Command.Flags().IntVar(&s.RetryMaximumAttempts, "retry-maximum-attempts", 0, "Maximum number of attempts. When exceeded the retries stop even if not expired yet. Setting this value to 1 disables retries. Setting this value to 0 means unlimited attempts(up to the timeouts).") s.Command.Flags().StringVar(&s.Identity, "identity", "", "Identity of the user submitting this request.") s.WorkflowReferenceOptions.buildFlags(cctx, s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { @@ -2687,6 +2696,7 @@ func NewTemporalWorkflowCommand(cctx *CommandContext, parent *TemporalCommand) * s.Command.AddCommand(&NewTemporalWorkflowExecuteCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalWorkflowFixHistoryJsonCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalWorkflowListCommand(cctx, &s).Command) + s.Command.AddCommand(&NewTemporalWorkflowMetadataCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalWorkflowQueryCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalWorkflowResetCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalWorkflowResultCommand(cctx, &s).Command) @@ -2911,13 +2921,42 @@ func NewTemporalWorkflowListCommand(cctx *CommandContext, parent *TemporalWorkfl return &s } +type TemporalWorkflowMetadataCommand struct { + Parent *TemporalWorkflowCommand + Command cobra.Command + WorkflowReferenceOptions + QueryModifiersOptions +} + +func NewTemporalWorkflowMetadataCommand(cctx *CommandContext, parent *TemporalWorkflowCommand) *TemporalWorkflowMetadataCommand { + var s TemporalWorkflowMetadataCommand + s.Parent = parent + s.Command.DisableFlagsInUseLine = true + s.Command.Use = "metadata [flags]" + s.Command.Short = "Query the Workflow for user-specified metadata" + if hasHighlighting { + s.Command.Long = "Issue a Query for and display user-set metadata like summary and\ndetails for a specific Workflow Execution:\n\n\x1b[1mtemporal workflow metadata \\\n --workflow-id YourWorkflowId\x1b[0m" + } else { + s.Command.Long = "Issue a Query for and display user-set metadata like summary and\ndetails for a specific Workflow Execution:\n\n```\ntemporal workflow metadata \\\n --workflow-id YourWorkflowId\n```" + } + s.Command.Args = cobra.NoArgs + s.WorkflowReferenceOptions.buildFlags(cctx, s.Command.Flags()) + s.QueryModifiersOptions.buildFlags(cctx, s.Command.Flags()) + s.Command.Run = func(c *cobra.Command, args []string) { + if err := s.run(cctx, args); err != nil { + cctx.Options.Fail(err) + } + } + return &s +} + type TemporalWorkflowQueryCommand struct { Parent *TemporalWorkflowCommand Command cobra.Command PayloadInputOptions WorkflowReferenceOptions - Name string - RejectCondition StringEnum + QueryModifiersOptions + Name string } func NewTemporalWorkflowQueryCommand(cctx *CommandContext, parent *TemporalWorkflowCommand) *TemporalWorkflowQueryCommand { @@ -2934,10 +2973,9 @@ func NewTemporalWorkflowQueryCommand(cctx *CommandContext, parent *TemporalWorkf s.Command.Args = cobra.NoArgs s.Command.Flags().StringVar(&s.Name, "name", "", "Query Type/Name. Required. Aliased as \"--type\".") _ = cobra.MarkFlagRequired(s.Command.Flags(), "name") - s.RejectCondition = NewStringEnum([]string{"not_open", "not_completed_cleanly"}, "") - s.Command.Flags().Var(&s.RejectCondition, "reject-condition", "Optional flag for rejecting Queries based on Workflow state. Accepted values: not_open, not_completed_cleanly.") s.PayloadInputOptions.buildFlags(cctx, s.Command.Flags()) s.WorkflowReferenceOptions.buildFlags(cctx, s.Command.Flags()) + s.QueryModifiersOptions.buildFlags(cctx, s.Command.Flags()) s.Command.Flags().SetNormalizeFunc(aliasNormalizer(map[string]string{ "type": "name", })) diff --git a/temporalcli/commands.workflow.go b/temporalcli/commands.workflow.go index 481f881f5..421fa354b 100644 --- a/temporalcli/commands.workflow.go +++ b/temporalcli/commands.workflow.go @@ -18,6 +18,7 @@ import ( deploymentpb "go.temporal.io/api/deployment/v1" "go.temporal.io/api/enums/v1" "go.temporal.io/api/query/v1" + sdkpb "go.temporal.io/api/sdk/v1" "go.temporal.io/api/update/v1" workflowpb "go.temporal.io/api/workflow/v1" "go.temporal.io/api/workflowservice/v1" @@ -26,6 +27,8 @@ import ( "google.golang.org/protobuf/types/known/fieldmaskpb" ) +const metadataQueryName = "__temporal_workflow_metadata" + func (c *TemporalWorkflowCancelCommand) run(cctx *CommandContext, args []string) error { cl, err := c.Parent.ClientOptions.dialClient(cctx) if err != nil { @@ -203,6 +206,11 @@ func (c *TemporalWorkflowUpdateOptionsCommand) run(cctx *CommandContext, args [] return nil } +func (c *TemporalWorkflowMetadataCommand) run(cctx *CommandContext, _ []string) error { + return queryHelper(cctx, c.Parent, PayloadInputOptions{}, + metadataQueryName, c.RejectCondition, c.WorkflowReferenceOptions) +} + func (c *TemporalWorkflowQueryCommand) run(cctx *CommandContext, args []string) error { return queryHelper(cctx, c.Parent, c.PayloadInputOptions, c.Name, c.RejectCondition, c.WorkflowReferenceOptions) @@ -637,14 +645,63 @@ func queryHelper(cctx *CommandContext, return cctx.Printer.PrintStructured(result, printer.StructuredOptions{}) } - cctx.Printer.Println(color.MagentaString("Query result:")) - output := struct { - QueryResult json.RawMessage `cli:",cardOmitEmpty"` - }{} - output.QueryResult, err = cctx.MarshalFriendlyJSONPayloads(result.QueryResult) - if err != nil { - return fmt.Errorf("failed to marshal query result: %w", err) - } + if queryType == metadataQueryName { + var metadata sdkpb.WorkflowMetadata + err := UnmarshalProtoJSONWithOptions(result.QueryResult.Payloads[0].Data, &metadata, true) + if err != nil { + return fmt.Errorf("failed to unmarshal metadata: %w", err) + } + cctx.Printer.Println(color.MagentaString("Metadata:")) + + qDefs := metadata.GetDefinition().GetQueryDefinitions() + if len(qDefs) > 0 { + cctx.Printer.Println(printer.NonJSONIndent, color.MagentaString("Query Definitions:")) + err := cctx.Printer.PrintStructured(qDefs, printer.StructuredOptions{ + Table: &printer.TableOptions{NoHeader: true}, + NonJSONExtraIndent: 1, + }) + if err != nil { + return err + } + } + sigDefs := metadata.GetDefinition().GetSignalDefinitions() + if len(sigDefs) > 0 { + cctx.Printer.Println(printer.NonJSONIndent, color.MagentaString("Signal Definitions:")) + err := cctx.Printer.PrintStructured(sigDefs, printer.StructuredOptions{ + Table: &printer.TableOptions{NoHeader: true}, + NonJSONExtraIndent: 1, + }) + if err != nil { + return err + } + } + updDefs := metadata.GetDefinition().GetUpdateDefinitions() + if len(updDefs) > 0 { + cctx.Printer.Println(printer.NonJSONIndent, color.MagentaString("Update Definitions:")) + err := cctx.Printer.PrintStructured(updDefs, printer.StructuredOptions{ + Table: &printer.TableOptions{NoHeader: true}, + NonJSONExtraIndent: 1, + }) + if err != nil { + return err + } + } + if metadata.GetCurrentDetails() != "" { + cctx.Printer.Println(printer.NonJSONIndent, color.MagentaString("Current Details:")) + cctx.Printer.Println(printer.NonJSONIndent, printer.NonJSONIndent, + metadata.GetCurrentDetails()) + } + return nil + } else { + cctx.Printer.Println(color.MagentaString("Query result:")) + output := struct { + QueryResult json.RawMessage `cli:",cardOmitEmpty"` + }{} + output.QueryResult, err = cctx.MarshalFriendlyJSONPayloads(result.QueryResult) + if err != nil { + return fmt.Errorf("failed to marshal query result: %w", err) + } - return cctx.Printer.PrintStructured(output, printer.StructuredOptions{}) + return cctx.Printer.PrintStructured(output, printer.StructuredOptions{}) + } } diff --git a/temporalcli/commands.workflow_test.go b/temporalcli/commands.workflow_test.go index af873bb71..81ae0721d 100644 --- a/temporalcli/commands.workflow_test.go +++ b/temporalcli/commands.workflow_test.go @@ -1148,3 +1148,96 @@ func (s *SharedServerSuite) testStackWorkflow(json bool) { s.Error(res.Err) s.Contains(res.Err.Error(), "query was rejected, workflow has status: Completed") } + +func (s *SharedServerSuite) TestWorkflow_MetadataJSON() { + s.testWorkflowMetadata(true) +} + +func (s *SharedServerSuite) TestWorkflow_Metadata() { + s.testWorkflowMetadata(false) +} + +func (s *SharedServerSuite) testWorkflowMetadata(json bool) { + // Make workflow wait for signal and then return it + s.Worker().OnDevWorkflow(func(ctx workflow.Context, a any) (any, error) { + done := false + workflow.Go(ctx, func(ctx workflow.Context) { + _ = workflow.Await(ctx, func() bool { + return done + }) + }) + workflow.SetQueryHandlerWithOptions(ctx, "my-query", func(arg string) (string, error) { + return "hi", nil + }, workflow.QueryHandlerOptions{Description: "q-desc"}) + workflow.SetUpdateHandlerWithOptions(ctx, "my-update", + func(ctx workflow.Context, arg string) (string, error) { return "hi", nil }, + workflow.UpdateHandlerOptions{Description: "upd-desc"}) + workflow.SetCurrentDetails(ctx, "current-deets") + workflow.GetSignalChannelWithOptions(ctx, "my-signal", + workflow.SignalChannelOptions{Description: "sig-desc"}).Receive(ctx, nil) + done = true + return nil, nil + + }) + + // Start the workflow + run, err := s.Client.ExecuteWorkflow( + s.Context, + client.StartWorkflowOptions{ + TaskQueue: s.Worker().Options.TaskQueue, + StaticSummary: "summie", + StaticDetails: "deets", + }, + DevWorkflow, + "ignored", + ) + s.NoError(err) + + args := []string{ + "workflow", "metadata", + "--address", s.Address(), + "-w", run.GetID(), + } + if json { + args = append(args, "-o", "json") + } + + res := s.Execute(args...) + s.NoError(res.Err) + if !json { + s.Contains(res.Stdout.String(), "Query Definitions:") + s.ContainsOnSameLine(res.Stdout.String(), "my-query", "q-desc") + s.Contains(res.Stdout.String(), "Signal Definitions:") + s.ContainsOnSameLine(res.Stdout.String(), "my-signal", "sig-desc") + s.Contains(res.Stdout.String(), "Update Definitions:") + s.ContainsOnSameLine(res.Stdout.String(), "my-update", "upd-desc") + s.Contains(res.Stdout.String(), "Current Details:") + s.Contains(res.Stdout.String(), "current-deets") + } else { + s.Contains(res.Stdout.String(), "queryDefinitions") + s.ContainsOnSameLine(res.Stdout.String(), "name", "my-query") + s.Contains(res.Stdout.String(), "signalDefinitions") + s.ContainsOnSameLine(res.Stdout.String(), "name", "my-signal") + s.Contains(res.Stdout.String(), "updateDefinitions") + s.ContainsOnSameLine(res.Stdout.String(), "name", "my-update") + s.ContainsOnSameLine(res.Stdout.String(), "currentDetails", "current-deets") + } + + // Unblock the workflow with a signal + s.NoError(s.Client.SignalWorkflow(s.Context, run.GetID(), "", "my-signal", nil)) + s.NoError(run.Get(s.Context, nil)) + + // Ensure query is rejected when using not open rejection condition + args = []string{ + "workflow", "metadata", + "--address", s.Address(), + "-w", run.GetID(), + "--reject-condition", "not_open", + } + if json { + args = append(args, "-o", "json") + } + res = s.Execute(args...) + s.Error(res.Err) + s.Contains(res.Err.Error(), "query was rejected, workflow has status: Completed") +} diff --git a/temporalcli/commandsgen/commands.yml b/temporalcli/commandsgen/commands.yml index 8046d7596..3df25670b 100644 --- a/temporalcli/commandsgen/commands.yml +++ b/temporalcli/commandsgen/commands.yml @@ -206,7 +206,7 @@ commands: - name: temporal activity summary: Complete, update, pause, unpause, reset or fail an Activity description: | - Update an Activity's options, manage activity lifecycle or update + Update an Activity's options, manage activity lifecycle or update an Activity's state to completed or failed. Updating activity state marks an Activity as successfully finished or as @@ -297,9 +297,9 @@ commands: - name: temporal activity update-options summary: Update Activity options description: | - Update Activity options. Specify the Activity and Workflow IDs, and - options you want to update. - Updates are incremental, only changing the specified options. + Update Activity options. Specify the Activity and Workflow IDs, and + options you want to update. + Updates are incremental, only changing the specified options. ``` temporal activity update-options \ @@ -327,22 +327,22 @@ commands: - name: schedule-to-close-timeout type: duration description: | - Indicates how long the caller is willing to wait for an activity - completion. + Indicates how long the caller is willing to wait for an activity + completion. Limits how long retries will be attempted. - name: schedule-to-start-timeout type: duration description: | - Limits time an activity task can stay in a task queue before a worker - picks it up. - This timeout is always non retryable, as all a retry would achieve is + Limits time an activity task can stay in a task queue before a worker + picks it up. + This timeout is always non retryable, as all a retry would achieve is to put it back into the same queue. Defaults to the schedule-to-close timeout or workflow execution timeout if not specified. - name: start-to-close-timeout type: duration description: | - Maximum time an activity is allowed to execute after being picked up + Maximum time an activity is allowed to execute after being picked up by a worker. This timeout is always retryable. - name: heartbeat-timeout type: duration @@ -356,7 +356,7 @@ commands: - name: retry-maximum-interval type: duration description: | - Maximum interval between retries. Exponential backoff leads to + Maximum interval between retries. Exponential backoff leads to interval increase. This value is the cap of the increase. - name: retry-backoff-coefficient @@ -371,7 +371,7 @@ commands: description: | Maximum number of attempts. When exceeded the retries stop even if not expired yet. - Setting this value to 1 disables retries. Setting this value to 0 + Setting this value to 1 disables retries. Setting this value to 0 means unlimited attempts(up to the timeouts). - name: identity type: string @@ -459,14 +459,14 @@ commands: - name: temporal activity reset summary: Reset an Activity description: | - Resetting an activity resets both the number of attempts and the activity - timeout. If activity is paused, it will be un-paused. + Resetting an activity resets both the number of attempts and the activity + timeout. If activity is paused, it will be un-paused. - If the `no-wait` flag is provided, the activity will be rescheduled + If the `no-wait` flag is provided, the activity will be rescheduled immediately. Even if the activity is currently running. - If the `no-wait` flag is not provided, the activity will be scheduled - after the current instance completes, if needed. - If the 'reset_heartbeats' flag is set, the activity heartbeat timer and + If the `no-wait` flag is not provided, the activity will be scheduled + after the current instance completes, if needed. + If the 'reset_heartbeats' flag is set, the activity heartbeat timer and heartbeats will be reset. Specify the Activity and Workflow IDs: @@ -490,7 +490,7 @@ commands: - name: no-wait type: bool description: | - Schedule the Activity immediately, even if its retry timeout has not + Schedule the Activity immediately, even if its retry timeout has not expired or the activity is currently running. - name: reset-heartbeats type: bool @@ -623,8 +623,8 @@ commands: - client docs: description-header: >- - Learn how to read or modify state associated with a Worker, - such as Worker Deployments. + Learn how to read or modify state associated with a Worker, + such as Worker Deployments. keywords: - worker - worker deployment @@ -2698,7 +2698,7 @@ commands: - workflow execute - workflow execution - workflow list - - workflow update-options + - workflow metadata - workflow query - workflow reset - workflow reset-batch @@ -2708,6 +2708,7 @@ commands: - workflow start - workflow terminate - workflow trace + - workflow update-options - name: temporal workflow cancel summary: Send cancellation to Workflow Execution @@ -2886,6 +2887,20 @@ commands: type: int description: Maximum number of Workflow Executions to display. + - name: temporal workflow metadata + summary: Query the Workflow for user-specified metadata + description: | + Issue a Query for and display user-set metadata like summary and + details for a specific Workflow Execution: + + ``` + temporal workflow metadata \ + --workflow-id YourWorkflowId + ``` + option-sets: + - workflow-reference + - query-modifiers + - name: temporal workflow update-options summary: Change Workflow Execution Options description: | @@ -2970,6 +2985,7 @@ commands: option-sets: - payload-input - workflow-reference + - query-modifiers options: - name: name type: string @@ -2977,13 +2993,6 @@ commands: required: true aliases: - type - - name: reject-condition - type: string-enum - description: | - Optional flag for rejecting Queries based on Workflow state. - enum-values: - - not_open - - not_completed_cleanly - name: temporal workflow reset summary: Move Workflow Execution history point @@ -3800,3 +3809,13 @@ option-sets: An external Nexus Endpoint that receives forwarded Nexus requests. May be used as an alternative to `--target-namespace` and `--target-task-queue`. + + - name: query-modifiers + options: + - name: reject-condition + type: string-enum + description: | + Optional flag for rejecting Queries based on Workflow state. + enum-values: + - not_open + - not_completed_cleanly diff --git a/temporalcli/internal/printer/printer.go b/temporalcli/internal/printer/printer.go index 6c3fdac11..d20b806f7 100644 --- a/temporalcli/internal/printer/printer.go +++ b/temporalcli/internal/printer/printer.go @@ -17,6 +17,8 @@ import ( "google.golang.org/protobuf/proto" ) +const NonJSONIndent = " " + type Colorer func(string, ...interface{}) string type Printer struct { @@ -99,6 +101,8 @@ type StructuredOptions struct { // printing. Table *TableOptions OverrideJSONPayloadShorthand *bool + // Indent this many additional times when printing non-JSON + NonJSONExtraIndent int } type Align int @@ -260,6 +264,7 @@ type col struct { width int cardOmitEmpty bool align Align + indentAmount int } type colVal struct { @@ -313,6 +318,7 @@ func adjustColsToOptions(cols []*col, options StructuredOptions) []*col { col.align = align } } + col.indentAmount = options.NonJSONExtraIndent + 1 adjusted = append(adjusted, col) } return adjusted @@ -331,8 +337,9 @@ func (p *Printer) printHeader(cols []*col) { colorer = color.MagentaString } for _, col := range cols { - // We want to indent even the first field - p.writeStr(" ") + for i := 0; i < col.indentAmount; i++ { + p.writeStr(NonJSONIndent) + } p.writeStr(tablewriter.Pad(colorer("%v", col.name), " ", col.width)) } p.writeStr("\n") @@ -346,8 +353,9 @@ func (p *Printer) printRows(cols []*col, rows []map[string]colVal) { func (p *Printer) printRow(cols []*col, row map[string]colVal) { for _, col := range cols { - // We want to indent even the first field - p.writeStr(" ") + for i := 0; i < col.indentAmount; i++ { + p.writeStr(NonJSONIndent) + } p.printCol(col, row[col.name].text) } p.writeStr("\n") @@ -377,6 +385,12 @@ func (p *Printer) printCards(cols []*col, rows []map[string]colVal) { func (p *Printer) printCard(cols []*col, row map[string]colVal) { nameValueRows := make([]map[string]colVal, 0, len(cols)) + indentAmount := 1 + // Since this option applies to everything in a structured print, there should be + // no difference among columns + if len(cols) > 0 { + indentAmount = cols[0].indentAmount + } for _, col := range cols { rowVal := row[col.name].val if !col.cardOmitEmpty || (rowVal != nil && !reflect.ValueOf(row[col.name].val).IsZero()) { @@ -387,10 +401,10 @@ func (p *Printer) printCard(cols []*col, row map[string]colVal) { } } nameValueCols := []*col{ - {name: "Name"}, + {name: "Name", indentAmount: indentAmount}, // We want to set the width to 1 here, because we want it to stretch as far // as it needs to the right - {name: "Value", width: 1}, + {name: "Value", width: 1, indentAmount: indentAmount}, } p.calculateUnsetColWidths(nameValueCols, nameValueRows) p.printRows(nameValueCols, nameValueRows)