Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions temporalcloudcli/commands.namespace.move.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package temporalcloudcli

import (
"context"
"errors"
"fmt"

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
45 changes: 40 additions & 5 deletions temporalcloudcli/commands.namespace.move_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
Expand Down Expand Up @@ -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},
Expand Down Expand Up @@ -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 &&
Expand Down Expand Up @@ -171,17 +191,32 @@ 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,
SourceProjectId: testMoveSource,
}
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.",
Expand All @@ -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`))
Expand Down
Loading