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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/products.gen.go

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

28 changes: 28 additions & 0 deletions products/udts/internal/udts/cmd.go
Original file line number Diff line number Diff line change
@@ -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
}
58 changes: 58 additions & 0 deletions products/udts/internal/udts/completion.go
Original file line number Diff line number Diff line change
@@ -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
}
241 changes: 241 additions & 0 deletions products/udts/internal/udts/create.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading