diff --git a/.gitignore b/.gitignore index b64014c..17fffc0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ ydbops +vendor diff --git a/cmd/maintenance.go b/cmd/maintenance.go deleted file mode 100644 index 41dc36c..0000000 --- a/cmd/maintenance.go +++ /dev/null @@ -1,31 +0,0 @@ -package cmd - -import ( - "github.com/spf13/cobra" - - "github.com/ydb-platform/ydbops/pkg/cli" - "github.com/ydb-platform/ydbops/pkg/options" -) - -func NewMaintenanceCmd() *cobra.Command { - rootOpts := options.RootOptionsInstance - restartOpts := options.RestartOptionsInstance - - cmd := cli.SetDefaultsOn(&cobra.Command{ - Use: "maintenance", - Short: "Request hosts from the Cluster Management System", - Long: `ydbops maintenance [command]: - Manage host maintenance operations: request and return hosts - with performed maintenance back to the cluster.`, - PreRunE: cli.PopulateProfileDefaultsAndValidate( - restartOpts, rootOpts, - ), - RunE: cli.RequireSubcommand, - }) - - restartOpts.DefineFlags(cmd.PersistentFlags()) - return cmd -} - -func init() { -} diff --git a/cmd/maintenance/complete.go b/cmd/maintenance/complete.go deleted file mode 100644 index a8f3afd..0000000 --- a/cmd/maintenance/complete.go +++ /dev/null @@ -1,60 +0,0 @@ -package maintenance - -import ( - "fmt" - - "github.com/spf13/cobra" - - "github.com/ydb-platform/ydbops/pkg/cli" - "github.com/ydb-platform/ydbops/pkg/client" - "github.com/ydb-platform/ydbops/pkg/maintenance" - "github.com/ydb-platform/ydbops/pkg/options" - "github.com/ydb-platform/ydbops/pkg/prettyprint" -) - -func NewCompleteCmd() *cobra.Command { - rootOpts := options.RootOptionsInstance - - taskIdOpts := &options.TaskIdOpts{} - completeOpts := &options.CompleteOpts{} - - cmd := cli.SetDefaultsOn(&cobra.Command{ - Use: "complete", - Short: "Declare the maintenance task completed", - Long: `ydbops maintenance complete: - Any hosts that have been given to you within the task will be considered returned to the cluster. - You must not perform any host maintenance after you called this command.`, - PreRunE: cli.PopulateProfileDefaultsAndValidate( - taskIdOpts, rootOpts, - ), - RunE: func(cmd *cobra.Command, args []string) error { - err := client.InitConnectionFactory( - *rootOpts, - options.Logger, - options.DefaultRetryCount, - ) - if err != nil { - return err - } - - result, err := maintenance.CompleteActions(taskIdOpts, completeOpts) - if err != nil { - return err - } - - fmt.Println(prettyprint.ResultToString(result)) - - return nil - }, - }) - - taskIdOpts.DefineFlags(cmd.PersistentFlags()) - completeOpts.DefineFlags(cmd.PersistentFlags()) - - options.RootOptionsInstance.DefineFlags(cmd.PersistentFlags()) - - return cmd -} - -func init() { -} diff --git a/cmd/maintenance/complete/complete.go b/cmd/maintenance/complete/complete.go new file mode 100644 index 0000000..96057a7 --- /dev/null +++ b/cmd/maintenance/complete/complete.go @@ -0,0 +1,40 @@ +package complete + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/ydb-platform/ydbops/pkg/cli" + "github.com/ydb-platform/ydbops/pkg/cmdutil" + "github.com/ydb-platform/ydbops/pkg/prettyprint" +) + +func New(f cmdutil.Factory) *cobra.Command { + opts := &Options{} + + cmd := cli.SetDefaultsOn(&cobra.Command{ + Use: "complete", + Short: "Declare the maintenance task completed", + Long: `ydbops maintenance complete: + Any hosts that have been given to you within the task will be considered returned to the cluster. + You must not perform any host maintenance after you called this command.`, + PreRunE: cli.PopulateProfileDefaultsAndValidate( + f.GetBaseOptions(), opts, + ), + RunE: func(cmd *cobra.Command, args []string) error { + result, err := f.GetCMSClient().CompleteActions(opts.TaskID, opts.HostFQDNs) + if err != nil { + return err + } + + fmt.Println(prettyprint.ResultToString(result)) + + return nil + }, + }) + + opts.DefineFlags(cmd.PersistentFlags()) + + return cmd +} diff --git a/cmd/maintenance/complete/options.go b/cmd/maintenance/complete/options.go new file mode 100644 index 0000000..ed01e1b --- /dev/null +++ b/cmd/maintenance/complete/options.go @@ -0,0 +1,30 @@ +package complete + +import ( + "fmt" + + "github.com/spf13/pflag" +) + +type Options struct { + TaskID string + HostFQDNs []string +} + +func (o *Options) DefineFlags(fs *pflag.FlagSet) { + fs.StringSliceVar(&o.HostFQDNs, "hosts", []string{}, + "FQDNs of hosts with completed maintenance") + fs.StringVar(&o.TaskID, "task-id", "", + "ID of your maintenance task (result of `ydbops maintenance host`)") +} + +func (o *Options) Validate() error { + // TODO(shmel1k@): remove copypaste between drop, create & refresh methods. + if len(o.HostFQDNs) == 0 { + return fmt.Errorf("--hosts unspecified") + } + if o.TaskID == "" { + return fmt.Errorf("--task-id unspecified, argument required") + } + return nil +} diff --git a/cmd/maintenance/create.go b/cmd/maintenance/create.go deleted file mode 100644 index 6c1f817..0000000 --- a/cmd/maintenance/create.go +++ /dev/null @@ -1,58 +0,0 @@ -package maintenance - -import ( - "fmt" - - "github.com/spf13/cobra" - - "github.com/ydb-platform/ydbops/pkg/cli" - "github.com/ydb-platform/ydbops/pkg/client" - "github.com/ydb-platform/ydbops/pkg/maintenance" - "github.com/ydb-platform/ydbops/pkg/options" -) - -func NewCreateCmd() *cobra.Command { - rootOpts := options.RootOptionsInstance - - maintenanceCreateOpts := &options.MaintenanceCreateOpts{} - - cmd := cli.SetDefaultsOn(&cobra.Command{ - Use: "create", - Short: "Create a maintenance task to obtain a set of hosts", - Long: `ydbops maintenance create: - Create a maintenance task, which allows taking the set of hosts out of the cluster.`, - PreRunE: cli.PopulateProfileDefaultsAndValidate( - maintenanceCreateOpts, rootOpts, - ), - RunE: func(cmd *cobra.Command, args []string) error { - err := client.InitConnectionFactory( - *rootOpts, - options.Logger, - options.DefaultRetryCount, - ) - if err != nil { - return err - } - - taskId, err := maintenance.CreateTask(maintenanceCreateOpts) - if err != nil { - return err - } - - fmt.Printf( - "Your task id is:\n\n%s\n\nPlease write it down for refreshing and completing the task later.\n", - taskId, - ) - - return nil - }, - }) - - maintenanceCreateOpts.DefineFlags(cmd.PersistentFlags()) - options.RootOptionsInstance.DefineFlags(cmd.PersistentFlags()) - - return cmd -} - -func init() { -} diff --git a/cmd/maintenance/create/create.go b/cmd/maintenance/create/create.go new file mode 100644 index 0000000..67edcb0 --- /dev/null +++ b/cmd/maintenance/create/create.go @@ -0,0 +1,56 @@ +package create + +import ( + "fmt" + "time" + + "github.com/google/uuid" + "github.com/spf13/cobra" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/ydb-platform/ydbops/pkg/cli" + "github.com/ydb-platform/ydbops/pkg/client/cms" + "github.com/ydb-platform/ydbops/pkg/cmdutil" + "github.com/ydb-platform/ydbops/pkg/rolling" +) + +func New(f cmdutil.Factory) *cobra.Command { + opts := &Options{ + RestartOptions: &rolling.RestartOptions{}, + } + + cmd := cli.SetDefaultsOn(&cobra.Command{ + Use: "create", + Short: "Create a maintenance task to obtain a set of hosts", + Long: `ydbops maintenance create: + Create a maintenance task, which allows taking the set of hosts out of the cluster.`, + PreRunE: cli.PopulateProfileDefaultsAndValidate( + f.GetBaseOptions(), opts, + ), + RunE: func(cmd *cobra.Command, args []string) error { + taskUID := cms.TaskUuidPrefix + uuid.New().String() + duration := time.Duration(opts.RestartOptions.RestartDuration) * time.Minute + taskId, err := f.GetCMSClient().CreateMaintenanceTask(cms.MaintenanceTaskParams{ + Hosts: opts.RestartOptions.Hosts, + Duration: durationpb.New(duration), + AvailabilityMode: opts.RestartOptions.GetAvailabilityMode(), + ScopeType: cms.HostScope, + TaskUID: taskUID, + }) + if err != nil { + return err + } + + fmt.Printf( + "Your task id is:\n\n%s\n\nPlease write it down for refreshing and completing the task later.\n", + taskId.GetTaskUid(), + ) + + return nil + }, + }) + + opts.DefineFlags(cmd.PersistentFlags()) + + return cmd +} diff --git a/cmd/maintenance/create/options.go b/cmd/maintenance/create/options.go new file mode 100644 index 0000000..781c39c --- /dev/null +++ b/cmd/maintenance/create/options.go @@ -0,0 +1,18 @@ +package create + +import ( + "github.com/spf13/pflag" + "github.com/ydb-platform/ydbops/pkg/rolling" +) + +type Options struct { + *rolling.RestartOptions +} + +func (o *Options) DefineFlags(fs *pflag.FlagSet) { + o.RestartOptions.DefineFlags(fs) +} + +func (o *Options) Validate() error { + return o.RestartOptions.Validate() +} diff --git a/cmd/maintenance/drop.go b/cmd/maintenance/drop.go deleted file mode 100644 index af5bd34..0000000 --- a/cmd/maintenance/drop.go +++ /dev/null @@ -1,53 +0,0 @@ -package maintenance - -import ( - "github.com/spf13/cobra" - - "github.com/ydb-platform/ydbops/pkg/cli" - "github.com/ydb-platform/ydbops/pkg/client" - "github.com/ydb-platform/ydbops/pkg/maintenance" - "github.com/ydb-platform/ydbops/pkg/options" -) - -func NewDropCmd() *cobra.Command { - rootOpts := options.RootOptionsInstance - - taskIdOpts := &options.TaskIdOpts{} - - cmd := cli.SetDefaultsOn(&cobra.Command{ - Use: "drop", - Short: "Drop an existing maintenance task", - Long: `ydbops maintenance drop: - Drops the maintenance task, meaning two things: - 1. Any hosts given within the maintenance task will be considered returned. - 2. Any hosts requested, but not yet given, will not be reserved for you any longer.`, - PreRunE: cli.PopulateProfileDefaultsAndValidate( - taskIdOpts, rootOpts, - ), - RunE: func(cmd *cobra.Command, args []string) error { - err := client.InitConnectionFactory( - *rootOpts, - options.Logger, - options.DefaultRetryCount, - ) - if err != nil { - return err - } - - err = maintenance.DropTask(taskIdOpts) - if err != nil { - return err - } - - return nil - }, - }) - - taskIdOpts.DefineFlags(cmd.PersistentFlags()) - options.RootOptionsInstance.DefineFlags(cmd.PersistentFlags()) - - return cmd -} - -func init() { -} diff --git a/cmd/maintenance/drop/drop.go b/cmd/maintenance/drop/drop.go new file mode 100644 index 0000000..e046db4 --- /dev/null +++ b/cmd/maintenance/drop/drop.go @@ -0,0 +1,31 @@ +package drop + +import ( + "github.com/spf13/cobra" + + "github.com/ydb-platform/ydbops/pkg/cli" + "github.com/ydb-platform/ydbops/pkg/cmdutil" +) + +func New(f cmdutil.Factory) *cobra.Command { + taskIdOpts := &Options{} + + cmd := cli.SetDefaultsOn(&cobra.Command{ + Use: "drop", + Short: "Drop an existing maintenance task", + Long: `ydbops maintenance drop: + Drops the maintenance task, meaning two things: + 1. Any hosts given within the maintenance task will be considered returned. + 2. Any hosts requested, but not yet given, will not be reserved for you any longer.`, + PreRunE: cli.PopulateProfileDefaultsAndValidate( + f.GetBaseOptions(), taskIdOpts, + ), + RunE: func(cmd *cobra.Command, args []string) error { + return f.GetCMSClient().DropTask(taskIdOpts.TaskID) + }, + }) + + taskIdOpts.DefineFlags(cmd.PersistentFlags()) + + return cmd +} diff --git a/cmd/maintenance/drop/options.go b/cmd/maintenance/drop/options.go new file mode 100644 index 0000000..b7cb1a5 --- /dev/null +++ b/cmd/maintenance/drop/options.go @@ -0,0 +1,23 @@ +package drop + +import ( + "fmt" + + "github.com/spf13/pflag" +) + +type Options struct { + TaskID string +} + +func (o *Options) DefineFlags(fs *pflag.FlagSet) { + fs.StringVar(&o.TaskID, "task-id", "", + "ID of your maintenance task (result of `ydbops maintenance host`)") +} + +func (o *Options) Validate() error { + if o.TaskID == "" { + return fmt.Errorf("--task-id unspecified, argument required") + } + return nil +} diff --git a/cmd/maintenance/list.go b/cmd/maintenance/list/list.go similarity index 62% rename from cmd/maintenance/list.go rename to cmd/maintenance/list/list.go index 549f3cd..832e3bc 100644 --- a/cmd/maintenance/list.go +++ b/cmd/maintenance/list/list.go @@ -1,4 +1,4 @@ -package maintenance +package list import ( "fmt" @@ -6,35 +6,29 @@ import ( "github.com/spf13/cobra" "github.com/ydb-platform/ydbops/pkg/cli" - "github.com/ydb-platform/ydbops/pkg/client" - "github.com/ydb-platform/ydbops/pkg/maintenance" - "github.com/ydb-platform/ydbops/pkg/options" + "github.com/ydb-platform/ydbops/pkg/cmdutil" "github.com/ydb-platform/ydbops/pkg/prettyprint" ) -func NewListCmd() *cobra.Command { - rootOpts := options.RootOptionsInstance +func New(f cmdutil.Factory) *cobra.Command { + _ = &Options{} cmd := cli.SetDefaultsOn(&cobra.Command{ Use: "list", Short: "List all existing maintenance tasks", - Long: `ydbops maintenance list: + Long: `ydbops maintenance list: List all existing maintenance tasks on the cluster. Can be useful if you lost your task id to refresh/complete your own task.`, PreRunE: cli.PopulateProfileDefaultsAndValidate( - rootOpts, + f.GetBaseOptions(), ), RunE: func(cmd *cobra.Command, args []string) error { - err := client.InitConnectionFactory( - *rootOpts, - options.Logger, - options.DefaultRetryCount, - ) + userSID, err := f.GetDiscoveryClient().WhoAmI() if err != nil { return err } - tasks, err := maintenance.ListTasks() + tasks, err := f.GetCMSClient().MaintenanceTasks(userSID) if err != nil { return err } @@ -52,10 +46,5 @@ func NewListCmd() *cobra.Command { }, }) - options.RootOptionsInstance.DefineFlags(cmd.PersistentFlags()) - return cmd } - -func init() { -} diff --git a/cmd/maintenance/list/options.go b/cmd/maintenance/list/options.go new file mode 100644 index 0000000..0233a0c --- /dev/null +++ b/cmd/maintenance/list/options.go @@ -0,0 +1,3 @@ +package list + +type Options struct{} diff --git a/cmd/maintenance/maintenance.go b/cmd/maintenance/maintenance.go new file mode 100644 index 0000000..53eaf0b --- /dev/null +++ b/cmd/maintenance/maintenance.go @@ -0,0 +1,42 @@ +package maintenance + +import ( + "github.com/spf13/cobra" + "github.com/ydb-platform/ydbops/cmd/maintenance/complete" + "github.com/ydb-platform/ydbops/cmd/maintenance/create" + "github.com/ydb-platform/ydbops/cmd/maintenance/drop" + "github.com/ydb-platform/ydbops/cmd/maintenance/list" + "github.com/ydb-platform/ydbops/cmd/maintenance/refresh" + "github.com/ydb-platform/ydbops/pkg/cli" + "github.com/ydb-platform/ydbops/pkg/cmdutil" + "github.com/ydb-platform/ydbops/pkg/command" +) + +type Options struct { + *command.BaseOptions +} + +func New(f cmdutil.Factory) *cobra.Command { + options := &Options{} + c := cli.SetDefaultsOn(&cobra.Command{ + Use: "maintenance", + Short: "Request hosts from the Cluster Management System", + Long: `ydbops maintenance [command]: + Manage host maintenance operations: request and return hosts + with performed maintenance back to the cluster.`, + PreRunE: cli.PopulateProfileDefaultsAndValidate( + options.BaseOptions, options, + ), + RunE: cli.RequireSubcommand, + }) + + c.AddCommand( + complete.New(f), + create.New(f), + drop.New(f), + list.New(f), + refresh.New(f), + ) + + return c +} diff --git a/cmd/maintenance/refresh/options.go b/cmd/maintenance/refresh/options.go new file mode 100644 index 0000000..63bcec0 --- /dev/null +++ b/cmd/maintenance/refresh/options.go @@ -0,0 +1,23 @@ +package refresh + +import ( + "fmt" + + "github.com/spf13/pflag" +) + +type Options struct { + TaskID string +} + +func (o *Options) DefineFlags(fs *pflag.FlagSet) { + fs.StringVar(&o.TaskID, "task-id", "", + "ID of your maintenance task (result of `ydbops maintenance host`)") +} + +func (o *Options) Validate() error { + if o.TaskID == "" { + return fmt.Errorf("--task-id unspecified, argument required") + } + return nil +} diff --git a/cmd/maintenance/refresh.go b/cmd/maintenance/refresh/refresh.go similarity index 50% rename from cmd/maintenance/refresh.go rename to cmd/maintenance/refresh/refresh.go index 3973b15..f67ec02 100644 --- a/cmd/maintenance/refresh.go +++ b/cmd/maintenance/refresh/refresh.go @@ -1,4 +1,4 @@ -package maintenance +package refresh import ( "fmt" @@ -6,36 +6,23 @@ import ( "github.com/spf13/cobra" "github.com/ydb-platform/ydbops/pkg/cli" - "github.com/ydb-platform/ydbops/pkg/client" - "github.com/ydb-platform/ydbops/pkg/maintenance" - "github.com/ydb-platform/ydbops/pkg/options" + "github.com/ydb-platform/ydbops/pkg/cmdutil" "github.com/ydb-platform/ydbops/pkg/prettyprint" ) -func NewRefreshCmd() *cobra.Command { - rootOpts := options.RootOptionsInstance - - taskIdOpts := &options.TaskIdOpts{} +func New(f cmdutil.Factory) *cobra.Command { + taskIdOpts := &Options{} cmd := cli.SetDefaultsOn(&cobra.Command{ Use: "refresh", Short: "Try to obtain previously reserved hosts", - Long: `ydbops maintenance refresh: + Long: `ydbops maintenance refresh: Performs a request to check whether any previously reserved hosts have become available.`, PreRunE: cli.PopulateProfileDefaultsAndValidate( - taskIdOpts, rootOpts, + f.GetBaseOptions(), taskIdOpts, ), RunE: func(cmd *cobra.Command, args []string) error { - err := client.InitConnectionFactory( - *rootOpts, - options.Logger, - options.DefaultRetryCount, - ) - if err != nil { - return err - } - - task, err := maintenance.RefreshTask(taskIdOpts) + task, err := f.GetCMSClient().RefreshTask(taskIdOpts.TaskID) if err != nil { return err } @@ -47,10 +34,6 @@ func NewRefreshCmd() *cobra.Command { }) taskIdOpts.DefineFlags(cmd.PersistentFlags()) - options.RootOptionsInstance.DefineFlags(cmd.PersistentFlags()) return cmd } - -func init() { -} diff --git a/cmd/restart.go b/cmd/restart.go deleted file mode 100644 index ea2626f..0000000 --- a/cmd/restart.go +++ /dev/null @@ -1,89 +0,0 @@ -package cmd - -import ( - "fmt" - - "github.com/spf13/cobra" - - "github.com/ydb-platform/ydbops/pkg/cli" - "github.com/ydb-platform/ydbops/pkg/client" - "github.com/ydb-platform/ydbops/pkg/options" - "github.com/ydb-platform/ydbops/pkg/rolling" - "github.com/ydb-platform/ydbops/pkg/rolling/restarters" -) - -func NewRestartCmd() *cobra.Command { - rootOpts := options.RootOptionsInstance - restartOpts := options.RestartOptionsInstance - - cmd := cli.SetDefaultsOn(&cobra.Command{ - Use: "restart", - Short: "Restarts a specified subset of nodes in the cluster", - Long: `ydbops restart: - Restarts a specified subset of nodes in the cluster. - By default will restart all nodes, filters can be specified to - narrow down what subset will be restarted.`, - PreRunE: cli.PopulateProfileDefaultsAndValidate( - restartOpts, rootOpts, - ), - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) > 0 { - return fmt.Errorf("Free args not expected: %v", args) - } - - var storageRestarter restarters.Restarter - var tenantRestarter restarters.Restarter - - err := client.InitConnectionFactory( - *rootOpts, - options.Logger, - options.DefaultRetryCount, - ) - if err != nil { - return err - } - - if restartOpts.KubeconfigPath != "" { - storageRestarter = restarters.NewStorageK8sRestarter( - options.Logger, - restartOpts.KubeconfigPath, - restartOpts.K8sNamespace, - ) - tenantRestarter = restarters.NewTenantK8sRestarter( - options.Logger, - restartOpts.KubeconfigPath, - restartOpts.K8sNamespace, - ) - } else { - storageRestarter = restarters.NewStorageSSHRestarter( - options.Logger, - restartOpts.SSHArgs, - restartOpts.CustomSystemdUnitName, - ) - tenantRestarter = restarters.NewTenantSSHRestarter( - options.Logger, - restartOpts.SSHArgs, - restartOpts.CustomSystemdUnitName, - ) - } - - bothUnspecified := !restartOpts.Storage && !restartOpts.Tenant - - if restartOpts.Storage || bothUnspecified { - err = rolling.ExecuteRolling(*restartOpts, options.Logger, storageRestarter) - } - - if err == nil && (restartOpts.Tenant || bothUnspecified) { - err = rolling.ExecuteRolling(*restartOpts, options.Logger, tenantRestarter) - } - - return err - }, - }) - - restartOpts.DefineFlags(cmd.PersistentFlags()) - return cmd -} - -func init() { -} diff --git a/cmd/restart/options.go b/cmd/restart/options.go new file mode 100644 index 0000000..f7d402d --- /dev/null +++ b/cmd/restart/options.go @@ -0,0 +1,14 @@ +package restart + +import ( + "github.com/spf13/pflag" + "github.com/ydb-platform/ydbops/pkg/rolling" +) + +type Options struct { + *rolling.RestartOptions +} + +func (o *Options) DefineFlags(fs *pflag.FlagSet) { + o.RestartOptions.DefineFlags(fs) +} diff --git a/cmd/restart/restart.go b/cmd/restart/restart.go new file mode 100644 index 0000000..03a3564 --- /dev/null +++ b/cmd/restart/restart.go @@ -0,0 +1,103 @@ +package restart + +import ( + "fmt" + "time" + + "github.com/spf13/cobra" + "go.uber.org/zap" + + "github.com/ydb-platform/ydbops/pkg/cli" + "github.com/ydb-platform/ydbops/pkg/cmdutil" + "github.com/ydb-platform/ydbops/pkg/command" + "github.com/ydb-platform/ydbops/pkg/options" + "github.com/ydb-platform/ydbops/pkg/rolling" + "github.com/ydb-platform/ydbops/pkg/rolling/restarters" +) + +var RestartCommandDescription = command.NewDescription( + "restart", + "Restarts a specified subset of nodes in the cluster", + `ydbops restart: + Restarts a specified subset of nodes in the cluster. + By default will restart all nodes, filters can be specified to + narrow down what subset will be restarted.`) + +func New( + f cmdutil.Factory, +) *cobra.Command { + opts := &Options{ + RestartOptions: &rolling.RestartOptions{}, + } + cmd := &cobra.Command{ + Use: RestartCommandDescription.GetUse(), + Short: RestartCommandDescription.GetShortDescription(), + Long: RestartCommandDescription.GetLongDescription(), + PreRunE: cli.PopulateProfileDefaultsAndValidate(f.GetBaseOptions(), opts.RestartOptions), + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + return fmt.Errorf("Free args not expected: %v", args) + } + + err := opts.Validate() + if err != nil { + return err + } + + var storageRestarter restarters.Restarter + var tenantRestarter restarters.Restarter + + if opts.RestartOptions.KubeconfigPath != "" { + storageRestarter = restarters.NewStorageK8sRestarter( + options.Logger, + &restarters.StorageK8sRestarterOptions{ + K8sRestarterOptions: &restarters.K8sRestarterOptions{ + KubeconfigPath: opts.RestartOptions.KubeconfigPath, + Namespace: opts.RestartOptions.K8sNamespace, + RestartDuration: time.Duration(opts.RestartOptions.RestartDuration) * time.Second, + }, + }, + ) + tenantRestarter = restarters.NewTenantK8sRestarter( + options.Logger, + &restarters.TenantK8sRestarterOptions{ + K8sRestarterOptions: &restarters.K8sRestarterOptions{ + KubeconfigPath: opts.RestartOptions.KubeconfigPath, + Namespace: opts.RestartOptions.K8sNamespace, + RestartDuration: time.Duration(opts.RestartOptions.RestartDuration) * time.Second, + }, + }, + ) + } else { + storageRestarter = restarters.NewStorageSSHRestarter( + options.Logger, + opts.RestartOptions.SSHArgs, + opts.RestartOptions.CustomSystemdUnitName, + ) + tenantRestarter = restarters.NewTenantSSHRestarter( + options.Logger, + opts.RestartOptions.SSHArgs, + opts.RestartOptions.CustomSystemdUnitName, + ) + } + + bothUnspecified := !opts.RestartOptions.Storage && !opts.RestartOptions.Tenant + + var executer rolling.Executer + if opts.RestartOptions.Storage || bothUnspecified { + // TODO(shmel1k@): add logger to NewExecuter parameters + executer = rolling.NewExecuter(opts.RestartOptions, zap.S(), f.GetCMSClient(), f.GetDiscoveryClient(), storageRestarter) + err = executer.Execute() + } + + if err == nil && (opts.RestartOptions.Tenant || bothUnspecified) { + executer = rolling.NewExecuter(opts.RestartOptions, zap.S(), f.GetCMSClient(), f.GetDiscoveryClient(), tenantRestarter) + } + err = executer.Execute() + return err + }, + } + opts.DefineFlags(cmd.Flags()) + + return cmd +} diff --git a/cmd/root.go b/cmd/root.go index fdc3f7c..589da80 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -3,46 +3,63 @@ package cmd import ( "github.com/fatih/color" "github.com/spf13/cobra" + "github.com/spf13/pflag" "go.uber.org/zap" "go.uber.org/zap/zapcore" "github.com/ydb-platform/ydbops/cmd/maintenance" + "github.com/ydb-platform/ydbops/cmd/restart" + "github.com/ydb-platform/ydbops/cmd/run" iCli "github.com/ydb-platform/ydbops/internal/cli" "github.com/ydb-platform/ydbops/pkg/cli" + "github.com/ydb-platform/ydbops/pkg/cmdutil" + "github.com/ydb-platform/ydbops/pkg/command" "github.com/ydb-platform/ydbops/pkg/options" ) -func addAndReturnCmd(cmd *cobra.Command, rest ...*cobra.Command) *cobra.Command { - for _, subCmd := range rest { - cmd.AddCommand(subCmd) - } - return cmd +var RootCommandDescription = command.NewDescription( + "ydbops", + "ydbops: a CLI tool for performing YDB cluster maintenance operations", + "ydbops: a CLI tool for performing YDB cluster maintenance operations", +) + +type RootOptions struct { + *command.BaseOptions } -func registerAllSubcommands(root *cobra.Command) { - _ = addAndReturnCmd(root, - NewRestartCmd(), - NewRunCmd(), - addAndReturnCmd(NewMaintenanceCmd(), - maintenance.NewCreateCmd(), - maintenance.NewListCmd(), - maintenance.NewDropCmd(), - maintenance.NewRefreshCmd(), - maintenance.NewCompleteCmd(), - ), - ) +func (r *RootOptions) Validate() error { + return r.BaseOptions.Validate() +} + +func (r *RootOptions) DefineFlags(fs *pflag.FlagSet) { + r.BaseOptions.DefineFlags(fs) } -var RootCmd *cobra.Command +func NewRootCommand( + logLevelSetter zap.AtomicLevel, + logger *zap.SugaredLogger, + boptions *command.BaseOptions, +) *cobra.Command { + roptions := &RootOptions{ + BaseOptions: boptions, + } + cmd := &cobra.Command{ + Use: RootCommandDescription.GetUse(), + Short: RootCommandDescription.GetShortDescription(), + Long: RootCommandDescription.GetLongDescription(), + // hide --completion for more compact --help + CompletionOptions: cobra.CompletionOptions{ + DisableDefaultCmd: true, + }, + SilenceUsage: true, + PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { + err := options.Validate() + if err != nil { + return err + } -func InitRootCmd(logLevelSetter zap.AtomicLevel, logger *zap.SugaredLogger) { - RootCmd = &cobra.Command{ - Use: "ydbops", - Short: "ydbops: a CLI tool for performing YDB cluster maintenance operations", - Long: "ydbops: a CLI tool for performing YDB cluster maintenance operations", - PersistentPreRunE: func(_ *cobra.Command, _ []string) error { logLevel := "info" - if options.RootOptionsInstance.Verbose { + if roptions.Verbose { logLevel = "debug" } @@ -54,33 +71,27 @@ func InitRootCmd(logLevelSetter zap.AtomicLevel, logger *zap.SugaredLogger) { logLevelSetter.SetLevel(lvc) zap.S().Debugf("Current logging level enabled: %s", logLevel) - return nil }, - // hide --completion for more compact --help - CompletionOptions: cobra.CompletionOptions{ - DisableDefaultCmd: true, - }, - SilenceUsage: true, - RunE: cli.RequireSubcommand, + RunE: cli.RequireSubcommand, } + roptions.DefineFlags(cmd.PersistentFlags()) - RootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) - - RootCmd.Flags().SortFlags = false - RootCmd.PersistentFlags().SortFlags = false + cmd.SetHelpCommand(&cobra.Command{ + Hidden: true, + }) + cmd.Flags().SortFlags = false + cmd.PersistentFlags().SortFlags = false + cmd.SetOutput(color.Output) + cmd.SetUsageTemplate(iCli.UsageTemplate) - RootCmd.SetOutput(color.Output) - - defer func() { - _ = logger.Sync() - }() - - options.Logger = logger - - options.RootOptionsInstance.DefineFlags(RootCmd.PersistentFlags()) - - registerAllSubcommands(RootCmd) + return cmd +} - RootCmd.SetUsageTemplate(iCli.UsageTemplate) +func InitRootCommandTree(root *cobra.Command, f cmdutil.Factory) { + root.AddCommand( + restart.New(f), + maintenance.New(f), + run.New(f), + ) } diff --git a/cmd/run.go b/cmd/run.go deleted file mode 100644 index 9fef71b..0000000 --- a/cmd/run.go +++ /dev/null @@ -1,74 +0,0 @@ -package cmd - -import ( - "fmt" - - "github.com/spf13/cobra" - - "github.com/ydb-platform/ydbops/pkg/cli" - "github.com/ydb-platform/ydbops/pkg/client" - "github.com/ydb-platform/ydbops/pkg/options" - "github.com/ydb-platform/ydbops/pkg/rolling" - "github.com/ydb-platform/ydbops/pkg/rolling/restarters" -) - -func NewRunCmd() *cobra.Command { - rootOpts := options.RootOptionsInstance - restartOpts := options.RestartOptionsInstance - restarter := restarters.NewRunRestarter(options.Logger) - - cmd := cli.SetDefaultsOn(&cobra.Command{ - Use: "run", - Short: "Run an arbitrary executable (e.g. shell code) in the context of the local machine", - Long: `ydbops restart run: - Run an arbitrary executable (e.g. shell code) in the context of the local machine - (where rolling-restart is launched). For example, if you want to execute ssh commands - on the ydb cluster node, you must write ssh commands yourself. See the examples. - - For every node released by CMS, ydbops will execute this payload independently. - - Restart will be treated as successful if your executable finished with a zero - return code. - - Certain environment variable will be passed to your executable on each run: - $HOSTNAME: the fqdn of the node currently released by CMS.`, - PreRunE: cli.PopulateProfileDefaultsAndValidate( - restartOpts, rootOpts, restarter.Opts, - ), - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) > 0 { - return fmt.Errorf("Free args not expected: %v", args) - } - - err := client.InitConnectionFactory( - *rootOpts, - options.Logger, - options.DefaultRetryCount, - ) - if err != nil { - return err - } - - bothUnspecified := !restartOpts.Storage && !restartOpts.Tenant - - if restartOpts.Storage || bothUnspecified { - restarter.SetStorageOnly() - err = rolling.ExecuteRolling(*restartOpts, options.Logger, restarter) - } - - if err == nil && (restartOpts.Tenant || bothUnspecified) { - restarter.SetDynnodeOnly() - err = rolling.ExecuteRolling(*restartOpts, options.Logger, restarter) - } - - return err - }, - }) - - restarter.Opts.DefineFlags(cmd.Flags()) - restartOpts.DefineFlags(cmd.Flags()) - return cmd -} - -func init() { -} diff --git a/cmd/run/options.go b/cmd/run/options.go new file mode 100644 index 0000000..e906c56 --- /dev/null +++ b/cmd/run/options.go @@ -0,0 +1,52 @@ +package run + +import ( + "errors" + "fmt" + "io/fs" + "os" + + "github.com/spf13/pflag" + "github.com/ydb-platform/ydbops/pkg/rolling" +) + +type Options struct { + *rolling.RestartOptions + PayloadFilePath string +} + +func (r *Options) DefineFlags(fs *pflag.FlagSet) { + r.RestartOptions.DefineFlags(fs) + fs.StringVar( + &r.PayloadFilePath, + "payload", + "", + "File path to arbitrary executable to run in the context of the local machine", + ) +} + +func (r *Options) Validate() error { + err := r.RestartOptions.Validate() + if err != nil { + return err + } + + if r.PayloadFilePath == "" { + return fmt.Errorf("empty --payload specified") + } + fileInfo, err := os.Stat(r.PayloadFilePath) + if errors.Is(err, fs.ErrNotExist) { + return fmt.Errorf("payload file '%s' does not exist", r.PayloadFilePath) + } + + // Apologies, this is really an idiomatic way to check the permission in Go. + // Just run some bitmagic. 0100 is octal, in binary it would be equivalent to: + // 000001000000 + // drwxrwxrwx + executableByOwner := 0o100 + if fileInfo.Mode()&fs.FileMode(executableByOwner) != fs.FileMode(executableByOwner) { + return fmt.Errorf("payload file '%s' is not executable by the owner", r.PayloadFilePath) + } + + return nil +} diff --git a/cmd/run/run.go b/cmd/run/run.go new file mode 100644 index 0000000..1b46de5 --- /dev/null +++ b/cmd/run/run.go @@ -0,0 +1,75 @@ +package run + +import ( + "fmt" + + "github.com/spf13/cobra" + "github.com/ydb-platform/ydbops/pkg/cli" + "github.com/ydb-platform/ydbops/pkg/cmdutil" + "github.com/ydb-platform/ydbops/pkg/command" + "github.com/ydb-platform/ydbops/pkg/options" + "github.com/ydb-platform/ydbops/pkg/rolling" + "github.com/ydb-platform/ydbops/pkg/rolling/restarters" + "go.uber.org/zap" +) + +var RunCommandDescription = command.NewDescription( + "run", + "Run an arbitrary executable (e.g. shell code) in the context of the local machine", + `ydbops restart run: + Run an arbitrary executable (e.g. shell code) in the context of the local machine + (where rolling-restart is launched). For example, if you want to execute ssh commands + on the ydb cluster node, you must write ssh commands yourself. See the examples. + + For every node released by CMS, ydbops will execute this payload independently. + + Restart will be treated as successful if your executable finished with a zero + return code. + + Certain environment variable will be passed to your executable on each run: + $HOSTNAME: the fqdn of the node currently released by CMS.`, +) + +func New( + f cmdutil.Factory, +) *cobra.Command { + opts := &Options{ + RestartOptions: &rolling.RestartOptions{}, + } + cmd := &cobra.Command{ + Use: RunCommandDescription.GetUse(), + Short: RunCommandDescription.GetShortDescription(), + Long: RunCommandDescription.GetLongDescription(), + PreRunE: cli.PopulateProfileDefaultsAndValidate(f.GetBaseOptions(), opts), + RunE: func(_ *cobra.Command, args []string) error { + if len(args) > 0 { + return fmt.Errorf("Free args not expected: %v", args) + } + + bothUnspecified := !opts.RestartOptions.Storage && !opts.RestartOptions.Tenant + + restarter := restarters.NewRunRestarter(zap.S(), &restarters.RunRestarterParams{ + PayloadFilePath: opts.PayloadFilePath, + }) + + var executer rolling.Executer + var err error + if opts.RestartOptions.Storage || bothUnspecified { + restarter.SetStorageOnly() + executer = rolling.NewExecuter(opts.RestartOptions, options.Logger, f.GetCMSClient(), f.GetDiscoveryClient(), restarter) + err = executer.Execute() + } + + if err == nil && (opts.RestartOptions.Tenant || bothUnspecified) { + restarter.SetDynnodeOnly() + executer = rolling.NewExecuter(opts.RestartOptions, options.Logger, f.GetCMSClient(), f.GetDiscoveryClient(), restarter) + err = executer.Execute() + } + + return err + }, + } + + opts.DefineFlags(cmd.Flags()) + return cmd +} diff --git a/main.go b/main.go index a3d99fa..92e9b72 100644 --- a/main.go +++ b/main.go @@ -3,10 +3,16 @@ package main import ( "os" + "github.com/ydb-platform/ydbops/cmd" + "github.com/ydb-platform/ydbops/pkg/client/auth/credentials" + "github.com/ydb-platform/ydbops/pkg/client/cms" + "github.com/ydb-platform/ydbops/pkg/client/connectionsfactory" + "github.com/ydb-platform/ydbops/pkg/client/discovery" + "github.com/ydb-platform/ydbops/pkg/cmdutil" + "github.com/ydb-platform/ydbops/pkg/command" + "github.com/ydb-platform/ydbops/pkg/options" "go.uber.org/zap" "go.uber.org/zap/zapcore" - - "github.com/ydb-platform/ydbops/cmd" ) func createLogger(level string) (zap.AtomicLevel, *zap.Logger) { @@ -26,9 +32,41 @@ func createLogger(level string) (zap.AtomicLevel, *zap.Logger) { return atom, logger } +var ( + factory cmdutil.Factory + baseOptions *command.BaseOptions + cmsClient cms.Client + discoveryClient discovery.Client +) + +func initFactory() { + factory = cmdutil.New(baseOptions, cmsClient, discoveryClient) +} + +func initClients( + cf connectionsfactory.Factory, + logger *zap.SugaredLogger, + cp credentials.Provider, +) { + cmsClient = cms.NewCMSClient(cf, logger, cp) + discoveryClient = discovery.NewDiscoveryClient(cf, logger, cp) +} + func main() { logLevelSetter, logger := createLogger("info") - cmd.InitRootCmd(logLevelSetter, logger.Sugar()) + baseOptions = &command.BaseOptions{} + root := cmd.NewRootCommand(logLevelSetter, logger.Sugar(), baseOptions) + cf := connectionsfactory.New(baseOptions) + + options.Logger = logger.Sugar() // TODO(shmel1k@): tmp hack + + credentialsProvider := credentials.New(baseOptions, cf, logger.Sugar(), nil) + initClients(cf, logger.Sugar(), credentialsProvider) + initFactory() - _ = cmd.RootCmd.Execute() + defer func() { + _ = logger.Sync() + }() + cmd.InitRootCommandTree(root, factory) + _ = root.Execute() } diff --git a/pkg/cli/cobra.go b/pkg/cli/cobra.go index 3c8a6db..d9162ca 100644 --- a/pkg/cli/cobra.go +++ b/pkg/cli/cobra.go @@ -7,13 +7,13 @@ import ( "github.com/spf13/cobra" "github.com/ydb-platform/ydbops/internal/cli" + "github.com/ydb-platform/ydbops/pkg/command" "github.com/ydb-platform/ydbops/pkg/options" "github.com/ydb-platform/ydbops/pkg/profile" ) -func PopulateProfileDefaultsAndValidate(optsArgs ...options.Options) func(*cobra.Command, []string) error { +func PopulateProfileDefaultsAndValidate(rootOpts *command.BaseOptions, optsArgs ...options.Options) func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { - rootOpts := options.RootOptionsInstance err := profile.FillDefaultsFromActiveProfile(rootOpts.ProfileFile, rootOpts.ActiveProfile) if err != nil { return err @@ -24,7 +24,7 @@ func PopulateProfileDefaultsAndValidate(optsArgs ...options.Options) func(*cobra return fmt.Errorf("%w\nTry '--help' option for more info", err) } } - return nil + return rootOpts.Validate() } } diff --git a/pkg/client/auth.go b/pkg/client/auth.go deleted file mode 100644 index e52eca5..0000000 --- a/pkg/client/auth.go +++ /dev/null @@ -1,114 +0,0 @@ -package client - -import ( - "context" - "fmt" - - "github.com/ydb-platform/ydb-go-genproto/Ydb_Auth_V1" - "github.com/ydb-platform/ydb-go-genproto/protos/Ydb" - "github.com/ydb-platform/ydb-go-genproto/protos/Ydb_Auth" - "github.com/ydb-platform/ydb-go-genproto/protos/Ydb_Operations" - "go.uber.org/zap" - "google.golang.org/protobuf/proto" - - "github.com/ydb-platform/ydbops/pkg/options" -) - -type Auth struct { - logger *zap.SugaredLogger - f *Factory -} - -func NewAuthClient(logger *zap.SugaredLogger, f *Factory) *Auth { - return &Auth{ - logger: logger, - f: f, - } -} - -func (c *Auth) Auth(grpcOpts options.GRPC, user, password string) (string, error) { - result := Ydb_Auth.LoginResult{} - - c.logger.Debug("Invoke Auth method") - _, err := c.ExecuteAuthMethod(&result, func(ctx context.Context, cl Ydb_Auth_V1.AuthServiceClient) (OperationResponse, error) { - return cl.Login(ctx, &Ydb_Auth.LoginRequest{ - OperationParams: c.f.OperationParams(), - User: user, - Password: password, - }) - }, grpcOpts) - if err != nil { - return "", err - } - c.logger.Debugf("Login response: %s... (token contents masked)", string([]rune(result.Token)[:20])) - return result.Token, nil -} - -func (c *Auth) ExecuteAuthMethod( - out proto.Message, - method func(context.Context, Ydb_Auth_V1.AuthServiceClient) (OperationResponse, error), - grpcOpts options.GRPC, -) (*Ydb_Operations.Operation, error) { - cc, err := c.f.Connection() - if err != nil { - return nil, err - } - - ctx, cancel := c.f.ContextWithoutAuth() - defer cancel() - - cl := Ydb_Auth_V1.NewAuthServiceClient(cc) - r, err := method(ctx, cl) - if err != nil { - c.logger.Errorf("Invocation error: %+v", err) - return nil, err - } - op := r.GetOperation() - LogOperation(c.logger, op) - - if out == nil { - return op, nil - } - - if err := op.Result.UnmarshalTo(out); err != nil { - return op, err - } - - if op.Status != Ydb.StatusIds_SUCCESS { - return op, fmt.Errorf("unsuccessful status code: %s", op.Status) - } - - return op, nil -} - -func initAuthToken( - rootOpts options.RootOptions, - logger *zap.SugaredLogger, - factory *Factory, -) error { - switch rootOpts.Auth.Type { - case options.Static: - authClient := NewAuthClient(logger, factory) - staticCreds := rootOpts.Auth.Creds.(*options.AuthStatic) - user := staticCreds.User - password := staticCreds.Password - logger.Debugf("Endpoint: %v", rootOpts.GRPC.Endpoint) - token, err := authClient.Auth(rootOpts.GRPC, user, password) - if err != nil { - return fmt.Errorf("failed to initialize static auth token: %w", err) - } - factory.token = token - case options.IamToken: - factory.token = rootOpts.Auth.Creds.(*options.AuthIAMToken).Token - case options.IamCreds: - return fmt.Errorf("TODO: IAM authorization from SA key not implemented yet") - case options.None: - return fmt.Errorf("determined credentials to be anonymous. Anonymous credentials are currently unsupported") - default: - return fmt.Errorf( - "internal error: authorization type not recognized after options validation, this should never happen", - ) - } - - return nil -} diff --git a/pkg/client/auth/auth.go b/pkg/client/auth/auth.go new file mode 100644 index 0000000..ca2bbc8 --- /dev/null +++ b/pkg/client/auth/auth.go @@ -0,0 +1,89 @@ +package auth + +import ( + "context" + "fmt" + + "github.com/ydb-platform/ydb-go-genproto/Ydb_Auth_V1" + "github.com/ydb-platform/ydb-go-genproto/protos/Ydb" + "github.com/ydb-platform/ydb-go-genproto/protos/Ydb_Auth" + "github.com/ydb-platform/ydb-go-genproto/protos/Ydb_Operations" + "go.uber.org/zap" + "google.golang.org/protobuf/proto" + + "github.com/ydb-platform/ydbops/pkg/client" + "github.com/ydb-platform/ydbops/pkg/client/connectionsfactory" + "github.com/ydb-platform/ydbops/pkg/utils" +) + +type Client interface { + Auth(string, string) (string, error) // TODO(shmel1k@): add context to params +} + +type defaultAuthClient struct { + logger *zap.SugaredLogger + f connectionsfactory.Factory +} + +func NewClient( + logger *zap.SugaredLogger, + f connectionsfactory.Factory, +) Client { + return &defaultAuthClient{ + logger: logger, + f: f, + } +} + +func (c *defaultAuthClient) executeAuthMethod( + out proto.Message, + method func(context.Context, Ydb_Auth_V1.AuthServiceClient) (client.OperationResponse, error), +) (*Ydb_Operations.Operation, error) { + cc, err := c.f.Create() + if err != nil { + return nil, err + } + + ctx := context.TODO() // XXX(shmel1k@): improve context behavior. + + cl := Ydb_Auth_V1.NewAuthServiceClient(cc) + r, err := method(ctx, cl) + if err != nil { + c.logger.Errorf("Invocation error: %+v", err) + return nil, err + } + op := r.GetOperation() + utils.LogOperation(c.logger, op) + + if out == nil { + return op, nil + } + + if err := op.Result.UnmarshalTo(out); err != nil { + return op, err + } + + if op.Status != Ydb.StatusIds_SUCCESS { + return op, fmt.Errorf("unsuccessful status code: %s", op.Status) + } + + return op, nil +} + +func (c *defaultAuthClient) Auth(user, password string) (string, error) { + result := Ydb_Auth.LoginResult{} + + c.logger.Debug("Invoke Auth method") + _, err := c.executeAuthMethod(&result, func(ctx context.Context, cl Ydb_Auth_V1.AuthServiceClient) (client.OperationResponse, error) { + return cl.Login(ctx, &Ydb_Auth.LoginRequest{ + OperationParams: c.f.OperationParams(), + User: user, + Password: password, + }) + }) + if err != nil { + return "", err + } + c.logger.Debugf("Login response: %s... (token contents masked)", string([]rune(result.Token)[:20])) + return result.Token, nil +} diff --git a/pkg/client/auth/credentials/iam_token.go b/pkg/client/auth/credentials/iam_token.go new file mode 100644 index 0000000..b39da6d --- /dev/null +++ b/pkg/client/auth/credentials/iam_token.go @@ -0,0 +1,33 @@ +package credentials + +import "context" + +type iamTokenCredentialsProvider struct { + token string +} + +// ContextWithAuth implements Provider. +func (i *iamTokenCredentialsProvider) ContextWithAuth(context.Context) (context.Context, context.CancelFunc) { + panic("unimplemented") +} + +// ContextWithoutAuth implements Provider. +func (i *iamTokenCredentialsProvider) ContextWithoutAuth(context.Context) (context.Context, context.CancelFunc) { + panic("unimplemented") +} + +// GetToken implements Provider. +func (i *iamTokenCredentialsProvider) GetToken() (string, error) { + return i.token, nil +} + +// Init implements Provider. +func (i *iamTokenCredentialsProvider) Init() error { + return nil +} + +func NewIamToken(token string) Provider { + return &iamTokenCredentialsProvider{ + token: token, + } +} diff --git a/pkg/client/auth/credentials/provider.go b/pkg/client/auth/credentials/provider.go new file mode 100644 index 0000000..955b487 --- /dev/null +++ b/pkg/client/auth/credentials/provider.go @@ -0,0 +1,100 @@ +package credentials + +import ( + "context" + "fmt" + "sync" + + "github.com/ydb-platform/ydbops/pkg/client/connectionsfactory" + "github.com/ydb-platform/ydbops/pkg/command" + "github.com/ydb-platform/ydbops/pkg/options" + "go.uber.org/zap" +) + +type Provider interface { + ContextWithAuth(context.Context) (context.Context, context.CancelFunc) // TODO(shmel1k@): think about compatibility + // with ydb-go-sdk + ContextWithoutAuth(context.Context) (context.Context, context.CancelFunc) + + GetToken() (string, error) + Init() error +} + +type baseProvider struct { + impl Provider + opts *command.BaseOptions + connectionsFactory connectionsfactory.Factory + logger *zap.SugaredLogger + + once sync.Once + initErr error +} + +// ContextWithAuth implements Provider. +func (b *baseProvider) ContextWithAuth(ctx context.Context) (context.Context, context.CancelFunc) { + err := b.Init() + if err != nil { + b.logger.Fatal(err) + } + return b.impl.ContextWithAuth(ctx) +} + +// ContextWithoutAuth implements Provider. +func (b *baseProvider) ContextWithoutAuth(ctx context.Context) (context.Context, context.CancelFunc) { + err := b.Init() + if err != nil { + b.logger.Fatal(err) + } + return b.impl.ContextWithoutAuth(ctx) +} + +// GetToken implements Provider. +func (b *baseProvider) GetToken() (string, error) { + err := b.Init() + if err != nil { + b.logger.Fatal(err) + } + return b.impl.GetToken() +} + +// Init implements Provider. +func (b *baseProvider) Init() error { + b.once.Do(func() { + // NOTE(shmel1k@): impl can be overridden during provider initialization. + // For example, in main, when credentials provider is known and initialized. + if b.impl != nil { + return + } + + switch b.opts.Auth.Type { + case options.Static: + staticCreds := b.opts.Auth.Creds.(*options.AuthStatic) + b.impl = NewStatic(staticCreds.User, staticCreds.Password, b.connectionsFactory, b.logger) + case options.IamToken: + b.impl = NewIamToken(b.opts.Auth.Creds.(*options.AuthIAMToken).Token) + case options.IamCreds: + b.initErr = fmt.Errorf("TODO: IAM authorization from SA key not implemented yet") + case options.None: + b.initErr = fmt.Errorf("determined credentials to be anonymous. Anonymous credentials are currently unsupported") + default: + b.initErr = fmt.Errorf( + "internal error: authorization type not recognized after options validation, this should never happen", + ) + } + }) + return b.initErr +} + +func New( + opts *command.BaseOptions, + connectionsFactory connectionsfactory.Factory, + logger *zap.SugaredLogger, + impl Provider, +) Provider { + return &baseProvider{ + impl: impl, + opts: opts, + connectionsFactory: connectionsFactory, + logger: logger, + } +} diff --git a/pkg/client/auth/credentials/static.go b/pkg/client/auth/credentials/static.go new file mode 100644 index 0000000..bb1695e --- /dev/null +++ b/pkg/client/auth/credentials/static.go @@ -0,0 +1,81 @@ +package credentials + +import ( + "context" + "sync" + + "github.com/ydb-platform/ydbops/pkg/client/auth" + "github.com/ydb-platform/ydbops/pkg/client/connectionsfactory" + "go.uber.org/zap" + "google.golang.org/grpc/metadata" +) + +type staticCredentialsProvider struct { + authClient auth.Client + connectionsFactory connectionsfactory.Factory + logger *zap.SugaredLogger + + initOnce sync.Once + + tokenOnce sync.Once + tokenErr error + + params *staticCredentialsProviderParams + token string +} + +type staticCredentialsProviderParams struct { + user string + password string +} + +// Init implements Provider. +func (s *staticCredentialsProvider) Init() error { + s.initOnce.Do(func() { + s.authClient = auth.NewClient(s.logger, s.connectionsFactory) + }) + return nil +} + +// GetToken implements Provider. +func (s *staticCredentialsProvider) GetToken() (string, error) { + // TODO(shmel1k@): probably, token can change time to time. + _ = s.Init() + + s.tokenOnce.Do(func() { + s.token, s.tokenErr = s.authClient.Auth(s.params.user, s.params.password) + }) + return s.token, s.tokenErr +} + +// ContextWithAuth implements Provider. +func (s *staticCredentialsProvider) ContextWithAuth(ctx context.Context) (context.Context, context.CancelFunc) { + _ = s.Init() + + tok, _ := s.GetToken() // TODO(shmel1k@): return err as params + ctx, cf := context.WithCancel(ctx) + return metadata.AppendToOutgoingContext(ctx, + "x-ydb-auth-ticket", tok), cf +} + +// ContextWithoutAuth implements Provider. +func (s *staticCredentialsProvider) ContextWithoutAuth(ctx context.Context) (context.Context, context.CancelFunc) { + _ = s.Init() + + return context.WithCancel(ctx) +} + +func NewStatic( + login, password string, + connectionsFactory connectionsfactory.Factory, + logger *zap.SugaredLogger, +) Provider { + return &staticCredentialsProvider{ + connectionsFactory: connectionsFactory, + logger: logger, + params: &staticCredentialsProviderParams{ + user: login, + password: password, + }, + } +} diff --git a/pkg/client/cms.go b/pkg/client/cms/cms.go similarity index 62% rename from pkg/client/cms.go rename to pkg/client/cms/cms.go index 5aaaf61..44a1db1 100644 --- a/pkg/client/cms.go +++ b/pkg/client/cms/cms.go @@ -1,4 +1,4 @@ -package client +package cms import ( "context" @@ -15,25 +15,51 @@ import ( "google.golang.org/protobuf/types/known/durationpb" "github.com/ydb-platform/ydbops/internal/collections" + "github.com/ydb-platform/ydbops/pkg/client" + "github.com/ydb-platform/ydbops/pkg/client/auth/credentials" + "github.com/ydb-platform/ydbops/pkg/client/connectionsfactory" + "github.com/ydb-platform/ydbops/pkg/utils" ) -type Cms struct { - logger *zap.SugaredLogger - f *Factory +const ( + defaultRetryCount = 5 +) + +type CMS interface { + Tenants() ([]string, error) + Nodes() ([]*Ydb_Maintenance.Node, error) +} + +type Client interface { + CMS + Maintenance + + Close() error +} + +type defaultCMSClient struct { + logger *zap.SugaredLogger + connectionsFactory connectionsfactory.Factory + credentialsProvider credentials.Provider } -func NewCMSClient(f *Factory, logger *zap.SugaredLogger) *Cms { - return &Cms{ - logger: logger, - f: f, +func NewCMSClient( + connectionsFactory connectionsfactory.Factory, + logger *zap.SugaredLogger, + cp credentials.Provider, +) Client { + return &defaultCMSClient{ + logger: logger, + connectionsFactory: connectionsFactory, + credentialsProvider: cp, } } -func (c *Cms) Tenants() ([]string, error) { +func (c *defaultCMSClient) Tenants() ([]string, error) { result := Ydb_Cms.ListDatabasesResult{} c.logger.Debug("Invoke ListDatabases method") - _, err := c.ExecuteCMSMethod(&result, func(ctx context.Context, cl Ydb_Cms_V1.CmsServiceClient) (OperationResponse, error) { - return cl.ListDatabases(ctx, &Ydb_Cms.ListDatabasesRequest{OperationParams: c.f.OperationParams()}) + _, err := c.executeCMSOperation(&result, func(ctx context.Context, cl Ydb_Cms_V1.CmsServiceClient) (client.OperationResponse, error) { + return cl.ListDatabases(ctx, &Ydb_Cms.ListDatabasesRequest{OperationParams: c.connectionsFactory.OperationParams()}) }) if err != nil { return nil, err @@ -47,12 +73,14 @@ func (c *Cms) Tenants() ([]string, error) { return s, nil } -func (c *Cms) Nodes() ([]*Ydb_Maintenance.Node, error) { +func (c *defaultCMSClient) Nodes() ([]*Ydb_Maintenance.Node, error) { result := Ydb_Maintenance.ListClusterNodesResult{} c.logger.Debug("Invoke ListClusterNodes method") - _, err := c.ExecuteMaintenanceMethod(&result, - func(ctx context.Context, cl Ydb_Maintenance_V1.MaintenanceServiceClient) (OperationResponse, error) { - return cl.ListClusterNodes(ctx, &Ydb_Maintenance.ListClusterNodesRequest{OperationParams: c.f.OperationParams()}) + _, err := c.executeMaintenanceOperation(&result, + func(ctx context.Context, cl Ydb_Maintenance_V1.MaintenanceServiceClient) (client.OperationResponse, error) { + return cl.ListClusterNodes(ctx, &Ydb_Maintenance.ListClusterNodesRequest{ + OperationParams: c.connectionsFactory.OperationParams(), + }) }, ) if err != nil { @@ -68,14 +96,14 @@ func (c *Cms) Nodes() ([]*Ydb_Maintenance.Node, error) { return nodes, nil } -func (c *Cms) MaintenanceTasks(userSID string) ([]string, error) { +func (c *defaultCMSClient) MaintenanceTasks(userSID string) ([]MaintenanceTask, error) { result := Ydb_Maintenance.ListMaintenanceTasksResult{} c.logger.Debug("Invoke ListMaintenanceTasks method") - _, err := c.ExecuteMaintenanceMethod(&result, - func(ctx context.Context, cl Ydb_Maintenance_V1.MaintenanceServiceClient) (OperationResponse, error) { + _, err := c.executeMaintenanceOperation(&result, + func(ctx context.Context, cl Ydb_Maintenance_V1.MaintenanceServiceClient) (client.OperationResponse, error) { return cl.ListMaintenanceTasks(ctx, &Ydb_Maintenance.ListMaintenanceTasksRequest{ - OperationParams: c.f.OperationParams(), + OperationParams: c.connectionsFactory.OperationParams(), User: &userSID, }, ) @@ -85,16 +113,16 @@ func (c *Cms) MaintenanceTasks(userSID string) ([]string, error) { return nil, err } - return result.TasksUids, nil + return c.queryEachTaskForActions(result.TasksUids) } -func (c *Cms) GetMaintenanceTask(taskID string) (MaintenanceTask, error) { +func (c *defaultCMSClient) GetMaintenanceTask(taskID string) (MaintenanceTask, error) { result := Ydb_Maintenance.GetMaintenanceTaskResult{} c.logger.Debug("Invoke GetMaintenanceTask method") - _, err := c.ExecuteMaintenanceMethod(&result, - func(ctx context.Context, cl Ydb_Maintenance_V1.MaintenanceServiceClient) (OperationResponse, error) { + _, err := c.executeMaintenanceOperation(&result, + func(ctx context.Context, cl Ydb_Maintenance_V1.MaintenanceServiceClient) (client.OperationResponse, error) { return cl.GetMaintenanceTask(ctx, &Ydb_Maintenance.GetMaintenanceTaskRequest{ - OperationParams: c.f.OperationParams(), + OperationParams: c.connectionsFactory.OperationParams(), TaskUid: taskID, }) }, @@ -159,9 +187,9 @@ func actionGroupsFromHosts(params MaintenanceTaskParams) []*Ydb_Maintenance.Acti return ags } -func (c *Cms) CreateMaintenanceTask(params MaintenanceTaskParams) (MaintenanceTask, error) { +func (c *defaultCMSClient) CreateMaintenanceTask(params MaintenanceTaskParams) (MaintenanceTask, error) { request := &Ydb_Maintenance.CreateMaintenanceTaskRequest{ - OperationParams: c.f.OperationParams(), + OperationParams: c.connectionsFactory.OperationParams(), TaskOptions: &Ydb_Maintenance.MaintenanceTaskOptions{ TaskUid: params.TaskUID, AvailabilityMode: params.AvailabilityMode, @@ -177,8 +205,8 @@ func (c *Cms) CreateMaintenanceTask(params MaintenanceTaskParams) (MaintenanceTa result := &Ydb_Maintenance.MaintenanceTaskResult{} c.logger.Debug("Invoke CreateMaintenanceTask method") - _, err := c.ExecuteMaintenanceMethod(result, - func(ctx context.Context, cl Ydb_Maintenance_V1.MaintenanceServiceClient) (OperationResponse, error) { + _, err := c.executeMaintenanceOperation(result, + func(ctx context.Context, cl Ydb_Maintenance_V1.MaintenanceServiceClient) (client.OperationResponse, error) { return cl.CreateMaintenanceTask(ctx, request) }, ) @@ -188,13 +216,13 @@ func (c *Cms) CreateMaintenanceTask(params MaintenanceTaskParams) (MaintenanceTa return result, nil } -func (c *Cms) RefreshMaintenanceTask(taskID string) (MaintenanceTask, error) { +func (c *defaultCMSClient) RefreshMaintenanceTask(taskID string) (MaintenanceTask, error) { result := Ydb_Maintenance.MaintenanceTaskResult{} c.logger.Debug("Invoke RefreshMaintenanceTask method") - _, err := c.ExecuteMaintenanceMethod(&result, - func(ctx context.Context, cl Ydb_Maintenance_V1.MaintenanceServiceClient) (OperationResponse, error) { + _, err := c.executeMaintenanceOperation(&result, + func(ctx context.Context, cl Ydb_Maintenance_V1.MaintenanceServiceClient) (client.OperationResponse, error) { return cl.RefreshMaintenanceTask(ctx, &Ydb_Maintenance.RefreshMaintenanceTaskRequest{ - OperationParams: c.f.OperationParams(), + OperationParams: c.connectionsFactory.OperationParams(), TaskUid: taskID, }) }, @@ -206,12 +234,12 @@ func (c *Cms) RefreshMaintenanceTask(taskID string) (MaintenanceTask, error) { return &result, nil } -func (c *Cms) DropMaintenanceTask(taskID string) (string, error) { +func (c *defaultCMSClient) DropMaintenanceTask(taskID string) (string, error) { c.logger.Debug("Invoke DropMaintenanceTask method") - op, err := c.ExecuteMaintenanceMethod(nil, - func(ctx context.Context, cl Ydb_Maintenance_V1.MaintenanceServiceClient) (OperationResponse, error) { + op, err := c.executeMaintenanceOperation(nil, + func(ctx context.Context, cl Ydb_Maintenance_V1.MaintenanceServiceClient) (client.OperationResponse, error) { return cl.DropMaintenanceTask(ctx, &Ydb_Maintenance.DropMaintenanceTaskRequest{ - OperationParams: c.f.OperationParams(), + OperationParams: c.connectionsFactory.OperationParams(), TaskUid: taskID, }) }, @@ -223,39 +251,38 @@ func (c *Cms) DropMaintenanceTask(taskID string) (string, error) { return op.Status.String(), nil } -func (c *Cms) CompleteAction(actionIds []*Ydb_Maintenance.ActionUid) (*Ydb_Maintenance.ManageActionResult, error) { +func (c *defaultCMSClient) CompleteAction(actionIds []*Ydb_Maintenance.ActionUid) (*Ydb_Maintenance.ManageActionResult, error) { result := Ydb_Maintenance.ManageActionResult{} c.logger.Debug("Invoke CompleteAction method") - op, err := c.ExecuteMaintenanceMethod(&result, - func(ctx context.Context, cl Ydb_Maintenance_V1.MaintenanceServiceClient) (OperationResponse, error) { + _, err := c.executeMaintenanceOperation(&result, + func(ctx context.Context, cl Ydb_Maintenance_V1.MaintenanceServiceClient) (client.OperationResponse, error) { return cl.CompleteAction(ctx, &Ydb_Maintenance.CompleteActionRequest{ - OperationParams: c.f.OperationParams(), + OperationParams: c.connectionsFactory.OperationParams(), ActionUids: actionIds, }) }, ) - _ = op if err != nil { return nil, err } return &result, nil } -func (c *Cms) ExecuteMaintenanceMethod( +func (c *defaultCMSClient) executeMaintenanceOperation( out proto.Message, - method func(context.Context, Ydb_Maintenance_V1.MaintenanceServiceClient) (OperationResponse, error), + method func(context.Context, Ydb_Maintenance_V1.MaintenanceServiceClient) (client.OperationResponse, error), ) (*Ydb_Operations.Operation, error) { - ctx, cancel, err := c.f.ContextWithAuth() - if err != nil { - return nil, err - } + ctx, cancel := c.credentialsProvider.ContextWithAuth(context.TODO()) defer cancel() - op, err := WrapWithRetries(c.f.GetRetryNumber(), func() (*Ydb_Operations.Operation, error) { - cc, err := c.f.Connection() + op, err := utils.WrapWithRetries(defaultRetryCount, func() (*Ydb_Operations.Operation, error) { + cc, err := c.connectionsFactory.Create() if err != nil { return nil, err } + defer func() { + _ = cc.Close() + }() cl := Ydb_Maintenance_V1.NewMaintenanceServiceClient(cc) r, err := method(ctx, cl) @@ -264,7 +291,7 @@ func (c *Cms) ExecuteMaintenanceMethod( return nil, err } op := r.GetOperation() - LogOperation(c.logger, op) + utils.LogOperation(c.logger, op) return op, nil }) if err != nil { @@ -286,18 +313,15 @@ func (c *Cms) ExecuteMaintenanceMethod( return op, nil } -func (c *Cms) ExecuteCMSMethod( +func (c *defaultCMSClient) executeCMSOperation( out proto.Message, - method func(context.Context, Ydb_Cms_V1.CmsServiceClient) (OperationResponse, error), + method func(context.Context, Ydb_Cms_V1.CmsServiceClient) (client.OperationResponse, error), ) (*Ydb_Operations.Operation, error) { - ctx, cancel, err := c.f.ContextWithAuth() - if err != nil { - return nil, err - } + ctx, cancel := c.credentialsProvider.ContextWithAuth(context.TODO()) defer cancel() - op, err := WrapWithRetries(c.f.GetRetryNumber(), func() (*Ydb_Operations.Operation, error) { - cc, err := c.f.Connection() + op, err := utils.WrapWithRetries(defaultRetryCount, func() (*Ydb_Operations.Operation, error) { + cc, err := c.connectionsFactory.Create() if err != nil { return nil, err } @@ -309,7 +333,7 @@ func (c *Cms) ExecuteCMSMethod( return nil, err } op := r.GetOperation() - LogOperation(c.logger, op) + utils.LogOperation(c.logger, op) return op, nil }) if err != nil { @@ -330,3 +354,7 @@ func (c *Cms) ExecuteCMSMethod( return op, nil } + +func (c *defaultCMSClient) Close() error { + return nil +} diff --git a/pkg/client/cms/maintenance.go b/pkg/client/cms/maintenance.go new file mode 100644 index 0000000..6a37152 --- /dev/null +++ b/pkg/client/cms/maintenance.go @@ -0,0 +1,117 @@ +package cms + +import ( + "context" + "fmt" + + "github.com/ydb-platform/ydb-go-genproto/draft/Ydb_Maintenance_V1" + "github.com/ydb-platform/ydb-go-genproto/draft/protos/Ydb_Maintenance" + "github.com/ydb-platform/ydbops/pkg/client" +) + +const ( + TaskUuidPrefix = "maintenance-" +) + +type CreateTaskParams struct { + HostFQDNs []string + MaintenanceDurationSeconds int + AvailabilityMode string +} + +type Maintenance interface { + CompleteAction([]*Ydb_Maintenance.ActionUid) (*Ydb_Maintenance.ManageActionResult, error) + CompleteActions(string, []string) (*Ydb_Maintenance.ManageActionResult, error) + CreateMaintenanceTask(MaintenanceTaskParams) (MaintenanceTask, error) + DropMaintenanceTask(string) (string, error) + DropTask(string) error + GetMaintenanceTask(string) (MaintenanceTask, error) + ListTasksForUser(string) ([]MaintenanceTask, error) + MaintenanceTasks(string) ([]MaintenanceTask, error) + RefreshMaintenanceTask(string) (MaintenanceTask, error) + RefreshTask(string) (MaintenanceTask, error) +} + +// CompleteActions implements Client. +func (d *defaultCMSClient) CompleteActions(taskID string, hostFQDNs []string) (*Ydb_Maintenance.ManageActionResult, error) { + task, err := d.GetMaintenanceTask(taskID) + if err != nil { + return nil, fmt.Errorf("failed to get maintenance task %v: %w", taskID, err) + } + + hostToActionUID := make(map[string]*Ydb_Maintenance.ActionUid) + for _, gs := range task.GetActionGroupStates() { + as := gs.ActionStates[0] + scope := as.Action.GetLockAction().Scope + host := scope.GetHost() + if host == "" { + return nil, fmt.Errorf("Trying to complete an action with nodeId scope, currently unimplemented") + } + + hostToActionUID[host] = as.ActionUid + } + + completedActions := []*Ydb_Maintenance.ActionUid{} + for _, host := range hostFQDNs { + actionUid, present := hostToActionUID[host] + if !present { + return nil, fmt.Errorf("Failed to complete host %s, corresponding CMS action not found.\n"+ + "This host either was never requested or already completed", host) + } + completedActions = append(completedActions, actionUid) + } + + return d.CompleteAction(completedActions) +} + +func (d *defaultCMSClient) queryEachTaskForActions(taskIds []string) ([]MaintenanceTask, error) { + tasks := []MaintenanceTask{} + for _, taskId := range taskIds { + task, err := d.GetMaintenanceTask(taskId) + if err != nil { + return nil, fmt.Errorf( + "failed to list all maintenance tasks, failure to obtain detailed info about task %v: %w", + taskId, + err, + ) + } + tasks = append(tasks, task) + } + + return tasks, nil +} + +// DropTask implements Client. +func (d *defaultCMSClient) DropTask(taskID string) error { + // TODO(shmel1k@): add status + _, err := d.DropMaintenanceTask(taskID) + if err != nil { + return err + } + + // TODO(shmel1k@): return back commentaries. + // fmt.Printf("Drop task %v status: %s\n", taskID, status) + + return nil +} + +// ListTasks implements Client. +func (d *defaultCMSClient) ListTasksForUser(userSID string) ([]MaintenanceTask, error) { + return d.MaintenanceTasks(userSID) +} + +// RefreshTask implements Client. +func (d *defaultCMSClient) RefreshTask(taskID string) (MaintenanceTask, error) { + var result Ydb_Maintenance.MaintenanceTaskResult + _, err := d.executeMaintenanceOperation(&result, func(ctx context.Context, cl Ydb_Maintenance_V1.MaintenanceServiceClient) (client.OperationResponse, error) { + return cl.RefreshMaintenanceTask(ctx, &Ydb_Maintenance.RefreshMaintenanceTaskRequest{ + OperationParams: d.connectionsFactory.OperationParams(), + TaskUid: taskID, + }) + }) + if err != nil { + return nil, err + } + + return &result, nil +} diff --git a/pkg/client/cms/types.go b/pkg/client/cms/types.go new file mode 100644 index 0000000..ae5b3e0 --- /dev/null +++ b/pkg/client/cms/types.go @@ -0,0 +1,42 @@ +package cms + +import ( + "github.com/ydb-platform/ydb-go-genproto/draft/protos/Ydb_Maintenance" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +type MaintenanceScopeType int + +const ( + NodeScope MaintenanceScopeType = 1 + HostScope MaintenanceScopeType = 2 +) + +type MaintenanceTaskParams struct { + TaskUID string + AvailabilityMode Ydb_Maintenance.AvailabilityMode + Duration *durationpb.Duration + + ScopeType MaintenanceScopeType + + Nodes []*Ydb_Maintenance.Node + Hosts []string +} + +type MaintenanceTask interface { + GetRetryAfter() *timestamppb.Timestamp + GetActionGroupStates() []*Ydb_Maintenance.ActionGroupStates + GetTaskUid() string +} + +type maintenanceTaskResult struct { + TaskUID string + ActionGroupStates []*Ydb_Maintenance.ActionGroupStates +} + +func (g *maintenanceTaskResult) GetRetryAfter() *timestamppb.Timestamp { return nil } +func (g *maintenanceTaskResult) GetActionGroupStates() []*Ydb_Maintenance.ActionGroupStates { + return g.ActionGroupStates +} +func (g *maintenanceTaskResult) GetTaskUid() string { return g.TaskUID } diff --git a/pkg/client/connectionsfactory/connections_factory.go b/pkg/client/connectionsfactory/connections_factory.go new file mode 100644 index 0000000..839f5c4 --- /dev/null +++ b/pkg/client/connectionsfactory/connections_factory.go @@ -0,0 +1,102 @@ +package connectionsfactory + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "os" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/ydb-platform/ydb-go-genproto/protos/Ydb_Operations" + "github.com/ydb-platform/ydbops/pkg/command" +) + +const ( + BufferSize = 32 << 20 +) + +type Factory interface { + Create() (*grpc.ClientConn, error) + OperationParams() *Ydb_Operations.OperationParams +} + +func New( + options *command.BaseOptions, +) Factory { + return &connectionsFactory{ + options: options, + } +} + +type connectionsFactory struct { + options *command.BaseOptions +} + +// OperationParams implements Factory. +func (f *connectionsFactory) OperationParams() *Ydb_Operations.OperationParams { + return &Ydb_Operations.OperationParams{ + OperationMode: Ydb_Operations.OperationParams_SYNC, + OperationTimeout: durationpb.New(time.Duration(f.options.GRPC.TimeoutSeconds) * time.Second), + CancelAfter: durationpb.New(time.Duration(f.options.GRPC.TimeoutSeconds) * time.Second), + } +} + +func (f *connectionsFactory) Create() (*grpc.ClientConn, error) { + cr, err := f.makeCredentials() + if err != nil { + return nil, fmt.Errorf("failed to load credentials: %w", err) + } + + return grpc.Dial(f.endpoint(), + grpc.WithTransportCredentials(cr), + grpc.WithDefaultCallOptions( + grpc.MaxCallSendMsgSize(BufferSize), + grpc.MaxCallRecvMsgSize(BufferSize))) +} + +func (f *connectionsFactory) endpoint() string { + // TODO decide if we want to support multiple endpoints or just one + // Endpoint in rootOpts will turn from string -> []string in this case + // + // for balancers, it does not really matter, one endpoint is enough. + // but if you specify node endpoint directly, if this particular node + // is dead, things get inconvenient. + return fmt.Sprintf("%s:%d", f.options.GRPC.Endpoint, f.options.GRPC.GRPCPort) +} + +func (f *connectionsFactory) makeCredentials() (credentials.TransportCredentials, error) { + if !f.options.GRPC.GRPCSecure { + return insecure.NewCredentials(), nil + } + + systemPool, err := x509.SystemCertPool() + if err != nil { + return nil, fmt.Errorf("failed to get the system cert pool: %w", err) + } + + if f.options.GRPC.CaFile != "" { + b, err := os.ReadFile(f.options.GRPC.CaFile) + if err != nil { + return nil, fmt.Errorf("failed to read the ca file: %w", err) + } + if !systemPool.AppendCertsFromPEM(b) { + return nil, fmt.Errorf("credentials: failed to append certificates") + } + } + + tlsConfig := &tls.Config{ + MinVersion: tls.VersionTLS12, + RootCAs: systemPool, + } + + if f.options.GRPC.GRPCSkipVerify { + tlsConfig.InsecureSkipVerify = true + } + + return credentials.NewTLS(tlsConfig), nil +} diff --git a/pkg/client/discovery.go b/pkg/client/discovery/discovery.go similarity index 61% rename from pkg/client/discovery.go rename to pkg/client/discovery/discovery.go index 7d6a17e..eb5ed92 100644 --- a/pkg/client/discovery.go +++ b/pkg/client/discovery/discovery.go @@ -1,4 +1,4 @@ -package client +package discovery import ( "context" @@ -10,23 +10,40 @@ import ( "github.com/ydb-platform/ydb-go-genproto/protos/Ydb_Operations" "go.uber.org/zap" "google.golang.org/protobuf/proto" + + "github.com/ydb-platform/ydbops/pkg/client" + "github.com/ydb-platform/ydbops/pkg/client/auth/credentials" + "github.com/ydb-platform/ydbops/pkg/client/connectionsfactory" + "github.com/ydb-platform/ydbops/pkg/utils" ) type Discovery struct { - logger *zap.SugaredLogger - f *Factory + logger *zap.SugaredLogger + connectionsFactory connectionsfactory.Factory + credentialsProvider credentials.Provider +} + +type Client interface { + ListEndpoints(string) ([]*Ydb_Discovery.EndpointInfo, error) + WhoAmI() (string, error) + Close() error } -func NewDiscoveryClient(f *Factory, logger *zap.SugaredLogger) *Discovery { +func NewDiscoveryClient( + f connectionsfactory.Factory, + logger *zap.SugaredLogger, + cp credentials.Provider, +) *Discovery { return &Discovery{ - logger: logger, - f: f, + logger: logger, + connectionsFactory: f, + credentialsProvider: cp, } } func (c *Discovery) ListEndpoints(database string) ([]*Ydb_Discovery.EndpointInfo, error) { result := Ydb_Discovery.ListEndpointsResult{} - _, err := c.ExecuteDiscoveryMethod(&result, func(ctx context.Context, cl Ydb_Discovery_V1.DiscoveryServiceClient) (OperationResponse, error) { + _, err := c.ExecuteDiscoveryMethod(&result, func(ctx context.Context, cl Ydb_Discovery_V1.DiscoveryServiceClient) (client.OperationResponse, error) { c.logger.Debug("Invoke ListEndpoints method") return cl.ListEndpoints(ctx, &Ydb_Discovery.ListEndpointsRequest{ Database: database, @@ -42,7 +59,7 @@ func (c *Discovery) ListEndpoints(database string) ([]*Ydb_Discovery.EndpointInf func (c *Discovery) WhoAmI() (string, error) { result := Ydb_Discovery.WhoAmIResult{} c.logger.Debug("Invoke WhoAmI method") - _, err := c.ExecuteDiscoveryMethod(&result, func(ctx context.Context, cl Ydb_Discovery_V1.DiscoveryServiceClient) (OperationResponse, error) { + _, err := c.ExecuteDiscoveryMethod(&result, func(ctx context.Context, cl Ydb_Discovery_V1.DiscoveryServiceClient) (client.OperationResponse, error) { return cl.WhoAmI(ctx, &Ydb_Discovery.WhoAmIRequest{IncludeGroups: false}) }) if err != nil { @@ -54,17 +71,17 @@ func (c *Discovery) WhoAmI() (string, error) { func (c *Discovery) ExecuteDiscoveryMethod( out proto.Message, - method func(context.Context, Ydb_Discovery_V1.DiscoveryServiceClient) (OperationResponse, error), + method func(context.Context, Ydb_Discovery_V1.DiscoveryServiceClient) (client.OperationResponse, error), ) (*Ydb_Operations.Operation, error) { - cc, err := c.f.Connection() + cc, err := c.connectionsFactory.Create() if err != nil { return nil, err } + defer func() { + _ = cc.Close() + }() - ctx, cancel, err := c.f.ContextWithAuth() - if err != nil { - return nil, err - } + ctx, cancel := c.credentialsProvider.ContextWithAuth(context.TODO()) defer cancel() cl := Ydb_Discovery_V1.NewDiscoveryServiceClient(cc) @@ -74,7 +91,7 @@ func (c *Discovery) ExecuteDiscoveryMethod( return nil, err } op := r.GetOperation() - LogOperation(c.logger, op) + utils.LogOperation(c.logger, op) if out == nil { return op, nil @@ -90,3 +107,7 @@ func (c *Discovery) ExecuteDiscoveryMethod( return op, nil } + +func (c *Discovery) Close() error { + return nil +} diff --git a/pkg/client/factory.go b/pkg/client/factory.go deleted file mode 100644 index 92e1027..0000000 --- a/pkg/client/factory.go +++ /dev/null @@ -1,160 +0,0 @@ -package client - -import ( - "context" - "crypto/tls" - "crypto/x509" - "fmt" - "os" - "sync" - "time" - - "github.com/ydb-platform/ydb-go-genproto/protos/Ydb_Operations" - "go.uber.org/zap" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/metadata" - "google.golang.org/protobuf/types/known/durationpb" - - "github.com/ydb-platform/ydbops/pkg/options" -) - -const ( - BufferSize = 32 << 20 -) - -var ( - factory *Factory - once sync.Once - initErr error - - cms *Cms - discovery *Discovery -) - -type OperationResponse interface { - GetOperation() *Ydb_Operations.Operation -} - -type Factory struct { - auth options.AuthOptions - grpc options.GRPC - retryNumber int - token string -} - -func InitConnectionFactory( - rootOpts options.RootOptions, - logger *zap.SugaredLogger, - retryNumber int, -) error { - once.Do(func() { - factory = &Factory{ - auth: rootOpts.Auth, - grpc: rootOpts.GRPC, - retryNumber: retryNumber, - } - - initErr = initAuthToken(rootOpts, logger, factory) - - if initErr != nil { - initErr = fmt.Errorf("failed to receive an auth token, rolling restart not started: %w", initErr) - } - - cms = NewCMSClient(factory, logger) - discovery = NewDiscoveryClient(factory, logger) - }) - - if initErr != nil { - return initErr - } - - return nil -} - -func (f *Factory) Connection() (*grpc.ClientConn, error) { - cr, err := f.makeCredentials() - if err != nil { - return nil, fmt.Errorf("failed to load credentials: %w", err) - } - - return grpc.Dial(f.endpoint(), - grpc.WithTransportCredentials(cr), - grpc.WithDefaultCallOptions( - grpc.MaxCallSendMsgSize(BufferSize), - grpc.MaxCallRecvMsgSize(BufferSize))) -} - -func (f *Factory) OperationParams() *Ydb_Operations.OperationParams { - return &Ydb_Operations.OperationParams{ - OperationMode: Ydb_Operations.OperationParams_SYNC, - OperationTimeout: durationpb.New(time.Duration(f.grpc.TimeoutSeconds) * time.Second), - CancelAfter: durationpb.New(time.Duration(f.grpc.TimeoutSeconds) * time.Second), - } -} - -func (f *Factory) ContextWithAuth() (context.Context, context.CancelFunc, error) { - ctx, cf := context.WithTimeout(context.Background(), time.Second*time.Duration(f.grpc.TimeoutSeconds)) - - return metadata.AppendToOutgoingContext(ctx, - "x-ydb-auth-ticket", f.token), cf, nil -} - -func (f *Factory) ContextWithoutAuth() (context.Context, context.CancelFunc) { - return context.WithTimeout(context.Background(), time.Second*time.Duration(f.grpc.TimeoutSeconds)) -} - -func (f *Factory) GetRetryNumber() int { - return f.retryNumber -} - -func GetCmsClient() *Cms { - return cms -} - -func GetDiscoveryClient() *Discovery { - return discovery -} - -func (f *Factory) endpoint() string { - // TODO decide if we want to support multiple endpoints or just one - // Endpoint in rootOpts will turn from string -> []string in this case - // - // for balancers, it does not really matter, one endpoint is enough. - // but if you specify node endpoint directly, if this particular node - // is dead, things get inconvenient. - return fmt.Sprintf("%s:%d", f.grpc.Endpoint, f.grpc.GRPCPort) -} - -func (f *Factory) makeCredentials() (credentials.TransportCredentials, error) { - if !f.grpc.GRPCSecure { - return insecure.NewCredentials(), nil - } - - systemPool, err := x509.SystemCertPool() - if err != nil { - return nil, fmt.Errorf("failed to get the system cert pool: %w", err) - } - - if f.grpc.CaFile != "" { - b, err := os.ReadFile(f.grpc.CaFile) - if err != nil { - return nil, fmt.Errorf("failed to read the ca file: %w", err) - } - if !systemPool.AppendCertsFromPEM(b) { - return nil, fmt.Errorf("credentials: failed to append certificates") - } - } - - tlsConfig := &tls.Config{ - MinVersion: tls.VersionTLS12, - RootCAs: systemPool, - } - - if f.grpc.GRPCSkipVerify { - tlsConfig.InsecureSkipVerify = true - } - - return credentials.NewTLS(tlsConfig), nil -} diff --git a/pkg/client/types.go b/pkg/client/types.go index 9447635..f4645a1 100644 --- a/pkg/client/types.go +++ b/pkg/client/types.go @@ -2,8 +2,8 @@ package client import ( "github.com/ydb-platform/ydb-go-genproto/draft/protos/Ydb_Maintenance" + "github.com/ydb-platform/ydb-go-genproto/protos/Ydb_Operations" "google.golang.org/protobuf/types/known/durationpb" - "google.golang.org/protobuf/types/known/timestamppb" ) type MaintenanceScopeType int @@ -24,19 +24,6 @@ type MaintenanceTaskParams struct { Hosts []string } -type MaintenanceTask interface { - GetRetryAfter() *timestamppb.Timestamp - GetActionGroupStates() []*Ydb_Maintenance.ActionGroupStates - GetTaskUid() string +type OperationResponse interface { + GetOperation() *Ydb_Operations.Operation } - -type maintenanceTaskResult struct { - TaskUID string - ActionGroupStates []*Ydb_Maintenance.ActionGroupStates -} - -func (g *maintenanceTaskResult) GetRetryAfter() *timestamppb.Timestamp { return nil } -func (g *maintenanceTaskResult) GetActionGroupStates() []*Ydb_Maintenance.ActionGroupStates { - return g.ActionGroupStates -} -func (g *maintenanceTaskResult) GetTaskUid() string { return g.TaskUID } diff --git a/pkg/cmdutil/cmdutil.go b/pkg/cmdutil/cmdutil.go new file mode 100644 index 0000000..b6eaaab --- /dev/null +++ b/pkg/cmdutil/cmdutil.go @@ -0,0 +1 @@ +package cmdutil diff --git a/pkg/cmdutil/factory.go b/pkg/cmdutil/factory.go new file mode 100644 index 0000000..9637d88 --- /dev/null +++ b/pkg/cmdutil/factory.go @@ -0,0 +1,50 @@ +package cmdutil + +import ( + "github.com/ydb-platform/ydb-go-genproto/protos/Ydb_Operations" + + "github.com/ydb-platform/ydbops/pkg/client/cms" + "github.com/ydb-platform/ydbops/pkg/client/discovery" + "github.com/ydb-platform/ydbops/pkg/command" +) + +type Factory interface { + GetCMSClient() cms.Client + GetDiscoveryClient() discovery.Client + GetBaseOptions() *command.BaseOptions + // GetAuthClient() auth.Client +} + +type OperationResponse interface { + GetOperation() *Ydb_Operations.Operation +} + +type factory struct { + cmsClient cms.Client + discoveryClient discovery.Client + opts *command.BaseOptions +} + +func New( + opts *command.BaseOptions, + cmsClient cms.Client, + discoveryClient discovery.Client, +) Factory { + return &factory{ + opts: opts, + cmsClient: cmsClient, + discoveryClient: discoveryClient, + } +} + +func (f *factory) GetCMSClient() cms.Client { + return f.cmsClient +} + +func (f *factory) GetDiscoveryClient() discovery.Client { + return f.discoveryClient +} + +func (f *factory) GetBaseOptions() *command.BaseOptions { + return f.opts +} diff --git a/pkg/options/root.go b/pkg/command/command.go similarity index 55% rename from pkg/options/root.go rename to pkg/command/command.go index 06fe42b..d47a9bb 100644 --- a/pkg/options/root.go +++ b/pkg/command/command.go @@ -1,4 +1,4 @@ -package options +package command import ( "errors" @@ -6,19 +6,24 @@ import ( "path/filepath" "github.com/spf13/pflag" + "github.com/ydb-platform/ydbops/pkg/options" ) -type RootOptions struct { - Auth AuthOptions - GRPC GRPC +type Description struct { + use string + shortDescription string + longDescription string +} + +type BaseOptions struct { + Auth options.AuthOptions + GRPC options.GRPC Verbose bool ProfileFile string ActiveProfile string } -var RootOptionsInstance = &RootOptions{} - -func (o *RootOptions) Validate() error { +func (o *BaseOptions) Validate() error { if err := o.GRPC.Validate(); err != nil { return err } @@ -29,10 +34,15 @@ func (o *RootOptions) Validate() error { return nil } -func (o *RootOptions) DefineFlags(fs *pflag.FlagSet) { +func (o *BaseOptions) DefineFlags(fs *pflag.FlagSet) { o.GRPC.DefineFlags(fs) o.Auth.DefineFlags(fs) + fs.StringVar( + &o.ActiveProfile, "profile", + "", + "Override currently set profile name from --config-file") + defaultProfileLocation := "" if home, present := os.LookupEnv("HOME"); present { defaultProfileLocation = filepath.Join(home, "ydb", "ydbops", "config", "config.yaml") @@ -50,10 +60,25 @@ func (o *RootOptions) DefineFlags(fs *pflag.FlagSet) { defaultProfileLocation, "Path to config file with profile data in yaml format. Default: $HOME/ydb/ydbops/config/config.yaml") - fs.StringVar( - &o.ActiveProfile, "profile", - "", - "Override currently set profile name from --profile-file") - fs.BoolVar(&o.Verbose, "verbose", false, "Switches log level from INFO to DEBUG") } + +func NewDescription(use, shortDescription, longDescription string) *Description { + return &Description{ + use: use, + shortDescription: shortDescription, + longDescription: longDescription, + } +} + +func (b *Description) GetUse() string { + return b.use +} + +func (b *Description) GetShortDescription() string { + return b.shortDescription +} + +func (b *Description) GetLongDescription() string { + return b.longDescription +} diff --git a/pkg/maintenance/host.go b/pkg/maintenance/host.go new file mode 100644 index 0000000..255536e --- /dev/null +++ b/pkg/maintenance/host.go @@ -0,0 +1,63 @@ +package maintenance + +import ( + "fmt" + + "github.com/google/uuid" + "github.com/ydb-platform/ydb-go-genproto/draft/protos/Ydb_Maintenance" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/ydb-platform/ydbops/pkg/client/cms" +) + +const ( + MaintenanceTaskPrefix = "maintenance-" +) + +func getNodesOnHost(cmsClient cms.Client, hostFQDN string) ([]*Ydb_Maintenance.Node, error) { + nodes, err := cmsClient.Nodes() + if err != nil { + return nil, err + } + + res := []*Ydb_Maintenance.Node{} + + for _, node := range nodes { + // TODO here is the non-trivial part with Kubernetes, surgically create a shared logic + // with Kubernetes restarters + if node.Host == hostFQDN { + res = append(res, node) + } + } + + return res, nil +} + +type RequestHostParams struct { + AvailabilityMode Ydb_Maintenance.AvailabilityMode + HostFQDN string + MaintenanceDuration *durationpb.Duration +} + +func RequestHost(cmsClient cms.Client, params *RequestHostParams) (string, error) { + taskUID := MaintenanceTaskPrefix + uuid.New().String() + + nodes, err := getNodesOnHost(cmsClient, params.HostFQDN) + if err != nil { + return "", err + } + + taskParams := cms.MaintenanceTaskParams{ + TaskUID: taskUID, + AvailabilityMode: params.AvailabilityMode, + Duration: params.MaintenanceDuration, + Nodes: nodes, + } + + task, err := cmsClient.CreateMaintenanceTask(taskParams) + if err != nil { + return "", fmt.Errorf("failed to create maintenance task: %w", err) + } + + return task.GetTaskUid(), nil +} diff --git a/pkg/maintenance/maintenance.go b/pkg/maintenance/maintenance.go deleted file mode 100644 index f1caea2..0000000 --- a/pkg/maintenance/maintenance.go +++ /dev/null @@ -1,136 +0,0 @@ -package maintenance - -import ( - "fmt" - - "github.com/google/uuid" - "github.com/ydb-platform/ydb-go-genproto/draft/protos/Ydb_Maintenance" - - "github.com/ydb-platform/ydbops/pkg/client" - "github.com/ydb-platform/ydbops/pkg/options" -) - -const ( - TaskUuidPrefix = "maintenance-" -) - -func CreateTask(opts *options.MaintenanceCreateOpts) (string, error) { - cms := client.GetCmsClient() - - taskUID := TaskUuidPrefix + uuid.New().String() - - taskParams := client.MaintenanceTaskParams{ - TaskUID: taskUID, - AvailabilityMode: opts.GetAvailabilityMode(), - Duration: opts.GetMaintenanceDuration(), - ScopeType: client.HostScope, - Hosts: opts.HostFQDNs, - } - - task, err := cms.CreateMaintenanceTask(taskParams) - if err != nil { - return "", fmt.Errorf("failed to create maintenance task: %w", err) - } - - return task.GetTaskUid(), nil -} - -func DropTask(opts *options.TaskIdOpts) error { - cmsClient := client.GetCmsClient() - status, err := cmsClient.DropMaintenanceTask(opts.TaskID) - if err != nil { - return err - } - - fmt.Printf("Drop task %v status: %s\n", opts.TaskID, status) - - return nil -} - -func queryEachTaskForActions(cmsClient *client.Cms, taskIds []string) ([]client.MaintenanceTask, error) { - tasks := []client.MaintenanceTask{} - for _, taskId := range taskIds { - task, err := cmsClient.GetMaintenanceTask(taskId) - if err != nil { - return nil, fmt.Errorf( - "failed to list all maintenance tasks, failure to obtain detailed info about task %v: %w", - taskId, - err, - ) - } - tasks = append(tasks, task) - } - - return tasks, nil -} - -func ListTasks() ([]client.MaintenanceTask, error) { - discoveryClient := client.GetDiscoveryClient() - userSID, err := discoveryClient.WhoAmI() - if err != nil { - return nil, fmt.Errorf("failed to determine the user SID: %w", err) - } - - cmsClient := client.GetCmsClient() - taskIds, err := cmsClient.MaintenanceTasks(userSID) - if err != nil { - return nil, fmt.Errorf("failed to list all maintenance tasks: %w", err) - } - - tasks, err := queryEachTaskForActions(cmsClient, taskIds) - if err != nil { - return nil, err - } - - return tasks, nil -} - -func CompleteActions( - taskIdOpts *options.TaskIdOpts, - completeOpts *options.CompleteOpts, -) (*Ydb_Maintenance.ManageActionResult, error) { - cmsClient := client.GetCmsClient() - task, err := cmsClient.GetMaintenanceTask(taskIdOpts.TaskID) - if err != nil { - return nil, fmt.Errorf("failed to get maintenance task %v: %w", taskIdOpts.TaskID, err) - } - - hostToActionUID := make(map[string]*Ydb_Maintenance.ActionUid) - for _, gs := range task.GetActionGroupStates() { - as := gs.ActionStates[0] - scope := as.Action.GetLockAction().Scope - host := scope.GetHost() - if host == "" { - return nil, fmt.Errorf("Trying to complete an action with nodeId scope, currently unimplemented") - } - - hostToActionUID[host] = as.ActionUid - } - - completedActions := []*Ydb_Maintenance.ActionUid{} - for _, host := range completeOpts.HostFQDNs { - actionUid, present := hostToActionUID[host] - if !present { - return nil, fmt.Errorf("Failed to complete host %s, corresponding CMS action not found.\n"+ - "This host either was never requested or already completed", host) - } - completedActions = append(completedActions, actionUid) - } - - result, err := cmsClient.CompleteAction(completedActions) - if err != nil { - return nil, err - } - - return result, err -} - -func RefreshTask(opts *options.TaskIdOpts) (client.MaintenanceTask, error) { - cmsClient := client.GetCmsClient() - task, err := cmsClient.RefreshMaintenanceTask(opts.TaskID) - if err != nil { - return nil, err - } - - return task, nil -} diff --git a/pkg/options/host.go b/pkg/options/host.go new file mode 100644 index 0000000..0684d05 --- /dev/null +++ b/pkg/options/host.go @@ -0,0 +1 @@ +package options diff --git a/pkg/options/maintenance.go b/pkg/options/maintenance.go deleted file mode 100644 index 9e94a43..0000000 --- a/pkg/options/maintenance.go +++ /dev/null @@ -1,94 +0,0 @@ -package options - -import ( - "fmt" - "strings" - "time" - - "github.com/spf13/pflag" - "github.com/ydb-platform/ydb-go-genproto/draft/protos/Ydb_Maintenance" - "google.golang.org/protobuf/types/known/durationpb" - - "github.com/ydb-platform/ydbops/internal/collections" -) - -type MaintenanceCreateOpts struct { - HostFQDNs []string - MaintenanceDurationSeconds int - AvailabilityMode string -} - -const ( - DefaultMaintenanceDurationSeconds = 3600 -) - -func (o *MaintenanceCreateOpts) DefineFlags(fs *pflag.FlagSet) { - fs.StringSliceVar(&o.HostFQDNs, "hosts", []string{}, - "Request the hosts with these FQDNs from the cluster") - - fs.StringVar(&o.AvailabilityMode, "availability-mode", "strong", - fmt.Sprintf("Availability mode. Available choices: %s", strings.Join(AvailabilityModes, ", "))) - - fs.IntVar(&o.MaintenanceDurationSeconds, "duration", DefaultMaintenanceDurationSeconds, - fmt.Sprintf("How much time do you need for maintenance, in seconds. Default: %v", - DefaultMaintenanceDurationSeconds)) -} - -func (o *MaintenanceCreateOpts) GetMaintenanceDuration() *durationpb.Duration { - return durationpb.New(time.Second * time.Duration(o.MaintenanceDurationSeconds)) -} - -func (o *MaintenanceCreateOpts) Validate() error { - if len(o.HostFQDNs) == 0 { - return fmt.Errorf("--hosts unspecified") - } - - if !collections.Contains(AvailabilityModes, o.AvailabilityMode) { - return fmt.Errorf("specified a non-existing availability mode: %s", o.AvailabilityMode) - } - - if o.MaintenanceDurationSeconds <= 0 { - return fmt.Errorf("specified invalid maintenance duration seconds: %d. Must be positive", o.MaintenanceDurationSeconds) - } - return nil -} - -// TODO this is copy paste? move to internals -func (o *MaintenanceCreateOpts) GetAvailabilityMode() Ydb_Maintenance.AvailabilityMode { - title := strings.ToUpper(fmt.Sprintf("availability_mode_%s", o.AvailabilityMode)) - value := Ydb_Maintenance.AvailabilityMode_value[title] - - return Ydb_Maintenance.AvailabilityMode(value) -} - -type TaskIdOpts struct { - TaskID string -} - -func (o *TaskIdOpts) DefineFlags(fs *pflag.FlagSet) { - fs.StringVar(&o.TaskID, "task-id", "", - "ID of your maintenance task (result of `ydbops maintenance host`)") -} - -func (o *TaskIdOpts) Validate() error { - if o.TaskID == "" { - return fmt.Errorf("--task-id unspecified, argument required") - } - return nil -} - -type CompleteOpts struct { - HostFQDNs []string -} - -func (o *CompleteOpts) DefineFlags(fs *pflag.FlagSet) { - fs.StringSliceVar(&o.HostFQDNs, "hosts", []string{}, - "FQDNs of hosts with completed maintenance") -} - -func (o *CompleteOpts) Validate() error { - if len(o.HostFQDNs) == 0 { - return fmt.Errorf("--hosts unspecified") - } - return nil -} diff --git a/pkg/options/options.go b/pkg/options/options.go index b6956c9..b223b60 100644 --- a/pkg/options/options.go +++ b/pkg/options/options.go @@ -7,6 +7,13 @@ import ( "github.com/ydb-platform/ydbops/internal/collections" ) +// AdditionalFlag allows adding extra flags not defined in command +// +// This behavior can be used if some additional parameters are added outside of some Option +// +// TODO(shmel1k@): improve comment +type AdditionalFlag func(fs *pflag.FlagSet) + // Options is an interface to defile options flags and validation logic type Options interface { DefineFlags(fs *pflag.FlagSet) diff --git a/pkg/options/options_suite_test.go b/pkg/options/options_suite_test.go index f059530..7ff507c 100644 --- a/pkg/options/options_suite_test.go +++ b/pkg/options/options_suite_test.go @@ -5,6 +5,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/ydb-platform/ydbops/pkg/utils" ) func TestOptions(t *testing.T) { @@ -15,7 +16,7 @@ func TestOptions(t *testing.T) { var _ = Describe("Test parsing SSHArgs", func() { DescribeTable("SSH arguments parsing", func(input string, expected []string) { - Expect(parseSSHArgs(input)).To(Equal(expected)) + Expect(utils.ParseSSHArgs(input)).To(Equal(expected)) }, Entry("whitespace separated arguments", "arg1 arg2 arg3", diff --git a/pkg/options/types.go b/pkg/options/types.go new file mode 100644 index 0000000..9a96c71 --- /dev/null +++ b/pkg/options/types.go @@ -0,0 +1,17 @@ +package options + +import "time" + +var AvailabilityModes = []string{"strong", "weak", "force"} + +type StartedTime struct { + Timestamp time.Time + Direction rune +} + +type VersionSpec struct { + Sign string + Major int + Minor int + Patch int +} diff --git a/pkg/prettyprint/prettyprint.go b/pkg/prettyprint/prettyprint.go index 4abbf23..63fedca 100644 --- a/pkg/prettyprint/prettyprint.go +++ b/pkg/prettyprint/prettyprint.go @@ -6,11 +6,10 @@ import ( "time" "github.com/ydb-platform/ydb-go-genproto/draft/protos/Ydb_Maintenance" - - "github.com/ydb-platform/ydbops/pkg/client" + "github.com/ydb-platform/ydbops/pkg/client/cms" ) -func TaskToString(task client.MaintenanceTask) string { +func TaskToString(task cms.MaintenanceTask) string { sb := strings.Builder{} sb.WriteString(fmt.Sprintf("Uid: %s\n", task.GetTaskUid())) diff --git a/pkg/options/restart.go b/pkg/rolling/options.go similarity index 82% rename from pkg/options/restart.go rename to pkg/rolling/options.go index a8456ad..6e862da 100644 --- a/pkg/options/restart.go +++ b/pkg/rolling/options.go @@ -1,4 +1,4 @@ -package options +package rolling import ( "fmt" @@ -7,15 +7,15 @@ import ( "strconv" "strings" "time" - "unicode" "unicode/utf8" "github.com/spf13/pflag" "github.com/ydb-platform/ydb-go-genproto/draft/protos/Ydb_Maintenance" - "google.golang.org/protobuf/types/known/durationpb" - "github.com/ydb-platform/ydbops/internal/collections" + "github.com/ydb-platform/ydbops/pkg/options" "github.com/ydb-platform/ydbops/pkg/profile" + "github.com/ydb-platform/ydbops/pkg/utils" + "google.golang.org/protobuf/types/known/durationpb" ) const ( @@ -25,20 +25,6 @@ const ( DefaultMaxStaticNodeId = 50000 ) -var AvailabilityModes = []string{"strong", "weak", "force"} - -type StartedTime struct { - Timestamp time.Time - Direction rune -} - -type VersionSpec struct { - Sign string - Major int - Minor int - Patch int -} - type RestartOptions struct { AvailabilityMode string Hosts []string @@ -48,8 +34,8 @@ type RestartOptions struct { Version string CMSQueryInterval int - StartedTime *StartedTime - VersionSpec *VersionSpec + StartedTime *options.StartedTime + VersionSpec *options.VersionSpec Continue bool @@ -73,10 +59,8 @@ var ( rawSSHUnparsedArgs string ) -var RestartOptionsInstance = &RestartOptions{} - func (o *RestartOptions) Validate() error { - if !collections.Contains(AvailabilityModes, o.AvailabilityMode) { + if !collections.Contains(options.AvailabilityModes, o.AvailabilityMode) { return fmt.Errorf("specified a non-existing availability mode: %s", o.AvailabilityMode) } @@ -117,7 +101,7 @@ func (o *RestartOptions) Validate() error { return fmt.Errorf("failed to parse --started: %w", err) } - o.StartedTime = &StartedTime{ + o.StartedTime = &options.StartedTime{ Timestamp: timestamp, Direction: directionRune, } @@ -132,7 +116,7 @@ func (o *RestartOptions) Validate() error { major, _ := strconv.Atoi(matches[2]) minor, _ := strconv.Atoi(matches[3]) patch, _ := strconv.Atoi(matches[4]) - o.VersionSpec = &VersionSpec{ + o.VersionSpec = &options.VersionSpec{ Sign: matches[1], Major: major, Minor: minor, @@ -146,7 +130,7 @@ func (o *RestartOptions) Validate() error { } } - o.SSHArgs = parseSSHArgs(rawSSHUnparsedArgs) + o.SSHArgs = utils.ParseSSHArgs(rawSSHUnparsedArgs) _, errFromIds := o.GetNodeIds() _, errFromFQDNs := o.GetNodeFQDNs() @@ -166,13 +150,13 @@ func (o *RestartOptions) DefineFlags(fs *pflag.FlagSet) { fs.BoolVar(&o.Tenant, "tenant", false, `Only include tenant nodes. Otherwise, include all nodes by default`) - fs.StringSliceVar(&o.TenantList, "tenant-list", []string{}, `Comma-delimited list of tenant names to restart. + fs.StringSliceVar(&o.TenantList, "tenant-list", []string{}, `Comma-delimited list of tenant names to restart. E.g.:'--tenant-list=name1,name2,name3'`) fs.StringVar(&o.CustomSystemdUnitName, "systemd-unit", "", "Specify custom systemd unit name to restart") fs.StringVar(&rawSSHUnparsedArgs, "ssh-args", "", - `This argument will be used when ssh-ing to the nodes. It may be used to override + `This argument will be used when ssh-ing to the nodes. It may be used to override the ssh command itself, ssh username or any additional arguments. Double quotes are can be escaped with backward slash '\'. Examples: @@ -180,7 +164,7 @@ Examples: 2) --ssh-args "ssh -o ProxyCommand=\"...\""`) fs.StringSliceVar(&o.Hosts, "hosts", o.Hosts, - `Restart only specified hosts. You can specify a list of host FQDNs or a list of node ids, + `Restart only specified hosts. You can specify a list of host FQDNs or a list of node ids, but you can not mix host FQDNs and node ids in this option. The list is comma-delimited. E.g.: '--hosts=1,2,3' or '--hosts=fqdn1,fqdn2,fqdn3'`) @@ -188,10 +172,10 @@ but you can not mix host FQDNs and node ids in this option. The list is comma-de `Comma-delimited list. Do not restart these hosts, even if they are explicitly specified in --hosts.`) fs.StringVar(&o.AvailabilityMode, "availability-mode", "strong", - fmt.Sprintf("Availability mode. Available choices: %s", strings.Join(AvailabilityModes, ", "))) + fmt.Sprintf("Availability mode. Available choices: %s", strings.Join(options.AvailabilityModes, ", "))) - fs.IntVar(&o.RestartDuration, "restart-duration", DefaultRestartDurationSeconds, - `CMS will release the node for maintenance for restart-duration * restart-retry-number seconds. Any maintenance + fs.IntVar(&o.RestartDuration, "duration", DefaultRestartDurationSeconds, + `CMS will release the node for maintenance for duration * restart-retry-number seconds. Any maintenance after that would be considered a regular cluster failure`) fs.IntVar(&o.RestartRetryNumber, "restart-retry-number", DefaultRetryCount, @@ -207,12 +191,12 @@ after that would be considered a regular cluster failure`) `Apply filter by node version. Format: [<|>|!=|==MAJOR.MINOR.PATCH], e.g. '--version !=24.1.2'`) fs.BoolVar(&o.Continue, "continue", false, - `Attempt to continue previous rolling restart, if there was one. The set of selected nodes -for this invocation must be the same as for the previous invocation, and this can not be verified at runtime since + `Attempt to continue previous rolling restart, if there was one. The set of selected nodes +for this invocation must be the same as for the previous invocation, and this can not be verified at runtime since the ydbops utility is stateless. Use at your own risk.`) fs.IntVar(&o.MaxStaticNodeId, "max-static-node-id", DefaultMaxStaticNodeId, - `This argument is used to help ydbops distinguish storage and dynamic nodes. + `This argument is used to help ydbops distinguish storage and dynamic nodes. Nodes with this nodeId or less will be considered storage.`) profile.PopulateFromProfileLater( @@ -268,34 +252,3 @@ func (o *RestartOptions) GetNodeIds() ([]uint32, error) { return ids, nil } - -func parseSSHArgs(rawArgs string) []string { - args := []string{} - isInsideQuotes := false - - rawRunes := []rune(rawArgs) - curArg := []rune{} - for i := 0; i < len(rawRunes); i++ { - if rawRunes[i] == '\\' && i+1 < len(rawRunes) && rawRunes[i+1] == '"' { - isInsideQuotes = !isInsideQuotes - i++ - curArg = append(curArg, '"') - continue - } - - if unicode.IsSpace(rawRunes[i]) && !isInsideQuotes { - if len(curArg) > 0 { - args = append(args, string(curArg)) - } - curArg = []rune{} - } else { - curArg = append(curArg, rawRunes[i]) - } - } - - if len(curArg) > 0 { - args = append(args, string(curArg)) - } - - return args -} diff --git a/pkg/rolling/restarters/common_k8s.go b/pkg/rolling/restarters/common_k8s.go index aa747d8..aacd9b2 100644 --- a/pkg/rolling/restarters/common_k8s.go +++ b/pkg/rolling/restarters/common_k8s.go @@ -12,8 +12,6 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" - - "github.com/ydb-platform/ydbops/pkg/options" ) const ( @@ -24,13 +22,19 @@ type k8sRestarter struct { k8sClient *kubernetes.Clientset FQDNToPodName map[string]string logger *zap.SugaredLogger + options *k8sRestarterOptions +} + +type k8sRestarterOptions struct { + restartDuration time.Duration } -func newK8sRestarter(logger *zap.SugaredLogger) k8sRestarter { +func newK8sRestarter(logger *zap.SugaredLogger, params *k8sRestarterOptions) k8sRestarter { return k8sRestarter{ k8sClient: nil, // initialized later FQDNToPodName: make(map[string]string), logger: logger, + options: params, } } @@ -141,6 +145,6 @@ func (r *k8sRestarter) restartNodeByRestartingPod(nodeFQDN, namespace string) er namespace, podName, oldUID, - time.Duration(options.RestartOptionsInstance.RestartDuration)*time.Second, + r.options.restartDuration, ) } diff --git a/pkg/rolling/restarters/run.go b/pkg/rolling/restarters/run.go index 60d399c..5a47528 100644 --- a/pkg/rolling/restarters/run.go +++ b/pkg/rolling/restarters/run.go @@ -14,7 +14,7 @@ const ( ) type RunRestarter struct { - Opts *RunOpts + Opts *RunRestarterParams logger *zap.SugaredLogger storageOnly bool dynnodeOnly bool @@ -22,7 +22,7 @@ type RunRestarter struct { func (r *RunRestarter) RestartNode(node *Ydb_Maintenance.Node) error { //nolint:gosec - cmd := exec.Command(r.Opts.PayloadFilepath) + cmd := exec.Command(r.Opts.PayloadFilePath) cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", HostnameEnvVar, node.Host)) @@ -42,9 +42,13 @@ func (r *RunRestarter) RestartNode(node *Ydb_Maintenance.Node) error { return nil } -func NewRunRestarter(logger *zap.SugaredLogger) *RunRestarter { +type RunRestarterParams struct { + PayloadFilePath string +} + +func NewRunRestarter(logger *zap.SugaredLogger, params *RunRestarterParams) *RunRestarter { return &RunRestarter{ - Opts: &RunOpts{}, + Opts: params, logger: logger, } } diff --git a/pkg/rolling/restarters/run_options.go b/pkg/rolling/restarters/run_options.go index 08c4a93..76fbc63 100644 --- a/pkg/rolling/restarters/run_options.go +++ b/pkg/rolling/restarters/run_options.go @@ -1,44 +1 @@ package restarters - -import ( - "errors" - "fmt" - "io/fs" - "os" - - "github.com/spf13/pflag" -) - -type RunOpts struct { - PayloadFilepath string -} - -func (o *RunOpts) DefineFlags(fs *pflag.FlagSet) { - fs.StringVar( - &o.PayloadFilepath, - "payload", - "", - "File path to arbitrary executable to run in the context of the local machine", - ) -} - -func (o *RunOpts) Validate() error { - if o.PayloadFilepath == "" { - return fmt.Errorf("empty --payload specified") - } - fileInfo, err := os.Stat(o.PayloadFilepath) - if errors.Is(err, fs.ErrNotExist) { - return fmt.Errorf("payload file '%s' does not exist", o.PayloadFilepath) - } - - // Apologies, this is really an idiomatic way to check the permission in Go. - // Just run some bitmagic. 0100 is octal, in binary it would be equivalent to: - // 000001000000 - // drwxrwxrwx - executableByOwner := 0o100 - if fileInfo.Mode()&fs.FileMode(executableByOwner) != fs.FileMode(executableByOwner) { - return fmt.Errorf("payload file '%s' is not executable by the owner", o.PayloadFilepath) - } - - return nil -} diff --git a/pkg/rolling/restarters/storage_k8s.go b/pkg/rolling/restarters/storage_k8s.go index 73d5bb1..d42aa2a 100644 --- a/pkg/rolling/restarters/storage_k8s.go +++ b/pkg/rolling/restarters/storage_k8s.go @@ -11,15 +11,21 @@ type StorageK8sRestarter struct { Opts *StorageK8sOpts } -func NewStorageK8sRestarter(logger *zap.SugaredLogger, kubeconfigPath, namespace string) *StorageK8sRestarter { +type StorageK8sRestarterOptions struct { + *K8sRestarterOptions +} + +func NewStorageK8sRestarter(logger *zap.SugaredLogger, params *StorageK8sRestarterOptions) *StorageK8sRestarter { return &StorageK8sRestarter{ Opts: &StorageK8sOpts{ k8sOpts: k8sOpts{ - kubeconfigPath: kubeconfigPath, - namespace: namespace, + kubeconfigPath: params.KubeconfigPath, + namespace: params.Namespace, }, }, - k8sRestarter: newK8sRestarter(logger), + k8sRestarter: newK8sRestarter(logger, &k8sRestarterOptions{ + restartDuration: params.RestartDuration, + }), } } diff --git a/pkg/rolling/restarters/tenant_k8s.go b/pkg/rolling/restarters/tenant_k8s.go index 784b99e..e5894d4 100644 --- a/pkg/rolling/restarters/tenant_k8s.go +++ b/pkg/rolling/restarters/tenant_k8s.go @@ -1,6 +1,8 @@ package restarters import ( + "time" + "github.com/ydb-platform/ydb-go-genproto/draft/protos/Ydb_Maintenance" "go.uber.org/zap" ) @@ -11,15 +13,27 @@ type TenantK8sRestarter struct { k8sRestarter } -func NewTenantK8sRestarter(logger *zap.SugaredLogger, kubeconfigPath, namespace string) *TenantK8sRestarter { +type K8sRestarterOptions struct { + RestartDuration time.Duration + KubeconfigPath string + Namespace string +} + +type TenantK8sRestarterOptions struct { + *K8sRestarterOptions +} + +func NewTenantK8sRestarter(logger *zap.SugaredLogger, params *TenantK8sRestarterOptions) *TenantK8sRestarter { return &TenantK8sRestarter{ Opts: &TenantK8sOpts{ k8sOpts: k8sOpts{ - kubeconfigPath: kubeconfigPath, - namespace: namespace, + kubeconfigPath: params.KubeconfigPath, + namespace: params.Namespace, }, }, - k8sRestarter: newK8sRestarter(logger), + k8sRestarter: newK8sRestarter(logger, &k8sRestarterOptions{ + restartDuration: params.RestartDuration, + }), } } diff --git a/pkg/rolling/rolling.go b/pkg/rolling/rolling.go index daf9ab0..df45f05 100644 --- a/pkg/rolling/rolling.go +++ b/pkg/rolling/rolling.go @@ -2,6 +2,7 @@ package rolling import ( "fmt" + "strings" "sync" "time" @@ -10,19 +11,19 @@ import ( "go.uber.org/zap" "github.com/ydb-platform/ydbops/internal/collections" - "github.com/ydb-platform/ydbops/pkg/client" - "github.com/ydb-platform/ydbops/pkg/options" + "github.com/ydb-platform/ydbops/pkg/client/cms" + "github.com/ydb-platform/ydbops/pkg/client/discovery" "github.com/ydb-platform/ydbops/pkg/prettyprint" "github.com/ydb-platform/ydbops/pkg/rolling/restarters" ) type Rolling struct { - cms *client.Cms - discovery *client.Discovery + cms cms.Client + discovery discovery.Client logger *zap.SugaredLogger state *state - opts options.RestartOptions + opts *RestartOptions restarter restarters.Restarter // TODO jorres@: maybe turn this into a local `map` @@ -45,37 +46,58 @@ const ( RestartTaskPrefix = "rolling-restart-" ) -func ExecuteRolling( - restartOpts options.RestartOptions, +type Executer interface { + Execute() error +} + +type executer struct { + cmsClient cms.Client + discoveryClient discovery.Client + opts *RestartOptions + logger *zap.SugaredLogger + restarter restarters.Restarter +} + +func NewExecuter( + opts *RestartOptions, logger *zap.SugaredLogger, - restarter restarters.Restarter, -) error { - cmsClient := client.GetCmsClient() - discoveryClient := client.GetDiscoveryClient() + cmsClient cms.Client, + discoveryClient discovery.Client, + rst restarters.Restarter, +) Executer { + return &executer{ + cmsClient: cmsClient, + discoveryClient: discoveryClient, + logger: logger, + restarter: rst, + opts: opts, // TODO(shmel1k@): create own options + } +} +func (e *executer) Execute() error { r := &Rolling{ - cms: cmsClient, - discovery: discoveryClient, - logger: logger, - opts: restartOpts, - restarter: restarter, + cms: e.cmsClient, + discovery: e.discoveryClient, + logger: e.logger, + opts: e.opts, + restarter: e.restarter, } var err error - if restartOpts.Continue { - logger.Info("Continue previous rolling restart") + if e.opts.Continue { + e.logger.Info("Continue previous rolling restart") err = r.DoRestartPrevious() } else { - logger.Info("Start rolling restart") + e.logger.Info("Start rolling restart") err = r.DoRestart() } if err != nil { - logger.Errorf("Failed to complete restart: %+v", err) + e.logger.Errorf("Failed to complete restart: %+v", err) return err } - logger.Info("Restart completed successfully") + e.logger.Info("Restart completed successfully") return nil } @@ -133,11 +155,11 @@ func (r *Rolling) DoRestart() error { return nil } - taskParams := client.MaintenanceTaskParams{ + taskParams := cms.MaintenanceTaskParams{ TaskUID: r.state.restartTaskUID, AvailabilityMode: r.opts.GetAvailabilityMode(), Duration: r.opts.GetRestartDuration(), - ScopeType: client.NodeScope, + ScopeType: cms.NodeScope, Nodes: nodesToRestart, } task, err := r.cms.CreateMaintenanceTask(taskParams) @@ -152,7 +174,7 @@ func (r *Rolling) DoRestartPrevious() error { return fmt.Errorf("--continue behavior not implemented yet") } -func (r *Rolling) cmsWaitingLoop(task client.MaintenanceTask, totalNodes int) error { +func (r *Rolling) cmsWaitingLoop(task cms.MaintenanceTask, totalNodes int) error { var ( err error delay time.Duration @@ -357,7 +379,7 @@ func (r *Rolling) cleanupOldRollingRestarts() error { } for _, previousTaskUID := range previousTasks { - _, err := r.cms.DropMaintenanceTask(previousTaskUID) + _, err := r.cms.DropMaintenanceTask(previousTaskUID.GetTaskUid()) if err != nil { return fmt.Errorf("failed to drop maintenance task: %w", err) } @@ -365,8 +387,26 @@ func (r *Rolling) cleanupOldRollingRestarts() error { return nil } -func (r *Rolling) logTask(task client.MaintenanceTask) { +func (r *Rolling) logTask(task cms.MaintenanceTask) { r.logger.Debugf("Maintenance task result:\n%s", prettyprint.TaskToString(task)) + sb := strings.Builder{} + sb.WriteString(fmt.Sprintf("Uid: %s\n", task.GetTaskUid())) + + if task.GetRetryAfter() != nil { + sb.WriteString(fmt.Sprintf("Retry after: %s\n", task.GetRetryAfter().AsTime().Format(time.DateTime))) + } + + for _, gs := range task.GetActionGroupStates() { + as := gs.ActionStates[0] + sb.WriteString(fmt.Sprintf(" Lock on node %d ", as.Action.GetLockAction().Scope.GetNodeId())) + if as.Status == Ydb_Maintenance.ActionState_ACTION_STATUS_PERFORMED { + sb.WriteString(fmt.Sprintf("PERFORMED, until: %s", as.Deadline.AsTime().Format(time.DateTime))) + } else { + sb.WriteString(fmt.Sprintf("PENDING, %s", as.GetReason().String())) + } + sb.WriteString("\n") + } + r.logger.Debugf("Maintenance task result:\n%s", sb.String()) } func (r *Rolling) logCompleteResult(result *Ydb_Maintenance.ManageActionResult) { diff --git a/pkg/client/request-utils.go b/pkg/utils/retries.go similarity index 56% rename from pkg/client/request-utils.go rename to pkg/utils/retries.go index 330d553..fd9b6ea 100644 --- a/pkg/client/request-utils.go +++ b/pkg/utils/retries.go @@ -1,19 +1,14 @@ -package client +package utils import ( "fmt" "math" - "strings" "time" - "github.com/ydb-platform/ydb-go-genproto/protos/Ydb" - "github.com/ydb-platform/ydb-go-genproto/protos/Ydb_Issue" "github.com/ydb-platform/ydb-go-genproto/protos/Ydb_Operations" "go.uber.org/zap" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - - "github.com/ydb-platform/ydbops/internal/collections" ) func backoffTimeAfter(attempt int) time.Duration { @@ -52,25 +47,3 @@ func WrapWithRetries( return nil, fmt.Errorf("number of retries exceeded: %v. Last error: %w", maxAttempts, lastError) } - -func LogOperation(logger *zap.SugaredLogger, op *Ydb_Operations.Operation) { - sb := strings.Builder{} - sb.WriteString(fmt.Sprintf("Operation status: %s", op.Status)) - - if len(op.Issues) > 0 { - sb.WriteString( - fmt.Sprintf("\nIssues:\n%s", - strings.Join(collections.Convert(op.Issues, - func(issue *Ydb_Issue.IssueMessage) string { - return fmt.Sprintf(" Severity: %d, code: %d, message: %s", issue.Severity, issue.IssueCode, issue.Message) - }, - ), "\n"), - )) - } - - if op.Status != Ydb.StatusIds_SUCCESS { - logger.Errorf("GRPC invocation unsuccessful:\n%s", sb.String()) - } else { - logger.Debugf("Invocation result:\n%s", sb.String()) - } -} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go new file mode 100644 index 0000000..d74c2b9 --- /dev/null +++ b/pkg/utils/utils.go @@ -0,0 +1,66 @@ +package utils + +import ( + "fmt" + "strings" + "unicode" + + "github.com/ydb-platform/ydb-go-genproto/protos/Ydb" + "github.com/ydb-platform/ydb-go-genproto/protos/Ydb_Issue" + "github.com/ydb-platform/ydb-go-genproto/protos/Ydb_Operations" + "github.com/ydb-platform/ydbops/internal/collections" + "go.uber.org/zap" +) + +func LogOperation(logger *zap.SugaredLogger, op *Ydb_Operations.Operation) { + sb := strings.Builder{} + sb.WriteString(fmt.Sprintf("Operation status: %s", op.Status)) + + if len(op.Issues) > 0 { + sb.WriteString( + fmt.Sprintf("\nIssues:\n%s", + strings.Join(collections.Convert(op.Issues, + func(issue *Ydb_Issue.IssueMessage) string { + return fmt.Sprintf(" Severity: %d, code: %d, message: %s", issue.Severity, issue.IssueCode, issue.Message) + }, + ), "\n"), + )) + } + + if op.Status != Ydb.StatusIds_SUCCESS { + logger.Errorf("GRPC invocation unsuccessful:\n%s", sb.String()) + } else { + logger.Debugf("Invocation result:\n%s", sb.String()) + } +} + +func ParseSSHArgs(rawArgs string) []string { + args := []string{} + isInsideQuotes := false + + rawRunes := []rune(rawArgs) + curArg := []rune{} + for i := 0; i < len(rawRunes); i++ { + if rawRunes[i] == '\\' && i+1 < len(rawRunes) && rawRunes[i+1] == '"' { + isInsideQuotes = !isInsideQuotes + i++ + curArg = append(curArg, '"') + continue + } + + if unicode.IsSpace(rawRunes[i]) && !isInsideQuotes { + if len(curArg) > 0 { + args = append(args, string(curArg)) + } + curArg = []rune{} + } else { + curArg = append(curArg, rawRunes[i]) + } + } + + if len(curArg) > 0 { + args = append(args, string(curArg)) + } + + return args +} diff --git a/tests/filtering_test.go b/tests/filtering_test.go index f79ef71..fe738bd 100644 --- a/tests/filtering_test.go +++ b/tests/filtering_test.go @@ -8,6 +8,7 @@ import ( "go.uber.org/zap" "github.com/ydb-platform/ydbops/pkg/options" + "github.com/ydb-platform/ydbops/pkg/rolling" "github.com/ydb-platform/ydbops/pkg/rolling/restarters" "github.com/ydb-platform/ydbops/tests/mock" ) @@ -40,7 +41,7 @@ var _ = Describe("Test storage Filter", func() { nodes := mock.CreateNodesFromShortConfig(nodeGroups, nodeInfoMap) filterSpec := restarters.FilterNodeParams{ - MaxStaticNodeId: options.DefaultMaxStaticNodeId, + MaxStaticNodeId: rolling.DefaultMaxStaticNodeId, StartedTime: &options.StartedTime{ Direction: '<', Timestamp: fiveMinutesAgoTimestamp, @@ -91,7 +92,7 @@ var _ = Describe("Test storage Filter", func() { nodes := mock.CreateNodesFromShortConfig(nodeGroups, nodeInfoMap) filterSpec := restarters.FilterNodeParams{ - MaxStaticNodeId: options.DefaultMaxStaticNodeId, + MaxStaticNodeId: rolling.DefaultMaxStaticNodeId, } clusterInfo := restarters.ClusterNodesInfo{ diff --git a/tests/maintenance_test.go b/tests/maintenance_test.go index f8581c6..dabf195 100644 --- a/tests/maintenance_test.go +++ b/tests/maintenance_test.go @@ -10,7 +10,7 @@ import ( "github.com/ydb-platform/ydb-go-genproto/protos/Ydb_Discovery" "google.golang.org/protobuf/proto" - "github.com/ydb-platform/ydbops/pkg/maintenance" + "github.com/ydb-platform/ydbops/pkg/client/cms" "github.com/ydb-platform/ydbops/tests/mock" ) @@ -29,12 +29,12 @@ var _ = Describe("Test Maintenance", func() { ydbopsInvocation: Command{ "--endpoint", "grpcs://localhost:2135", "--verbose", - "--availability-mode", "strong", "--user", mock.TestUser, - "--cms-query-interval", "1", "--ca-file", filepath.Join(".", "test-data", "ssl-data", "ca.crt"), "maintenance", "create", + "--availability-mode", "strong", + "--cms-query-interval", "1", "--hosts=ydb-1.ydb.tech,ydb-2.ydb.tech", }, expectedRequests: []proto.Message{ @@ -53,19 +53,19 @@ var _ = Describe("Test Maintenance", func() { }, expectedOutputRegexps: []string{ // Your task id is:\n\n\n\nPlease write it down for refreshing and completing the task later.\n - fmt.Sprintf("Your task id is:\n\n%s%s\n\n", maintenance.TaskUuidPrefix, uuidRegexpString), + fmt.Sprintf("Your task id is:\n\n%s%s\n\n", cms.TaskUuidPrefix, uuidRegexpString), }, }, { ydbopsInvocation: Command{ "--endpoint", "grpcs://localhost:2135", "--verbose", - "--availability-mode", "strong", "--user", mock.TestUser, - "--cms-query-interval", "1", "--ca-file", filepath.Join(".", "test-data", "ssl-data", "ca.crt"), "maintenance", "list", + // "--availability-mode", "strong", + // "--cms-query-interval", "1", }, expectedRequests: []proto.Message{ &Ydb_Auth.LoginRequest{ @@ -81,7 +81,7 @@ var _ = Describe("Test Maintenance", func() { }, }, expectedOutputRegexps: []string{ - fmt.Sprintf("Uid: %s%s\n", maintenance.TaskUuidPrefix, uuidRegexpString), + fmt.Sprintf("Uid: %s%s\n", cms.TaskUuidPrefix, uuidRegexpString), " Lock on host ydb-1.ydb.tech", "PERFORMED", " Lock on host ydb-2.ydb.tech", @@ -92,15 +92,15 @@ var _ = Describe("Test Maintenance", func() { ydbopsInvocation: Command{ "--endpoint", "grpcs://localhost:2135", "--verbose", - "--availability-mode", "strong", "--user", mock.TestUser, - "--cms-query-interval", "1", "--ca-file", filepath.Join(".", "test-data", "ssl-data", "ca.crt"), "maintenance", "complete", "--task-id", testWillInsertTaskUuid, "--hosts=ydb-1.ydb.tech", + // "--availability-mode", "strong", + // "--cms-query-interval", "1", }, expectedRequests: []proto.Message{ &Ydb_Auth.LoginRequest{ @@ -128,14 +128,14 @@ var _ = Describe("Test Maintenance", func() { ydbopsInvocation: Command{ "--endpoint", "grpcs://localhost:2135", "--verbose", - "--availability-mode", "strong", "--user", mock.TestUser, - "--cms-query-interval", "1", "--ca-file", filepath.Join(".", "test-data", "ssl-data", "ca.crt"), "maintenance", "refresh", "--task-id", testWillInsertTaskUuid, + // "--availability-mode", "strong", + // "--cms-query-interval", "1", }, expectedRequests: []proto.Message{ &Ydb_Auth.LoginRequest{ @@ -147,7 +147,7 @@ var _ = Describe("Test Maintenance", func() { }, }, expectedOutputRegexps: []string{ - fmt.Sprintf("Uid: %s%s\n", maintenance.TaskUuidPrefix, uuidRegexpString), + fmt.Sprintf("Uid: %s%s\n", cms.TaskUuidPrefix, uuidRegexpString), " Lock on host ydb-2.ydb.tech", "PERFORMED", }, @@ -156,15 +156,15 @@ var _ = Describe("Test Maintenance", func() { ydbopsInvocation: Command{ "--endpoint", "grpcs://localhost:2135", "--verbose", - "--availability-mode", "strong", "--user", mock.TestUser, - "--cms-query-interval", "1", "--ca-file", filepath.Join(".", "test-data", "ssl-data", "ca.crt"), "maintenance", "complete", "--task-id", testWillInsertTaskUuid, "--hosts=ydb-2.ydb.tech", + // "--availability-mode", "strong", + // "--cms-query-interval", "1", }, expectedRequests: []proto.Message{ &Ydb_Auth.LoginRequest{ @@ -192,12 +192,12 @@ var _ = Describe("Test Maintenance", func() { ydbopsInvocation: Command{ "--endpoint", "grpcs://localhost:2135", "--verbose", - "--availability-mode", "strong", "--user", mock.TestUser, - "--cms-query-interval", "1", "--ca-file", filepath.Join(".", "test-data", "ssl-data", "ca.crt"), "maintenance", "list", + // "--availability-mode", "strong", + // "--cms-query-interval", "1", }, expectedRequests: []proto.Message{ &Ydb_Auth.LoginRequest{ diff --git a/tests/mock/cms-nodes.go b/tests/mock/cms-nodes.go index 7ae8bf0..11980dd 100644 --- a/tests/mock/cms-nodes.go +++ b/tests/mock/cms-nodes.go @@ -50,7 +50,7 @@ func MakeActionGroupsFromNodeIds(nodeIDs ...uint32) []*Ydb_Maintenance.ActionGro NodeId: nodeID, }, }, - Duration: durationpb.New(time.Duration(180) * time.Second), + Duration: durationpb.New(180 * time.Second), }, }, }, @@ -75,7 +75,7 @@ func MakeActionGroupsFromHostFQDNs(hostFQDNs ...string) []*Ydb_Maintenance.Actio Host: hostFQDN, }, }, - Duration: durationpb.New(time.Duration(1) * time.Hour), + Duration: durationpb.New(1 * time.Hour), }, }, }, diff --git a/tests/run_e2e_test.go b/tests/run_e2e_test.go index 0340ba3..787f265 100644 --- a/tests/run_e2e_test.go +++ b/tests/run_e2e_test.go @@ -15,7 +15,7 @@ import ( "google.golang.org/protobuf/proto" "google.golang.org/protobuf/testing/protocmp" - "github.com/ydb-platform/ydbops/pkg/maintenance" + "github.com/ydb-platform/ydbops/pkg/client/cms" "github.com/ydb-platform/ydbops/pkg/options" blackmagic "github.com/ydb-platform/ydbops/tests/black-magic" "github.com/ydb-platform/ydbops/tests/mock" @@ -107,7 +107,7 @@ func RunTestCase(tc TestCase) { if strings.Contains(expectedOutputRegexp, "Your task id is:") { uuidOnlyRegexp := regexp.MustCompile( fmt.Sprintf("(%s%s)", - maintenance.TaskUuidPrefix, + cms.TaskUuidPrefix, uuidRegexpString, ), )