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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/cli/internal/cmd/lifecycle/evict.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
183 changes: 160 additions & 23 deletions src/cli/internal/cmd/lifecycle/lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ limitations under the License.
package lifecycle

import (
"bufio"
"context"
"errors"
"fmt"
"io"
"strings"
"time"

Expand All @@ -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"

Expand Down Expand Up @@ -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
}
Expand All @@ -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 {
Expand All @@ -157,26 +259,52 @@ 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)
}

return vmop.New(
client,
vmop.WithCreateOnly(l.opts.CreateOnly),
vmop.WithCreateOnly(l.opts.CreateOnly || severalVms),
vmop.WithWaitComplete(l.opts.WaitComplete),
vmop.WithForce(forcePtr),
)
Expand Down Expand Up @@ -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.")
}
Expand Down
1 change: 0 additions & 1 deletion src/cli/internal/cmd/lifecycle/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 0 additions & 1 deletion src/cli/internal/cmd/lifecycle/restart.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 0 additions & 1 deletion src/cli/internal/cmd/lifecycle/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 0 additions & 1 deletion src/cli/internal/cmd/lifecycle/stop.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading