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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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.18.0
go.temporal.io/cloud-sdk v0.19.0
go.temporal.io/sdk v1.44.1
go.temporal.io/sdk/contrib/envconfig v1.0.0
golang.org/x/oauth2 v0.36.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ go.temporal.io/api v1.63.6-0.20260811224032-89570b10e9e2 h1:VeNTxlOHETym01zX6kSX
go.temporal.io/api v1.63.6-0.20260811224032-89570b10e9e2/go.mod h1:SrlW2JMwVlDP4nRWSNznUFqnSHd+YeMDS1BkYo63HCQ=
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/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.

63 changes: 63 additions & 0 deletions temporalcloudcli/async/mock/mock.go

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

14 changes: 14 additions & 0 deletions temporalcloudcli/async/poller.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ type (
HandleCreateAsyncOperationResponse(ctx context.Context, response RespWithAsyncOp, err error) error
HandleUpdateOperation(ctx context.Context, response RespWithAsyncOp, err error) error
HandleDeleteOperation(ctx context.Context, response RespWithAsyncOp, err error) error
// HandleOperation surfaces an operation the server already treats as idempotent, so
// there is no "already in the desired state" rejection for the client to translate.
HandleOperation(ctx context.Context, response RespWithAsyncOp, err error) error
// AwaitAsyncOperation blocks until the operation with the given ID reaches a terminal state,
// then prints the final operation details. Use this when you already have an operation ID
// and just need to wait for it to complete.
Expand Down Expand Up @@ -100,6 +103,17 @@ func (p *poller) HandleDeleteOperation(
return p.handleAsyncOperation(ctx, response)
}

func (p *poller) HandleOperation(
ctx context.Context,
response RespWithAsyncOp,
err error,
) error {
if err != nil {
return fmt.Errorf("operation failed: %w", err)
}
return p.handleAsyncOperation(ctx, response)
}

func (p *poller) AwaitAsyncOperation(ctx context.Context, asyncOpID string) error {
finalOp, err := p.waitForAsyncOperation(ctx, asyncOpID)
if err != nil {
Expand Down
49 changes: 49 additions & 0 deletions temporalcloudcli/commands.gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -1896,6 +1896,7 @@ func NewCloudNamespaceCommand(cctx *CommandContext, parent *CloudCommand) *Cloud
s.Command.AddCommand(&NewCloudNamespaceHaCommand(cctx, &s).Command)
s.Command.AddCommand(&NewCloudNamespaceLifecycleCommand(cctx, &s).Command)
s.Command.AddCommand(&NewCloudNamespaceListCommand(cctx, &s).Command)
s.Command.AddCommand(&NewCloudNamespaceMoveToProjectCommand(cctx, &s).Command)
s.Command.AddCommand(&NewCloudNamespaceMtlsCommand(cctx, &s).Command)
s.Command.AddCommand(&NewCloudNamespaceRetentionCommand(cctx, &s).Command)
s.Command.AddCommand(&NewCloudNamespaceSearchAttributeCommand(cctx, &s).Command)
Expand Down Expand Up @@ -3670,6 +3671,54 @@ func NewCloudNamespaceListCommand(cctx *CommandContext, parent *CloudNamespaceCo
return &s
}

type CloudNamespaceMoveToProjectCommand struct {
Parent *CloudNamespaceCommand
Command cobra.Command
ClientOptions
NamespaceOptions
ResourceVersionOptions
DestinationProjectId string
SourceProjectId string
ConnectivityRuleId []string
NoConnectivityRules bool
AsyncOperationId string
Async bool
PollInterval cliext.FlagDuration
}

func NewCloudNamespaceMoveToProjectCommand(cctx *CommandContext, parent *CloudNamespaceCommand) *CloudNamespaceMoveToProjectCommand {
var s CloudNamespaceMoveToProjectCommand
s.Parent = parent
s.Command.DisableFlagsInUseLine = true
s.Command.Use = "move-to-project [flags]"
s.Command.Short = "Move a namespace to a different project"
if hasHighlighting {
s.Command.Long = "Move a namespace to a different project within the same account.\n\nThe namespace keeps its identity, endpoint, and running workflow\nexecutions. Only its project and the access derived from that project\nchange. Principals who reached the namespace only through the source\nproject lose access once the move completes.\n\nConnectivity rules are scoped to a project, so the rules the namespace\nshould have in the destination must be chosen in the same command. Pass\n--connectivity-rule-id once per rule, or --no-connectivity-rules to move\nthe namespace without any. Omit both only when the namespace has no\nconnectivity rules today.\n\nExample:\n\n\x1b[1mtemporal cloud namespace move-to-project \\\n --namespace my-namespace.my-account \\\n --destination-project-id my-destination-project \\\n --source-project-id my-source-project \\\n --connectivity-rule-id <rule-id>\x1b[0m"
} else {
s.Command.Long = "Move a namespace to a different project within the same account.\n\nThe namespace keeps its identity, endpoint, and running workflow\nexecutions. Only its project and the access derived from that project\nchange. Principals who reached the namespace only through the source\nproject lose access once the move completes.\n\nConnectivity rules are scoped to a project, so the rules the namespace\nshould have in the destination must be chosen in the same command. Pass\n--connectivity-rule-id once per rule, or --no-connectivity-rules to move\nthe namespace without any. Omit both only when the namespace has no\nconnectivity rules today.\n\nExample:\n\n```\ntemporal cloud namespace move-to-project \\\n --namespace my-namespace.my-account \\\n --destination-project-id my-destination-project \\\n --source-project-id my-source-project \\\n --connectivity-rule-id <rule-id>\n```"
}
s.Command.Args = cobra.NoArgs
s.Command.Flags().StringVar(&s.DestinationProjectId, "destination-project-id", "", "The ID of the project to move the namespace 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 namespace is expected to currently belong to. The move fails if the namespace has since moved elsewhere. Required.")
_ = cobra.MarkFlagRequired(s.Command.Flags(), "source-project-id")
s.Command.Flags().StringArrayVar(&s.ConnectivityRuleId, "connectivity-rule-id", nil, "The ID of a connectivity rule the namespace should have in the destination project. Repeat to specify multiple. Mutually exclusive with --no-connectivity-rules.")
s.Command.Flags().BoolVar(&s.NoConnectivityRules, "no-connectivity-rules", false, "Move the namespace without any connectivity rules in the destination project. Mutually exclusive with --connectivity-rule-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.NamespaceOptions.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 CloudNamespaceMtlsCommand struct {
Parent *CloudNamespaceCommand
Command cobra.Command
Expand Down
74 changes: 74 additions & 0 deletions temporalcloudcli/commands.namespace.move.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package temporalcloudcli

import (
"errors"
"fmt"

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

func (c *CloudNamespaceMoveToProjectCommand) run(cctx *CommandContext, _ []string) error {
if c.NoConnectivityRules && len(c.ConnectivityRuleId) > 0 {
return errors.New("--connectivity-rule-id and --no-connectivity-rules are mutually exclusive")
}
seen := make(map[string]struct{}, len(c.ConnectivityRuleId))
for _, id := range c.ConnectivityRuleId {
if _, dup := seen[id]; dup {
return fmt.Errorf("connectivity rule ID %q specified more than once", id)
}
seen[id] = struct{}{}
}

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

yes, err := cctx.GetPrompter().PromptYes(fmt.Sprintf(
"Move namespace %q from project %q to project %q",
c.Namespace, ns.GetProjectId(), c.DestinationProjectId,
))
if err != nil {
return err
}
if !yes {
return errors.New("Aborting move.")
}

rv := ns.ResourceVersion
if c.ResourceVersion != "" {
rv = c.ResourceVersion
}
req := &cloudservice.MoveNamespaceToProjectRequest{
Namespace: c.Namespace,
DestinationProjectId: c.DestinationProjectId,
ExpectedSourceProjectId: c.SourceProjectId,
ResourceVersion: rv,
AsyncOperationId: c.AsyncOperationId,
}
// Leaving the oneof unset asserts the namespace has no rules to carry over; the server
// rejects that when it does. It is not the same as selecting no rules.
switch {
case c.NoConnectivityRules:
req.DestinationConnectivityRules = &cloudservice.MoveNamespaceToProjectRequest_Unrestricted{
Unrestricted: &cloudservice.NoConnectivityRules{},
}
case len(c.ConnectivityRuleId) > 0:
req.DestinationConnectivityRules = &cloudservice.MoveNamespaceToProjectRequest_RuleIds{
RuleIds: &cloudservice.ConnectivityRuleIDs{ConnectivityRuleIds: c.ConnectivityRuleId},
}
}

resp, err := client.MoveNamespaceToProject(cctx, req)
poller := cctx.GetPoller(client, AsyncOperationOptions{
AsyncOperationId: c.AsyncOperationId,
Async: c.Async,
PollInterval: c.PollInterval,
})
return poller.HandleOperation(cctx, resp, err)
}
Loading
Loading