Skip to content

Commit 91c5661

Browse files
authored
Add 'tcld migration' commands (#446)
1 parent 8ac5a03 commit 91c5661

5 files changed

Lines changed: 843 additions & 1 deletion

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: clean test bins lint tools
1+
.PHONY: clean test bins lint tools tcld
22
PROJECT_ROOT = github.com/temporalio/tcld
33

44
# default target

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,53 @@ The `--namespace-role` flag can be repeated for each namespace role the group sh
234234

235235
The account and namespace roles replace the definition, so any namespace roles omitted will be removed from the group level access.
236236

237+
# Migration Management (Preview)
238+
239+
*The Migration feature is currently in "Preview Release". Customers must be invited to use this feature. Please reach out to Temporal Cloud support for more information.*
240+
241+
Migrations provide a way to migrate a namespace and its workflow between a self-hosted Temporal server and Temporal Cloud. Migrations rely on active/passive replication built-in to Temporal. Before starting a migration, deploy the [s2s-proxy](https://github.com/temporalio/s2s-proxy/) alongside your self-hosted cluster and obtain a migration endpoint id from Temporal Cloud support. Please reach out to Temporal Cloud support for more information.
242+
243+
### Start a migration
244+
245+
To start a migration, provide the migration endpoint id and the source and target namespace names.
246+
Starting the migration enables active/passive namespace replication.
247+
248+
```
249+
tcld migration start --endpoint-id <endpoint-id> --source-namespace <source-namespace> --target-namespace <target-namespace>
250+
```
251+
252+
### Get a migration
253+
254+
```
255+
tcld migration get --id <migration-id>
256+
```
257+
258+
### Perform handover during a migration
259+
260+
To handover, provide the migration id and the replica id.
261+
Handover changes the active replica to the given replica.
262+
The active replica is the replica currently accepting write operations.
263+
264+
```
265+
tcld migration handover --id <migration-id> --to-replica-id <to-replica-id>
266+
```
267+
268+
### Confirm a migration
269+
270+
Confirming the migration completes the migration and disables replication.
271+
272+
```
273+
tcld migration confirm --id <migration-id>
274+
```
275+
276+
### Abort a migration
277+
278+
Aborting the migration cancels the migration and disables replication.
279+
280+
```
281+
tcld migration abort --id <migration-id>
282+
```
283+
237284
# Asynchronous Operations
238285
Any update operations making changes to the namespaces or user groups hosted on Temporal Cloud are asynchronous. Such operations are tracked using a `request-id` that can be passed in when invoking the update operation or will be auto-generated by the server if one is not specified. Once an asynchronous request is initiated, a `request-id` is returned. Use the `request get` command to query the status of an asynchronous request.
239286
```

app/migration.go

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
package app
2+
3+
import (
4+
"context"
5+
6+
"github.com/urfave/cli/v2"
7+
"google.golang.org/grpc"
8+
9+
"github.com/temporalio/tcld/protogen/api/cloud/cloudservice/v1"
10+
"github.com/temporalio/tcld/protogen/api/cloud/namespace/v1"
11+
)
12+
13+
type (
14+
MigrationClient struct {
15+
client cloudservice.CloudServiceClient
16+
ctx context.Context
17+
}
18+
GetMigrationClientFn func(ctx *cli.Context) (*MigrationClient, error)
19+
)
20+
21+
func NewMigrationClient(ctx context.Context, conn *grpc.ClientConn) *MigrationClient {
22+
return &MigrationClient{
23+
client: cloudservice.NewCloudServiceClient(conn),
24+
ctx: ctx,
25+
}
26+
}
27+
28+
func GetMigrationClient(ctx *cli.Context) (*MigrationClient, error) {
29+
ct, conn, err := GetServerConnection(ctx)
30+
if err != nil {
31+
return nil, err
32+
}
33+
return NewMigrationClient(ct, conn), nil
34+
}
35+
36+
func (c *MigrationClient) getMigration(migrationId string) (*namespace.Migration, error) {
37+
resp, err := c.client.GetMigration(c.ctx, &cloudservice.GetMigrationRequest{
38+
MigrationId: migrationId,
39+
})
40+
if err != nil {
41+
return nil, err
42+
}
43+
return resp.Migration, nil
44+
}
45+
46+
func (c *MigrationClient) listMigrations() error {
47+
totalRes := &cloudservice.GetMigrationsResponse{}
48+
pageToken := ""
49+
for {
50+
resp, err := c.client.GetMigrations(c.ctx, &cloudservice.GetMigrationsRequest{
51+
PageToken: pageToken,
52+
})
53+
if err != nil {
54+
return err
55+
}
56+
totalRes.Migrations = append(totalRes.Migrations, resp.Migrations...)
57+
pageToken = resp.NextPageToken
58+
if len(pageToken) == 0 {
59+
return PrintProto(totalRes)
60+
}
61+
}
62+
}
63+
64+
func (c *MigrationClient) startMigration(requestId, migrationEndpointId, sourceNamespace, targetNamespace string) error {
65+
resp, err := c.client.StartMigration(c.ctx, &cloudservice.StartMigrationRequest{
66+
Spec: &namespace.MigrationSpec{
67+
MigrationEndpointId: migrationEndpointId,
68+
Spec: &namespace.MigrationSpec_ToCloudSpec{
69+
ToCloudSpec: &namespace.MigrationToCloudSpec{
70+
SourceNamespace: sourceNamespace,
71+
TargetNamespace: targetNamespace,
72+
},
73+
},
74+
},
75+
AsyncOperationId: requestId,
76+
})
77+
if err != nil {
78+
return err
79+
}
80+
return PrintProto(resp)
81+
}
82+
83+
func (c *MigrationClient) migrationHandover(requestId, migrationId, replicaId string) error {
84+
resp, err := c.client.HandoverNamespace(c.ctx, &cloudservice.HandoverNamespaceRequest{
85+
MigrationId: migrationId,
86+
ToReplicaId: replicaId,
87+
AsyncOperationId: requestId,
88+
})
89+
if err != nil {
90+
return err
91+
}
92+
return PrintProto(resp)
93+
}
94+
95+
func (c *MigrationClient) confirmMigration(requestId, migrationId string) error {
96+
resp, err := c.client.ConfirmMigration(c.ctx, &cloudservice.ConfirmMigrationRequest{
97+
MigrationId: migrationId,
98+
AsyncOperationId: requestId,
99+
})
100+
if err != nil {
101+
return err
102+
}
103+
return PrintProto(resp)
104+
}
105+
106+
func (c *MigrationClient) abortMigration(requestId, migrationId string) error {
107+
resp, err := c.client.AbortMigration(c.ctx, &cloudservice.AbortMigrationRequest{
108+
MigrationId: migrationId,
109+
AsyncOperationId: requestId,
110+
})
111+
if err != nil {
112+
return err
113+
}
114+
return PrintProto(resp)
115+
}
116+
117+
func NewMigrationCommand(getMigrationClient GetMigrationClientFn) (CommandOut, error) {
118+
var c *MigrationClient
119+
migrationIdFlag := &cli.StringFlag{
120+
Name: "id",
121+
Aliases: []string{"i"},
122+
Usage: "Migration id",
123+
Required: true,
124+
}
125+
migrationEndpointIdFlag := &cli.StringFlag{
126+
Name: "endpoint-id",
127+
Aliases: []string{"e"},
128+
Usage: "Migration endpoint id",
129+
Required: true,
130+
}
131+
sourceNamespaceFlag := &cli.StringFlag{
132+
Name: "source-namespace",
133+
Aliases: []string{"s"},
134+
Usage: "Source namespace name",
135+
Required: true,
136+
}
137+
targetNamespaceFlag := &cli.StringFlag{
138+
Name: "target-namespace",
139+
Aliases: []string{"t"},
140+
Usage: "Target namespace name",
141+
Required: true,
142+
}
143+
toReplicaIdFlag := &cli.StringFlag{
144+
Name: "to-replica-id",
145+
Aliases: []string{"rp"},
146+
Usage: "The id of the replica to make active",
147+
Required: true,
148+
}
149+
150+
return CommandOut{
151+
Command: &cli.Command{
152+
Name: "migration",
153+
Aliases: []string{"m"},
154+
Before: func(ctx *cli.Context) error {
155+
var err error
156+
c, err = getMigrationClient(ctx)
157+
return err
158+
},
159+
Usage: "(private preview) Manage migrations between self-hosted Temporal and Temporal cloud",
160+
Subcommands: []*cli.Command{
161+
{
162+
Name: "get",
163+
Aliases: []string{"g"},
164+
Usage: "Get a migration",
165+
Flags: []cli.Flag{
166+
migrationIdFlag,
167+
},
168+
Action: func(ctx *cli.Context) error {
169+
id := ctx.String(migrationIdFlag.Name)
170+
m, err := c.getMigration(id)
171+
if err != nil {
172+
return err
173+
}
174+
return PrintProto(m)
175+
},
176+
},
177+
{
178+
Name: "list",
179+
Aliases: []string{"l"},
180+
Usage: "List migrations",
181+
Flags: []cli.Flag{},
182+
Action: func(ctx *cli.Context) error {
183+
return c.listMigrations()
184+
},
185+
},
186+
{
187+
Name: "start",
188+
Aliases: []string{"s"},
189+
Usage: "Start a new migration",
190+
Flags: []cli.Flag{
191+
RequestIDFlag,
192+
migrationEndpointIdFlag,
193+
sourceNamespaceFlag,
194+
targetNamespaceFlag,
195+
},
196+
Action: func(ctx *cli.Context) error {
197+
return c.startMigration(
198+
ctx.String(RequestIDFlag.Name),
199+
ctx.String(migrationEndpointIdFlag.Name),
200+
ctx.String(sourceNamespaceFlag.Name),
201+
ctx.String(targetNamespaceFlag.Name),
202+
)
203+
},
204+
},
205+
{
206+
Name: "handover",
207+
Aliases: []string{"s"},
208+
Usage: "Handover the namespace from on-prem to cloud, or from cloud back to on-prem",
209+
Flags: []cli.Flag{
210+
RequestIDFlag,
211+
migrationIdFlag,
212+
toReplicaIdFlag,
213+
},
214+
Action: func(ctx *cli.Context) error {
215+
return c.migrationHandover(
216+
ctx.String(RequestIDFlag.Name),
217+
ctx.String(migrationIdFlag.Name),
218+
ctx.String(toReplicaIdFlag.Name),
219+
)
220+
},
221+
},
222+
{
223+
Name: "confirm",
224+
Aliases: []string{"c"},
225+
Usage: "Confirm the migration",
226+
Flags: []cli.Flag{
227+
RequestIDFlag,
228+
migrationIdFlag,
229+
},
230+
Action: func(ctx *cli.Context) error {
231+
return c.confirmMigration(
232+
ctx.String(RequestIDFlag.Name),
233+
ctx.String(migrationIdFlag.Name),
234+
)
235+
},
236+
},
237+
{
238+
Name: "abort",
239+
Aliases: []string{"a"},
240+
Usage: "Abort the migration",
241+
Flags: []cli.Flag{
242+
RequestIDFlag,
243+
migrationIdFlag,
244+
},
245+
Action: func(ctx *cli.Context) error {
246+
return c.abortMigration(
247+
ctx.String(RequestIDFlag.Name),
248+
ctx.String(migrationIdFlag.Name),
249+
)
250+
},
251+
},
252+
},
253+
},
254+
}, nil
255+
}

0 commit comments

Comments
 (0)