diff --git a/internal/temporalcli/commands.gen.go b/internal/temporalcli/commands.gen.go index 0165f6e30..7dae4ab5b 100644 --- a/internal/temporalcli/commands.gen.go +++ b/internal/temporalcli/commands.gen.go @@ -2152,12 +2152,12 @@ func NewTemporalOperatorNamespaceUpdateCommand(cctx *CommandContext, parent *Tem s.Command.Use = "update [flags]" s.Command.Short = "Update a Namespace" if hasHighlighting { - s.Command.Long = "Update a Namespace using properties you specify.\n\n\x1b[1mtemporal operator namespace update [options]\x1b[0m\n\nAssign a Namespace's active Cluster (Service):\n\n\x1b[1mtemporal operator namespace update \\\n --namespace YourNamespaceName \\\n --active-cluster NewActiveCluster\x1b[0m\n\nPromote a Namespace for multi-region data replication:\n\n\x1b[1mtemporal operator namespace update \\\n --namespace YourNamespaceName \\\n --promote-global\x1b[0m\n\nYou may update archives that were previously enabled or disabled. Note: URI\nvalues for archival states can't be changed once enabled.\n\n\x1b[1mtemporal operator namespace update \\\n --namespace YourNamespaceName \\\n --history-archival-state enabled \\\n --visibility-archival-state disabled\x1b[0m" + s.Command.Long = "Update a Namespace using properties you specify.\n\n\x1b[1mtemporal operator namespace update [options]\x1b[0m\n\nAssign a Namespace's active Cluster (Service):\n\n\x1b[1mtemporal operator namespace update \\\n --namespace YourNamespaceName \\\n --active-cluster NewActiveCluster\x1b[0m\n\nActive Cluster changes cannot be combined with other Namespace updates.\nUpdate Namespace configuration, including the replication Cluster list,\nbefore changing the active Cluster in a separate command.\n\nPromote a Namespace for multi-region data replication:\n\n\x1b[1mtemporal operator namespace update \\\n --namespace YourNamespaceName \\\n --promote-global\x1b[0m\n\nYou may update archives that were previously enabled or disabled. Note: URI\nvalues for archival states can't be changed once enabled.\n\n\x1b[1mtemporal operator namespace update \\\n --namespace YourNamespaceName \\\n --history-archival-state enabled \\\n --visibility-archival-state disabled\x1b[0m" } else { - s.Command.Long = "Update a Namespace using properties you specify.\n\n```\ntemporal operator namespace update [options]\n```\n\nAssign a Namespace's active Cluster (Service):\n\n```\ntemporal operator namespace update \\\n --namespace YourNamespaceName \\\n --active-cluster NewActiveCluster\n```\n\nPromote a Namespace for multi-region data replication:\n\n```\ntemporal operator namespace update \\\n --namespace YourNamespaceName \\\n --promote-global\n```\n\nYou may update archives that were previously enabled or disabled. Note: URI\nvalues for archival states can't be changed once enabled.\n\n```\ntemporal operator namespace update \\\n --namespace YourNamespaceName \\\n --history-archival-state enabled \\\n --visibility-archival-state disabled\n```" + s.Command.Long = "Update a Namespace using properties you specify.\n\n```\ntemporal operator namespace update [options]\n```\n\nAssign a Namespace's active Cluster (Service):\n\n```\ntemporal operator namespace update \\\n --namespace YourNamespaceName \\\n --active-cluster NewActiveCluster\n```\n\nActive Cluster changes cannot be combined with other Namespace updates.\nUpdate Namespace configuration, including the replication Cluster list,\nbefore changing the active Cluster in a separate command.\n\nPromote a Namespace for multi-region data replication:\n\n```\ntemporal operator namespace update \\\n --namespace YourNamespaceName \\\n --promote-global\n```\n\nYou may update archives that were previously enabled or disabled. Note: URI\nvalues for archival states can't be changed once enabled.\n\n```\ntemporal operator namespace update \\\n --namespace YourNamespaceName \\\n --history-archival-state enabled \\\n --visibility-archival-state disabled\n```" } s.Command.Args = cobra.MaximumNArgs(1) - s.Command.Flags().StringVar(&s.ActiveCluster, "active-cluster", "", "Active Cluster (Service) name.") + s.Command.Flags().StringVar(&s.ActiveCluster, "active-cluster", "", "Active Cluster (Service) name. Cannot be combined with options that modify Namespace configuration.") s.Command.Flags().StringArrayVar(&s.Cluster, "cluster", nil, "Cluster (Service) names.") s.Command.Flags().StringArrayVar(&s.Data, "data", nil, "Namespace data as `KEY=VALUE` pairs. Keys must be identifiers, and values must be JSON values. For example: `YourKey={\"your\": \"value\"}` Can be passed multiple times.") s.Command.Flags().StringVar(&s.Description, "description", "", "Namespace description.") diff --git a/internal/temporalcli/commands.operator_namespace.go b/internal/temporalcli/commands.operator_namespace.go index ca4b6c6d5..caf2bc0d0 100644 --- a/internal/temporalcli/commands.operator_namespace.go +++ b/internal/temporalcli/commands.operator_namespace.go @@ -2,6 +2,7 @@ package temporalcli import ( "fmt" + "strings" "github.com/fatih/color" "github.com/temporalio/cli/internal/printer" @@ -193,6 +194,10 @@ func (c *TemporalOperatorNamespaceListCommand) run(cctx *CommandContext, args [] } func (c *TemporalOperatorNamespaceUpdateCommand) run(cctx *CommandContext, args []string) error { + if err := c.validateFlags(); err != nil { + return err + } + cl, err := dialClient(cctx, &c.Parent.Parent.ClientOptions) if err != nil { return err @@ -206,10 +211,6 @@ func (c *TemporalOperatorNamespaceUpdateCommand) run(cctx *CommandContext, args var updateRequest *workflowservice.UpdateNamespaceRequest - if c.PromoteGlobal && len(c.ActiveCluster) > 0 { - return fmt.Errorf("both --promote-global and --active-cluster flags cannot be set together") - } - if c.PromoteGlobal { cctx.Printer.Printlnf("Will promote local namespace to global namespace for:%s, other flag will be omitted. "+ "If it is already global namespace, this will be no-op.\n", nsName) @@ -218,7 +219,7 @@ func (c *TemporalOperatorNamespaceUpdateCommand) run(cctx *CommandContext, args PromoteNamespace: true, } } else if len(c.ActiveCluster) > 0 { - cctx.Printer.Printlnf("Will set active cluster name to: %s, other flag will be omitted.\n", c.ActiveCluster) + cctx.Printer.Printlnf("Will set active cluster name to: %s.\n", c.ActiveCluster) replicationConfig := &replication.NamespaceReplicationConfig{ ActiveClusterName: c.ActiveCluster, } @@ -304,6 +305,42 @@ func (c *TemporalOperatorNamespaceUpdateCommand) run(cctx *CommandContext, args return nil } +func (c *TemporalOperatorNamespaceUpdateCommand) validateFlags() error { + if len(c.ActiveCluster) == 0 { + return nil + } + if c.PromoteGlobal { + return fmt.Errorf("both --promote-global and --active-cluster flags cannot be set together") + } + + incompatibleFlagNames := [...]string{ + "cluster", + "data", + "description", + "email", + "history-archival-state", + "history-uri", + "replication-state", + "retention", + "visibility-archival-state", + "visibility-uri", + } + var incompatibleFlags []string + for _, flag := range incompatibleFlagNames { + if c.Command.Flags().Changed(flag) { + incompatibleFlags = append(incompatibleFlags, "--"+flag) + } + } + if len(incompatibleFlags) == 0 { + return nil + } + + return fmt.Errorf( + "--active-cluster cannot be combined with %s; update namespace configuration before changing the active cluster", + strings.Join(incompatibleFlags, ", "), + ) +} + func printNamespaceDescriptions(cctx *CommandContext, responses ...*workflowservice.DescribeNamespaceResponse) error { namespaces := make([]map[string]any, len(responses)) for i, resp := range responses { diff --git a/internal/temporalcli/commands.operator_namespace_test.go b/internal/temporalcli/commands.operator_namespace_test.go index 261fe07a0..d3ed50d01 100644 --- a/internal/temporalcli/commands.operator_namespace_test.go +++ b/internal/temporalcli/commands.operator_namespace_test.go @@ -3,13 +3,71 @@ package temporalcli_test import ( "fmt" "os" + "testing" "time" + "github.com/stretchr/testify/require" "github.com/temporalio/cli/internal/temporalcli" "go.temporal.io/api/enums/v1" "go.temporal.io/api/workflowservice/v1" ) +func TestNamespaceUpdate_ActiveClusterRejectsOtherUpdates(t *testing.T) { + tests := []struct { + name string + args []string + wantError string + }{ + {name: "cluster", args: []string{"--cluster", "cluster-a"}, wantError: "--active-cluster cannot be combined with --cluster"}, + {name: "data", args: []string{"--data", "key=value"}, wantError: "--active-cluster cannot be combined with --data"}, + {name: "description", args: []string{"--description", "description"}, wantError: "--active-cluster cannot be combined with --description"}, + {name: "email", args: []string{"--email", "owner@example.com"}, wantError: "--active-cluster cannot be combined with --email"}, + {name: "promote global", args: []string{"--promote-global"}, wantError: "both --promote-global and --active-cluster flags cannot be set together"}, + {name: "history archival state", args: []string{"--history-archival-state", "enabled"}, wantError: "--active-cluster cannot be combined with --history-archival-state"}, + {name: "history URI", args: []string{"--history-uri", "file:///tmp/history"}, wantError: "--active-cluster cannot be combined with --history-uri"}, + {name: "replication state", args: []string{"--replication-state", "normal"}, wantError: "--active-cluster cannot be combined with --replication-state"}, + {name: "retention", args: []string{"--retention", "24h"}, wantError: "--active-cluster cannot be combined with --retention"}, + {name: "visibility archival state", args: []string{"--visibility-archival-state", "enabled"}, wantError: "--active-cluster cannot be combined with --visibility-archival-state"}, + {name: "visibility URI", args: []string{"--visibility-uri", "file:///tmp/visibility"}, wantError: "--active-cluster cannot be combined with --visibility-uri"}, + } + + for _, output := range []string{"text", "json"} { + t.Run(output, func(t *testing.T) { + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + h := NewCommandHarness(t) + args := []string{ + "operator", "namespace", "update", + "--address", "127.0.0.1:1", + "--namespace", "test-namespace", + "--active-cluster", "cluster-b", + "--output", output, + } + res := h.Execute(append(args, test.args...)...) + + require.ErrorContains(t, res.Err, test.wantError) + require.Empty(t, res.Stdout.String()) + }) + } + }) + } +} + +func TestNamespaceUpdate_ActiveClusterReportsAllConflicts(t *testing.T) { + h := NewCommandHarness(t) + res := h.Execute( + "operator", "namespace", "update", + "--address", "127.0.0.1:1", + "--namespace", "test-namespace", + "--active-cluster", "cluster-b", + "--cluster", "cluster-a", + "--description", "description", + ) + + require.ErrorContains(t, res.Err, "--active-cluster cannot be combined with --cluster, --description") + require.Empty(t, res.Stdout.String()) +} + func (s *SharedServerSuite) TestOperator_NamespaceCreateListAndDescribe() { nsName := "test_namespace" res := s.Execute( @@ -107,6 +165,27 @@ func (s *SharedServerSuite) TestNamespaceUpdate() { s.Equal("v3", describeResp.NamespaceInfo.Data["k3"]) } +func (s *SharedServerSuite) TestNamespaceUpdate_ActiveClusterAlone() { + nsName := "test-namespace-update-active-cluster" + res := s.Execute( + "operator", "namespace", "create", + "--address", s.Address(), + "--namespace", nsName, + "--global", + "--active-cluster", "active", + "--cluster", "active", + ) + require.NoError(s.T(), res.Err) + + res = s.Execute( + "operator", "namespace", "update", + "--address", s.Address(), + "--namespace", nsName, + "--active-cluster", "active", + ) + require.NoError(s.T(), res.Err) +} + func (s *SharedServerSuite) TestNamespaceUpdate_NamespaceDontExist() { nsName := "missing-namespace" res := s.Execute( diff --git a/internal/temporalcli/commands.yaml b/internal/temporalcli/commands.yaml index 6d24a8a00..c360525fb 100644 --- a/internal/temporalcli/commands.yaml +++ b/internal/temporalcli/commands.yaml @@ -2694,6 +2694,10 @@ commands: --active-cluster NewActiveCluster ``` + Active Cluster changes cannot be combined with other Namespace updates. + Update Namespace configuration, including the replication Cluster list, + before changing the active Cluster in a separate command. + Promote a Namespace for multi-region data replication: ``` @@ -2715,7 +2719,9 @@ commands: options: - name: active-cluster type: string - description: Active Cluster (Service) name. + description: | + Active Cluster (Service) name. + Cannot be combined with options that modify Namespace configuration. - name: cluster type: string[] description: Cluster (Service) names.