From ec3de3a0993a298e0298d8392113ef7b992a4b53 Mon Sep 17 00:00:00 2001 From: Pasha Sviderski Date: Wed, 6 Aug 2025 16:57:09 +1000 Subject: [PATCH] feat: ask whether to reset already initialised machine on 'machine init/add' --- internal/cli/cli.go | 44 +++++++++++-------------------- internal/cli/machine.go | 58 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 29 deletions(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 675281cd..5a5acf0a 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -6,8 +6,8 @@ import ( "fmt" "net/netip" "os" + "slices" - "github.com/charmbracelet/huh" "github.com/docker/cli/cli/streams" "github.com/psviderski/uncloud/internal/cli/config" "github.com/psviderski/uncloud/internal/fs" @@ -195,7 +195,7 @@ func (cli *CLI) initRemoteMachine(ctx context.Context, opts InitClusterOptions) return nil, fmt.Errorf("inspect machine: %w", err) } if minfo.Id != "" { - if err = cli.promptResetMachine(); err != nil { + if err = promptResetMachine(ctx, machineClient.MachineClient); err != nil { return nil, err } } @@ -293,7 +293,18 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client return nil, nil, fmt.Errorf("inspect machine: %w", err) } if minfo.Id != "" { - if err = cli.promptResetMachine(); err != nil { + // Check if the machine is already a member of this cluster. + machines, err := c.ListMachines(ctx, nil) + if err != nil { + return nil, nil, fmt.Errorf("list cluster machines: %w", err) + } + if slices.ContainsFunc(machines, func(m *pb.MachineMember) bool { + return m.Machine.Id == minfo.Id + }) { + return nil, nil, fmt.Errorf("machine is already a member of this cluster (%s)", minfo.Name) + } + + if err = promptResetMachine(ctx, machineClient.MachineClient); err != nil { return nil, nil, err } } @@ -344,7 +355,7 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client return nil, nil, fmt.Errorf("add machine to cluster (context '%s'): %w", contextName, err) } - // List other machines in the cluster to include them in the join request. + // Get the most up-to-date list of other machines in the cluster to include them in the join request. machines, err := c.ListMachines(ctx, nil) if err != nil { return nil, nil, fmt.Errorf("list cluster machines: %w", err) @@ -434,31 +445,6 @@ func provisionRemoteMachine( return machineClient, nil } -func (cli *CLI) promptResetMachine() error { - var confirm bool - form := huh.NewForm( - huh.NewGroup( - huh.NewConfirm(). - Title( - "The remote machine is already initialised as a cluster member. Do you want to reset it first?", - ). - Affirmative("Yes!"). - Negative("No"). - Value(&confirm), - ), - ).WithAccessible(true) - if err := form.Run(); err != nil { - return fmt.Errorf("prompt user to confirm: %w", err) - } - - if !confirm { - return fmt.Errorf("remote machine is already initialised as a cluster member") - } - // TODO: implement resetting the remote machine. - return fmt.Errorf("resetting the remote machine is not implemented yet. " + - "Please manually run 'uncloud-uninstall' on the remote machine to fully uninstall Uncloud from it") -} - // ProgressOut returns an output stream for progress writer. func (cli *CLI) ProgressOut() *streams.Out { return streams.NewOut(os.Stdout) diff --git a/internal/cli/machine.go b/internal/cli/machine.go index 1760a4cb..56f7c6d2 100644 --- a/internal/cli/machine.go +++ b/internal/cli/machine.go @@ -5,8 +5,13 @@ import ( "fmt" "os" "strings" + "time" + "github.com/cenkalti/backoff/v4" + "github.com/charmbracelet/huh" + "github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/sshexec" + "google.golang.org/protobuf/types/known/emptypb" ) const ( @@ -79,3 +84,56 @@ func provisionMachine(ctx context.Context, exec sshexec.Executor, version string } return nil } + +func promptResetMachine(ctx context.Context, machineClient pb.MachineClient) error { + var confirm bool + form := huh.NewForm( + huh.NewGroup( + huh.NewConfirm(). + Title( + "The remote machine is already initialised as a cluster member. Do you want to reset it first?\n" + + "This will:\n" + + "- Remove all service containers from the machine\n" + + "- Reset the machine to the uninitialised state", + ). + Affirmative("Yes!"). + Negative("No"). + Value(&confirm), + ), + ).WithAccessible(true) + if err := form.Run(); err != nil { + return fmt.Errorf("prompt user to confirm: %w", err) + } + + if !confirm { + return fmt.Errorf("remote machine is already initialised as a cluster member") + } + + if _, err := machineClient.Reset(ctx, &pb.ResetRequest{}); err != nil { + return fmt.Errorf("reset remote machine: %w. You can also manually run 'uncloud-uninstall' "+ + "on the remote machine to fully uninstall Uncloud from it", err) + } + fmt.Println("Resetting the remote machine...") + if err := waitMachineReady(ctx, machineClient, 1*time.Minute); err != nil { + return fmt.Errorf("wait for machine to be ready after reset: %w", err) + } + + return nil +} + +// waitMachineReady waits for the machine to be ready to serve requests. +func waitMachineReady(ctx context.Context, machineClient pb.MachineClient, timeout time.Duration) error { + boff := backoff.WithContext(backoff.NewExponentialBackOff( + backoff.WithMaxInterval(1*time.Second), + backoff.WithMaxElapsedTime(timeout), + ), ctx) + + inspect := func() error { + _, err := machineClient.Inspect(ctx, &emptypb.Empty{}) + if err != nil { + return fmt.Errorf("inspect machine: %w", err) + } + return nil + } + return backoff.Retry(inspect, boff) +}