diff --git a/cmd/products.gen.go b/cmd/products.gen.go index f539d1327c..1b48110afa 100644 --- a/cmd/products.gen.go +++ b/cmd/products.gen.go @@ -24,6 +24,7 @@ import ( "github.com/ucloud/ucloud-cli/products/udisk" "github.com/ucloud/ucloud-cli/products/udns" "github.com/ucloud/ucloud-cli/products/udpn" + "github.com/ucloud/ucloud-cli/products/udts" "github.com/ucloud/ucloud-cli/products/ufs" "github.com/ucloud/ucloud-cli/products/ugn" "github.com/ucloud/ucloud-cli/products/uhadoop" @@ -66,6 +67,7 @@ func registeredProducts() []cli.Product { udisk.New(), udns.New(), udpn.New(), + udts.New(), ufs.New(), ugn.New(), uhadoop.New(), diff --git a/products/udts/internal/udts/cmd.go b/products/udts/internal/udts/cmd.go new file mode 100644 index 0000000000..05eddb5bef --- /dev/null +++ b/products/udts/internal/udts/cmd.go @@ -0,0 +1,28 @@ +package udts + +import ( + "github.com/spf13/cobra" + + "github.com/ucloud/ucloud-cli/pkg/cli" +) + +// NewCommand builds the `udts` root command. +func NewCommand(ctx *cli.Context) *cobra.Command { + cmd := &cobra.Command{ + Use: "udts", + Short: "Manage UDTS data transmission tasks", + Long: "Manage UDTS (UCloud Data Transmission Service) tasks for data migration and integration.", + } + // Verbs added in subsequent tasks: list, create, delete, start, stop, status, history, precheck, precheck-result + cmd.AddCommand(newList(ctx)) + cmd.AddCommand(newStatus(ctx)) + cmd.AddCommand(newDescribe(ctx)) + cmd.AddCommand(newStart(ctx)) + cmd.AddCommand(newStop(ctx)) + cmd.AddCommand(newCreate(ctx)) + cmd.AddCommand(newDelete(ctx)) + cmd.AddCommand(newHistory(ctx)) + cmd.AddCommand(newPrecheck(ctx)) + cmd.AddCommand(newPrecheckResult(ctx)) + return cmd +} diff --git a/products/udts/internal/udts/completion.go b/products/udts/internal/udts/completion.go new file mode 100644 index 0000000000..4f13c84260 --- /dev/null +++ b/products/udts/internal/udts/completion.go @@ -0,0 +1,58 @@ +package udts + +import ( + "fmt" + + udtssdk "github.com/ucloud/ucloud-sdk-go/services/udts" + sdk "github.com/ucloud/ucloud-sdk-go/ucloud" + + "github.com/ucloud/ucloud-cli/pkg/cli" +) + +// listTaskIDs returns the resource-id completion candidates for --task-id, +// in the conventional "taskId/name" form. states, when non-nil, restricts +// candidates to those whose Status is in the set. +func listTaskIDs(ctx *cli.Context, states []string, region, projectID string) []string { + client := cli.NewServiceClient(ctx, udtssdk.NewClient) + req := client.NewListUDTSTaskRequest() + req.Region = sdk.String(region) + req.ProjectId = sdk.String(projectID) + req.Limit = sdk.String("100") + + resp, err := client.ListUDTSTask(req) + if err != nil { + // Completion must never error out the shell. + return nil + } + + candidates := make([]string, 0, len(resp.Data)) + for _, t := range resp.Data { + if !stateAllowed(t.Status, states) { + continue + } + candidates = append(candidates, fmt.Sprintf("%s/%s", t.TaskId, t.Name)) + } + return candidates +} + +// stateAllowed reports whether state passes the optional allow-list. +// A nil allow-list means "any state". +func stateAllowed(state string, states []string) bool { + if states == nil { + return true + } + for _, s := range states { + if s == state { + return true + } + } + return false +} + +// derefStr safely dereferences a *string bound by a flag. +func derefStr(p *string) string { + if p == nil { + return "" + } + return *p +} diff --git a/products/udts/internal/udts/create.go b/products/udts/internal/udts/create.go new file mode 100644 index 0000000000..49bc4b98f6 --- /dev/null +++ b/products/udts/internal/udts/create.go @@ -0,0 +1,241 @@ +package udts + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/spf13/cobra" + + udtssdk "github.com/ucloud/ucloud-sdk-go/services/udts" + + "github.com/ucloud/ucloud-cli/pkg/cli" + "github.com/ucloud/ucloud-cli/pkg/command" +) + +// Static enum candidate sets shared by create and precheck. +var ( + taskTypeValues = []string{"transfer", "integration"} + srcModeValues = []string{"full", "incremental", "full+incremental", "bidirectional"} + srcNwTypeValues = []string{"public", "user", "dedicated_line"} + srcServiceTypeValues = []string{"small", "medium", "large", "2xlarge", "4xlarge"} + tgtNwTypeValues = []string{"user"} +) + +func newCreate(ctx *cli.Context) *cobra.Command { + client := cli.NewServiceClient(ctx, udtssdk.NewClient) + req := &createUDTSTaskRequest{} + + var async bool + var yes bool + var configPath string + var srcSSLCAFile, srcSSLCertFile, srcSSLKeyFile string + var tgtSSLCAFile, tgtSSLCertFile, tgtSSLKeyFile string + + // Allocate Source/Target so flag binding into node pointers works. + req.Source = make([]sourceParam, 1) + req.Source[0].MySQLNode = &sourceMySQLNode{} + req.Source[0].MySQLNode.SyncData = &udtssdk.CreateUDTSTaskParamSourceMySQLNodeSyncData{} + req.Source[0].MongoNode = &sourceMongoNode{} + req.Source[0].RedisNode = &sourceRedisNode{} + req.Source[0].TiDBNode = &sourceTiDBNode{} + req.Target = &target{} + req.Target.MySQLNode = &targetMySQLNode{} + req.Target.MongoNode = &targetMongoNode{} + req.Target.RedisNode = &targetRedisNode{} + req.Target.TiDBNode = &targetTiDBNode{} + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a UDTS task", + Long: "Create a UDTS data transmission task. For mysql-to-mysql full migration, use flat flags; for other database types or advanced options, use --config with a JSON file. It is recommended to run udts precheck first and confirm the precheck result passes before creating a task.", + Run: func(c *cobra.Command, args []string) { + ok, err := ctx.Confirm(yes, "It is recommended to run 'udts precheck' first. Have you confirmed the precheck result passes?") + if err != nil { + ctx.HandleError(err) + return + } + if !ok { + return + } + if configPath != "" { + if err := loadCreateConfig(configPath, req); err != nil { + ctx.HandleError(fmt.Errorf("load --config: %w", err)) + return + } + } + if err := applySrcSSLFlags(srcSSLCAFile, srcSSLCertFile, srcSSLKeyFile, req.Source[0].MySQLNode); err != nil { + ctx.HandleError(err) + return + } + if err := applyTgtSSLFlags(tgtSSLCAFile, tgtSSLCertFile, tgtSSLKeyFile, req.Target.MySQLNode); err != nil { + ctx.HandleError(err) + return + } + w := ctx.ProgressWriter() + resp, err := createUDTSTask(client, req) + if err != nil { + ctx.HandleError(err) + return + } + taskID := resp.taskID() + text := fmt.Sprintf("%s[%s] is creating", commandName, taskID) + if async { + fmt.Fprintln(w, text) + } else { + ctx.PollerTo(w, describeByID(ctx)).Spoll(taskID, text, []string{ + stateCreated, stateChecking, stateFailed, stateFailedUnrecoverable, + }) + } + ctx.EmitResult(cli.OpResultRow{ResourceID: taskID, Action: "create", Status: "Creating"}) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + // Common flags + req.Name = flags.String("name", "", "Required. Task name, max 128 characters.") + req.Type = flags.String("type", "", "Required. Task type: transfer or integration.") + req.MaxRetryCount = flags.String("max-retry-count", "0", "Optional. Max retry count (0-5).") + req.Remark = flags.String("remark", "", "Optional. Remark.") + flags.StringVar(&configPath, "config", "", "Optional. JSON file with full Source/Target configuration.") + flags.BoolVar(&async, "async", false, "Optional. Do not wait for creation to finish.") + + // Source flags (mysql common case) + req.Source[0].DataType = flags.String("src-data-type", "mysql", "Optional. Source database type.") + req.Source[0].Mode = flags.String("src-mode", "", "Optional. Source migration mode: full/incremental/full+incremental/bidirectional.") + req.Source[0].NWType = flags.String("src-nwtype", "", "Optional. Source network type: public/user/dedicated_line.") + req.Source[0].ServiceType = flags.String("src-service-type", "", "Optional. Service type: small/medium/large/2xlarge/4xlarge.") + req.Source[0].BandwidthLimit = flags.Int("src-bandwidth-limit", 0, "Optional. Source bandwidth limit in MB/s.") + req.Source[0].MySQLNode.Host = flags.String("src-host", "", "Optional. Source MySQL host.") + req.Source[0].MySQLNode.Port = flags.Int("src-port", 3306, "Optional. Source MySQL port.") + req.Source[0].MySQLNode.User = flags.String("src-user", "", "Optional. Source MySQL username.") + req.Source[0].MySQLNode.Password = flags.String("src-password", "", "Optional. Source MySQL password.") + req.Source[0].MySQLNode.Database = flags.String("src-db", "", "Optional. Source database name.") + req.Source[0].MySQLNode.Table = flags.String("src-table", "", "Optional. Source table name.") + req.Source[0].MySQLNode.VPCId = flags.String("src-vpc-id", "", "Optional. Source VPC ID.") + req.Source[0].MySQLNode.SubnetId = flags.String("src-subnet-id", "", "Optional. Source subnet ID.") + req.Source[0].MySQLNode.DataRegion = flags.String("src-data-region", "", "Optional. Source data region.") + req.Source[0].MySQLNode.SyncData.BinlogName = flags.String("src-binlog-name", "", "Optional. Incremental binlog name (show master status).") + req.Source[0].MySQLNode.SyncData.BinlogPos = flags.Int("src-binlog-pos", 0, "Optional. Incremental binlog pos (show master status).") + req.Source[0].MySQLNode.SyncData.BinlogGTID = flags.String("src-binlog-gtid", "", "Optional. Incremental binlog GTID (show master status).") + flags.StringVar(&srcSSLCAFile, "src-ssl-ca-file", "", "Optional. Path to source MySQL SSL CA certificate file (PEM, base64-encoded by CLI).") + flags.StringVar(&srcSSLCertFile, "src-ssl-cert-file", "", "Optional. Path to source MySQL SSL client certificate file (PEM, base64-encoded by CLI).") + flags.StringVar(&srcSSLKeyFile, "src-ssl-key-file", "", "Optional. Path to source MySQL SSL client key file (PEM, base64-encoded by CLI).") + + // Source MongoDB flags (when src-data-type is mongo) + req.Source[0].MongoNode.Host = flags.String("src-mongo-host", "", "Optional. Source MongoDB host(s), comma-separated for replica set.") + req.Source[0].MongoNode.User = flags.String("src-mongo-user", "", "Optional. Source MongoDB username.") + req.Source[0].MongoNode.Password = flags.String("src-mongo-password", "", "Optional. Source MongoDB password.") + req.Source[0].MongoNode.Database = flags.String("src-mongo-db", "", "Optional. Source MongoDB database name to migrate.") + req.Source[0].MongoNode.Collection = flags.String("src-mongo-collection", "", "Optional. Source MongoDB collection name.") + req.Source[0].MongoNode.AuthenticationDB = flags.String("src-mongo-auth-db", "", "Optional. Source MongoDB authentication database (typically admin).") + + // Source Redis flags (when src-data-type is redis) + req.Source[0].RedisNode.Address = flags.String("src-redis-address", "", "Optional. Source Redis address (master; ';' separated for cluster).") + req.Source[0].RedisNode.Password = flags.String("src-redis-password", "", "Optional. Source Redis password.") + req.Source[0].RedisNode.Type = flags.String("src-redis-type", "", "Optional. Source Redis mode: cluster/standalone/udredis.") + req.Source[0].RedisNode.IsRump = flags.String("src-redis-is-rump", "", "Optional. Use rump for full migration (true/false).") + req.Source[0].RedisNode.VPCId = flags.String("src-redis-vpc-id", "", "Optional. Source Redis VPC ID.") + req.Source[0].RedisNode.SubnetId = flags.String("src-redis-subnet-id", "", "Optional. Source Redis subnet ID.") + req.Source[0].RedisNode.DataRegion = flags.String("src-redis-data-region", "", "Optional. Source Redis data region.") + + // Source TiDB flags (when src-data-type is tidb) + req.Source[0].TiDBNode.Host = flags.String("src-tidb-host", "", "Optional. Source TiDB host.") + req.Source[0].TiDBNode.Port = flags.Int("src-tidb-port", 6603, "Optional. Source TiDB port.") + req.Source[0].TiDBNode.User = flags.String("src-tidb-user", "", "Optional. Source TiDB username.") + req.Source[0].TiDBNode.Password = flags.String("src-tidb-password", "", "Optional. Source TiDB password.") + req.Source[0].TiDBNode.Database = flags.String("src-tidb-db", "", "Optional. Source TiDB database name.") + req.Source[0].TiDBNode.Table = flags.String("src-tidb-table", "", "Optional. Source TiDB table name.") + req.Source[0].TiDBNode.VPCId = flags.String("src-tidb-vpc-id", "", "Optional. Source TiDB VPC ID.") + req.Source[0].TiDBNode.SubnetId = flags.String("src-tidb-subnet-id", "", "Optional. Source TiDB subnet ID.") + req.Source[0].TiDBNode.DataRegion = flags.String("src-tidb-data-region", "", "Optional. Source TiDB data region.") + + // Target flags (mysql common case) + req.Target.DataType = flags.String("tgt-data-type", "mysql", "Optional. Target database type.") + req.Target.NWType = flags.String("tgt-nwtype", "", "Optional. Target network type (user).") + req.Target.BandwidthLimit = flags.Int("tgt-bandwidth-limit", 0, "Optional. Target bandwidth limit in MB/s.") + req.Target.MySQLNode.Host = flags.String("tgt-host", "", "Optional. Target MySQL host.") + req.Target.MySQLNode.Port = flags.Int("tgt-port", 3306, "Optional. Target MySQL port.") + req.Target.MySQLNode.User = flags.String("tgt-user", "", "Optional. Target MySQL username.") + req.Target.MySQLNode.Password = flags.String("tgt-password", "", "Optional. Target MySQL password.") + req.Target.MySQLNode.VPCId = flags.String("tgt-vpc-id", "", "Optional. Target VPC ID.") + req.Target.MySQLNode.SubnetId = flags.String("tgt-subnet-id", "", "Optional. Target subnet ID.") + req.Target.MySQLNode.DataRegion = flags.String("tgt-data-region", "", "Optional. Target data region.") + req.Target.MySQLNode.NoBinlog = flags.Bool("tgt-no-binlog", false, "Optional. Disable binlog on target during full migration.") + flags.StringVar(&tgtSSLCAFile, "tgt-ssl-ca-file", "", "Optional. Path to target MySQL SSL CA certificate file (PEM, base64-encoded by CLI).") + flags.StringVar(&tgtSSLCertFile, "tgt-ssl-cert-file", "", "Optional. Path to target MySQL SSL client certificate file (PEM, base64-encoded by CLI).") + flags.StringVar(&tgtSSLKeyFile, "tgt-ssl-key-file", "", "Optional. Path to target MySQL SSL client key file (PEM, base64-encoded by CLI).") + + // Target MongoDB flags (when tgt-data-type is mongo) + req.Target.MongoNode.Host = flags.String("tgt-mongo-host", "", "Optional. Target MongoDB host(s), comma-separated for replica set.") + req.Target.MongoNode.User = flags.String("tgt-mongo-user", "", "Optional. Target MongoDB username.") + req.Target.MongoNode.Password = flags.String("tgt-mongo-password", "", "Optional. Target MongoDB password.") + req.Target.MongoNode.AuthenticationDB = flags.String("tgt-mongo-auth-db", "", "Optional. Target MongoDB authentication database (typically admin).") + + // Target Redis flags (when tgt-data-type is redis) + req.Target.RedisNode.Address = flags.String("tgt-redis-address", "", "Optional. Target Redis address (master; ';' separated for cluster).") + req.Target.RedisNode.Password = flags.String("tgt-redis-password", "", "Optional. Target Redis password.") + req.Target.RedisNode.Type = flags.String("tgt-redis-type", "", "Optional. Target Redis mode: cluster/standalone/udredis.") + req.Target.RedisNode.IsRump = flags.String("tgt-redis-is-rump", "", "Optional. Use rump for full migration (true/false).") + req.Target.RedisNode.VPCId = flags.String("tgt-redis-vpc-id", "", "Optional. Target Redis VPC ID.") + req.Target.RedisNode.SubnetId = flags.String("tgt-redis-subnet-id", "", "Optional. Target Redis subnet ID.") + req.Target.RedisNode.DataRegion = flags.String("tgt-redis-data-region", "", "Optional. Target Redis data region.") + + // Target TiDB flags (when tgt-data-type is tidb) + req.Target.TiDBNode.Host = flags.String("tgt-tidb-host", "", "Optional. Target TiDB host.") + req.Target.TiDBNode.Port = flags.Int("tgt-tidb-port", 6603, "Optional. Target TiDB port.") + req.Target.TiDBNode.User = flags.String("tgt-tidb-user", "", "Optional. Target TiDB username.") + req.Target.TiDBNode.Password = flags.String("tgt-tidb-password", "", "Optional. Target TiDB password.") + req.Target.TiDBNode.Database = flags.String("tgt-tidb-db", "", "Optional. Target TiDB database name.") + req.Target.TiDBNode.VPCId = flags.String("tgt-tidb-vpc-id", "", "Optional. Target TiDB VPC ID.") + req.Target.TiDBNode.SubnetId = flags.String("tgt-tidb-subnet-id", "", "Optional. Target TiDB subnet ID.") + req.Target.TiDBNode.DataRegion = flags.String("tgt-tidb-data-region", "", "Optional. Target TiDB data region.") + + flags.BoolVarP(&yes, "yes", "y", false, "Optional. Skip the precheck confirmation prompt.") + + ctx.BindCommonParams(cmd, req) + + command.SetFlagValues(cmd, "type", taskTypeValues...) + command.SetFlagValues(cmd, "src-mode", srcModeValues...) + command.SetFlagValues(cmd, "src-nwtype", srcNwTypeValues...) + command.SetFlagValues(cmd, "src-service-type", srcServiceTypeValues...) + command.SetFlagValues(cmd, "tgt-nwtype", tgtNwTypeValues...) + + cmd.MarkFlagRequired("name") + cmd.MarkFlagRequired("type") + + return cmd +} + +// loadCreateConfig reads a JSON file and decodes its Source/Target fields +// into req. Flag-set scalar fields (Name/Type) take precedence over config. +func loadCreateConfig(path string, req *createUDTSTaskRequest) error { + data, err := os.ReadFile(path) + if err != nil { + return err + } + var cfg struct { + Source []sourceParam `json:"Source"` + Target *target `json:"Target"` + Name *string `json:"Name,omitempty"` + Type *string `json:"Type,omitempty"` + } + if err := json.Unmarshal(data, &cfg); err != nil { + return err + } + if len(cfg.Source) > 0 { + req.Source = cfg.Source + } + if cfg.Target != nil { + req.Target = cfg.Target + } + if cfg.Name != nil && *req.Name == "" { + req.Name = cfg.Name + } + if cfg.Type != nil && *req.Type == "" { + req.Type = cfg.Type + } + return nil +} diff --git a/products/udts/internal/udts/delete.go b/products/udts/internal/udts/delete.go new file mode 100644 index 0000000000..6fd2dcec77 --- /dev/null +++ b/products/udts/internal/udts/delete.go @@ -0,0 +1,70 @@ +package udts + +import ( + "fmt" + + "github.com/spf13/cobra" + + sdkudts "github.com/ucloud/ucloud-sdk-go/services/udts" + sdk "github.com/ucloud/ucloud-sdk-go/ucloud" + + "github.com/ucloud/ucloud-cli/pkg/cli" + "github.com/ucloud/ucloud-cli/pkg/command" +) + +func newDelete(ctx *cli.Context) *cobra.Command { + client := cli.NewServiceClient(ctx, sdkudts.NewClient) + req := &deleteUDTSTaskRequest{} + + var ids []string + var yes bool + var taskType string + + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete UDTS tasks", + Long: "Delete one or more UDTS tasks by task ID. This is a destructive operation.", + Run: func(c *cobra.Command, args []string) { + ok, err := ctx.Confirm(yes, "Are you sure you want to delete the task(s)?") + if err != nil { + ctx.HandleError(err) + return + } + if !ok { + return + } + w := ctx.ProgressWriter() + results := []cli.OpResultRow{} + for _, idName := range ids { + id := ctx.PickResourceID(idName) + req.TaskId = sdk.String(id) + req.Type = sdk.String(taskType) + if _, err := deleteUDTSTask(client, req); err != nil { + ctx.HandleError(err) + continue + } + fmt.Fprintf(w, "%s[%s] deleted\n", commandName, id) + results = append(results, cli.OpResultRow{ResourceID: id, Action: "delete", Status: "Deleted"}) + } + ctx.EmitResult(results...) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringSliceVar(&ids, resourceIDFlag, nil, "Required. Task ID(s) to delete.") + flags.BoolVarP(&yes, "yes", "y", false, "Optional. Skip the confirmation prompt.") + flags.StringVar(&taskType, "type", "transfer", "Optional. Task type: transfer or integration.") + + ctx.BindRegion(cmd, req) + ctx.BindProjectID(cmd, req) + + cmd.MarkFlagRequired(resourceIDFlag) + command.SetCompletion(cmd, resourceIDFlag, func() []string { + return listTaskIDs(ctx, nil, derefStr(req.Region), derefStr(req.ProjectId)) + }) + command.SetFlagValues(cmd, "type", taskTypeValues...) + + return cmd +} diff --git a/products/udts/internal/udts/describe.go b/products/udts/internal/udts/describe.go new file mode 100644 index 0000000000..9e124d1d64 --- /dev/null +++ b/products/udts/internal/udts/describe.go @@ -0,0 +1,206 @@ +package udts + +import ( + "fmt" + + "github.com/spf13/cobra" + + udtssdk "github.com/ucloud/ucloud-sdk-go/services/udts" + sdk "github.com/ucloud/ucloud-sdk-go/ucloud" + + "github.com/ucloud/ucloud-cli/pkg/cli" + "github.com/ucloud/ucloud-cli/pkg/command" +) + +func newDescribe(ctx *cli.Context) *cobra.Command { + client := cli.NewServiceClient(ctx, udtssdk.NewClient) + req := client.NewGetUDTSTaskConfigureRequest() + + var taskId string + + cmd := &cobra.Command{ + Use: "describe", + Short: "Show the configuration of a UDTS task", + Long: "Show the full source/target configuration of a UDTS task by task ID.", + Run: func(c *cobra.Command, args []string) { + req.TaskId = sdk.String(ctx.PickResourceID(taskId)) + resp, err := getUDTSTaskConfigure(client, req) + if err != nil { + ctx.HandleError(err) + return + } + d := resp.Data + rows := []cli.DescribeRow{ + {Attribute: "TaskId", Content: d.TaskId}, + {Attribute: "Name", Content: orDash(d.Name)}, + {Attribute: "Type", Content: orDash(d.Type)}, + {Attribute: "MaxRetryCount", Content: fmt.Sprintf("%d", d.MaxRetryCount)}, + } + rows = append(rows, sourceRows("Source", d.Source)...) + rows = append(rows, sourceRows("Target", d.Target)...) + ctx.PrintList(rows) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringVar(&taskId, resourceIDFlag, "", "Required. Task ID of the UDTS task.") + + ctx.BindRegion(cmd, req) + ctx.BindProjectID(cmd, req) + + cmd.MarkFlagRequired(resourceIDFlag) + command.SetCompletion(cmd, resourceIDFlag, func() []string { + return listTaskIDs(ctx, nil, derefStr(req.Region), derefStr(req.ProjectId)) + }) + + return cmd +} + +// sourceRows flattens a configSourceNode (used for both Source and Target in +// ConfigData) into DescribeRow pairs, prefixed by label ("Source" or "Target"). +// Only fields relevant to the node's DataType are emitted. +func sourceRows(label string, s configSourceNode) []cli.DescribeRow { + rows := []cli.DescribeRow{ + {Attribute: label + ".DataType", Content: orDash(s.DataType)}, + {Attribute: label + ".NWType", Content: orDash(s.NWType)}, + {Attribute: label + ".BandwidthLimit", Content: fmt.Sprintf("%d", s.BandwidthLimit)}, + } + switch s.DataType { + case "mysql": + n := s.MySQLNode + rows = append(rows, + cli.DescribeRow{Attribute: label + ".Host", Content: orDash(n.Host)}, + cli.DescribeRow{Attribute: label + ".Port", Content: fmt.Sprintf("%d", n.Port)}, + cli.DescribeRow{Attribute: label + ".User", Content: orDash(n.User)}, + cli.DescribeRow{Attribute: label + ".Database", Content: orDash(n.DataBase)}, + cli.DescribeRow{Attribute: label + ".Table", Content: orDash(n.Table)}, + cli.DescribeRow{Attribute: label + ".VPCId", Content: orDash(n.VPCId)}, + cli.DescribeRow{Attribute: label + ".SubnetId", Content: orDash(n.SubnetId)}, + cli.DescribeRow{Attribute: label + ".DataRegion", Content: orDash(n.DataRegion)}, + cli.DescribeRow{Attribute: label + ".BinlogName", Content: orDash(n.SyncData.BinlogName)}, + cli.DescribeRow{Attribute: label + ".BinlogPos", Content: fmt.Sprintf("%d", n.SyncData.BinlogPos)}, + cli.DescribeRow{Attribute: label + ".BinlogGTID", Content: orDash(n.SyncData.BinlogGTID)}, + ) + case "mongo": + n := s.MongoNode + rows = append(rows, + cli.DescribeRow{Attribute: label + ".Host", Content: orDash(n.Host)}, + cli.DescribeRow{Attribute: label + ".User", Content: orDash(n.User)}, + cli.DescribeRow{Attribute: label + ".Database", Content: orDash(n.Database)}, + cli.DescribeRow{Attribute: label + ".Collection", Content: orDash(n.Collection)}, + cli.DescribeRow{Attribute: label + ".AuthenticationDB", Content: orDash(n.AuthenticationDB)}, + cli.DescribeRow{Attribute: label + ".VPCId", Content: orDash(n.VPCId)}, + cli.DescribeRow{Attribute: label + ".SubnetId", Content: orDash(n.SubnetId)}, + cli.DescribeRow{Attribute: label + ".DataRegion", Content: orDash(n.DataRegion)}, + ) + case "tidb": + n := s.TiDBNode + rows = append(rows, + cli.DescribeRow{Attribute: label + ".Host", Content: orDash(n.Host)}, + cli.DescribeRow{Attribute: label + ".Port", Content: fmt.Sprintf("%d", n.Port)}, + cli.DescribeRow{Attribute: label + ".User", Content: orDash(n.User)}, + cli.DescribeRow{Attribute: label + ".Database", Content: orDash(n.DataBase)}, + cli.DescribeRow{Attribute: label + ".Table", Content: orDash(n.Table)}, + cli.DescribeRow{Attribute: label + ".VPCId", Content: orDash(n.VPCId)}, + cli.DescribeRow{Attribute: label + ".SubnetId", Content: orDash(n.SubnetId)}, + cli.DescribeRow{Attribute: label + ".DataRegion", Content: orDash(n.DataRegion)}, + ) + case "redis": + n := s.RedisNode + rows = append(rows, + cli.DescribeRow{Attribute: label + ".Address", Content: orDash(n.Address)}, + cli.DescribeRow{Attribute: label + ".Password", Content: orDash(n.Password)}, + cli.DescribeRow{Attribute: label + ".Type", Content: orDash(n.Type)}, + cli.DescribeRow{Attribute: label + ".IsRump", Content: fmt.Sprintf("%v", n.IsRump)}, + cli.DescribeRow{Attribute: label + ".DataRegion", Content: orDash(n.DataRegion)}, + cli.DescribeRow{Attribute: label + ".VPCId", Content: orDash(n.VPCId)}, + cli.DescribeRow{Attribute: label + ".SubnetId", Content: orDash(n.SubnetId)}, + ) + case "csv": + n := s.CSVNode + rows = append(rows, + cli.DescribeRow{Attribute: label + ".URL", Content: orDash(n.URL)}, + cli.DescribeRow{Attribute: label + ".DupAction", Content: orDash(n.DupAction)}, + cli.DescribeRow{Attribute: label + ".KeepExistData", Content: fmt.Sprintf("%v", n.KeepExistData)}, + cli.DescribeRow{Attribute: label + ".Columns", Content: orDash(n.Columns)}, + ) + case "dynamodb": + n := s.DynamodbNode + rows = append(rows, + cli.DescribeRow{Attribute: label + ".AccessKey", Content: orDash(n.AccessKey)}, + cli.DescribeRow{Attribute: label + ".AccessSecretKey", Content: orDash(n.AccessSecretKey)}, + cli.DescribeRow{Attribute: label + ".SessionToken", Content: orDash(n.SessionToken)}, + cli.DescribeRow{Attribute: label + ".SourceEndpointURL", Content: orDash(n.SourceEndpointURL)}, + cli.DescribeRow{Attribute: label + ".Region", Content: orDash(n.Region)}, + ) + case "es": + n := s.ElasticsearchNode + rows = append(rows, + cli.DescribeRow{Attribute: label + ".URL", Content: orDash(n.URL)}, + cli.DescribeRow{Attribute: label + ".User", Content: orDash(n.User)}, + cli.DescribeRow{Attribute: label + ".VPCId", Content: orDash(n.VPCId)}, + cli.DescribeRow{Attribute: label + ".SubnetId", Content: orDash(n.SubnetId)}, + cli.DescribeRow{Attribute: label + ".DataRegion", Content: orDash(n.DataRegion)}, + cli.DescribeRow{Attribute: label + ".ShardCount", Content: fmt.Sprintf("%d", n.ShardCount)}, + cli.DescribeRow{Attribute: label + ".ReplicaCount", Content: fmt.Sprintf("%d", n.ReplicaCount)}, + ) + case "kafka": + n := s.KafkaNode + rows = append(rows, + cli.DescribeRow{Attribute: label + ".BrokerServers", Content: orDash(n.BrokerServers)}, + cli.DescribeRow{Attribute: label + ".TopicPrefix", Content: orDash(n.TopicPrefix)}, + cli.DescribeRow{Attribute: label + ".TopicPartitionNums", Content: fmt.Sprintf("%d", n.TopicPartitionNums)}, + cli.DescribeRow{Attribute: label + ".VPCId", Content: orDash(n.VPCId)}, + cli.DescribeRow{Attribute: label + ".SubnetId", Content: orDash(n.SubnetId)}, + cli.DescribeRow{Attribute: label + ".DataRegion", Content: orDash(n.DataRegion)}, + ) + case "clickhouse": + n := s.ClickHouseNode + rows = append(rows, + cli.DescribeRow{Attribute: label + ".TCPAddress", Content: orDash(n.TCPAddress)}, + cli.DescribeRow{Attribute: label + ".HTTPAddress", Content: orDash(n.HTTPAddress)}, + cli.DescribeRow{Attribute: label + ".User", Content: orDash(n.User)}, + cli.DescribeRow{Attribute: label + ".VPCId", Content: orDash(n.VPCId)}, + cli.DescribeRow{Attribute: label + ".SubnetId", Content: orDash(n.SubnetId)}, + cli.DescribeRow{Attribute: label + ".DataRegion", Content: orDash(n.DataRegion)}, + cli.DescribeRow{Attribute: label + ".CreateTableMode", Content: orDash(n.CreateTableMode)}, + cli.DescribeRow{Attribute: label + ".TableEngine", Content: orDash(n.TableEngine)}, + ) + case "maxir": + n := s.MaxirNode + rows = append(rows, + cli.DescribeRow{Attribute: label + ".Host", Content: orDash(n.Host)}, + cli.DescribeRow{Attribute: label + ".Port", Content: fmt.Sprintf("%d", n.Port)}, + cli.DescribeRow{Attribute: label + ".User", Content: orDash(n.User)}, + cli.DescribeRow{Attribute: label + ".Database", Content: orDash(n.Database)}, + cli.DescribeRow{Attribute: label + ".VPCId", Content: orDash(n.VPCId)}, + cli.DescribeRow{Attribute: label + ".SubnetId", Content: orDash(n.SubnetId)}, + cli.DescribeRow{Attribute: label + ".DataRegion", Content: orDash(n.DataRegion)}, + ) + case "postgre": + n := s.PostgreNode + rows = append(rows, + cli.DescribeRow{Attribute: label + ".Host", Content: orDash(n.Host)}, + cli.DescribeRow{Attribute: label + ".Port", Content: fmt.Sprintf("%d", n.Port)}, + cli.DescribeRow{Attribute: label + ".User", Content: orDash(n.User)}, + cli.DescribeRow{Attribute: label + ".Database", Content: orDash(n.Database)}, + cli.DescribeRow{Attribute: label + ".VPCId", Content: orDash(n.VPCId)}, + cli.DescribeRow{Attribute: label + ".SubnetId", Content: orDash(n.SubnetId)}, + cli.DescribeRow{Attribute: label + ".DataRegion", Content: orDash(n.DataRegion)}, + ) + case "sqlserver": + n := s.SQLServerNode + rows = append(rows, + cli.DescribeRow{Attribute: label + ".Host", Content: orDash(n.Host)}, + cli.DescribeRow{Attribute: label + ".Port", Content: fmt.Sprintf("%d", n.Port)}, + cli.DescribeRow{Attribute: label + ".User", Content: orDash(n.User)}, + cli.DescribeRow{Attribute: label + ".Database", Content: orDash(n.Database)}, + cli.DescribeRow{Attribute: label + ".VPCId", Content: orDash(n.VPCId)}, + cli.DescribeRow{Attribute: label + ".SubnetId", Content: orDash(n.SubnetId)}, + cli.DescribeRow{Attribute: label + ".DataRegion", Content: orDash(n.DataRegion)}, + ) + } + return rows +} diff --git a/products/udts/internal/udts/history.go b/products/udts/internal/udts/history.go new file mode 100644 index 0000000000..71895bb7a7 --- /dev/null +++ b/products/udts/internal/udts/history.go @@ -0,0 +1,56 @@ +package udts + +import ( + "github.com/spf13/cobra" + + udtssdk "github.com/ucloud/ucloud-sdk-go/services/udts" + + "github.com/ucloud/ucloud-cli/pkg/cli" + "github.com/ucloud/ucloud-cli/pkg/command" +) + +func newHistory(ctx *cli.Context) *cobra.Command { + client := cli.NewServiceClient(ctx, udtssdk.NewClient) + req := client.NewGetUDTSTaskHistoryRequest() + + cmd := &cobra.Command{ + Use: "history", + Short: "Show state transition history of a UDTS task", + Long: "Show the state transition history of a UDTS task by task ID.", + Run: func(c *cobra.Command, args []string) { + *req.TaskId = ctx.PickResourceID(*req.TaskId) + resp, err := client.GetUDTSTaskHistory(req) + if err != nil { + ctx.HandleError(err) + return + } + rows := make([]historyRow, 0, len(resp.Data)) + for _, h := range resp.Data { + rows = append(rows, historyRow{ + CreateTimeH: h.CreateTimeH, + AntState: h.AntState, + AntID: h.AntID, + CreateTime: h.CreateTime, + }) + } + ctx.PrintList(rows) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + req.TaskId = flags.String(resourceIDFlag, "", "Required. Task ID of the UDTS task.") + req.Type = flags.String("type", "", "Optional. Task type: transfer or integration.") + + ctx.BindRegion(cmd, req) + ctx.BindProjectID(cmd, req) + + cmd.MarkFlagRequired(resourceIDFlag) + command.SetCompletion(cmd, resourceIDFlag, func() []string { + return listTaskIDs(ctx, nil, derefStr(req.Region), derefStr(req.ProjectId)) + }) + command.SetFlagValues(cmd, "type", taskTypeValues...) + + return cmd +} diff --git a/products/udts/internal/udts/invoke.go b/products/udts/internal/udts/invoke.go new file mode 100644 index 0000000000..9235ccec32 --- /dev/null +++ b/products/udts/internal/udts/invoke.go @@ -0,0 +1,597 @@ +package udts + +import ( + udtssdk "github.com/ucloud/ucloud-sdk-go/services/udts" + "github.com/ucloud/ucloud-sdk-go/ucloud/request" + "github.com/ucloud/ucloud-sdk-go/ucloud/response" +) + +// --- CreateUDTSTask (SDK response.Data mistyped; Target lacks SSLSecurity) --- + +// targetMySQLNode mirrors the SDK's TargetMySQLNode plus SSLSecurity. +// Fields are declared flat (not embedded) so JSON tags stay at the top level. +type targetMySQLNode struct { + DataRegion *string `json:"DataRegion,omitempty"` + Host *string `json:"Host,omitempty"` + NoBinlog *bool `json:"NoBinlog,omitempty"` + Password *string `json:"Password,omitempty"` + Port *int `json:"Port,omitempty"` + SubnetId *string `json:"SubnetId,omitempty"` + User *string `json:"User,omitempty"` + VPCId *string `json:"VPCId,omitempty"` + SSLSecurity *targetMySQLNodeSSLSecurity `json:"SSLSecurity,omitempty"` + NoLocks *bool `json:"NoLocks,omitempty"` + ResourceID *string `json:"ResourceID,omitempty"` + Zone *string `json:"Zone,omitempty"` +} + +type targetMySQLNodeSSLSecurity struct { + SSLCA *string `json:"SSLCA,omitempty"` + SSLCert *string `json:"SSLCert,omitempty"` + SSLKey *string `json:"SSLKey,omitempty"` +} + +// target extends the SDK's Target with the enriched MySQLNode. +type target struct { + BandwidthLimit *int `json:"BandwidthLimit,omitempty"` + DataType *string `json:"DataType,omitempty"` + Mode *string `json:"Mode,omitempty"` + MySQLNode *targetMySQLNode `json:"MySQLNode,omitempty"` + MongoNode *targetMongoNode `json:"MongoNode,omitempty"` + RedisNode *targetRedisNode `json:"RedisNode,omitempty"` + TiDBNode *targetTiDBNode `json:"TiDBNode,omitempty"` + CSVNode *csvNode `json:"CSVNode,omitempty"` + DynamodbNode *dynamodbNode `json:"DynamodbNode,omitempty"` + ElasticsearchNode *elasticsearchNode `json:"ElasticsearchNode,omitempty"` + KafkaNode *kafkaNode `json:"KafkaNode,omitempty"` + ClickHouseNode *clickHouseNode `json:"ClickHouseNode,omitempty"` + MaxirNode *maxirNode `json:"MaxirNode,omitempty"` + PostgreNode *postgreNode `json:"PostgreNode,omitempty"` + SQLServerNode *sqlServerNode `json:"SQLServerNode,omitempty"` + NWType *string `json:"NWType,omitempty"` +} + +// --- Source MySQL node (SDK type lacks NoLocks/ResourceID/Zone) --- + +// sourceMySQLNode mirrors the SDK's CreateUDTSTaskParamSourceMySQLNode but adds +// NoLocks, ResourceID, Zone (used by bidirectional/UDB scenarios) that the SDK +// request struct omits. Using a custom type ensures --config passthrough doesn't +// drop these fields. +type sourceMySQLNode struct { + DataRegion *string `json:"DataRegion,omitempty"` + Database *string `json:"Database,omitempty"` + DupAction *string `json:"DupAction,omitempty"` + Host *string `json:"Host,omitempty"` + KeepExistData *bool `json:"KeepExistData,omitempty"` + Password *string `json:"Password,omitempty"` + Port *int `json:"Port,omitempty"` + QueryData []udtssdk.CreateUDTSTaskParamSourceMySQLNodeQueryData `json:"QueryData,omitempty"` + SSLSecurity *udtssdk.CreateUDTSTaskParamSourceMySQLNodeSSLSecurity `json:"SSLSecurity,omitempty"` + SubnetId *string `json:"SubnetId,omitempty"` + SyncData *udtssdk.CreateUDTSTaskParamSourceMySQLNodeSyncData `json:"SyncData,omitempty"` + Table *string `json:"Table,omitempty"` + User *string `json:"User,omitempty"` + VPCId *string `json:"VPCId,omitempty"` + NoLocks *bool `json:"NoLocks,omitempty"` + ResourceID *string `json:"ResourceID,omitempty"` + Zone *string `json:"Zone,omitempty"` +} + +// --- MongoDB node types (SDK lacks MongoNode entirely) --- + +// sourceMongoNode is the source MongoDB node config. +// Per API doc: source has Database/Collection; target does not. +type sourceMongoNode struct { + Host *string `json:"Host,omitempty"` + User *string `json:"User,omitempty"` + Password *string `json:"Password,omitempty"` + Database *string `json:"Database,omitempty"` + Collection *string `json:"Collection,omitempty"` + DataRegion *string `json:"DataRegion,omitempty"` + VPCId *string `json:"VPCId,omitempty"` + SubnetId *string `json:"SubnetId,omitempty"` + AuthenticationDB *string `json:"AuthenticationDB,omitempty"` + Type *string `json:"Type,omitempty"` + MongosUrl *string `json:"MongosUrl,omitempty"` + MongocsUrl *string `json:"MongocsUrl,omitempty"` + MongosUser *string `json:"MongosUser,omitempty"` + MongosPwd *string `json:"MongosPwd,omitempty"` + MongocsUser *string `json:"MongocsUser,omitempty"` + MongocsPwd *string `json:"MongocsPwd,omitempty"` + KeepExistData *bool `json:"KeepExistData,omitempty"` + ResourceID *string `json:"ResourceID,omitempty"` + Zone *string `json:"Zone,omitempty"` +} + +// targetMongoNode is the target MongoDB node config (no Database/Collection). +type targetMongoNode struct { + Host *string `json:"Host,omitempty"` + User *string `json:"User,omitempty"` + Password *string `json:"Password,omitempty"` + DataRegion *string `json:"DataRegion,omitempty"` + VPCId *string `json:"VPCId,omitempty"` + SubnetId *string `json:"SubnetId,omitempty"` + AuthenticationDB *string `json:"AuthenticationDB,omitempty"` + Type *string `json:"Type,omitempty"` + MongosUrl *string `json:"MongosUrl,omitempty"` + MongocsUrl *string `json:"MongocsUrl,omitempty"` + MongosUser *string `json:"MongosUser,omitempty"` + MongosPwd *string `json:"MongosPwd,omitempty"` + MongocsUser *string `json:"MongocsUser,omitempty"` + MongocsPwd *string `json:"MongocsPwd,omitempty"` + KeepExistData *bool `json:"KeepExistData,omitempty"` + ResourceID *string `json:"ResourceID,omitempty"` + Zone *string `json:"Zone,omitempty"` +} + +// --- Redis node types (SDK request struct lacks RedisNode) --- + +// sourceRedisNode is the source Redis node config. +type sourceRedisNode struct { + Address *string `json:"Address,omitempty"` + Password *string `json:"Password,omitempty"` + Type *string `json:"Type,omitempty"` + IsRump *string `json:"IsRump,omitempty"` + DataRegion *string `json:"DataRegion,omitempty"` + VPCId *string `json:"VPCId,omitempty"` + SubnetId *string `json:"SubnetId,omitempty"` + ResourceID *string `json:"ResourceID,omitempty"` + Zone *string `json:"Zone,omitempty"` +} + +// targetRedisNode is the target Redis node config (same shape as source). +type targetRedisNode struct { + Address *string `json:"Address,omitempty"` + Password *string `json:"Password,omitempty"` + Type *string `json:"Type,omitempty"` + IsRump *string `json:"IsRump,omitempty"` + DataRegion *string `json:"DataRegion,omitempty"` + VPCId *string `json:"VPCId,omitempty"` + SubnetId *string `json:"SubnetId,omitempty"` + ResourceID *string `json:"ResourceID,omitempty"` + Zone *string `json:"Zone,omitempty"` +} + +// --- TiDB node types (SDK request struct lacks TiDBNode) --- + +// sourceTiDBNode is the source TiDB node config. +type sourceTiDBNode struct { + Host *string `json:"Host,omitempty"` + Port *int `json:"Port,omitempty"` + User *string `json:"User,omitempty"` + Password *string `json:"Password,omitempty"` + Database *string `json:"Database,omitempty"` + Table *string `json:"Table,omitempty"` + VPCId *string `json:"VPCId,omitempty"` + SubnetId *string `json:"SubnetId,omitempty"` + DataRegion *string `json:"DataRegion,omitempty"` +} + +// targetTiDBNode is the target TiDB node config (no Table). +type targetTiDBNode struct { + Host *string `json:"Host,omitempty"` + Port *int `json:"Port,omitempty"` + User *string `json:"User,omitempty"` + Password *string `json:"Password,omitempty"` + Database *string `json:"Database,omitempty"` + VPCId *string `json:"VPCId,omitempty"` + SubnetId *string `json:"SubnetId,omitempty"` + DataRegion *string `json:"DataRegion,omitempty"` + MySQLAutoIncrement *string `json:"MySQLAutoIncrement,omitempty"` +} + +// --- Other node types (SDK request struct lacks them; --config only) --- +// These mirror the API/console field names so --config JSON passes through +// without loss. Source and Target share the same struct; the encoder skips +// nil/empty fields, so unused fields on either side are omitted. + +type csvNode struct { + URL *string `json:"URL,omitempty"` + DupAction *string `json:"DupAction,omitempty"` + KeepExistData *bool `json:"KeepExistData,omitempty"` + Columns *string `json:"Columns,omitempty"` + UpdatePolicy []policyData `json:"UpdatePolicy,omitempty"` + SetPolicy []policyData `json:"SetPolicy,omitempty"` +} + +// policyData mirrors the API's PolicyData (used by CSV DupAction=update). +type policyData struct { + Column string `json:"Column"` + Data string `json:"Data"` + Type string `json:"Type"` +} + +type dynamodbNode struct { + AccessKey *string `json:"AccessKey,omitempty"` + AccessSecretKey *string `json:"AccessSecretKey,omitempty"` + SessionToken *string `json:"SessionToken,omitempty"` + SourceEndpointURL *string `json:"SourceEndpointURL,omitempty"` + Region *string `json:"Region,omitempty"` +} + +type elasticsearchNode struct { + VPCId *string `json:"VPCId,omitempty"` + SubnetId *string `json:"SubnetId,omitempty"` + DataRegion *string `json:"DataRegion,omitempty"` + Password *string `json:"Password,omitempty"` + User *string `json:"User,omitempty"` + URL *string `json:"URL,omitempty"` + KeepTarget *bool `json:"KeepTarget,omitempty"` + ShardCount *int `json:"ShardCount,omitempty"` + ReplicaCount *int `json:"ReplicaCount,omitempty"` +} + +type kafkaNode struct { + VPCId *string `json:"VPCId,omitempty"` + SubnetId *string `json:"SubnetId,omitempty"` + DataRegion *string `json:"DataRegion,omitempty"` + BrokerServers *string `json:"BrokerServers,omitempty"` + TopicPrefix *string `json:"TopicPrefix,omitempty"` + TopicPartitionNums *int `json:"TopicPartitionNums,omitempty"` +} + +type clickHouseNode struct { + VPCId *string `json:"VPCId,omitempty"` + SubnetId *string `json:"SubnetId,omitempty"` + DataRegion *string `json:"DataRegion,omitempty"` + TCPAddress *string `json:"TCPAddress,omitempty"` + HTTPAddress *string `json:"HTTPAddress,omitempty"` + Password *string `json:"Password,omitempty"` + User *string `json:"User,omitempty"` + CreateTableMode *string `json:"CreateTableMode,omitempty"` + TableEngine *string `json:"TableEngine,omitempty"` +} + +type maxirNode struct { + VPCId *string `json:"VPCId,omitempty"` + SubnetId *string `json:"SubnetId,omitempty"` + DataRegion *string `json:"DataRegion,omitempty"` + Host *string `json:"Host,omitempty"` + Port *int `json:"Port,omitempty"` + User *string `json:"User,omitempty"` + Password *string `json:"Password,omitempty"` + Database *string `json:"Database,omitempty"` +} + +type postgreNode struct { + Host *string `json:"Host,omitempty"` + Port *int `json:"Port,omitempty"` + User *string `json:"User,omitempty"` + Password *string `json:"Password,omitempty"` + Database *string `json:"Database,omitempty"` + VPCId *string `json:"VPCId,omitempty"` + SubnetId *string `json:"SubnetId,omitempty"` + DataRegion *string `json:"DataRegion,omitempty"` +} + +type sqlServerNode struct { + Host *string `json:"Host,omitempty"` + Port *int `json:"Port,omitempty"` + User *string `json:"User,omitempty"` + Password *string `json:"Password,omitempty"` + Database *string `json:"Database,omitempty"` + VPCId *string `json:"VPCId,omitempty"` + SubnetId *string `json:"SubnetId,omitempty"` + DataRegion *string `json:"DataRegion,omitempty"` +} + +// sourceParam mirrors the SDK's CreateUDTSTaskParamSource but adds MongoNode, +// RedisNode, TiDBNode (with flat flags), plus node structs for all other types +// (udw/csv/ufile/oracle/dynamodb/es/kafka/clickhouse/maxir/postgre/sqlserver/ +// udb/udb-mysql) that are passed through via --config only. +type sourceParam struct { + BandwidthLimit *int `json:"BandwidthLimit,omitempty"` + DataType *string `json:"DataType,omitempty"` + Mode *string `json:"Mode,omitempty"` + MySQLNode *sourceMySQLNode `json:"MySQLNode,omitempty"` + MongoNode *sourceMongoNode `json:"MongoNode,omitempty"` + RedisNode *sourceRedisNode `json:"RedisNode,omitempty"` + TiDBNode *sourceTiDBNode `json:"TiDBNode,omitempty"` + CSVNode *csvNode `json:"CSVNode,omitempty"` + DynamodbNode *dynamodbNode `json:"DynamodbNode,omitempty"` + ElasticsearchNode *elasticsearchNode `json:"ElasticsearchNode,omitempty"` + KafkaNode *kafkaNode `json:"KafkaNode,omitempty"` + ClickHouseNode *clickHouseNode `json:"ClickHouseNode,omitempty"` + MaxirNode *maxirNode `json:"MaxirNode,omitempty"` + PostgreNode *postgreNode `json:"PostgreNode,omitempty"` + SQLServerNode *sqlServerNode `json:"SQLServerNode,omitempty"` + NWType *string `json:"NWType,omitempty"` + ServiceType *string `json:"ServiceType,omitempty"` +} + +// createUDTSTaskRequest is the request schema for CreateUDTSTask. +// It mirrors the SDK request but uses a custom Target with SSLSecurity. +type createUDTSTaskRequest struct { + request.CommonBase + ChargeType *string `json:"ChargeType,omitempty"` + CouponId *string `json:"CouponId,omitempty"` + IsUnidirection *string `json:"IsUnidirection,omitempty"` + MaxRetryCount *string `json:"MaxRetryCount,omitempty"` + Name *string `json:"Name,omitempty"` + Quantity *int `json:"Quantity,omitempty"` + Query *string `json:"Query,omitempty"` + Remark *string `json:"Remark,omitempty"` + Source []sourceParam `json:"Source,omitempty"` + Target *target `json:"Target,omitempty"` + Type *string `json:"Type,omitempty"` +} + +// createUDTSTaskResData matches the API doc's CreateUDTSTaskResData. +type createUDTSTaskResData struct { + TaskId string `json:"TaskId"` +} + +// createUDTSTaskResponse matches the API doc's CreateUDTSTask response. +type createUDTSTaskResponse struct { + response.CommonBase + Data createUDTSTaskResData `json:"Data"` + TaskId []string `json:"TaskId"` +} + +func (r *createUDTSTaskResponse) taskID() string { + if r.Data.TaskId != "" { + return r.Data.TaskId + } + if len(r.TaskId) > 0 { + return r.TaskId[0] + } + return "" +} + +func createUDTSTask(client *udtssdk.UDTSClient, req *createUDTSTaskRequest) (*createUDTSTaskResponse, error) { + var resp createUDTSTaskResponse + if err := client.InvokeAction("CreateUDTSTask", req, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +// --- DeleteUDTSTask (no typed SDK method) --- + +// deleteUDTSTaskRequest is the request schema for the DeleteUDTSTask action. +type deleteUDTSTaskRequest struct { + request.CommonBase + TaskId *string `json:"TaskId,omitempty"` + Type *string `json:"Type,omitempty"` +} + +// deleteUDTSTaskResponse is the response schema for the DeleteUDTSTask action. +type deleteUDTSTaskResponse struct { + response.CommonBase +} + +// deleteUDTSTask calls the DeleteUDTSTask action via InvokeAction. +func deleteUDTSTask(client *udtssdk.UDTSClient, req *deleteUDTSTaskRequest) (*deleteUDTSTaskResponse, error) { + var resp deleteUDTSTaskResponse + if err := client.InvokeAction("DeleteUDTSTask", req, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +// --- CreateUDTSPrecheckTask (no typed SDK method) --- + +// createUDTSPrecheckTaskRequest is the request schema for CreateUDTSPrecheckTask. +// Source/Target reuse the SDK's CreateUDTSTaskParam* types (same shape). +type createUDTSPrecheckTaskRequest struct { + request.CommonBase + Name *string `json:"Name,omitempty"` + Type *string `json:"Type,omitempty"` + MaxRetryCount *string `json:"MaxRetryCount,omitempty"` + Remark *string `json:"Remark,omitempty"` + Source []sourceParam `json:"Source,omitempty"` + Target *target `json:"Target,omitempty"` +} + +// createUDTSPrecheckTaskResponse holds the returned precheck TaskID(s). +type createUDTSPrecheckTaskResponse struct { + response.CommonBase + TaskID []string `json:"TaskID"` +} + +// createUDTSPrecheckTask calls CreateUDTSPrecheckTask via InvokeAction. +func createUDTSPrecheckTask(client *udtssdk.UDTSClient, req *createUDTSPrecheckTaskRequest) (*createUDTSPrecheckTaskResponse, error) { + var resp createUDTSPrecheckTaskResponse + if err := client.InvokeAction("CreateUDTSPrecheckTask", req, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +// --- GetUDTSTaskConfigure (SDK response Source lacks MongoNode) --- + +// configRedisNode holds the Redis node config from GetUDTSTaskConfigure. +// Uses bool for IsRump (API returns bool), unlike the SDK's string type. +type configRedisNode struct { + Address string `json:"Address"` + Password string `json:"Password"` + Type string `json:"Type"` + IsRump bool `json:"IsRump"` + DataRegion string `json:"DataRegion"` + VPCId string `json:"VPCId"` + SubnetId string `json:"SubnetId"` + ResourceID string `json:"ResourceID"` + Zone string `json:"Zone"` +} + +// configSourceNode mirrors the SDK's Source but adds MongoNode and all other +// node types the SDK response model lacks, so GetUDTSTaskConfigure doesn't +// silently drop them during JSON deserialization. +type configSourceNode struct { + BandwidthLimit int `json:"BandwidthLimit"` + CSVNode udtssdk.CSVNode `json:"CSVNode"` + DataType string `json:"DataType"` + IsDedicatedLine bool `json:"IsDedicatedLine"` + MySQLNode udtssdk.MySQLNode `json:"MySQLNode"` + MongoNode configMongoNode `json:"MongoNode"` + NWType string `json:"NWType"` + RedisNode configRedisNode `json:"RedisNode"` + TiDBNode udtssdk.TiDBNode `json:"TiDBNode"` + DynamodbNode configDynamodbNode `json:"DynamodbNode"` + ElasticsearchNode configEsNode `json:"ElasticsearchNode"` + KafkaNode configKafkaNode `json:"KafkaNode"` + ClickHouseNode configClickHouseNode `json:"ClickHouseNode"` + MaxirNode configMaxirNode `json:"MaxirNode"` + PostgreNode configPostgreNode `json:"PostgreNode"` + SQLServerNode configSQLServerNode `json:"SQLServerNode"` +} + +// configDynamodbNode holds the DynamoDB node config from GetUDTSTaskConfigure. +type configDynamodbNode struct { + AccessKey string `json:"AccessKey"` + AccessSecretKey string `json:"AccessSecretKey"` + SessionToken string `json:"SessionToken"` + SourceEndpointURL string `json:"SourceEndpointURL"` + Region string `json:"Region"` +} + +// configEsNode holds the Elasticsearch node config. +type configEsNode struct { + VPCId string `json:"VPCId"` + SubnetId string `json:"SubnetId"` + DataRegion string `json:"DataRegion"` + Password string `json:"Password"` + User string `json:"User"` + URL string `json:"URL"` + KeepTarget bool `json:"KeepTarget"` + ShardCount int `json:"ShardCount"` + ReplicaCount int `json:"ReplicaCount"` +} + +// configKafkaNode holds the Kafka node config. +type configKafkaNode struct { + VPCId string `json:"VPCId"` + SubnetId string `json:"SubnetId"` + DataRegion string `json:"DataRegion"` + BrokerServers string `json:"BrokerServers"` + TopicPrefix string `json:"TopicPrefix"` + TopicPartitionNums int `json:"TopicPartitionNums"` +} + +// configClickHouseNode holds the ClickHouse node config. +type configClickHouseNode struct { + VPCId string `json:"VPCId"` + SubnetId string `json:"SubnetId"` + DataRegion string `json:"DataRegion"` + TCPAddress string `json:"TCPAddress"` + HTTPAddress string `json:"HTTPAddress"` + Password string `json:"Password"` + User string `json:"User"` + CreateTableMode string `json:"CreateTableMode"` + TableEngine string `json:"TableEngine"` +} + +// configMaxirNode holds the Maxir node config. +type configMaxirNode struct { + VPCId string `json:"VPCId"` + SubnetId string `json:"SubnetId"` + DataRegion string `json:"DataRegion"` + Host string `json:"Host"` + Port int `json:"Port"` + User string `json:"User"` + Password string `json:"Password"` + Database string `json:"Database"` +} + +// configPostgreNode holds the PostgreSQL node config. +type configPostgreNode struct { + Host string `json:"Host"` + Port int `json:"Port"` + User string `json:"User"` + Password string `json:"Password"` + Database string `json:"Database"` + VPCId string `json:"VPCId"` + SubnetId string `json:"SubnetId"` + DataRegion string `json:"DataRegion"` +} + +// configSQLServerNode holds the SQLServer node config. +type configSQLServerNode struct { + Host string `json:"Host"` + Port int `json:"Port"` + User string `json:"User"` + Password string `json:"Password"` + Database string `json:"Database"` + VPCId string `json:"VPCId"` + SubnetId string `json:"SubnetId"` + DataRegion string `json:"DataRegion"` +} + +// configMongoNode holds the MongoDB node config from GetUDTSTaskConfigure. +// Source and Target use the same shape (Target just leaves Database/Collection empty). +type configMongoNode struct { + Host string `json:"Host"` + User string `json:"User"` + Password string `json:"Password"` + Database string `json:"Database"` + Collection string `json:"Collection"` + DataRegion string `json:"DataRegion"` + VPCId string `json:"VPCId"` + SubnetId string `json:"SubnetId"` + AuthenticationDB string `json:"AuthenticationDB"` +} + +// configData mirrors the SDK's ConfigData but uses configSourceNode (with MongoNode). +type configData struct { + MaxRetryCount int `json:"MaxRetryCount"` + Name string `json:"Name"` + Source configSourceNode `json:"Source"` + Target configSourceNode `json:"Target"` + TaskId string `json:"TaskId"` + Type string `json:"Type"` +} + +// getUDTSTaskConfigureResponse holds the task configuration. +type getUDTSTaskConfigureResponse struct { + response.CommonBase + Data configData `json:"Data"` +} + +// getUDTSTaskConfigure calls GetUDTSTaskConfigure via InvokeAction, using a +// custom response type that preserves MongoNode (the SDK's Source lacks it). +func getUDTSTaskConfigure(client *udtssdk.UDTSClient, req *udtssdk.GetUDTSTaskConfigureRequest) (*getUDTSTaskConfigureResponse, error) { + var resp getUDTSTaskConfigureResponse + if err := client.InvokeAction("GetUDTSTaskConfigure", req, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +// --- GetUDTSPrecheckTask (no typed SDK method) --- + +// getUDTSPrecheckTaskRequest is the request schema for GetUDTSPrecheckTask. +type getUDTSPrecheckTaskRequest struct { + request.CommonBase + TaskId *string `json:"TaskId,omitempty"` + Type *string `json:"Type,omitempty"` +} + +// precheckResultItem matches the API doc's CheckResultItem. +type precheckResultItem struct { + Type string `json:"Type"` + State string `json:"State"` + ErrMessage string `json:"ErrMessage"` + Instruction string `json:"Instruction"` + IsTarget bool `json:"IsTarget"` +} + +// precheckResultData matches the API doc's CheckResult. +type precheckResultData struct { + State string `json:"State"` + PrecheckResultItem []precheckResultItem `json:"PrecheckResultItem"` +} + +// getUDTSPrecheckTaskResponse holds the precheck result. +type getUDTSPrecheckTaskResponse struct { + response.CommonBase + Data precheckResultData `json:"Data"` +} + +// getUDTSPrecheckTask calls GetUDTSPrecheckTask via InvokeAction. +func getUDTSPrecheckTask(client *udtssdk.UDTSClient, req *getUDTSPrecheckTaskRequest) (*getUDTSPrecheckTaskResponse, error) { + var resp getUDTSPrecheckTaskResponse + if err := client.InvokeAction("GetUDTSPrecheckTask", req, &resp); err != nil { + return nil, err + } + return &resp, nil +} diff --git a/products/udts/internal/udts/list.go b/products/udts/internal/udts/list.go new file mode 100644 index 0000000000..46e85cb49a --- /dev/null +++ b/products/udts/internal/udts/list.go @@ -0,0 +1,68 @@ +package udts + +import ( + "github.com/spf13/cobra" + + udtssdk "github.com/ucloud/ucloud-sdk-go/services/udts" + + "github.com/ucloud/ucloud-cli/pkg/cli" + "github.com/ucloud/ucloud-cli/pkg/command" +) + +func newList(ctx *cli.Context) *cobra.Command { + client := cli.NewServiceClient(ctx, udtssdk.NewClient) + req := client.NewListUDTSTaskRequest() + + cmd := &cobra.Command{ + Use: "list", + Short: "List UDTS tasks", + Long: "List UDTS data transmission tasks in the active region and project.", + Run: func(c *cobra.Command, args []string) { + resp, err := client.ListUDTSTask(req) + if err != nil { + ctx.HandleError(err) + return + } + rows := make([]taskRow, 0, len(resp.Data)) + for _, t := range resp.Data { + rows = append(rows, taskRow{ + TaskId: t.TaskId, + Name: t.Name, + Type: t.Type, + ServiceType: t.ServiceType, + Status: t.Status, + Progress: formatProgress(t.Progress.Percentage), + CurRetryCount: t.CurRetryCount, + MaxRetryCount: t.MaxRetryCount, + ChargeType: t.ChargeType, + CreateTime: formatTimestamp(t.CreateTime), + ExpireTime: formatTimestamp(t.ExpireTime), + AutoRenew: t.AutoRenew, + IsExpire: t.IsExpire, + }) + } + ctx.PrintList(rows) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + // Limit/Offset are *string in the UDTS SDK (not *int), so BindCommonParams + // would panic on reflection. Bind region/zone/project individually and + // wire limit/offset manually with string flags. + var limit, offset string + flags.StringVar(&limit, "limit", "20", "Optional. Maximum number of tasks per page.") + flags.StringVar(&offset, "offset", "0", "Optional. Page offset.") + req.Limit = &limit + req.Offset = &offset + + req.Type = flags.String("type", "", "Optional. Task type: transfer or integration.") + command.SetFlagValues(cmd, "type", "transfer", "integration") + + ctx.BindRegion(cmd, req) + ctx.BindZone(cmd, req) + ctx.BindProjectID(cmd, req) + + return cmd +} diff --git a/products/udts/internal/udts/poll.go b/products/udts/internal/udts/poll.go new file mode 100644 index 0000000000..08955c92b8 --- /dev/null +++ b/products/udts/internal/udts/poll.go @@ -0,0 +1,29 @@ +package udts + +import ( + udtssdk "github.com/ucloud/ucloud-sdk-go/services/udts" + sdk "github.com/ucloud/ucloud-sdk-go/ucloud" + "github.com/ucloud/ucloud-sdk-go/ucloud/request" + + "github.com/ucloud/ucloud-cli/pkg/cli" +) + +// describeByID returns the Poller describe func: given a task id it fetches +// the current task status via GetUDTSTaskStatus so the Poller can read its +// Status field. The poller reads stateFields ["State","Status"]; StatusData +// has a Status field, so it is picked up. +func describeByID(ctx *cli.Context) func(string, *request.CommonBase) (interface{}, error) { + return func(id string, common *request.CommonBase) (interface{}, error) { + client := cli.NewServiceClient(ctx, udtssdk.NewClient) + req := client.NewGetUDTSTaskStatusRequest() + if common != nil { + req.CommonBase = *common + } + req.TaskId = sdk.String(id) + resp, err := client.GetUDTSTaskStatus(req) + if err != nil { + return nil, err + } + return &resp.Data, nil + } +} diff --git a/products/udts/internal/udts/precheck.go b/products/udts/internal/udts/precheck.go new file mode 100644 index 0000000000..aaba6feeb6 --- /dev/null +++ b/products/udts/internal/udts/precheck.go @@ -0,0 +1,212 @@ +package udts + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/spf13/cobra" + + udtssdk "github.com/ucloud/ucloud-sdk-go/services/udts" + + "github.com/ucloud/ucloud-cli/pkg/cli" + "github.com/ucloud/ucloud-cli/pkg/command" +) + +func newPrecheck(ctx *cli.Context) *cobra.Command { + client := cli.NewServiceClient(ctx, udtssdk.NewClient) + req := &createUDTSPrecheckTaskRequest{} + + var configPath string + var srcSSLCAFile, srcSSLCertFile, srcSSLKeyFile string + var tgtSSLCAFile, tgtSSLCertFile, tgtSSLKeyFile string + + // Allocate Source/Target for flag binding (mysql common case). + req.Source = make([]sourceParam, 1) + req.Source[0].MySQLNode = &sourceMySQLNode{} + req.Source[0].MySQLNode.SyncData = &udtssdk.CreateUDTSTaskParamSourceMySQLNodeSyncData{} + req.Source[0].MongoNode = &sourceMongoNode{} + req.Source[0].RedisNode = &sourceRedisNode{} + req.Source[0].TiDBNode = &sourceTiDBNode{} + req.Target = &target{} + req.Target.MySQLNode = &targetMySQLNode{} + req.Target.MongoNode = &targetMongoNode{} + req.Target.RedisNode = &targetRedisNode{} + req.Target.TiDBNode = &targetTiDBNode{} + + cmd := &cobra.Command{ + Use: "precheck", + Short: "Create a precheck task for UDTS migration", + Long: "Create a precheck task to verify source/target connectivity and configuration before migration. Uses the same parameters as create.", + Run: func(c *cobra.Command, args []string) { + if configPath != "" { + if err := loadPrecheckConfig(configPath, req); err != nil { + ctx.HandleError(fmt.Errorf("load --config: %w", err)) + return + } + } + if err := applySrcSSLFlags(srcSSLCAFile, srcSSLCertFile, srcSSLKeyFile, req.Source[0].MySQLNode); err != nil { + ctx.HandleError(err) + return + } + if err := applyTgtSSLFlags(tgtSSLCAFile, tgtSSLCertFile, tgtSSLKeyFile, req.Target.MySQLNode); err != nil { + ctx.HandleError(err) + return + } + w := ctx.ProgressWriter() + resp, err := createUDTSPrecheckTask(client, req) + if err != nil { + ctx.HandleError(err) + return + } + fmt.Fprintf(w, "Precheck task created. Task ID(s): %v\n", resp.TaskID) + results := make([]cli.OpResultRow, 0, len(resp.TaskID)) + for _, id := range resp.TaskID { + results = append(results, cli.OpResultRow{ResourceID: id, Action: "precheck", Status: "Checking"}) + } + ctx.EmitResult(results...) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + req.Name = flags.String("name", "", "Required. Task name, max 128 characters.") + req.Type = flags.String("type", "", "Required. Task type: transfer or integration.") + req.MaxRetryCount = flags.String("max-retry-count", "0", "Optional. Max retry count (0-5).") + req.Remark = flags.String("remark", "", "Optional. Remark.") + flags.StringVar(&configPath, "config", "", "Optional. JSON file with full Source/Target configuration.") + + req.Source[0].DataType = flags.String("src-data-type", "mysql", "Optional. Source database type.") + req.Source[0].Mode = flags.String("src-mode", "", "Optional. Migration mode: full/incremental/full+incremental/bidirectional.") + req.Source[0].NWType = flags.String("src-nwtype", "", "Optional. Source network type: public/user/dedicated_line.") + req.Source[0].ServiceType = flags.String("src-service-type", "", "Optional. Service type: small/medium/large/2xlarge/4xlarge.") + req.Source[0].BandwidthLimit = flags.Int("src-bandwidth-limit", 0, "Optional. Source bandwidth limit in MB/s.") + req.Source[0].MySQLNode.Host = flags.String("src-host", "", "Optional. Source MySQL host.") + req.Source[0].MySQLNode.Port = flags.Int("src-port", 3306, "Optional. Source MySQL port.") + req.Source[0].MySQLNode.User = flags.String("src-user", "", "Optional. Source MySQL username.") + req.Source[0].MySQLNode.Password = flags.String("src-password", "", "Optional. Source MySQL password.") + req.Source[0].MySQLNode.Database = flags.String("src-db", "", "Optional. Source database name.") + req.Source[0].MySQLNode.Table = flags.String("src-table", "", "Optional. Source table name.") + req.Source[0].MySQLNode.VPCId = flags.String("src-vpc-id", "", "Optional. Source VPC ID.") + req.Source[0].MySQLNode.SubnetId = flags.String("src-subnet-id", "", "Optional. Source subnet ID.") + req.Source[0].MySQLNode.DataRegion = flags.String("src-data-region", "", "Optional. Source data region.") + req.Source[0].MySQLNode.SyncData.BinlogName = flags.String("src-binlog-name", "", "Optional. Incremental binlog name (show master status).") + req.Source[0].MySQLNode.SyncData.BinlogPos = flags.Int("src-binlog-pos", 0, "Optional. Incremental binlog pos (show master status).") + req.Source[0].MySQLNode.SyncData.BinlogGTID = flags.String("src-binlog-gtid", "", "Optional. Incremental binlog GTID (show master status).") + flags.StringVar(&srcSSLCAFile, "src-ssl-ca-file", "", "Optional. Path to source MySQL SSL CA certificate file (PEM, base64-encoded by CLI).") + flags.StringVar(&srcSSLCertFile, "src-ssl-cert-file", "", "Optional. Path to source MySQL SSL client certificate file (PEM, base64-encoded by CLI).") + flags.StringVar(&srcSSLKeyFile, "src-ssl-key-file", "", "Optional. Path to source MySQL SSL client key file (PEM, base64-encoded by CLI).") + + // Source MongoDB flags (when src-data-type is mongo) + req.Source[0].MongoNode.Host = flags.String("src-mongo-host", "", "Optional. Source MongoDB host(s), comma-separated for replica set.") + req.Source[0].MongoNode.User = flags.String("src-mongo-user", "", "Optional. Source MongoDB username.") + req.Source[0].MongoNode.Password = flags.String("src-mongo-password", "", "Optional. Source MongoDB password.") + req.Source[0].MongoNode.Database = flags.String("src-mongo-db", "", "Optional. Source MongoDB database name to migrate.") + req.Source[0].MongoNode.Collection = flags.String("src-mongo-collection", "", "Optional. Source MongoDB collection name.") + req.Source[0].MongoNode.AuthenticationDB = flags.String("src-mongo-auth-db", "", "Optional. Source MongoDB authentication database (typically admin).") + + // Source Redis flags (when src-data-type is redis) + req.Source[0].RedisNode.Address = flags.String("src-redis-address", "", "Optional. Source Redis address (master; ';' separated for cluster).") + req.Source[0].RedisNode.Password = flags.String("src-redis-password", "", "Optional. Source Redis password.") + req.Source[0].RedisNode.Type = flags.String("src-redis-type", "", "Optional. Source Redis mode: cluster/standalone/udredis.") + req.Source[0].RedisNode.IsRump = flags.String("src-redis-is-rump", "", "Optional. Use rump for full migration (true/false).") + req.Source[0].RedisNode.VPCId = flags.String("src-redis-vpc-id", "", "Optional. Source Redis VPC ID.") + req.Source[0].RedisNode.SubnetId = flags.String("src-redis-subnet-id", "", "Optional. Source Redis subnet ID.") + req.Source[0].RedisNode.DataRegion = flags.String("src-redis-data-region", "", "Optional. Source Redis data region.") + + // Source TiDB flags (when src-data-type is tidb) + req.Source[0].TiDBNode.Host = flags.String("src-tidb-host", "", "Optional. Source TiDB host.") + req.Source[0].TiDBNode.Port = flags.Int("src-tidb-port", 6603, "Optional. Source TiDB port.") + req.Source[0].TiDBNode.User = flags.String("src-tidb-user", "", "Optional. Source TiDB username.") + req.Source[0].TiDBNode.Password = flags.String("src-tidb-password", "", "Optional. Source TiDB password.") + req.Source[0].TiDBNode.Database = flags.String("src-tidb-db", "", "Optional. Source TiDB database name.") + req.Source[0].TiDBNode.Table = flags.String("src-tidb-table", "", "Optional. Source TiDB table name.") + req.Source[0].TiDBNode.VPCId = flags.String("src-tidb-vpc-id", "", "Optional. Source TiDB VPC ID.") + req.Source[0].TiDBNode.SubnetId = flags.String("src-tidb-subnet-id", "", "Optional. Source TiDB subnet ID.") + req.Source[0].TiDBNode.DataRegion = flags.String("src-tidb-data-region", "", "Optional. Source TiDB data region.") + + req.Target.DataType = flags.String("tgt-data-type", "mysql", "Optional. Target database type.") + req.Target.NWType = flags.String("tgt-nwtype", "", "Optional. Target network type (user).") + req.Target.BandwidthLimit = flags.Int("tgt-bandwidth-limit", 0, "Optional. Target bandwidth limit in MB/s.") + req.Target.MySQLNode.Host = flags.String("tgt-host", "", "Optional. Target MySQL host.") + req.Target.MySQLNode.Port = flags.Int("tgt-port", 3306, "Optional. Target MySQL port.") + req.Target.MySQLNode.User = flags.String("tgt-user", "", "Optional. Target MySQL username.") + req.Target.MySQLNode.Password = flags.String("tgt-password", "", "Optional. Target MySQL password.") + req.Target.MySQLNode.VPCId = flags.String("tgt-vpc-id", "", "Optional. Target VPC ID.") + req.Target.MySQLNode.SubnetId = flags.String("tgt-subnet-id", "", "Optional. Target subnet ID.") + req.Target.MySQLNode.DataRegion = flags.String("tgt-data-region", "", "Optional. Target data region.") + req.Target.MySQLNode.NoBinlog = flags.Bool("tgt-no-binlog", false, "Optional. Disable binlog on target during full migration.") + flags.StringVar(&tgtSSLCAFile, "tgt-ssl-ca-file", "", "Optional. Path to target MySQL SSL CA certificate file (PEM, base64-encoded by CLI).") + flags.StringVar(&tgtSSLCertFile, "tgt-ssl-cert-file", "", "Optional. Path to target MySQL SSL client certificate file (PEM, base64-encoded by CLI).") + flags.StringVar(&tgtSSLKeyFile, "tgt-ssl-key-file", "", "Optional. Path to target MySQL SSL client key file (PEM, base64-encoded by CLI).") + + // Target MongoDB flags (when tgt-data-type is mongo) + req.Target.MongoNode.Host = flags.String("tgt-mongo-host", "", "Optional. Target MongoDB host(s), comma-separated for replica set.") + req.Target.MongoNode.User = flags.String("tgt-mongo-user", "", "Optional. Target MongoDB username.") + req.Target.MongoNode.Password = flags.String("tgt-mongo-password", "", "Optional. Target MongoDB password.") + req.Target.MongoNode.AuthenticationDB = flags.String("tgt-mongo-auth-db", "", "Optional. Target MongoDB authentication database (typically admin).") + + // Target Redis flags (when tgt-data-type is redis) + req.Target.RedisNode.Address = flags.String("tgt-redis-address", "", "Optional. Target Redis address (master; ';' separated for cluster).") + req.Target.RedisNode.Password = flags.String("tgt-redis-password", "", "Optional. Target Redis password.") + req.Target.RedisNode.Type = flags.String("tgt-redis-type", "", "Optional. Target Redis mode: cluster/standalone/udredis.") + req.Target.RedisNode.IsRump = flags.String("tgt-redis-is-rump", "", "Optional. Use rump for full migration (true/false).") + req.Target.RedisNode.VPCId = flags.String("tgt-redis-vpc-id", "", "Optional. Target Redis VPC ID.") + req.Target.RedisNode.SubnetId = flags.String("tgt-redis-subnet-id", "", "Optional. Target Redis subnet ID.") + req.Target.RedisNode.DataRegion = flags.String("tgt-redis-data-region", "", "Optional. Target Redis data region.") + + // Target TiDB flags (when tgt-data-type is tidb) + req.Target.TiDBNode.Host = flags.String("tgt-tidb-host", "", "Optional. Target TiDB host.") + req.Target.TiDBNode.Port = flags.Int("tgt-tidb-port", 6603, "Optional. Target TiDB port.") + req.Target.TiDBNode.User = flags.String("tgt-tidb-user", "", "Optional. Target TiDB username.") + req.Target.TiDBNode.Password = flags.String("tgt-tidb-password", "", "Optional. Target TiDB password.") + req.Target.TiDBNode.Database = flags.String("tgt-tidb-db", "", "Optional. Target TiDB database name.") + req.Target.TiDBNode.VPCId = flags.String("tgt-tidb-vpc-id", "", "Optional. Target TiDB VPC ID.") + req.Target.TiDBNode.SubnetId = flags.String("tgt-tidb-subnet-id", "", "Optional. Target TiDB subnet ID.") + req.Target.TiDBNode.DataRegion = flags.String("tgt-tidb-data-region", "", "Optional. Target TiDB data region.") + + ctx.BindRegion(cmd, req) + ctx.BindProjectID(cmd, req) + + command.SetFlagValues(cmd, "type", taskTypeValues...) + command.SetFlagValues(cmd, "src-mode", srcModeValues...) + command.SetFlagValues(cmd, "src-nwtype", srcNwTypeValues...) + command.SetFlagValues(cmd, "src-service-type", srcServiceTypeValues...) + command.SetFlagValues(cmd, "tgt-nwtype", tgtNwTypeValues...) + + cmd.MarkFlagRequired("name") + cmd.MarkFlagRequired("type") + + return cmd +} + +// loadPrecheckConfig reads a JSON file and decodes it into the precheck request. +func loadPrecheckConfig(path string, req *createUDTSPrecheckTaskRequest) error { + data, err := os.ReadFile(path) + if err != nil { + return err + } + var cfg struct { + Source []sourceParam `json:"Source"` + Target *target `json:"Target"` + Name *string `json:"Name,omitempty"` + Type *string `json:"Type,omitempty"` + } + if err := json.Unmarshal(data, &cfg); err != nil { + return err + } + if len(cfg.Source) > 0 { + req.Source = cfg.Source + } + if cfg.Target != nil { + req.Target = cfg.Target + } + if cfg.Name != nil && *req.Name == "" { + req.Name = cfg.Name + } + if cfg.Type != nil && *req.Type == "" { + req.Type = cfg.Type + } + return nil +} diff --git a/products/udts/internal/udts/precheck_result.go b/products/udts/internal/udts/precheck_result.go new file mode 100644 index 0000000000..cef096c9bd --- /dev/null +++ b/products/udts/internal/udts/precheck_result.go @@ -0,0 +1,59 @@ +package udts + +import ( + "github.com/spf13/cobra" + + udtssdk "github.com/ucloud/ucloud-sdk-go/services/udts" + sdk "github.com/ucloud/ucloud-sdk-go/ucloud" + + "github.com/ucloud/ucloud-cli/pkg/cli" + "github.com/ucloud/ucloud-cli/pkg/command" +) + +func newPrecheckResult(ctx *cli.Context) *cobra.Command { + client := cli.NewServiceClient(ctx, udtssdk.NewClient) + req := &getUDTSPrecheckTaskRequest{} + + var taskType string + + cmd := &cobra.Command{ + Use: "precheck-result", + Short: "Get the result of a UDTS precheck task", + Long: "Get the result of a UDTS precheck task by task ID.", + Run: func(c *cobra.Command, args []string) { + taskId, _ := c.Flags().GetString(resourceIDFlag) + req.TaskId = sdk.String(ctx.PickResourceID(taskId)) + req.Type = sdk.String(taskType) + resp, err := getUDTSPrecheckTask(client, req) + if err != nil { + ctx.HandleError(err) + return + } + rows := make([]precheckResultRow, 0, len(resp.Data.PrecheckResultItem)) + for _, item := range resp.Data.PrecheckResultItem { + rows = append(rows, precheckResultRow{ + Type: item.Type, + State: item.State, + ErrMessage: item.ErrMessage, + Instruction: item.Instruction, + IsTarget: item.IsTarget, + }) + } + ctx.PrintList(rows) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + flags.String(resourceIDFlag, "", "Required. Task ID of the precheck task.") + flags.StringVar(&taskType, "type", "", "Optional. Task type: transfer or integration.") + + ctx.BindRegion(cmd, req) + ctx.BindProjectID(cmd, req) + + cmd.MarkFlagRequired(resourceIDFlag) + command.SetFlagValues(cmd, "type", taskTypeValues...) + + return cmd +} diff --git a/products/udts/internal/udts/rows.go b/products/udts/internal/udts/rows.go new file mode 100644 index 0000000000..efe10d7e50 --- /dev/null +++ b/products/udts/internal/udts/rows.go @@ -0,0 +1,67 @@ +package udts + +import ( + "fmt" + + "github.com/ucloud/ucloud-cli/internal/common" +) + +// commandName is the top-level command name for the UDTS product. +const commandName = "udts" + +// resourceIDFlag is the resource-id flag name (--task-id). +const resourceIDFlag = "task-id" + +// taskRow is the output struct for `udts list`. Field names match API doc +// ListDataItem; field declaration order = table column order. +type taskRow struct { + TaskId string + Name string + Type string + ServiceType string + Status string + Progress string + CurRetryCount int + MaxRetryCount int + ChargeType string + CreateTime string + ExpireTime string + AutoRenew string + IsExpire string +} + +// formatProgress returns a human-readable progress string from Percentage. +func formatProgress(p float64) string { + if p <= 0 { + return "-" + } + return fmt.Sprintf("%.1f%%", p) +} + +// formatTimestamp returns a human-readable time from a unix timestamp. +// Returns "-" for zero. Uses the platform's internal/common.FormatDateTime. +func formatTimestamp(ts int) string { + if ts <= 0 { + return "-" + } + return common.FormatDateTime(ts) +} + +// historyRow is the output struct for `udts history`. Fields match API doc +// TaskHistoryItem. +type historyRow struct { + CreateTimeH string + AntState string + AntID string + CreateTime int +} + +// precheckResultRow is the output struct for `udts precheck-result`. +// Fields match API doc CheckResultItem. +type precheckResultRow struct { + Type string + State string + ErrMessage string + Instruction string + IsTarget bool +} diff --git a/products/udts/internal/udts/ssl.go b/products/udts/internal/udts/ssl.go new file mode 100644 index 0000000000..8449064660 --- /dev/null +++ b/products/udts/internal/udts/ssl.go @@ -0,0 +1,80 @@ +package udts + +import ( + "encoding/base64" + "fmt" + "os" + + udtssdk "github.com/ucloud/ucloud-sdk-go/services/udts" +) + +// applySrcSSLFlags reads SSL certificate files and sets SSLSecurity on the +// source MySQLNode. If no paths are provided, it does nothing. +func applySrcSSLFlags(caFile, certFile, keyFile string, node *sourceMySQLNode) error { + if caFile == "" && certFile == "" && keyFile == "" { + return nil + } + if node.SSLSecurity == nil { + node.SSLSecurity = &udtssdk.CreateUDTSTaskParamSourceMySQLNodeSSLSecurity{} + } + if caFile != "" { + b, err := os.ReadFile(caFile) + if err != nil { + return fmt.Errorf("read source SSL CA file %s: %w", caFile, err) + } + node.SSLSecurity.SSLCA = strPtr(base64.StdEncoding.EncodeToString(b)) + } + if certFile != "" { + b, err := os.ReadFile(certFile) + if err != nil { + return fmt.Errorf("read source SSL cert file %s: %w", certFile, err) + } + node.SSLSecurity.SSLCert = strPtr(base64.StdEncoding.EncodeToString(b)) + } + if keyFile != "" { + b, err := os.ReadFile(keyFile) + if err != nil { + return fmt.Errorf("read source SSL key file %s: %w", keyFile, err) + } + node.SSLSecurity.SSLKey = strPtr(base64.StdEncoding.EncodeToString(b)) + } + return nil +} + +// applyTgtSSLFlags reads SSL certificate files and sets SSLSecurity on the +// target MySQLNode. The SDK's TargetMySQLNode lacks SSLSecurity; we accept the +// files and set them on our enriched targetMySQLNode type. If no paths are +// provided, it does nothing. +func applyTgtSSLFlags(caFile, certFile, keyFile string, node *targetMySQLNode) error { + if caFile == "" && certFile == "" && keyFile == "" { + return nil + } + if node.SSLSecurity == nil { + node.SSLSecurity = &targetMySQLNodeSSLSecurity{} + } + if caFile != "" { + b, err := os.ReadFile(caFile) + if err != nil { + return fmt.Errorf("read target SSL CA file %s: %w", caFile, err) + } + node.SSLSecurity.SSLCA = strPtr(base64.StdEncoding.EncodeToString(b)) + } + if certFile != "" { + b, err := os.ReadFile(certFile) + if err != nil { + return fmt.Errorf("read target SSL cert file %s: %w", certFile, err) + } + node.SSLSecurity.SSLCert = strPtr(base64.StdEncoding.EncodeToString(b)) + } + if keyFile != "" { + b, err := os.ReadFile(keyFile) + if err != nil { + return fmt.Errorf("read target SSL key file %s: %w", keyFile, err) + } + node.SSLSecurity.SSLKey = strPtr(base64.StdEncoding.EncodeToString(b)) + } + return nil +} + +// strPtr returns a pointer to the given string. +func strPtr(s string) *string { return &s } diff --git a/products/udts/internal/udts/start.go b/products/udts/internal/udts/start.go new file mode 100644 index 0000000000..4a80132198 --- /dev/null +++ b/products/udts/internal/udts/start.go @@ -0,0 +1,69 @@ +package udts + +import ( + "fmt" + + "github.com/spf13/cobra" + + udtssdk "github.com/ucloud/ucloud-sdk-go/services/udts" + sdk "github.com/ucloud/ucloud-sdk-go/ucloud" + + "github.com/ucloud/ucloud-cli/pkg/cli" + "github.com/ucloud/ucloud-cli/pkg/command" +) + +func newStart(ctx *cli.Context) *cobra.Command { + client := cli.NewServiceClient(ctx, udtssdk.NewClient) + req := client.NewStartUDTSTaskRequest() + + var ids []string + var async bool + + cmd := &cobra.Command{ + Use: "start", + Short: "Start UDTS tasks", + Long: "Start one or more stopped UDTS tasks.", + Run: func(c *cobra.Command, args []string) { + w := ctx.ProgressWriter() + results := []cli.OpResultRow{} + for _, idName := range ids { + id := ctx.PickResourceID(idName) + req.TaskId = sdk.String(id) + if _, err := client.StartUDTSTask(req); err != nil { + ctx.HandleError(err) + continue + } + text := fmt.Sprintf("%s[%s] is starting", commandName, id) + if async { + fmt.Fprintln(w, text) + } else { + ctx.PollerTo(w, describeByID(ctx)).Spoll(id, text, []string{ + stateStarted, stateChecking, stateDumping, stateLoading, + stateSyncing, stateSynced, stateDone, stateSuccess, + stateFailed, stateFailedUnrecoverable, + }) + } + results = append(results, cli.OpResultRow{ResourceID: id, Action: "start", Status: "Starting"}) + } + ctx.EmitResult(results...) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringSliceVar(&ids, resourceIDFlag, nil, "Required. Task ID(s) to start.") + req.Type = flags.String("type", "", "Optional. Task type: transfer or integration.") + flags.BoolVar(&async, "async", false, "Optional. Do not wait for the operation to finish.") + + ctx.BindRegion(cmd, req) + ctx.BindProjectID(cmd, req) + + cmd.MarkFlagRequired(resourceIDFlag) + command.SetFlagValues(cmd, "type", "transfer", "integration") + command.SetCompletion(cmd, resourceIDFlag, func() []string { + return listTaskIDs(ctx, []string{stateCreated, stateStopped, stateFailed, stateFailedUnrecoverable}, derefStr(req.Region), derefStr(req.ProjectId)) + }) + + return cmd +} diff --git a/products/udts/internal/udts/status.go b/products/udts/internal/udts/status.go new file mode 100644 index 0000000000..68e523591c --- /dev/null +++ b/products/udts/internal/udts/status.go @@ -0,0 +1,71 @@ +package udts + +import ( + "fmt" + + "github.com/spf13/cobra" + + udtssdk "github.com/ucloud/ucloud-sdk-go/services/udts" + + "github.com/ucloud/ucloud-cli/pkg/cli" + "github.com/ucloud/ucloud-cli/pkg/command" +) + +func newStatus(ctx *cli.Context) *cobra.Command { + client := cli.NewServiceClient(ctx, udtssdk.NewClient) + req := client.NewGetUDTSTaskStatusRequest() + + cmd := &cobra.Command{ + Use: "status", + Short: "Show runtime status of a UDTS task", + Long: "Show the runtime status, progress, and sync position of a UDTS task.", + Run: func(c *cobra.Command, args []string) { + *req.TaskId = ctx.PickResourceID(*req.TaskId) + resp, err := client.GetUDTSTaskStatus(req) + if err != nil { + ctx.HandleError(err) + return + } + d := resp.Data + rows := []cli.DescribeRow{ + {Attribute: "TaskId", Content: *req.TaskId}, + {Attribute: "Status", Content: d.Status}, + {Attribute: "FailedMessage", Content: orDash(d.FailedMessage)}, + {Attribute: "CurRetryCount", Content: fmt.Sprintf("%d", d.CurRetryCount)}, + {Attribute: "MaxRetryCount", Content: fmt.Sprintf("%d", d.MaxRetryCount)}, + {Attribute: "Progress(%)", Content: formatProgress(d.Progress.Percentage)}, + {Attribute: "Migrated", Content: fmt.Sprintf("%d / %d", d.Progress.CurCount, d.Progress.TotalCount)}, + {Attribute: "Elapsed(s)", Content: fmt.Sprintf("%d / %d", d.Progress.CurDuration, d.Progress.TotalDuration)}, + {Attribute: "BinlogName", Content: d.Sync.BinlogName}, + {Attribute: "BinlogPos", Content: fmt.Sprintf("%d", d.Sync.BinlogPos)}, + {Attribute: "BinlogGTID", Content: d.Sync.BinlogGTID}, + } + ctx.PrintList(rows) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + req.TaskId = flags.String(resourceIDFlag, "", "Required. Task ID of the UDTS task.") + req.Type = flags.String("type", "", "Optional. Task type: transfer or integration.") + + ctx.BindRegion(cmd, req) + ctx.BindProjectID(cmd, req) + + cmd.MarkFlagRequired(resourceIDFlag) + command.SetFlagValues(cmd, "type", "transfer", "integration") + command.SetCompletion(cmd, resourceIDFlag, func() []string { + return listTaskIDs(ctx, nil, derefStr(req.Region), derefStr(req.ProjectId)) + }) + + return cmd +} + +// orDash returns s if non-empty, otherwise "-". +func orDash(s string) string { + if s == "" { + return "-" + } + return s +} diff --git a/products/udts/internal/udts/status_const.go b/products/udts/internal/udts/status_const.go new file mode 100644 index 0000000000..f0bd89e41d --- /dev/null +++ b/products/udts/internal/udts/status_const.go @@ -0,0 +1,19 @@ +package udts + +// Terminal states a Poller waits on. Sourced from StatusData.Status enum. +const ( + stateCreated = "Created" + stateChecking = "Checking" + stateStarted = "Started" + stateDumping = "Dumping" + stateLoading = "Loading" + stateSyncing = "Syncing" + stateSynced = "Synced" + stateStopped = "Stopped" + stateDone = "Done" + stateSuccess = "Success" + stateFailed = "Failed" + stateFailedUnrecoverable = "FailedUnrecoverable" + stateStoppedUnrecoverable = "StoppedUnrecoverable" + stateRetryPending = "RetryPending" +) diff --git a/products/udts/internal/udts/stop.go b/products/udts/internal/udts/stop.go new file mode 100644 index 0000000000..95e9cf22bb --- /dev/null +++ b/products/udts/internal/udts/stop.go @@ -0,0 +1,68 @@ +package udts + +import ( + "fmt" + + "github.com/spf13/cobra" + + udtssdk "github.com/ucloud/ucloud-sdk-go/services/udts" + sdk "github.com/ucloud/ucloud-sdk-go/ucloud" + + "github.com/ucloud/ucloud-cli/pkg/cli" + "github.com/ucloud/ucloud-cli/pkg/command" +) + +func newStop(ctx *cli.Context) *cobra.Command { + client := cli.NewServiceClient(ctx, udtssdk.NewClient) + req := client.NewStopUDTSTaskRequest() + + var ids []string + var async bool + + cmd := &cobra.Command{ + Use: "stop", + Short: "Stop UDTS tasks", + Long: "Stop one or more running UDTS tasks.", + Run: func(c *cobra.Command, args []string) { + w := ctx.ProgressWriter() + results := []cli.OpResultRow{} + for _, idName := range ids { + id := ctx.PickResourceID(idName) + req.TaskId = sdk.String(id) + if _, err := client.StopUDTSTask(req); err != nil { + ctx.HandleError(err) + continue + } + text := fmt.Sprintf("%s[%s] is stopping", commandName, id) + if async { + fmt.Fprintln(w, text) + } else { + ctx.PollerTo(w, describeByID(ctx)).Spoll(id, text, []string{ + stateStopped, stateStoppedUnrecoverable, + stateFailed, stateFailedUnrecoverable, + }) + } + results = append(results, cli.OpResultRow{ResourceID: id, Action: "stop", Status: "Stopping"}) + } + ctx.EmitResult(results...) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringSliceVar(&ids, resourceIDFlag, nil, "Required. Task ID(s) to stop.") + req.Type = flags.String("type", "", "Optional. Task type: transfer or integration.") + flags.BoolVar(&async, "async", false, "Optional. Do not wait for the operation to finish.") + + ctx.BindRegion(cmd, req) + ctx.BindProjectID(cmd, req) + + cmd.MarkFlagRequired(resourceIDFlag) + command.SetFlagValues(cmd, "type", "transfer", "integration") + command.SetCompletion(cmd, resourceIDFlag, func() []string { + return listTaskIDs(ctx, []string{stateStarted, stateChecking, stateDumping, stateLoading, stateSyncing, stateSynced, stateRetryPending}, derefStr(req.Region), derefStr(req.ProjectId)) + }) + + return cmd +} diff --git a/products/udts/product.go b/products/udts/product.go new file mode 100644 index 0000000000..fb95a5612c --- /dev/null +++ b/products/udts/product.go @@ -0,0 +1,24 @@ +// Package udts implements the UDTS (UCloud Data Transmission Service) product +// for the ucloud-cli platform. +package udts + +import ( + "github.com/spf13/cobra" + + "github.com/ucloud/ucloud-cli/pkg/cli" + internaludts "github.com/ucloud/ucloud-cli/products/udts/internal/udts" +) + +type product struct{} + +func New() cli.Product { return product{} } + +func (product) Metadata() cli.Metadata { + return cli.Metadata{Name: "udts", Commands: []string{"udts"}} +} + +func (product) NewCommand(ctx *cli.Context) []*cobra.Command { + return []*cobra.Command{internaludts.NewCommand(ctx)} +} + +var _ cli.Product = (*product)(nil) diff --git a/products/udts/product.yaml b/products/udts/product.yaml new file mode 100644 index 0000000000..7a489f04de --- /dev/null +++ b/products/udts/product.yaml @@ -0,0 +1,6 @@ +name: udts +owners: + - immy.bao +commands: + - udts +enabled: true diff --git a/products/udts/testdata/cmdtree.golden b/products/udts/testdata/cmdtree.golden new file mode 100644 index 0000000000..6ba05cb23c --- /dev/null +++ b/products/udts/testdata/cmdtree.golden @@ -0,0 +1,216 @@ +ucloud udts use=udts short=Manage UDTS data transmission tasks +ucloud udts create use=create short=Create a UDTS task + flag=async short= default=false required= + flag=charge-type short= default=Month required= + flag=config short= default= required= + flag=max-retry-count short= default=0 required= + flag=name short= default= required=true + flag=project-id short= default= required= + flag=quantity short= default=1 required= + flag=region short= default= required= + flag=remark short= default= required= + flag=src-bandwidth-limit short= default=0 required= + flag=src-binlog-gtid short= default= required= + flag=src-binlog-name short= default= required= + flag=src-binlog-pos short= default=0 required= + flag=src-data-region short= default= required= + flag=src-data-type short= default=mysql required= + flag=src-db short= default= required= + flag=src-host short= default= required= + flag=src-mode short= default= required= + flag=src-mongo-auth-db short= default= required= + flag=src-mongo-collection short= default= required= + flag=src-mongo-db short= default= required= + flag=src-mongo-host short= default= required= + flag=src-mongo-password short= default= required= + flag=src-mongo-user short= default= required= + flag=src-nwtype short= default= required= + flag=src-password short= default= required= + flag=src-port short= default=3306 required= + flag=src-redis-address short= default= required= + flag=src-redis-data-region short= default= required= + flag=src-redis-is-rump short= default= required= + flag=src-redis-password short= default= required= + flag=src-redis-subnet-id short= default= required= + flag=src-redis-type short= default= required= + flag=src-redis-vpc-id short= default= required= + flag=src-service-type short= default= required= + flag=src-ssl-ca-file short= default= required= + flag=src-ssl-cert-file short= default= required= + flag=src-ssl-key-file short= default= required= + flag=src-subnet-id short= default= required= + flag=src-table short= default= required= + flag=src-tidb-data-region short= default= required= + flag=src-tidb-db short= default= required= + flag=src-tidb-host short= default= required= + flag=src-tidb-password short= default= required= + flag=src-tidb-port short= default=6603 required= + flag=src-tidb-subnet-id short= default= required= + flag=src-tidb-table short= default= required= + flag=src-tidb-user short= default= required= + flag=src-tidb-vpc-id short= default= required= + flag=src-user short= default= required= + flag=src-vpc-id short= default= required= + flag=tgt-bandwidth-limit short= default=0 required= + flag=tgt-data-region short= default= required= + flag=tgt-data-type short= default=mysql required= + flag=tgt-host short= default= required= + flag=tgt-mongo-auth-db short= default= required= + flag=tgt-mongo-host short= default= required= + flag=tgt-mongo-password short= default= required= + flag=tgt-mongo-user short= default= required= + flag=tgt-no-binlog short= default=false required= + flag=tgt-nwtype short= default= required= + flag=tgt-password short= default= required= + flag=tgt-port short= default=3306 required= + flag=tgt-redis-address short= default= required= + flag=tgt-redis-data-region short= default= required= + flag=tgt-redis-is-rump short= default= required= + flag=tgt-redis-password short= default= required= + flag=tgt-redis-subnet-id short= default= required= + flag=tgt-redis-type short= default= required= + flag=tgt-redis-vpc-id short= default= required= + flag=tgt-ssl-ca-file short= default= required= + flag=tgt-ssl-cert-file short= default= required= + flag=tgt-ssl-key-file short= default= required= + flag=tgt-subnet-id short= default= required= + flag=tgt-tidb-data-region short= default= required= + flag=tgt-tidb-db short= default= required= + flag=tgt-tidb-host short= default= required= + flag=tgt-tidb-password short= default= required= + flag=tgt-tidb-port short= default=6603 required= + flag=tgt-tidb-subnet-id short= default= required= + flag=tgt-tidb-user short= default= required= + flag=tgt-tidb-vpc-id short= default= required= + flag=tgt-user short= default= required= + flag=tgt-vpc-id short= default= required= + flag=type short= default= required=true + flag=yes short=y default=false required= + flag=zone short= default= required= +ucloud udts delete use=delete short=Delete UDTS tasks + flag=project-id short= default= required= + flag=region short= default= required= + flag=task-id short= default=[] required=true + flag=type short= default=transfer required= + flag=yes short=y default=false required= +ucloud udts describe use=describe short=Show the configuration of a UDTS task + flag=project-id short= default= required= + flag=region short= default= required= + flag=task-id short= default= required=true +ucloud udts history use=history short=Show state transition history of a UDTS task + flag=project-id short= default= required= + flag=region short= default= required= + flag=task-id short= default= required=true + flag=type short= default= required= +ucloud udts list use=list short=List UDTS tasks + flag=limit short= default=20 required= + flag=offset short= default=0 required= + flag=project-id short= default= required= + flag=region short= default= required= + flag=type short= default= required= + flag=zone short= default= required= +ucloud udts precheck use=precheck short=Create a precheck task for UDTS migration + flag=config short= default= required= + flag=max-retry-count short= default=0 required= + flag=name short= default= required=true + flag=project-id short= default= required= + flag=region short= default= required= + flag=remark short= default= required= + flag=src-bandwidth-limit short= default=0 required= + flag=src-binlog-gtid short= default= required= + flag=src-binlog-name short= default= required= + flag=src-binlog-pos short= default=0 required= + flag=src-data-region short= default= required= + flag=src-data-type short= default=mysql required= + flag=src-db short= default= required= + flag=src-host short= default= required= + flag=src-mode short= default= required= + flag=src-mongo-auth-db short= default= required= + flag=src-mongo-collection short= default= required= + flag=src-mongo-db short= default= required= + flag=src-mongo-host short= default= required= + flag=src-mongo-password short= default= required= + flag=src-mongo-user short= default= required= + flag=src-nwtype short= default= required= + flag=src-password short= default= required= + flag=src-port short= default=3306 required= + flag=src-redis-address short= default= required= + flag=src-redis-data-region short= default= required= + flag=src-redis-is-rump short= default= required= + flag=src-redis-password short= default= required= + flag=src-redis-subnet-id short= default= required= + flag=src-redis-type short= default= required= + flag=src-redis-vpc-id short= default= required= + flag=src-service-type short= default= required= + flag=src-ssl-ca-file short= default= required= + flag=src-ssl-cert-file short= default= required= + flag=src-ssl-key-file short= default= required= + flag=src-subnet-id short= default= required= + flag=src-table short= default= required= + flag=src-tidb-data-region short= default= required= + flag=src-tidb-db short= default= required= + flag=src-tidb-host short= default= required= + flag=src-tidb-password short= default= required= + flag=src-tidb-port short= default=6603 required= + flag=src-tidb-subnet-id short= default= required= + flag=src-tidb-table short= default= required= + flag=src-tidb-user short= default= required= + flag=src-tidb-vpc-id short= default= required= + flag=src-user short= default= required= + flag=src-vpc-id short= default= required= + flag=tgt-bandwidth-limit short= default=0 required= + flag=tgt-data-region short= default= required= + flag=tgt-data-type short= default=mysql required= + flag=tgt-host short= default= required= + flag=tgt-mongo-auth-db short= default= required= + flag=tgt-mongo-host short= default= required= + flag=tgt-mongo-password short= default= required= + flag=tgt-mongo-user short= default= required= + flag=tgt-no-binlog short= default=false required= + flag=tgt-nwtype short= default= required= + flag=tgt-password short= default= required= + flag=tgt-port short= default=3306 required= + flag=tgt-redis-address short= default= required= + flag=tgt-redis-data-region short= default= required= + flag=tgt-redis-is-rump short= default= required= + flag=tgt-redis-password short= default= required= + flag=tgt-redis-subnet-id short= default= required= + flag=tgt-redis-type short= default= required= + flag=tgt-redis-vpc-id short= default= required= + flag=tgt-ssl-ca-file short= default= required= + flag=tgt-ssl-cert-file short= default= required= + flag=tgt-ssl-key-file short= default= required= + flag=tgt-subnet-id short= default= required= + flag=tgt-tidb-data-region short= default= required= + flag=tgt-tidb-db short= default= required= + flag=tgt-tidb-host short= default= required= + flag=tgt-tidb-password short= default= required= + flag=tgt-tidb-port short= default=6603 required= + flag=tgt-tidb-subnet-id short= default= required= + flag=tgt-tidb-user short= default= required= + flag=tgt-tidb-vpc-id short= default= required= + flag=tgt-user short= default= required= + flag=tgt-vpc-id short= default= required= + flag=type short= default= required=true +ucloud udts precheck-result use=precheck-result short=Get the result of a UDTS precheck task + flag=project-id short= default= required= + flag=region short= default= required= + flag=task-id short= default= required=true + flag=type short= default= required= +ucloud udts start use=start short=Start UDTS tasks + flag=async short= default=false required= + flag=project-id short= default= required= + flag=region short= default= required= + flag=task-id short= default=[] required=true + flag=type short= default= required= +ucloud udts status use=status short=Show runtime status of a UDTS task + flag=project-id short= default= required= + flag=region short= default= required= + flag=task-id short= default= required=true + flag=type short= default= required= +ucloud udts stop use=stop short=Stop UDTS tasks + flag=async short= default=false required= + flag=project-id short= default= required= + flag=region short= default= required= + flag=task-id short= default=[] required=true + flag=type short= default= required= diff --git a/products/udts/testdata/completion.golden b/products/udts/testdata/completion.golden new file mode 100644 index 0000000000..69b88de6b0 --- /dev/null +++ b/products/udts/testdata/completion.golden @@ -0,0 +1,46 @@ +ucloud udts create charge-type static Dynamic,Month,Year +ucloud udts create project-id dynamic +ucloud udts create region dynamic +ucloud udts create src-mode static bidirectional,full,full+incremental,incremental +ucloud udts create src-nwtype static dedicated_line,public,user +ucloud udts create src-service-type static 2xlarge,4xlarge,large,medium,small +ucloud udts create tgt-nwtype static user +ucloud udts create type static integration,transfer +ucloud udts create zone dynamic +ucloud udts delete project-id dynamic +ucloud udts delete region dynamic +ucloud udts delete task-id dynamic +ucloud udts delete type static integration,transfer +ucloud udts describe project-id dynamic +ucloud udts describe region dynamic +ucloud udts describe task-id dynamic +ucloud udts history project-id dynamic +ucloud udts history region dynamic +ucloud udts history task-id dynamic +ucloud udts history type static integration,transfer +ucloud udts list project-id dynamic +ucloud udts list region dynamic +ucloud udts list type static integration,transfer +ucloud udts list zone dynamic +ucloud udts precheck project-id dynamic +ucloud udts precheck region dynamic +ucloud udts precheck src-mode static bidirectional,full,full+incremental,incremental +ucloud udts precheck src-nwtype static dedicated_line,public,user +ucloud udts precheck src-service-type static 2xlarge,4xlarge,large,medium,small +ucloud udts precheck tgt-nwtype static user +ucloud udts precheck type static integration,transfer +ucloud udts precheck-result project-id dynamic +ucloud udts precheck-result region dynamic +ucloud udts precheck-result type static integration,transfer +ucloud udts start project-id dynamic +ucloud udts start region dynamic +ucloud udts start task-id dynamic +ucloud udts start type static integration,transfer +ucloud udts status project-id dynamic +ucloud udts status region dynamic +ucloud udts status task-id dynamic +ucloud udts status type static integration,transfer +ucloud udts stop project-id dynamic +ucloud udts stop region dynamic +ucloud udts stop task-id dynamic +ucloud udts stop type static integration,transfer