diff --git a/src/cli/internal/cmd/lifecycle/evict.go b/src/cli/internal/cmd/lifecycle/evict.go index a65993a57a..765d6a355a 100644 --- a/src/cli/internal/cmd/lifecycle/evict.go +++ b/src/cli/internal/cmd/lifecycle/evict.go @@ -28,7 +28,6 @@ func NewEvictCommand() *cobra.Command { Use: "evict (VirtualMachine)", Short: "Evict a virtual machine.", Example: lifecycle.Usage(), - Args: templates.ExactArgs("evict", 1), RunE: lifecycle.Run, } AddCommandLineArgs(cmd.Flags(), &lifecycle.opts) diff --git a/src/cli/internal/cmd/lifecycle/lifecycle.go b/src/cli/internal/cmd/lifecycle/lifecycle.go index 8735ca9afe..e1e62d08d6 100644 --- a/src/cli/internal/cmd/lifecycle/lifecycle.go +++ b/src/cli/internal/cmd/lifecycle/lifecycle.go @@ -17,9 +17,11 @@ limitations under the License. package lifecycle import ( + "bufio" "context" "errors" "fmt" + "io" "strings" "time" @@ -29,6 +31,7 @@ import ( "golang.org/x/text/language" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" @@ -87,12 +90,29 @@ func DefaultOptions() Options { // although in reality, it is `false`. This flag should be refactored. // Consider changing it to a `silence` flag, which could be useful in scripting. type Options struct { + Confirm bool Force bool WaitComplete bool CreateOnly bool + All bool + Selector map[string]string Timeout time.Duration } +func (o *Options) validate(args []string) error { + if len(args) > 0 && o.All { + return fmt.Errorf("cannot use --all flag with specific keys") + } + if len(args) > 0 && len(o.Selector) > 0 { + return fmt.Errorf("cannot use --label-selector flag with specific keys") + } + if o.All && len(o.Selector) > 0 { + return fmt.Errorf("cannot use --all and --label-selector flags together") + } + + return nil +} + type MigrationOpts struct { TargetNodeName string } @@ -102,40 +122,122 @@ func (l *Lifecycle) Run(cmd *cobra.Command, args []string) error { if err != nil { return err } - name, namespace, err := l.getNameNamespace(defaultNamespace, args) - key := types.NamespacedName{Namespace: namespace, Name: name} - if err != nil { + + if err = l.opts.validate(args); err != nil { return err } + + var keys []types.NamespacedName + + switch { + case l.opts.All: + keys, err = l.getVirtualMachines(cmd.Context(), defaultNamespace, client, metav1.ListOptions{}) + if err != nil { + return fmt.Errorf("failed to get virtual machines in namespace %q: %w", defaultNamespace, err) + } + case len(l.opts.Selector) > 0: + selector := labels.SelectorFromSet(l.opts.Selector).String() + opts := metav1.ListOptions{LabelSelector: selector} + + keys, err = l.getVirtualMachines(cmd.Context(), defaultNamespace, client, opts) + if err != nil { + return fmt.Errorf("failed to get virtual machines in namespace %q with selector: %q: %w", defaultNamespace, selector, err) + } + default: + keys, err = l.getNamespacedNames(defaultNamespace, args) + if err != nil { + return fmt.Errorf("failed to parse keys: %w", err) + } + } + + if len(keys) == 0 { + return fmt.Errorf("no one virtual machine found for execute command") + } + forceSet := cmd.Flags().Changed(forceFlag) - mgr := l.getManager(client, forceSet) + mgr := l.getManager(client, forceSet, len(keys) > 1) ctx, cancel := context.WithTimeout(context.Background(), l.opts.Timeout) defer cancel() - var msg string + switch l.cmd { case Stop: - cmd.Printf("Stopping virtual machine %q\n", key.String()) - msg, err = mgr.Stop(ctx, name, namespace) + for _, key := range keys { + l.withConfirm(cmd, Stop, key, func() { + cmd.Printf("Stopping virtual machine %q\n", key.String()) + msg, err := mgr.Stop(ctx, key.Name, key.Namespace) + l.handleMsgError(cmd, msg, err) + }) + } case Start: - cmd.Printf("Starting virtual machine %q\n", key.String()) - msg, err = mgr.Start(ctx, name, namespace) + for _, key := range keys { + l.withConfirm(cmd, Start, key, func() { + cmd.Printf("Starting virtual machine %q\n", key.String()) + msg, err := mgr.Start(ctx, key.Name, key.Namespace) + l.handleMsgError(cmd, msg, err) + }) + } case Restart: - cmd.Printf("Restarting virtual machine %q\n", key.String()) - msg, err = mgr.Restart(ctx, name, namespace) + for _, key := range keys { + l.withConfirm(cmd, Restart, key, func() { + cmd.Printf("Restarting virtual machine %q\n", key.String()) + msg, err := mgr.Restart(ctx, key.Name, key.Namespace) + l.handleMsgError(cmd, msg, err) + }) + } case Evict: - cmd.Printf("Evicting virtual machine %q\n", key.String()) - msg, err = mgr.Evict(ctx, name, namespace) + for _, key := range keys { + l.withConfirm(cmd, Evict, key, func() { + cmd.Printf("Evicting virtual machine %q\n", key.String()) + msg, err := mgr.Evict(ctx, key.Name, key.Namespace) + l.handleMsgError(cmd, msg, err) + }) + } case Migrate: - cmd.Printf("Migrating virtual machine %q\n", key.String()) - msg, err = mgr.Migrate(ctx, name, namespace, l.migrationOpts.TargetNodeName) + for _, key := range keys { + l.withConfirm(cmd, Migrate, key, func() { + cmd.Printf("Migrating virtual machine %q\n", key.String()) + msg, err := mgr.Migrate(ctx, key.Name, key.Namespace, l.migrationOpts.TargetNodeName) + l.handleMsgError(cmd, msg, err) + }) + } default: return fmt.Errorf("invalid command %q", l.cmd) } + + return nil +} + +func (l *Lifecycle) withConfirm(cmd *cobra.Command, command Command, key types.NamespacedName, fn func()) { + if l.opts.Confirm { + fn() + return + } + + cmd.Printf("Are you sure you want to execute command %q for virtual machine %q? [y/N] ", command, key.String()) + reader := bufio.NewReader(cmd.InOrStdin()) + answer, err := reader.ReadString('\n') + if err != nil && !errors.Is(err, io.EOF) { + cmd.PrintErrf("Error: failed to read confirmation: %s\n", err) + return + } + + answer = strings.TrimSpace(strings.ToLower(answer)) + if answer != "y" && answer != "yes" { + cmd.Printf("Skipping virtual machine %q\n", key.String()) + return + } + + fn() +} + +func (l *Lifecycle) handleMsgError(cmd *cobra.Command, msg string, err error) { if msg != "" { - cmd.Printf("%s", msg) + cmd.Printf("%s\n", msg) + } + if err != nil { + cmd.Printf("Error: %s\n", err.Error()) } - return err } func (l *Lifecycle) Usage() string { @@ -157,18 +259,44 @@ func (l *Lifecycle) Usage() string { return usage } -func (l *Lifecycle) getNameNamespace(defaultNamespace string, args []string) (string, string, error) { - namespace, name, err := templates.ParseTarget(args[0]) +func (l *Lifecycle) getNamespacedName(defaultNamespace, arg string) (types.NamespacedName, error) { + namespace, name, err := templates.ParseTarget(arg) if err != nil { - return "", "", err + return types.NamespacedName{}, err } if namespace == "" { namespace = defaultNamespace } - return name, namespace, nil + return types.NamespacedName{Namespace: namespace, Name: name}, nil +} + +func (l *Lifecycle) getNamespacedNames(defaultNamespace string, args []string) ([]types.NamespacedName, error) { + var keys []types.NamespacedName + for _, arg := range args { + key, err := l.getNamespacedName(defaultNamespace, arg) + if err != nil { + return nil, err + } + keys = append(keys, key) + } + return keys, nil +} + +func (l *Lifecycle) getVirtualMachines(ctx context.Context, namespace string, client kubeclient.Client, opts metav1.ListOptions) ([]types.NamespacedName, error) { + vmList, err := client.VirtualMachines(namespace).List(ctx, opts) + if err != nil { + return nil, err + } + + var keys []types.NamespacedName + for _, vm := range vmList.Items { + keys = append(keys, types.NamespacedName{Namespace: vm.Namespace, Name: vm.Name}) + } + + return keys, nil } -func (l *Lifecycle) getManager(client kubeclient.Client, forceSet bool) Manager { +func (l *Lifecycle) getManager(client kubeclient.Client, forceSet, severalVms bool) Manager { var forcePtr *bool if forceSet { forcePtr = ptr.To(l.opts.Force) @@ -176,7 +304,7 @@ func (l *Lifecycle) getManager(client kubeclient.Client, forceSet bool) Manager return vmop.New( client, - vmop.WithCreateOnly(l.opts.CreateOnly), + vmop.WithCreateOnly(l.opts.CreateOnly || severalVms), vmop.WithWaitComplete(l.opts.WaitComplete), vmop.WithForce(forcePtr), ) @@ -216,19 +344,28 @@ func (l *Lifecycle) ValidateNodeName(cmd *cobra.Command, vmName, targetNodeName } const ( + confirmFlag, confirmFlagShort = "yes", "y" forceFlag, forceFlagShort = "force", "f" waitFlag, waitFlagShort = "wait", "w" createOnlyFlag, createOnlyFlagShort = "create-only", "c" + allFlag = "all" + selectorFlag, selectorFlagShort = "label-selector", "l" timeoutFlag, timeoutFlagShort = "timeout", "t" ) func AddCommandLineArgs(flagset *pflag.FlagSet, opts *Options) { + flagset.BoolVarP(&opts.Confirm, confirmFlag, confirmFlagShort, opts.Confirm, + "Set this flag to confirm the action without prompting for confirmation.") flagset.BoolVarP(&opts.Force, forceFlag, forceFlagShort, opts.Force, "Set this flag to force the operation.") flagset.BoolVarP(&opts.WaitComplete, waitFlag, waitFlagShort, opts.WaitComplete, "Set this flag to wait for the operation to complete.") flagset.BoolVarP(&opts.CreateOnly, createOnlyFlag, createOnlyFlagShort, opts.CreateOnly, "Set this flag to only create the action without status warnings or notifications.") + flagset.BoolVar(&opts.All, allFlag, opts.All, + "Set this flag to apply the action to all VMs.") + flagset.StringToStringVarP(&opts.Selector, selectorFlag, selectorFlagShort, opts.Selector, + "Set this flag to apply the action to VMs with the specified labels.") flagset.DurationVarP(&opts.Timeout, timeoutFlag, timeoutFlagShort, opts.Timeout, "Set this flag to change the timeout.") } diff --git a/src/cli/internal/cmd/lifecycle/migrate.go b/src/cli/internal/cmd/lifecycle/migrate.go index d1785c1d51..6a3c3428f4 100644 --- a/src/cli/internal/cmd/lifecycle/migrate.go +++ b/src/cli/internal/cmd/lifecycle/migrate.go @@ -29,7 +29,6 @@ func NewMigrateCommand() *cobra.Command { Use: "migrate (VirtualMachine)", Short: "Migrate a virtual machine.", Example: lifecycle.Usage(), - Args: templates.ExactArgs("migrate", 1), PreRunE: func(cmd *cobra.Command, args []string) error { vmName := args[0] err := lifecycle.ValidateNodeName(cmd, vmName, lifecycle.migrationOpts.TargetNodeName) diff --git a/src/cli/internal/cmd/lifecycle/restart.go b/src/cli/internal/cmd/lifecycle/restart.go index 44552510d2..161abe48c6 100644 --- a/src/cli/internal/cmd/lifecycle/restart.go +++ b/src/cli/internal/cmd/lifecycle/restart.go @@ -28,7 +28,6 @@ func NewRestartCommand() *cobra.Command { Use: "restart (VirtualMachine)", Short: "Restart a virtual machine.", Example: lifecycle.Usage(), - Args: templates.ExactArgs("restart", 1), RunE: lifecycle.Run, } AddCommandLineArgs(cmd.Flags(), &lifecycle.opts) diff --git a/src/cli/internal/cmd/lifecycle/start.go b/src/cli/internal/cmd/lifecycle/start.go index cad458a9be..eb0b45172f 100644 --- a/src/cli/internal/cmd/lifecycle/start.go +++ b/src/cli/internal/cmd/lifecycle/start.go @@ -28,7 +28,6 @@ func NewStartCommand() *cobra.Command { Use: "start (VirtualMachine)", Short: "Start a virtual machine.", Example: lifecycle.Usage(), - Args: templates.ExactArgs("start", 1), RunE: lifecycle.Run, } AddCommandLineArgs(cmd.Flags(), &lifecycle.opts) diff --git a/src/cli/internal/cmd/lifecycle/stop.go b/src/cli/internal/cmd/lifecycle/stop.go index e09d52cb68..6b8ead2637 100644 --- a/src/cli/internal/cmd/lifecycle/stop.go +++ b/src/cli/internal/cmd/lifecycle/stop.go @@ -28,7 +28,6 @@ func NewStopCommand() *cobra.Command { Use: "stop (VirtualMachine)", Short: "Stop a virtual machine.", Example: lifecycle.Usage(), - Args: templates.ExactArgs("stop", 1), RunE: lifecycle.Run, } AddCommandLineArgs(cmd.Flags(), &lifecycle.opts)