Skip to content
Draft
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
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,5 @@ require (
)

tool github.com/vektra/mockery/v3

replace go.temporal.io/cloud-sdk => /Users/harry/work/cloud-sdk-go
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,6 @@ 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.19.0 h1:Vjr8a75hdxbq6VAiZJz7WboqhGA2NhmXl5A6ZFrVt60=
go.temporal.io/cloud-sdk v0.19.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=
Expand Down
83 changes: 83 additions & 0 deletions internal/cloudservice/mock/mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 47 additions & 0 deletions temporalcloudcli/commands.gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -4611,6 +4611,7 @@ func NewCloudNexusEndpointCommand(cctx *CommandContext, parent *CloudNexusComman
s.Command.AddCommand(&NewCloudNexusEndpointDeleteCommand(cctx, &s).Command)
s.Command.AddCommand(&NewCloudNexusEndpointGetCommand(cctx, &s).Command)
s.Command.AddCommand(&NewCloudNexusEndpointListCommand(cctx, &s).Command)
s.Command.AddCommand(&NewCloudNexusEndpointMoveToProjectCommand(cctx, &s).Command)
s.Command.AddCommand(&NewCloudNexusEndpointUpdateCommand(cctx, &s).Command)
return &s
}
Expand Down Expand Up @@ -4900,6 +4901,52 @@ func NewCloudNexusEndpointListCommand(cctx *CommandContext, parent *CloudNexusEn
return &s
}

type CloudNexusEndpointMoveToProjectCommand struct {
Parent *CloudNexusEndpointCommand
Command cobra.Command
ClientOptions
ResourceVersionOptions
Name string
Id string
DestinationProjectId string
SourceProjectId string
AsyncOperationId string
Async bool
PollInterval cliext.FlagDuration
}

func NewCloudNexusEndpointMoveToProjectCommand(cctx *CommandContext, parent *CloudNexusEndpointCommand) *CloudNexusEndpointMoveToProjectCommand {
var s CloudNexusEndpointMoveToProjectCommand
s.Parent = parent
s.Command.DisableFlagsInUseLine = true
s.Command.Use = "move-to-project [flags]"
s.Command.Short = "Move a Nexus Endpoint to a different project"
if hasHighlighting {
s.Command.Long = "Move a Nexus Endpoint to a different project within the same account.\nSpecify either \x1b[1m--name\x1b[0m or \x1b[1m--id\x1b[0m (exactly one is required).\n\nThe endpoint keeps its ID, name, target, allowed caller namespaces, and\nrouting, so Nexus calls through it are unaffected. Only the project that\ngoverns access to it changes: access granted through the source project\nis lost, and access granted through the destination project applies.\n\nExample:\n\n\x1b[1mtemporal cloud nexus endpoint move-to-project \\\n --name my-endpoint \\\n --destination-project-id my-destination-project \\\n --source-project-id my-source-project\x1b[0m"
} else {
s.Command.Long = "Move a Nexus Endpoint to a different project within the same account.\nSpecify either `--name` or `--id` (exactly one is required).\n\nThe endpoint keeps its ID, name, target, allowed caller namespaces, and\nrouting, so Nexus calls through it are unaffected. Only the project that\ngoverns access to it changes: access granted through the source project\nis lost, and access granted through the destination project applies.\n\nExample:\n\n```\ntemporal cloud nexus endpoint move-to-project \\\n --name my-endpoint \\\n --destination-project-id my-destination-project \\\n --source-project-id my-source-project\n```"
}
s.Command.Args = cobra.NoArgs
s.Command.Flags().StringVar(&s.Name, "name", "", "The name of the Nexus Endpoint to move.")
s.Command.Flags().StringVar(&s.Id, "id", "", "The ID of the Nexus Endpoint to move.")
s.Command.Flags().StringVar(&s.DestinationProjectId, "destination-project-id", "", "The ID of the project to move the Nexus Endpoint to. Required.")
_ = cobra.MarkFlagRequired(s.Command.Flags(), "destination-project-id")
s.Command.Flags().StringVar(&s.SourceProjectId, "source-project-id", "", "The ID of the project the Nexus Endpoint is expected to currently belong to. The move fails if the endpoint has since moved elsewhere. Required.")
_ = cobra.MarkFlagRequired(s.Command.Flags(), "source-project-id")
s.Command.Flags().StringVar(&s.AsyncOperationId, "async-operation-id", "", "Custom identifier for tracking this async operation. If not provided, a unique ID is generated automatically.")
s.Command.Flags().BoolVar(&s.Async, "async", false, "Return immediately after initiating the operation instead of waiting for completion. Use the returned operation ID to check status later.")
s.PollInterval = cliext.MustParseFlagDuration("1s")
s.Command.Flags().Var(&s.PollInterval, "poll-interval", "Time to wait between status checks when waiting for operation completion. Cannot be greater than 10 minutes. Supports minutes (m) and seconds (s).")
s.ClientOptions.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 CloudNexusEndpointUpdateCommand struct {
Parent *CloudNexusEndpointCommand
Command cobra.Command
Expand Down
59 changes: 59 additions & 0 deletions temporalcloudcli/commands.nexus.move.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package temporalcloudcli

import (
"errors"
"fmt"

cloudservice "go.temporal.io/cloud-sdk/api/cloudservice/v1"
)

func (c *CloudNexusEndpointMoveToProjectCommand) run(cctx *CommandContext, _ []string) error {
client, err := cctx.GetCloudClient(c.ClientOptions)
if err != nil {
return err
}

endpoint, err := resolveNexusEndpoint(cctx, client, c.Name, c.Id)
if err != nil {
return err
}

names, err := projectDisplayNames(cctx, client, endpoint.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 Nexus Endpoint %q from project %s to project %s",
endpoint.GetSpec().GetName(),
projectLabel(names, endpoint.GetProjectId()),
projectLabel(names, c.DestinationProjectId),
))
if err != nil {
return err
}
if !yes {
return errors.New("Aborting move.")
}

rv := endpoint.GetResourceVersion()
if c.ResourceVersion != "" {
rv = c.ResourceVersion
}
resp, err := client.MoveNexusEndpointToProject(cctx, &cloudservice.MoveNexusEndpointToProjectRequest{
EndpointId: endpoint.GetId(),
DestinationProjectId: c.DestinationProjectId,
ExpectedSourceProjectId: c.SourceProjectId,
ResourceVersion: rv,
AsyncOperationId: c.AsyncOperationId,
})
poller := cctx.GetPoller(client, AsyncOperationOptions{
AsyncOperationId: c.AsyncOperationId,
Async: c.Async,
PollInterval: c.PollInterval,
})
return poller.HandleIdempotentOperation(cctx, resp, err)
}
151 changes: 151 additions & 0 deletions temporalcloudcli/commands.nexus.move_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package temporalcloudcli_test

import (
"errors"
"testing"

"github.com/stretchr/testify/mock"
cloudservice "go.temporal.io/cloud-sdk/api/cloudservice/v1"
nexusv1 "go.temporal.io/cloud-sdk/api/nexus/v1"
operation "go.temporal.io/cloud-sdk/api/operation/v1"

cloudmock "github.com/temporalio/cloud-cli/internal/cloudservice/mock"
"github.com/temporalio/cloud-cli/temporalcloudcli"
)

var testMoveEndpoint = &nexusv1.Endpoint{
Id: "ep-123",
ResourceVersion: "v1",
ProjectId: testMoveSource,
Spec: &nexusv1.EndpointSpec{Name: "my-endpoint"},
}

func expectEndpointByName(c *cloudmock.MockCloudServiceClient) {
c.EXPECT().
GetNexusEndpoints(mock.Anything, &cloudservice.GetNexusEndpointsRequest{Name: "my-endpoint"}, mock.Anything).
Return(&cloudservice.GetNexusEndpointsResponse{Endpoints: []*nexusv1.Endpoint{testMoveEndpoint}}, nil)
}

func expectEndpointByID(c *cloudmock.MockCloudServiceClient) {
c.EXPECT().
GetNexusEndpoint(mock.Anything, &cloudservice.GetNexusEndpointRequest{EndpointId: "ep-123"}, mock.Anything).
Return(&cloudservice.GetNexusEndpointResponse{Endpoint: testMoveEndpoint}, nil)
}

func expectBothProjectsFound(c *cloudmock.MockCloudServiceClient) {
expectProjectLookup(c, testMoveProject(testMoveSource, "source"), testMoveProject(testMoveDest, "destination"))
}

func expectEndpointMove(c *cloudmock.MockCloudServiceClient, want *cloudservice.MoveNexusEndpointToProjectRequest) {
c.EXPECT().
MoveNexusEndpointToProject(mock.Anything, want, mock.Anything).
Return(&cloudservice.MoveNexusEndpointToProjectResponse{
AsyncOperation: &operation.AsyncOperation{Id: "op-move"},
}, nil)
}

func TestNexusEndpointMoveToProject(t *testing.T) {
tests := []struct {
name string
cmd temporalcloudcli.CloudNexusEndpointMoveToProjectCommand
resourceVersion string
cloudClientExpectations func(*cloudmock.MockCloudServiceClient)
}{
{
name: "ByNameWithFetchedResourceVersion",
cmd: temporalcloudcli.CloudNexusEndpointMoveToProjectCommand{Name: "my-endpoint"},
cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) {
expectEndpointByName(c)
expectBothProjectsFound(c)
expectEndpointMove(c, &cloudservice.MoveNexusEndpointToProjectRequest{
EndpointId: "ep-123",
DestinationProjectId: testMoveDest,
ExpectedSourceProjectId: testMoveSource,
ResourceVersion: "v1",
})
},
},
{
name: "ByIDWithResourceVersionOverride",
cmd: temporalcloudcli.CloudNexusEndpointMoveToProjectCommand{Id: "ep-123"},
resourceVersion: "v-override",
cloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) {
expectEndpointByID(c)
expectBothProjectsFound(c)
expectEndpointMove(c, &cloudservice.MoveNexusEndpointToProjectRequest{
EndpointId: "ep-123",
DestinationProjectId: testMoveDest,
ExpectedSourceProjectId: testMoveSource,
ResourceVersion: "v-override",
})
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := tt.cmd
cmd.DestinationProjectId = testMoveDest
cmd.SourceProjectId = testMoveSource
cmd.ResourceVersion = tt.resourceVersion
temporalcloudcli.TestCommand(t, &cmd, temporalcloudcli.TestCommandOptions{
CloudClientExpectations: tt.cloudClientExpectations,
PromptOptions: temporalcloudcli.TestPromptOptions{
ExpectPromptYes: true,
ExpectPromptYesMessage: `Move Nexus Endpoint "my-endpoint" from project "source" (proj-source) to project "destination" (proj-dest)`,
PromptResult: true,
},
AsyncPollerOptions: temporalcloudcli.TestAsyncPollerOptions{AsyncOperationID: "op-move"},
})
})
}
}

func TestNexusEndpointMoveToProjectUnknownDestination(t *testing.T) {
cmd := temporalcloudcli.CloudNexusEndpointMoveToProjectCommand{
Name: "my-endpoint",
DestinationProjectId: testMoveDest,
SourceProjectId: testMoveSource,
}
temporalcloudcli.TestCommand(t, &cmd, temporalcloudcli.TestCommandOptions{
CloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) {
expectEndpointByName(c)
expectProjectLookup(c, testMoveProject(testMoveSource, "source"))
},
ExpectedError: `destination project "proj-dest" not found`,
})
}

func TestNexusEndpointMoveToProjectPromptDeclined(t *testing.T) {
cmd := temporalcloudcli.CloudNexusEndpointMoveToProjectCommand{
Name: "my-endpoint",
DestinationProjectId: testMoveDest,
SourceProjectId: testMoveSource,
}
temporalcloudcli.TestCommand(t, &cmd, temporalcloudcli.TestCommandOptions{
CloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) {
expectEndpointByName(c)
expectBothProjectsFound(c)
},
PromptOptions: temporalcloudcli.TestPromptOptions{ExpectPromptYes: true, PromptResult: false},
ExpectedError: "Aborting move.",
})
}

func TestNexusEndpointMoveToProjectSurfacesRejection(t *testing.T) {
cmd := temporalcloudcli.CloudNexusEndpointMoveToProjectCommand{
Name: "my-endpoint",
DestinationProjectId: testMoveDest,
SourceProjectId: testMoveSource,
}
temporalcloudcli.TestCommand(t, &cmd, temporalcloudcli.TestCommandOptions{
CloudClientExpectations: func(c *cloudmock.MockCloudServiceClient) {
expectEndpointByName(c)
expectBothProjectsFound(c)
c.EXPECT().
MoveNexusEndpointToProject(mock.Anything, mock.Anything, mock.Anything).
Return(nil, errors.New("resource limit reached: 5"))
},
PromptOptions: temporalcloudcli.TestPromptOptions{ExpectPromptYes: true, PromptResult: true},
ExpectedError: "resource limit reached: 5",
})
}
Loading
Loading