diff --git a/temporalcloudcli/commands.namespace.move.go b/temporalcloudcli/commands.namespace.move.go index 4d6219c..839f0ea 100644 --- a/temporalcloudcli/commands.namespace.move.go +++ b/temporalcloudcli/commands.namespace.move.go @@ -1,6 +1,7 @@ package temporalcloudcli import ( + "context" "errors" "fmt" @@ -29,9 +30,17 @@ func (c *CloudNamespaceMoveToProjectCommand) run(cctx *CommandContext, _ []strin } ns := res.Namespace + names, err := projectDisplayNames(cctx, client, ns.GetProjectId(), c.DestinationProjectId) + if err != nil { + return err + } + if _, ok := names[c.DestinationProjectId]; !ok { + return fmt.Errorf("destination project %q not found", c.DestinationProjectId) + } + yes, err := cctx.GetPrompter().PromptYes(fmt.Sprintf( - "Move namespace %q from project %q to project %q", - c.Namespace, ns.GetProjectId(), c.DestinationProjectId, + "Move namespace %q from project %s to project %s", + c.Namespace, projectLabel(names, ns.GetProjectId()), projectLabel(names, c.DestinationProjectId), )) if err != nil { return err @@ -72,3 +81,28 @@ func (c *CloudNamespaceMoveToProjectCommand) run(cctx *CommandContext, _ []strin }) return poller.HandleIdempotentOperation(cctx, resp, err) } + +// projectDisplayNames maps project IDs to display names. A project that does not exist, or that +// the caller cannot see, is absent from the result rather than reported as an error. +func projectDisplayNames( + ctx context.Context, + client cloudservice.CloudServiceClient, + ids ...string, +) (map[string]string, error) { + res, err := client.GetProjects(ctx, &cloudservice.GetProjectsRequest{ProjectIds: ids}) + if err != nil { + return nil, err + } + names := make(map[string]string, len(res.GetProjects())) + for _, p := range res.GetProjects() { + names[p.GetId()] = p.GetSpec().GetDisplayName() + } + return names, nil +} + +func projectLabel(names map[string]string, id string) string { + if name, ok := names[id]; ok { + return fmt.Sprintf("%q (%s)", name, id) + } + return fmt.Sprintf("%q", id) +} diff --git a/temporalcloudcli/commands.namespace.move_test.go b/temporalcloudcli/commands.namespace.move_test.go index 1f0e04e..38b9b1d 100644 --- a/temporalcloudcli/commands.namespace.move_test.go +++ b/temporalcloudcli/commands.namespace.move_test.go @@ -9,6 +9,7 @@ import ( 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" + projectv1 "go.temporal.io/cloud-sdk/api/project/v1" cloudmock "github.com/temporalio/cloud-cli/internal/cloudservice/mock" "github.com/temporalio/cloud-cli/temporalcloudcli" @@ -34,6 +35,25 @@ func expectGetNamespaceForMove(c *cloudmock.MockCloudServiceClient) { }, nil) } +func testMoveProject(id, displayName string) *projectv1.Project { + return &projectv1.Project{Id: id, Spec: &projectv1.ProjectSpec{DisplayName: displayName}} +} + +func expectProjectLookup(c *cloudmock.MockCloudServiceClient, found ...*projectv1.Project) { + c.EXPECT(). + GetProjects( + mock.Anything, + &cloudservice.GetProjectsRequest{ProjectIds: []string{testMoveSource, testMoveDest}}, + mock.Anything, + ). + Return(&cloudservice.GetProjectsResponse{Projects: found}, nil) +} + +func expectMoveLookups(c *cloudmock.MockCloudServiceClient) { + expectGetNamespaceForMove(c) + expectProjectLookup(c, testMoveProject(testMoveSource, "source"), testMoveProject(testMoveDest, "destination")) +} + func expectMove( c *cloudmock.MockCloudServiceClient, matches func(*cloudservice.MoveNamespaceToProjectRequest) bool, @@ -90,7 +110,7 @@ func TestNamespaceMoveToProjectConnectivitySelection(t *testing.T) { cmd.Namespace = testMoveNamespace temporalcloudcli.TestCommand(t, &cmd, temporalcloudcli.TestCommandOptions{ CloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { - expectGetNamespaceForMove(c) + expectMoveLookups(c) expectMove(c, tt.wantRequest) }, PromptOptions: temporalcloudcli.TestPromptOptions{ExpectPromptYes: true, PromptResult: true}, @@ -119,7 +139,7 @@ func TestNamespaceMoveToProjectRequestFields(t *testing.T) { cmd.ResourceVersion = tt.resourceVersion temporalcloudcli.TestCommand(t, &cmd, temporalcloudcli.TestCommandOptions{ CloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { - expectGetNamespaceForMove(c) + expectMoveLookups(c) expectMove(c, func(req *cloudservice.MoveNamespaceToProjectRequest) bool { return req.GetNamespace() == testMoveNamespace && req.GetDestinationProjectId() == testMoveDest && @@ -171,6 +191,21 @@ func TestNamespaceMoveToProjectRejectsInvalidRuleSelection(t *testing.T) { } } +func TestNamespaceMoveToProjectUnknownDestination(t *testing.T) { + cmd := temporalcloudcli.CloudNamespaceMoveToProjectCommand{ + DestinationProjectId: testMoveDest, + SourceProjectId: testMoveSource, + } + cmd.Namespace = testMoveNamespace + temporalcloudcli.TestCommand(t, &cmd, temporalcloudcli.TestCommandOptions{ + CloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { + expectGetNamespaceForMove(c) + expectProjectLookup(c, testMoveProject(testMoveSource, "source")) + }, + ExpectedError: `destination project "proj-dest" not found`, + }) +} + func TestNamespaceMoveToProjectPromptDeclined(t *testing.T) { cmd := temporalcloudcli.CloudNamespaceMoveToProjectCommand{ DestinationProjectId: testMoveDest, @@ -178,10 +213,10 @@ func TestNamespaceMoveToProjectPromptDeclined(t *testing.T) { } cmd.Namespace = testMoveNamespace temporalcloudcli.TestCommand(t, &cmd, temporalcloudcli.TestCommandOptions{ - CloudClientExpectations: expectGetNamespaceForMove, + CloudClientExpectations: expectMoveLookups, PromptOptions: temporalcloudcli.TestPromptOptions{ ExpectPromptYes: true, - ExpectPromptYesMessage: `Move namespace "my-ns.my-acct" from project "proj-source" to project "proj-dest"`, + ExpectPromptYesMessage: `Move namespace "my-ns.my-acct" from project "source" (proj-source) to project "destination" (proj-dest)`, PromptResult: false, }, ExpectedError: "Aborting move.", @@ -196,7 +231,7 @@ func TestNamespaceMoveToProjectSurfacesRejection(t *testing.T) { cmd.Namespace = testMoveNamespace temporalcloudcli.TestCommand(t, &cmd, temporalcloudcli.TestCommandOptions{ CloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) { - expectGetNamespaceForMove(c) + expectMoveLookups(c) c.EXPECT(). MoveNamespaceToProject(mock.Anything, mock.Anything, mock.Anything). Return(nil, errors.New(`namespace "my-ns.my-acct" has a migration in progress`))