From 1375015fe02c302f4571cd7e22b905ae5a1eda15 Mon Sep 17 00:00:00 2001 From: daehan Date: Mon, 31 Aug 2026 18:20:39 -0400 Subject: [PATCH 1/4] implement namespace description --- go.mod | 2 +- go.sum | 4 +- temporalcloudcli/commands.gen.go | 85 ++++++ .../commands.namespace.create_test.go | 2 + .../commands.namespace.description.go | 66 +++++ .../commands.namespace.description_test.go | 253 ++++++++++++++++++ temporalcloudcli/commands.namespace.go | 5 +- temporalcloudcli/commands.yml | 57 ++++ 8 files changed, 470 insertions(+), 4 deletions(-) create mode 100644 temporalcloudcli/commands.namespace.description.go create mode 100644 temporalcloudcli/commands.namespace.description_test.go diff --git a/go.mod b/go.mod index bd0ba80..4c663dc 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/temporalio/cli/cliext v0.0.0-20260602200703-8bb57b77ad55 go.temporal.io/api v1.63.6-0.20260811224032-89570b10e9e2 - go.temporal.io/cloud-sdk v0.16.0 + go.temporal.io/cloud-sdk v0.16.1-0.20260826195225-e4c5e7f3aa55 go.temporal.io/sdk v1.44.1 go.temporal.io/sdk/contrib/envconfig v1.0.0 golang.org/x/oauth2 v0.36.0 diff --git a/go.sum b/go.sum index 717472b..8b3b588 100644 --- a/go.sum +++ b/go.sum @@ -154,8 +154,8 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09 go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.temporal.io/api v1.63.6-0.20260811224032-89570b10e9e2 h1:VeNTxlOHETym01zX6kSXHIVw4nc2RrX/KVgepQ/dr/8= go.temporal.io/api v1.63.6-0.20260811224032-89570b10e9e2/go.mod h1:SrlW2JMwVlDP4nRWSNznUFqnSHd+YeMDS1BkYo63HCQ= -go.temporal.io/cloud-sdk v0.16.0 h1:8EgFMyc3M1XV2+OIOOuKeyhUI8Kjy7YCCbmGYlRw1No= -go.temporal.io/cloud-sdk v0.16.0/go.mod h1:W2O9t9tvo3Q/LhGgYdj8JijWbN5C84os+cz/BadIHYI= +go.temporal.io/cloud-sdk v0.16.1-0.20260826195225-e4c5e7f3aa55 h1:XVb15Q93gtvLnF7u4y3EHEZLfeVIHf5cIxwGmnpDTrQ= +go.temporal.io/cloud-sdk v0.16.1-0.20260826195225-e4c5e7f3aa55/go.mod h1:W2O9t9tvo3Q/LhGgYdj8JijWbN5C84os+cz/BadIHYI= go.temporal.io/sdk v1.44.1 h1:Mt2OZLZpqkzDIdg9YyQzO0Rb/HqCDnnqHlIAGAJ5gqM= go.temporal.io/sdk v1.44.1/go.mod h1:vkApR12F9/Y8OR+hkxe7WyXQFuCX6clhzqnAk6rzDAM= go.temporal.io/sdk/contrib/envconfig v1.0.0 h1:1Q/swVgB4EW/p3k7rI9/4hpU4/DC57FSRbU90+UisXw= diff --git a/temporalcloudcli/commands.gen.go b/temporalcloudcli/commands.gen.go index 4dca4f7..e98d80a 100644 --- a/temporalcloudcli/commands.gen.go +++ b/temporalcloudcli/commands.gen.go @@ -1888,6 +1888,7 @@ func NewCloudNamespaceCommand(cctx *CommandContext, parent *CloudCommand) *Cloud s.Command.AddCommand(&NewCloudNamespaceConnectivityCommand(cctx, &s).Command) s.Command.AddCommand(&NewCloudNamespaceCreateCommand(cctx, &s).Command) s.Command.AddCommand(&NewCloudNamespaceDeleteCommand(cctx, &s).Command) + s.Command.AddCommand(&NewCloudNamespaceDescriptionCommand(cctx, &s).Command) s.Command.AddCommand(&NewCloudNamespaceEditCommand(cctx, &s).Command) s.Command.AddCommand(&NewCloudNamespaceExportCommand(cctx, &s).Command) s.Command.AddCommand(&NewCloudNamespaceFairnessCommand(cctx, &s).Command) @@ -2406,6 +2407,7 @@ type CloudNamespaceCreateCommand struct { SearchAttribute []string ConnectionRuleId []string ProjectId string + Description string } func NewCloudNamespaceCreateCommand(cctx *CommandContext, parent *CloudNamespaceCommand) *CloudNamespaceCreateCommand { @@ -2432,6 +2434,7 @@ func NewCloudNamespaceCreateCommand(cctx *CommandContext, parent *CloudNamespace s.Command.Flags().StringArrayVar(&s.SearchAttribute, "search-attribute", nil, "Custom search attribute as 'name=Type' (e.g. --search-attribute myAttr=Keyword). Valid types: Text, Keyword, Int, Double, Bool, Datetime, KeywordList. Repeat to add multiple.") s.Command.Flags().StringArrayVar(&s.ConnectionRuleId, "connection-rule-id", nil, "Private connectivity rule ID. Repeat to specify multiple.") s.Command.Flags().StringVar(&s.ProjectId, "project-id", "", "The ID of the project to create the namespace in. If omitted, the namespace is created in the account's default project.") + s.Command.Flags().StringVar(&s.Description, "description", "", "The description is a human-readable description of the namespace purpose. Must be at most 255 printable ASCII characters plus whitespace. Optional, default is empty.") s.ClientOptions.BuildFlags(s.Command.Flags()) s.AsyncOperationOptions.BuildFlags(s.Command.Flags()) s.CodecServerOptions.BuildFlags(s.Command.Flags()) @@ -2483,6 +2486,88 @@ func NewCloudNamespaceDeleteCommand(cctx *CommandContext, parent *CloudNamespace return &s } +type CloudNamespaceDescriptionCommand struct { + Parent *CloudNamespaceCommand + Command cobra.Command +} + +func NewCloudNamespaceDescriptionCommand(cctx *CommandContext, parent *CloudNamespaceCommand) *CloudNamespaceDescriptionCommand { + var s CloudNamespaceDescriptionCommand + s.Parent = parent + s.Command.Use = "description" + s.Command.Short = "Manage namespace description" + s.Command.Long = "Commands for viewing and updating the description of a Temporal Cloud\nnamespace. The description is a human-readable description of the\nnamespace purpose. Must be at most 255 printable ASCII characters plus\nwhitespace. Optional, default is empty." + s.Command.Args = cobra.NoArgs + s.Command.AddCommand(&NewCloudNamespaceDescriptionGetCommand(cctx, &s).Command) + s.Command.AddCommand(&NewCloudNamespaceDescriptionSetCommand(cctx, &s).Command) + return &s +} + +type CloudNamespaceDescriptionGetCommand struct { + Parent *CloudNamespaceDescriptionCommand + Command cobra.Command + ClientOptions + NamespaceOptions +} + +func NewCloudNamespaceDescriptionGetCommand(cctx *CommandContext, parent *CloudNamespaceDescriptionCommand) *CloudNamespaceDescriptionGetCommand { + var s CloudNamespaceDescriptionGetCommand + s.Parent = parent + s.Command.DisableFlagsInUseLine = true + s.Command.Use = "get [flags]" + s.Command.Short = "Get namespace description" + if hasHighlighting { + s.Command.Long = "Retrieve the current description for a Temporal Cloud namespace. The\ndescription is a human-readable description of the namespace purpose.\n\nExample:\n\n\x1b[1mtemporal cloud namespace description get --namespace my-namespace.my-account\x1b[0m" + } else { + s.Command.Long = "Retrieve the current description for a Temporal Cloud namespace. The\ndescription is a human-readable description of the namespace purpose.\n\nExample:\n\n```\ntemporal cloud namespace description get --namespace my-namespace.my-account\n```" + } + s.Command.Args = cobra.NoArgs + s.ClientOptions.BuildFlags(s.Command.Flags()) + s.NamespaceOptions.BuildFlags(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 CloudNamespaceDescriptionSetCommand struct { + Parent *CloudNamespaceDescriptionCommand + Command cobra.Command + ClientOptions + NamespaceOptions + AsyncOperationOptions + ResourceVersionOptions + Value string +} + +func NewCloudNamespaceDescriptionSetCommand(cctx *CommandContext, parent *CloudNamespaceDescriptionCommand) *CloudNamespaceDescriptionSetCommand { + var s CloudNamespaceDescriptionSetCommand + s.Parent = parent + s.Command.DisableFlagsInUseLine = true + s.Command.Use = "set [flags]" + s.Command.Short = "Set namespace description" + if hasHighlighting { + s.Command.Long = "Set the description for a Temporal Cloud namespace without changing other\nnamespace settings. The description is a human-readable description of\nthe namespace purpose. Must be at most 255 printable ASCII characters\nplus whitespace. Pass an empty string to clear the description.\n\nExample:\n\n\x1b[1mtemporal cloud namespace description set --namespace my-namespace.my-account --value \"Updated namespace description\"\x1b[0m" + } else { + s.Command.Long = "Set the description for a Temporal Cloud namespace without changing other\nnamespace settings. The description is a human-readable description of\nthe namespace purpose. Must be at most 255 printable ASCII characters\nplus whitespace. Pass an empty string to clear the description.\n\nExample:\n\n```\ntemporal cloud namespace description set --namespace my-namespace.my-account --value \"Updated namespace description\"\n```" + } + s.Command.Args = cobra.NoArgs + s.Command.Flags().StringVar(&s.Value, "value", "", "New description for the namespace. Must be at most 255 printable ASCII characters plus whitespace. Pass an empty string to clear it. Required.") + _ = cobra.MarkFlagRequired(s.Command.Flags(), "value") + s.ClientOptions.BuildFlags(s.Command.Flags()) + s.NamespaceOptions.BuildFlags(s.Command.Flags()) + s.AsyncOperationOptions.BuildFlags(s.Command.Flags()) + s.ResourceVersionOptions.BuildFlags(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 CloudNamespaceEditCommand struct { Parent *CloudNamespaceCommand Command cobra.Command diff --git a/temporalcloudcli/commands.namespace.create_test.go b/temporalcloudcli/commands.namespace.create_test.go index ff00d91..bfd0e72 100644 --- a/temporalcloudcli/commands.namespace.create_test.go +++ b/temporalcloudcli/commands.namespace.create_test.go @@ -143,6 +143,7 @@ func TestCreateNamespace_BuildsSpec(t *testing.T) { "MyText": namespacev1.NamespaceSpec_SEARCH_ATTRIBUTE_TYPE_TEXT, "MyKeyword": namespacev1.NamespaceSpec_SEARCH_ATTRIBUTE_TYPE_KEYWORD, }, + Description: "Example namespace description", } mockCloud := cloudmock.NewMockCloudServiceClient(t) @@ -176,6 +177,7 @@ func TestCreateNamespace_BuildsSpec(t *testing.T) { }, }, SearchAttribute: []string{"MyText=Text", "MyKeyword=Keyword"}, + Description: "Example namespace description", Cloud: mockCloud, Printer: &printer.Printer{Output: &buf, JSON: true}, Prompter: mockPrompter, diff --git a/temporalcloudcli/commands.namespace.description.go b/temporalcloudcli/commands.namespace.description.go new file mode 100644 index 0000000..3f6dec0 --- /dev/null +++ b/temporalcloudcli/commands.namespace.description.go @@ -0,0 +1,66 @@ +package temporalcloudcli + +import ( + "errors" + + cloudservice "go.temporal.io/cloud-sdk/api/cloudservice/v1" + namespacev1 "go.temporal.io/cloud-sdk/api/namespace/v1" + "google.golang.org/protobuf/proto" + + "github.com/temporalio/cloud-cli/temporalcloudcli/internal/printer" +) + +func (c *CloudNamespaceDescriptionGetCommand) run(cctx *CommandContext, _ []string) error { + client, err := cctx.GetCloudClient(c.ClientOptions) + if err != nil { + return err + } + res, err := client.GetNamespace(cctx, &cloudservice.GetNamespaceRequest{Namespace: c.Namespace}) + if err != nil { + return err + } + + result := struct { + Namespace string `json:"namespace"` + Description string `json:"description"` + }{ + Namespace: res.Namespace.Namespace, + Description: res.Namespace.Spec.GetDescription(), + } + return cctx.Printer.PrintStructured(result, printer.StructuredOptions{}) +} + +func (c *CloudNamespaceDescriptionSetCommand) run(cctx *CommandContext, _ []string) error { + client, err := cctx.GetCloudClient(c.ClientOptions) + if err != nil { + return err + } + res, err := client.GetNamespace(cctx, &cloudservice.GetNamespaceRequest{Namespace: c.Namespace}) + if err != nil { + return err + } + + ns := res.Namespace + newSpec := proto.Clone(ns.Spec).(*namespacev1.NamespaceSpec) + newSpec.Description = c.Value + + yes, err := cctx.GetPrompter().PromptApply(ns.Spec, newSpec, false) + if err != nil { + return err + } + if !yes { + return errors.New("Aborting.") + } + + rv := ns.ResourceVersion + if c.ResourceVersion != "" { + rv = c.ResourceVersion + } + resp, err := client.UpdateNamespace(cctx, &cloudservice.UpdateNamespaceRequest{ + Namespace: c.Namespace, + Spec: newSpec, + ResourceVersion: rv, + AsyncOperationId: c.AsyncOperationId, + }) + return cctx.GetPoller(client, c.AsyncOperationOptions).HandleUpdateOperation(cctx, resp, err) +} diff --git a/temporalcloudcli/commands.namespace.description_test.go b/temporalcloudcli/commands.namespace.description_test.go new file mode 100644 index 0000000..39fead8 --- /dev/null +++ b/temporalcloudcli/commands.namespace.description_test.go @@ -0,0 +1,253 @@ +package temporalcloudcli_test + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/mock" + cloudservice "go.temporal.io/cloud-sdk/api/cloudservice/v1" + namespacev1 "go.temporal.io/cloud-sdk/api/namespace/v1" + operation "go.temporal.io/cloud-sdk/api/operation/v1" + "google.golang.org/protobuf/proto" + + cloudmock "github.com/temporalio/cloud-cli/internal/cloudservice/mock" + "github.com/temporalio/cloud-cli/temporalcloudcli" +) + +func TestNamespaceDescriptionGet(t *testing.T) { + getNSReq := &cloudservice.GetNamespaceRequest{Namespace: "my-ns.my-acct"} + tests := []struct { + name string + cmd temporalcloudcli.CloudNamespaceDescriptionGetCommand + cloudClientExpectations func(*cloudmock.MockCloudServiceClient) + expectedErr string + expectedJsonOutput any + }{ + { + name: "Populated", + cmd: temporalcloudcli.CloudNamespaceDescriptionGetCommand{NamespaceOptions: temporalcloudcli.NamespaceOptions{Namespace: "my-ns.my-acct"}}, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetNamespace(mock.Anything, getNSReq, mock.Anything). + Return(&cloudservice.GetNamespaceResponse{Namespace: &namespacev1.Namespace{ + Namespace: "my-ns.my-acct", + Spec: &namespacev1.NamespaceSpec{ + Description: "Example namespace description", + }, + }}, nil) + }, + expectedJsonOutput: map[string]any{ + "namespace": "my-ns.my-acct", + "description": "Example namespace description", + }, + }, + { + name: "Empty", + cmd: temporalcloudcli.CloudNamespaceDescriptionGetCommand{NamespaceOptions: temporalcloudcli.NamespaceOptions{Namespace: "my-ns.my-acct"}}, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetNamespace(mock.Anything, getNSReq, mock.Anything). + Return(&cloudservice.GetNamespaceResponse{Namespace: &namespacev1.Namespace{ + Namespace: "my-ns.my-acct", + Spec: &namespacev1.NamespaceSpec{}, + }}, nil) + }, + expectedJsonOutput: map[string]any{ + "namespace": "my-ns.my-acct", + "description": "", + }, + }, + { + name: "GetNamespaceError", + cmd: temporalcloudcli.CloudNamespaceDescriptionGetCommand{NamespaceOptions: temporalcloudcli.NamespaceOptions{Namespace: "my-ns.my-acct"}}, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetNamespace(mock.Anything, getNSReq, mock.Anything). + Return(nil, errors.New("namespace not found")) + }, + expectedErr: "namespace not found", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + temporalcloudcli.TestCommand(t, &tt.cmd, temporalcloudcli.TestCommandOptions{ + CloudClientExpectations: tt.cloudClientExpectations, + JSONOutput: true, + ExpectedError: tt.expectedErr, + ExpectedOutputJson: tt.expectedJsonOutput, + }) + }) + } +} + +func TestNamespaceDescriptionSet(t *testing.T) { + getNSReq := &cloudservice.GetNamespaceRequest{Namespace: "my-ns.my-acct"} + existingNS := func(description string) *namespacev1.Namespace { + return &namespacev1.Namespace{ + Namespace: "my-ns.my-acct", + ResourceVersion: "rv-fetched", + Spec: &namespacev1.NamespaceSpec{ + Name: "my-ns", + Regions: []string{"aws-us-east-1"}, + RetentionDays: 30, + Description: description, + }, + } + } + matchUpdate := func(currentDescription, newDescription, rv, asyncID string) any { + wantSpec := proto.Clone(existingNS(currentDescription).Spec).(*namespacev1.NamespaceSpec) + wantSpec.Description = newDescription + want := &cloudservice.UpdateNamespaceRequest{ + Namespace: "my-ns.my-acct", + Spec: wantSpec, + ResourceVersion: rv, + AsyncOperationId: asyncID, + } + return mock.MatchedBy(func(req *cloudservice.UpdateNamespaceRequest) bool { + return proto.Equal(req, want) + }) + } + + tests := []struct { + name string + cmd temporalcloudcli.CloudNamespaceDescriptionSetCommand + cloudClientExpectations func(*cloudmock.MockCloudServiceClient) + promptOptions temporalcloudcli.TestPromptOptions + asyncPollerOptions temporalcloudcli.TestAsyncPollerOptions + expectedErr string + }{ + { + name: "Set", + cmd: temporalcloudcli.CloudNamespaceDescriptionSetCommand{ + NamespaceOptions: temporalcloudcli.NamespaceOptions{Namespace: "my-ns.my-acct"}, + Value: "Updated namespace description", + }, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetNamespace(mock.Anything, getNSReq, mock.Anything). + Return(&cloudservice.GetNamespaceResponse{Namespace: existingNS("")}, nil) + c.EXPECT(). + UpdateNamespace(mock.Anything, matchUpdate("", "Updated namespace description", "rv-fetched", ""), mock.Anything). + Return(&cloudservice.UpdateNamespaceResponse{ + AsyncOperation: &operation.AsyncOperation{Id: "op-set"}, + }, nil) + }, + promptOptions: temporalcloudcli.TestPromptOptions{ExpectPrompApply: true, PromptResult: true}, + asyncPollerOptions: temporalcloudcli.TestAsyncPollerOptions{AsyncOperationID: "op-set"}, + }, + { + name: "Clear", + cmd: temporalcloudcli.CloudNamespaceDescriptionSetCommand{ + NamespaceOptions: temporalcloudcli.NamespaceOptions{Namespace: "my-ns.my-acct"}, + Value: "", + }, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetNamespace(mock.Anything, getNSReq, mock.Anything). + Return(&cloudservice.GetNamespaceResponse{Namespace: existingNS("old")}, nil) + c.EXPECT(). + UpdateNamespace(mock.Anything, matchUpdate("old", "", "rv-fetched", ""), mock.Anything). + Return(&cloudservice.UpdateNamespaceResponse{ + AsyncOperation: &operation.AsyncOperation{Id: "op-clear"}, + }, nil) + }, + promptOptions: temporalcloudcli.TestPromptOptions{ExpectPrompApply: true, PromptResult: true}, + asyncPollerOptions: temporalcloudcli.TestAsyncPollerOptions{AsyncOperationID: "op-clear"}, + }, + { + name: "ResourceVersionOverride", + cmd: temporalcloudcli.CloudNamespaceDescriptionSetCommand{ + NamespaceOptions: temporalcloudcli.NamespaceOptions{Namespace: "my-ns.my-acct"}, + Value: "Updated namespace description", + ResourceVersionOptions: temporalcloudcli.ResourceVersionOptions{ResourceVersion: "rv-user"}, + }, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetNamespace(mock.Anything, getNSReq, mock.Anything). + Return(&cloudservice.GetNamespaceResponse{Namespace: existingNS("")}, nil) + c.EXPECT(). + UpdateNamespace(mock.Anything, matchUpdate("", "Updated namespace description", "rv-user", ""), mock.Anything). + Return(&cloudservice.UpdateNamespaceResponse{ + AsyncOperation: &operation.AsyncOperation{Id: "op-rv"}, + }, nil) + }, + promptOptions: temporalcloudcli.TestPromptOptions{ExpectPrompApply: true, PromptResult: true}, + asyncPollerOptions: temporalcloudcli.TestAsyncPollerOptions{AsyncOperationID: "op-rv"}, + }, + { + name: "AsyncOperationIdOverride", + cmd: temporalcloudcli.CloudNamespaceDescriptionSetCommand{ + NamespaceOptions: temporalcloudcli.NamespaceOptions{Namespace: "my-ns.my-acct"}, + Value: "Updated namespace description", + AsyncOperationOptions: temporalcloudcli.AsyncOperationOptions{AsyncOperationId: "op-custom"}, + }, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetNamespace(mock.Anything, getNSReq, mock.Anything). + Return(&cloudservice.GetNamespaceResponse{Namespace: existingNS("")}, nil) + c.EXPECT(). + UpdateNamespace(mock.Anything, matchUpdate("", "Updated namespace description", "rv-fetched", "op-custom"), mock.Anything). + Return(&cloudservice.UpdateNamespaceResponse{ + AsyncOperation: &operation.AsyncOperation{Id: "op-custom"}, + }, nil) + }, + promptOptions: temporalcloudcli.TestPromptOptions{ExpectPrompApply: true, PromptResult: true}, + asyncPollerOptions: temporalcloudcli.TestAsyncPollerOptions{AsyncOperationID: "op-custom"}, + }, + { + name: "GetNamespaceError", + cmd: temporalcloudcli.CloudNamespaceDescriptionSetCommand{ + NamespaceOptions: temporalcloudcli.NamespaceOptions{Namespace: "my-ns.my-acct"}, + Value: "Updated namespace description", + }, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetNamespace(mock.Anything, getNSReq, mock.Anything). + Return(nil, errors.New("namespace not found")) + }, + expectedErr: "namespace not found", + }, + { + name: "UpdateNamespaceError", + cmd: temporalcloudcli.CloudNamespaceDescriptionSetCommand{ + NamespaceOptions: temporalcloudcli.NamespaceOptions{Namespace: "my-ns.my-acct"}, + Value: "Updated namespace description", + }, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetNamespace(mock.Anything, getNSReq, mock.Anything). + Return(&cloudservice.GetNamespaceResponse{Namespace: existingNS("")}, nil) + c.EXPECT(). + UpdateNamespace(mock.Anything, matchUpdate("", "Updated namespace description", "rv-fetched", ""), mock.Anything). + Return(nil, errors.New("update failed")) + }, + promptOptions: temporalcloudcli.TestPromptOptions{ExpectPrompApply: true, PromptResult: true}, + expectedErr: "update failed", + }, + { + name: "PromptDeclined", + cmd: temporalcloudcli.CloudNamespaceDescriptionSetCommand{ + NamespaceOptions: temporalcloudcli.NamespaceOptions{Namespace: "my-ns.my-acct"}, + Value: "Updated namespace description", + }, + cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + c.EXPECT(). + GetNamespace(mock.Anything, getNSReq, mock.Anything). + Return(&cloudservice.GetNamespaceResponse{Namespace: existingNS("")}, nil) + }, + promptOptions: temporalcloudcli.TestPromptOptions{ExpectPrompApply: true, PromptResult: false}, + expectedErr: "Aborting.", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + temporalcloudcli.TestCommand(t, &tt.cmd, temporalcloudcli.TestCommandOptions{ + CloudClientExpectations: tt.cloudClientExpectations, + PromptOptions: tt.promptOptions, + AsyncPollerOptions: tt.asyncPollerOptions, + JSONOutput: true, + ExpectedError: tt.expectedErr, + }) + }) + } +} diff --git a/temporalcloudcli/commands.namespace.go b/temporalcloudcli/commands.namespace.go index 49c39f9..805360b 100644 --- a/temporalcloudcli/commands.namespace.go +++ b/temporalcloudcli/commands.namespace.go @@ -305,7 +305,7 @@ func (c *CloudNamespaceListCommand) run(cctx *CommandContext, _ []string) error }, printer.PrintResourceOptions{ Fields: []string{"Namespace", "ProjectId", "State", "CreatedTime"}, - SpecFields: []string{"Regions"}, + SpecFields: []string{"Regions", "Description"}, }, printer.TableOptions{}, ) @@ -329,6 +329,7 @@ type ( CodecIncludeCrossOriginCredentials bool ConnectionRuleIDs []string ProjectID string + Description string Cloud cloudservice.CloudServiceClient Printer *printer.Printer @@ -390,6 +391,7 @@ func CreateNamespace(ctx context.Context, params CreateNamespaceParams) error { Name: params.Name, Regions: params.Regions, RetentionDays: params.RetentionDays, + Description: params.Description, ApiKeyAuth: &namespacev1.ApiKeyAuthSpec{Enabled: params.ApiKeyAuthEnabled}, MtlsAuth: &namespacev1.MtlsAuthSpec{Enabled: params.MtlsAuthEnabled}, Lifecycle: &namespacev1.LifecycleSpec{EnableDeleteProtection: params.EnableDeleteProtection}, @@ -465,6 +467,7 @@ func (c *CloudNamespaceCreateCommand) run(cctx *CommandContext, _ []string) erro CodecIncludeCrossOriginCredentials: c.CodecIncludeCrossOriginCredentials, ConnectionRuleIDs: c.ConnectionRuleId, ProjectID: projectID, + Description: c.Description, Cloud: cloudClient.CloudService(), Printer: cctx.Printer, Prompter: newPrompter(cctx), diff --git a/temporalcloudcli/commands.yml b/temporalcloudcli/commands.yml index c5c8d73..d82077a 100644 --- a/temporalcloudcli/commands.yml +++ b/temporalcloudcli/commands.yml @@ -440,6 +440,63 @@ commands: description: | The ID of the project to create the namespace in. If omitted, the namespace is created in the account's default project. + - name: description + type: string + description: | + The description is a human-readable description of the namespace + purpose. Must be at most 255 printable ASCII characters plus + whitespace. Optional, default is empty. + + # Namespace description commands + - name: cloud namespace description + summary: Manage namespace description + description: | + Commands for viewing and updating the description of a Temporal Cloud + namespace. The description is a human-readable description of the + namespace purpose. Must be at most 255 printable ASCII characters plus + whitespace. Optional, default is empty. + has-init: false + - name: cloud namespace description get + summary: Get namespace description + description: | + Retrieve the current description for a Temporal Cloud namespace. The + description is a human-readable description of the namespace purpose. + + Example: + + ``` + temporal cloud namespace description get --namespace my-namespace.my-account + ``` + has-init: false + option-sets: + - client + - namespace + - name: cloud namespace description set + summary: Set namespace description + description: | + Set the description for a Temporal Cloud namespace without changing other + namespace settings. The description is a human-readable description of + the namespace purpose. Must be at most 255 printable ASCII characters + plus whitespace. Pass an empty string to clear the description. + + Example: + + ``` + temporal cloud namespace description set --namespace my-namespace.my-account --value "Updated namespace description" + ``` + has-init: false + option-sets: + - client + - namespace + - async-operation + - resource-version + options: + - name: value + type: string + description: | + New description for the namespace. Must be at most 255 printable + ASCII characters plus whitespace. Pass an empty string to clear it. + required: true # Namespace retention commands - name: cloud namespace retention From 03e20f50984ed66fad42aa74c3f71d939265256f Mon Sep 17 00:00:00 2001 From: daehan Date: Tue, 1 Sep 2026 13:35:37 -0400 Subject: [PATCH 2/4] remove unnecessary word --- go.mod | 2 +- go.sum | 4 ++-- temporalcloudcli/commands.gen.go | 12 ++++++------ temporalcloudcli/commands.yml | 12 ++++++------ 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index 4c663dc..a1a6ade 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/temporalio/cli/cliext v0.0.0-20260602200703-8bb57b77ad55 go.temporal.io/api v1.63.6-0.20260811224032-89570b10e9e2 - go.temporal.io/cloud-sdk v0.16.1-0.20260826195225-e4c5e7f3aa55 + go.temporal.io/cloud-sdk v0.17.1-0.20260901002544-11c44c6026c7 go.temporal.io/sdk v1.44.1 go.temporal.io/sdk/contrib/envconfig v1.0.0 golang.org/x/oauth2 v0.36.0 diff --git a/go.sum b/go.sum index 8b3b588..12cc678 100644 --- a/go.sum +++ b/go.sum @@ -154,8 +154,8 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09 go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.temporal.io/api v1.63.6-0.20260811224032-89570b10e9e2 h1:VeNTxlOHETym01zX6kSXHIVw4nc2RrX/KVgepQ/dr/8= go.temporal.io/api v1.63.6-0.20260811224032-89570b10e9e2/go.mod h1:SrlW2JMwVlDP4nRWSNznUFqnSHd+YeMDS1BkYo63HCQ= -go.temporal.io/cloud-sdk v0.16.1-0.20260826195225-e4c5e7f3aa55 h1:XVb15Q93gtvLnF7u4y3EHEZLfeVIHf5cIxwGmnpDTrQ= -go.temporal.io/cloud-sdk v0.16.1-0.20260826195225-e4c5e7f3aa55/go.mod h1:W2O9t9tvo3Q/LhGgYdj8JijWbN5C84os+cz/BadIHYI= +go.temporal.io/cloud-sdk v0.17.1-0.20260901002544-11c44c6026c7 h1:mvMFUVowYubd+1/WM6xHX+qcQ3y0EqsPvPA7vGNjHEs= +go.temporal.io/cloud-sdk v0.17.1-0.20260901002544-11c44c6026c7/go.mod h1:W2O9t9tvo3Q/LhGgYdj8JijWbN5C84os+cz/BadIHYI= go.temporal.io/sdk v1.44.1 h1:Mt2OZLZpqkzDIdg9YyQzO0Rb/HqCDnnqHlIAGAJ5gqM= go.temporal.io/sdk v1.44.1/go.mod h1:vkApR12F9/Y8OR+hkxe7WyXQFuCX6clhzqnAk6rzDAM= go.temporal.io/sdk/contrib/envconfig v1.0.0 h1:1Q/swVgB4EW/p3k7rI9/4hpU4/DC57FSRbU90+UisXw= diff --git a/temporalcloudcli/commands.gen.go b/temporalcloudcli/commands.gen.go index e98d80a..7701ba6 100644 --- a/temporalcloudcli/commands.gen.go +++ b/temporalcloudcli/commands.gen.go @@ -2434,7 +2434,7 @@ func NewCloudNamespaceCreateCommand(cctx *CommandContext, parent *CloudNamespace s.Command.Flags().StringArrayVar(&s.SearchAttribute, "search-attribute", nil, "Custom search attribute as 'name=Type' (e.g. --search-attribute myAttr=Keyword). Valid types: Text, Keyword, Int, Double, Bool, Datetime, KeywordList. Repeat to add multiple.") s.Command.Flags().StringArrayVar(&s.ConnectionRuleId, "connection-rule-id", nil, "Private connectivity rule ID. Repeat to specify multiple.") s.Command.Flags().StringVar(&s.ProjectId, "project-id", "", "The ID of the project to create the namespace in. If omitted, the namespace is created in the account's default project.") - s.Command.Flags().StringVar(&s.Description, "description", "", "The description is a human-readable description of the namespace purpose. Must be at most 255 printable ASCII characters plus whitespace. Optional, default is empty.") + s.Command.Flags().StringVar(&s.Description, "description", "", "The description is a human-readable description of the namespace. Must be at most 255 printable ASCII characters plus whitespace. Optional, default is empty.") s.ClientOptions.BuildFlags(s.Command.Flags()) s.AsyncOperationOptions.BuildFlags(s.Command.Flags()) s.CodecServerOptions.BuildFlags(s.Command.Flags()) @@ -2496,7 +2496,7 @@ func NewCloudNamespaceDescriptionCommand(cctx *CommandContext, parent *CloudName s.Parent = parent s.Command.Use = "description" s.Command.Short = "Manage namespace description" - s.Command.Long = "Commands for viewing and updating the description of a Temporal Cloud\nnamespace. The description is a human-readable description of the\nnamespace purpose. Must be at most 255 printable ASCII characters plus\nwhitespace. Optional, default is empty." + s.Command.Long = "Commands for viewing and updating the description of a Temporal Cloud\nnamespace. The description is a human-readable description of the\nnamespace. Must be at most 255 printable ASCII characters plus\nwhitespace. Optional, default is empty." s.Command.Args = cobra.NoArgs s.Command.AddCommand(&NewCloudNamespaceDescriptionGetCommand(cctx, &s).Command) s.Command.AddCommand(&NewCloudNamespaceDescriptionSetCommand(cctx, &s).Command) @@ -2517,9 +2517,9 @@ func NewCloudNamespaceDescriptionGetCommand(cctx *CommandContext, parent *CloudN s.Command.Use = "get [flags]" s.Command.Short = "Get namespace description" if hasHighlighting { - s.Command.Long = "Retrieve the current description for a Temporal Cloud namespace. The\ndescription is a human-readable description of the namespace purpose.\n\nExample:\n\n\x1b[1mtemporal cloud namespace description get --namespace my-namespace.my-account\x1b[0m" + s.Command.Long = "Retrieve the current description for a Temporal Cloud namespace. The\ndescription is a human-readable description of the namespace.\n\nExample:\n\n\x1b[1mtemporal cloud namespace description get --namespace my-namespace.my-account\x1b[0m" } else { - s.Command.Long = "Retrieve the current description for a Temporal Cloud namespace. The\ndescription is a human-readable description of the namespace purpose.\n\nExample:\n\n```\ntemporal cloud namespace description get --namespace my-namespace.my-account\n```" + s.Command.Long = "Retrieve the current description for a Temporal Cloud namespace. The\ndescription is a human-readable description of the namespace.\n\nExample:\n\n```\ntemporal cloud namespace description get --namespace my-namespace.my-account\n```" } s.Command.Args = cobra.NoArgs s.ClientOptions.BuildFlags(s.Command.Flags()) @@ -2549,9 +2549,9 @@ func NewCloudNamespaceDescriptionSetCommand(cctx *CommandContext, parent *CloudN s.Command.Use = "set [flags]" s.Command.Short = "Set namespace description" if hasHighlighting { - s.Command.Long = "Set the description for a Temporal Cloud namespace without changing other\nnamespace settings. The description is a human-readable description of\nthe namespace purpose. Must be at most 255 printable ASCII characters\nplus whitespace. Pass an empty string to clear the description.\n\nExample:\n\n\x1b[1mtemporal cloud namespace description set --namespace my-namespace.my-account --value \"Updated namespace description\"\x1b[0m" + s.Command.Long = "Set the description for a Temporal Cloud namespace without changing other\nnamespace settings. The description is a human-readable description of\nthe namespace. Must be at most 255 printable ASCII characters\nplus whitespace. Pass an empty string to clear the description.\n\nExample:\n\n\x1b[1mtemporal cloud namespace description set --namespace my-namespace.my-account --value \"Updated namespace description\"\x1b[0m" } else { - s.Command.Long = "Set the description for a Temporal Cloud namespace without changing other\nnamespace settings. The description is a human-readable description of\nthe namespace purpose. Must be at most 255 printable ASCII characters\nplus whitespace. Pass an empty string to clear the description.\n\nExample:\n\n```\ntemporal cloud namespace description set --namespace my-namespace.my-account --value \"Updated namespace description\"\n```" + s.Command.Long = "Set the description for a Temporal Cloud namespace without changing other\nnamespace settings. The description is a human-readable description of\nthe namespace. Must be at most 255 printable ASCII characters\nplus whitespace. Pass an empty string to clear the description.\n\nExample:\n\n```\ntemporal cloud namespace description set --namespace my-namespace.my-account --value \"Updated namespace description\"\n```" } s.Command.Args = cobra.NoArgs s.Command.Flags().StringVar(&s.Value, "value", "", "New description for the namespace. Must be at most 255 printable ASCII characters plus whitespace. Pass an empty string to clear it. Required.") diff --git a/temporalcloudcli/commands.yml b/temporalcloudcli/commands.yml index d82077a..14bb54a 100644 --- a/temporalcloudcli/commands.yml +++ b/temporalcloudcli/commands.yml @@ -443,9 +443,9 @@ commands: - name: description type: string description: | - The description is a human-readable description of the namespace - purpose. Must be at most 255 printable ASCII characters plus - whitespace. Optional, default is empty. + The description is a human-readable description of the namespace. + Must be at most 255 printable ASCII characters plus whitespace. + Optional, default is empty. # Namespace description commands - name: cloud namespace description @@ -453,14 +453,14 @@ commands: description: | Commands for viewing and updating the description of a Temporal Cloud namespace. The description is a human-readable description of the - namespace purpose. Must be at most 255 printable ASCII characters plus + namespace. Must be at most 255 printable ASCII characters plus whitespace. Optional, default is empty. has-init: false - name: cloud namespace description get summary: Get namespace description description: | Retrieve the current description for a Temporal Cloud namespace. The - description is a human-readable description of the namespace purpose. + description is a human-readable description of the namespace. Example: @@ -476,7 +476,7 @@ commands: description: | Set the description for a Temporal Cloud namespace without changing other namespace settings. The description is a human-readable description of - the namespace purpose. Must be at most 255 printable ASCII characters + the namespace. Must be at most 255 printable ASCII characters plus whitespace. Pass an empty string to clear the description. Example: From 8c5966f9ee8fe453382aef946ca7b564105a0e31 Mon Sep 17 00:00:00 2001 From: daehan Date: Tue, 1 Sep 2026 15:46:36 -0400 Subject: [PATCH 3/4] bump the cloud-sdk version --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a1a6ade..d2e9d18 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/temporalio/cli/cliext v0.0.0-20260602200703-8bb57b77ad55 go.temporal.io/api v1.63.6-0.20260811224032-89570b10e9e2 - go.temporal.io/cloud-sdk v0.17.1-0.20260901002544-11c44c6026c7 + go.temporal.io/cloud-sdk v0.18.0 go.temporal.io/sdk v1.44.1 go.temporal.io/sdk/contrib/envconfig v1.0.0 golang.org/x/oauth2 v0.36.0 diff --git a/go.sum b/go.sum index 12cc678..d7b9ef5 100644 --- a/go.sum +++ b/go.sum @@ -154,8 +154,8 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09 go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.temporal.io/api v1.63.6-0.20260811224032-89570b10e9e2 h1:VeNTxlOHETym01zX6kSXHIVw4nc2RrX/KVgepQ/dr/8= go.temporal.io/api v1.63.6-0.20260811224032-89570b10e9e2/go.mod h1:SrlW2JMwVlDP4nRWSNznUFqnSHd+YeMDS1BkYo63HCQ= -go.temporal.io/cloud-sdk v0.17.1-0.20260901002544-11c44c6026c7 h1:mvMFUVowYubd+1/WM6xHX+qcQ3y0EqsPvPA7vGNjHEs= -go.temporal.io/cloud-sdk v0.17.1-0.20260901002544-11c44c6026c7/go.mod h1:W2O9t9tvo3Q/LhGgYdj8JijWbN5C84os+cz/BadIHYI= +go.temporal.io/cloud-sdk v0.18.0 h1:7jGf76PwO6teN/wCkFlrf5yEsJcUrtbMw1hxV/l0a9M= +go.temporal.io/cloud-sdk v0.18.0/go.mod h1:W2O9t9tvo3Q/LhGgYdj8JijWbN5C84os+cz/BadIHYI= go.temporal.io/sdk v1.44.1 h1:Mt2OZLZpqkzDIdg9YyQzO0Rb/HqCDnnqHlIAGAJ5gqM= go.temporal.io/sdk v1.44.1/go.mod h1:vkApR12F9/Y8OR+hkxe7WyXQFuCX6clhzqnAk6rzDAM= go.temporal.io/sdk/contrib/envconfig v1.0.0 h1:1Q/swVgB4EW/p3k7rI9/4hpU4/DC57FSRbU90+UisXw= From 5c32ddb1badc9d4018db19def0a839f15992eacb Mon Sep 17 00:00:00 2001 From: daehan Date: Tue, 1 Sep 2026 16:07:23 -0400 Subject: [PATCH 4/4] add missing field to golden --- .../removes_option_deprecated_field_without_suffix.golden.json | 1 + 1 file changed, 1 insertion(+) diff --git a/temporalcloudcli/internal/protoutils/testdata/strip_deprecated/removes_option_deprecated_field_without_suffix.golden.json b/temporalcloudcli/internal/protoutils/testdata/strip_deprecated/removes_option_deprecated_field_without_suffix.golden.json index 1cde4f0..b376d63 100644 --- a/temporalcloudcli/internal/protoutils/testdata/strip_deprecated/removes_option_deprecated_field_without_suffix.golden.json +++ b/temporalcloudcli/internal/protoutils/testdata/strip_deprecated/removes_option_deprecated_field_without_suffix.golden.json @@ -3,6 +3,7 @@ "capacitySpec": null, "codecServer": null, "connectivityRuleIds": [], + "description": "", "fairness": null, "highAvailability": null, "lifecycle": null,