From fdbffbf020a584a970654432b0137db4d46c2800 Mon Sep 17 00:00:00 2001 From: Pasha Sviderski Date: Thu, 16 Apr 2026 17:28:05 +1000 Subject: [PATCH] fix: close stale ControlMaster ssh connection for machine init/add --- internal/cli/cli.go | 37 +++++++++++++++++++++------------- internal/cli/machine.go | 11 ++++++++++ pkg/client/connector/sshcli.go | 10 +++++++++ 3 files changed, 44 insertions(+), 14 deletions(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 43e94f43..144e5136 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -547,26 +547,35 @@ func provisionOrConnectRemoteMachine( } // Use the system 'ssh' command (default). - exec := sshexec.NewSSHCLIRemote( - remoteMachine.User, - remoteMachine.Host, - remoteMachine.Port, - remoteMachine.KeyPath, - ) - - if !skipInstall { - if err := provisionMachine(ctx, exec, version); err != nil { - return nil, fmt.Errorf("provision machine: %w", err) - } - } - sshConfig := &connector.SSHConnectorConfig{ User: remoteMachine.User, Host: remoteMachine.Host, Port: remoteMachine.Port, KeyPath: remoteMachine.KeyPath, } - machineClient, err := client.New(ctx, connector.NewSSHCLIConnector(sshConfig)) + conn := connector.NewSSHCLIConnector(sshConfig) + + if !skipInstall { + exec := sshexec.NewSSHCLIRemote( + remoteMachine.User, + remoteMachine.Host, + remoteMachine.Port, + remoteMachine.KeyPath, + ) + if err := provisionMachine(ctx, exec, version); err != nil { + return nil, fmt.Errorf("provision machine: %w", err) + } + + if remoteMachine.User != rootUser { + // provisionMachine has just added the user to the uncloud group. Any SSH ControlMaster left over from + // a previous uc invocation (e.g. a failed uc command against the uninitialised machine) still holds + // the old user groups and would deny access to /run/uncloud/uncloud.sock. Close the current session + // if it exists so the next session picks up the new groups. + conn.CloseControlMaster(ctx) + } + } + + machineClient, err := client.New(ctx, conn) if err != nil { return nil, fmt.Errorf("connect to remote machine: %w", err) } diff --git a/internal/cli/machine.go b/internal/cli/machine.go index 816c0261..1a2851c4 100644 --- a/internal/cli/machine.go +++ b/internal/cli/machine.go @@ -72,6 +72,17 @@ func provisionMachine(ctx context.Context, exec sshexec.Executor, version string " echo '%[1]s ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/%[1]s", user) } + if strings.Contains(err.Error(), "not in the sudoers file") { + return fmt.Errorf( + "user '%[1]s' is not in the sudo group or sudoers file so cannot use sudo, but Uncloud needs "+ + "passwordless sudo or root access to install and configure the uncloudd daemon on the remote "+ + "machine.\n\n"+ + "Possible solutions:\n"+ + "1. Use root user or a user with passwordless sudo instead.\n"+ + "2. Grant passwordless sudo to the user '%[1]s' by running on the remote machine as root:\n"+ + " echo '%[1]s ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/%[1]s", + user) + } return fmt.Errorf("sudo command failed for user '%s': %w. "+ "Please ensure the user has sudo privileges or use root user instead", user, err) } diff --git a/pkg/client/connector/sshcli.go b/pkg/client/connector/sshcli.go index bc1fd73d..bd0b1d6a 100644 --- a/pkg/client/connector/sshcli.go +++ b/pkg/client/connector/sshcli.go @@ -207,6 +207,16 @@ func (c *SSHCLIConnector) CheckTCPForwarding(ctx context.Context) error { return c.fwdCheckErr } +// CloseControlMaster terminates the SSH ControlMaster process for this destination so the next connection starts +// a fresh SSH session. No-op if no master is running or the control socket is not configured. Errors are ignored. +func (c *SSHCLIConnector) CloseControlMaster(ctx context.Context) { + if c.controlSockPath == "" { + return + } + args := append(c.buildSSHArgs(), "-O", "exit") + _ = exec.CommandContext(ctx, "ssh", args...).Run() +} + func (c *SSHCLIConnector) Close() error { // Individual connections are managed by gRPC and closed when the gRPC connection closes. // The SSH control socket may persist for connection reuse across CLI invocations.