feat: make connection method using system 'ssh' the default (add ssh+go:// fallback)

This commit is contained in:
Pasha Sviderski
2026-03-27 20:27:36 +10:00
parent 0a9a1db815
commit 06c2bc55f2
60 changed files with 206 additions and 152 deletions
+52 -50
View File
@@ -263,8 +263,8 @@ func (cli *CLI) initRemoteMachine(ctx context.Context, opts InitClusterOptions)
SSHKeyFile: opts.RemoteMachine.KeyPath,
MachineID: resp.Machine.Id,
}
if opts.RemoteMachine.UseSSHCLI {
connCfg.SSHCLI = config.NewSSHDestination(
if opts.RemoteMachine.UseSSHGo {
connCfg.SSHGo = config.NewSSHDestination(
opts.RemoteMachine.User,
opts.RemoteMachine.Host,
opts.RemoteMachine.Port,
@@ -467,8 +467,8 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
SSHKeyFile: opts.RemoteMachine.KeyPath,
MachineID: addResp.Machine.Id,
}
if opts.RemoteMachine.UseSSHCLI {
connCfg.SSHCLI = config.NewSSHDestination(
if opts.RemoteMachine.UseSSHGo {
connCfg.SSHGo = config.NewSSHDestination(
opts.RemoteMachine.User,
opts.RemoteMachine.Host,
opts.RemoteMachine.Port,
@@ -498,73 +498,75 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (*client
func provisionOrConnectRemoteMachine(
ctx context.Context, remoteMachine *RemoteMachine, skipInstall bool, version string,
) (*client.Client, error) {
// Use SSH CLI
if remoteMachine.UseSSHCLI {
exec := sshexec.NewSSHCLIRemote(
remoteMachine.User,
remoteMachine.Host,
remoteMachine.Port,
remoteMachine.KeyPath,
// Use Go's built-in SSH library.
if remoteMachine.UseSSHGo {
sshClient, err := sshexec.Connect(
remoteMachine.User, remoteMachine.Host, remoteMachine.Port, remoteMachine.KeyPath,
)
// If the SSH connection using SSH agent fails and no key path is provided, try to use the default SSH key.
if err != nil && remoteMachine.KeyPath == "" {
remoteMachine.KeyPath = DefaultSSHKeyPath
sshClient, err = sshexec.Connect(
remoteMachine.User, remoteMachine.Host, remoteMachine.Port, remoteMachine.KeyPath,
)
}
if err != nil {
return nil, fmt.Errorf(
"SSH login to remote machine %s: %w",
config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port), err,
)
}
if !skipInstall {
if err := provisionMachine(ctx, exec, version); err != nil {
// Provision the remote machine by installing the Uncloud daemon and dependencies over SSH.
exec := sshexec.NewRemote(sshClient)
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,
var machineClient *client.Client
if remoteMachine.User == "root" || skipInstall {
// Create a machine API client over the established SSH connection to the remote machine.
machineClient, err = client.New(ctx, connector.NewSSHConnectorFromClient(sshClient))
} else {
// Since the user is not root, we need to establish a new SSH connection to make the user's addition
// to the uncloud group effective, thus allowing access to the Uncloud daemon Unix socket.
sshConfig := &connector.SSHConnectorConfig{
User: remoteMachine.User,
Host: remoteMachine.Host,
Port: remoteMachine.Port,
KeyPath: remoteMachine.KeyPath,
}
machineClient, err = client.New(ctx, connector.NewSSHConnector(sshConfig))
}
machineClient, err := client.New(ctx, connector.NewSSHCLIConnector(sshConfig))
if err != nil {
return nil, fmt.Errorf("connect to remote machine: %w", err)
}
return machineClient, nil
}
// Use Go SSH
sshClient, err := sshexec.Connect(remoteMachine.User, remoteMachine.Host, remoteMachine.Port, remoteMachine.KeyPath)
// If the SSH connection using SSH agent fails and no key path is provided, try to use the default SSH key.
if err != nil && remoteMachine.KeyPath == "" {
remoteMachine.KeyPath = DefaultSSHKeyPath
sshClient, err = sshexec.Connect(
remoteMachine.User, remoteMachine.Host, remoteMachine.Port, remoteMachine.KeyPath,
)
}
if err != nil {
return nil, fmt.Errorf(
"SSH login to remote machine %s: %w",
config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port), err,
)
}
// Use the system 'ssh' command (default).
exec := sshexec.NewSSHCLIRemote(
remoteMachine.User,
remoteMachine.Host,
remoteMachine.Port,
remoteMachine.KeyPath,
)
if !skipInstall {
// Provision the remote machine by installing the Uncloud daemon and dependencies over SSH.
exec := sshexec.NewRemote(sshClient)
if err = provisionMachine(ctx, exec, version); err != nil {
if err := provisionMachine(ctx, exec, version); err != nil {
return nil, fmt.Errorf("provision machine: %w", err)
}
}
var machineClient *client.Client
if remoteMachine.User == "root" || skipInstall {
// Create a machine API client over the established SSH connection to the remote machine.
machineClient, err = client.New(ctx, connector.NewSSHConnectorFromClient(sshClient))
} else {
// Since the user is not root, we need to establish a new SSH connection to make the user's addition
// to the uncloud group effective, thus allowing access to the Uncloud daemon Unix socket.
sshConfig := &connector.SSHConnectorConfig{
User: remoteMachine.User,
Host: remoteMachine.Host,
Port: remoteMachine.Port,
KeyPath: remoteMachine.KeyPath,
}
machineClient, err = client.New(ctx, connector.NewSSHConnector(sshConfig))
sshConfig := &connector.SSHConnectorConfig{
User: remoteMachine.User,
Host: remoteMachine.Host,
Port: remoteMachine.Port,
KeyPath: remoteMachine.KeyPath,
}
machineClient, err := client.New(ctx, connector.NewSSHCLIConnector(sshConfig))
if err != nil {
return nil, fmt.Errorf("connect to remote machine: %w", err)
}
+14 -5
View File
@@ -12,8 +12,12 @@ import (
)
type MachineConnection struct {
SSH SSHDestination `yaml:"ssh,omitempty"`
SSHCLI SSHDestination `yaml:"ssh_cli,omitempty"`
// SSH uses the system ssh CLI command to connect. This is the default SSH connection method.
SSH SSHDestination `yaml:"ssh,omitempty"`
// SSHCLI is a backward-compatible alias for SSH.
SSHCLI SSHDestination `yaml:"ssh_cli,omitempty"`
// SSHGo uses Go's built-in SSH library to connect.
SSHGo SSHDestination `yaml:"ssh_go,omitempty"`
SSHKeyFile string `yaml:"ssh_key_file,omitempty"`
// TCP is the address and port of the machine's API server.
// The pointer is used to omit the field when not set. Otherwise, yaml marshalling includes an empty object.
@@ -29,7 +33,9 @@ func (c *MachineConnection) String() string {
if c.SSH != "" {
return "ssh://" + string(c.SSH)
} else if c.SSHCLI != "" {
return "ssh+cli://" + string(c.SSHCLI)
return "ssh://" + string(c.SSHCLI)
} else if c.SSHGo != "" {
return "ssh+go://" + string(c.SSHGo)
} else if c.TCP != nil && c.TCP.IsValid() {
return fmt.Sprintf("tcp://%s", c.TCP)
} else if c.Unix != "" {
@@ -46,6 +52,9 @@ func (c *MachineConnection) Validate() error {
if c.SSHCLI != "" {
setCount++
}
if c.SSHGo != "" {
setCount++
}
if c.TCP != nil && c.TCP.IsValid() {
setCount++
}
@@ -54,10 +63,10 @@ func (c *MachineConnection) Validate() error {
}
if setCount == 0 {
return errors.New("no connection method specified (ssh, ssh_cli, tcp, or unix required)")
return errors.New("no connection method specified (ssh, ssh_go, tcp, or unix required)")
}
if setCount > 1 {
return errors.New("only one connection method allowed per connection (ssh, ssh_cli, tcp, or unix)")
return errors.New("only one connection method allowed per connection (ssh, ssh_go, tcp, or unix)")
}
return nil
+35 -5
View File
@@ -30,18 +30,32 @@ func TestMachineConnection_String(t *testing.T) {
want: "ssh://user@host.com:2222",
},
{
name: "ssh_cli connection",
name: "ssh_cli connection (backward compat alias for ssh)",
conn: MachineConnection{
SSHCLI: "user@host.com",
},
want: "ssh+cli://user@host.com",
want: "ssh://user@host.com",
},
{
name: "ssh_cli connection with port",
name: "ssh_cli connection with port (backward compat alias for ssh)",
conn: MachineConnection{
SSHCLI: "user@host.com:2222",
},
want: "ssh+cli://user@host.com:2222",
want: "ssh://user@host.com:2222",
},
{
name: "ssh_go connection",
conn: MachineConnection{
SSHGo: "user@host.com",
},
want: "ssh+go://user@host.com",
},
{
name: "ssh_go connection with port",
conn: MachineConnection{
SSHGo: "user@host.com:2222",
},
want: "ssh+go://user@host.com:2222",
},
{
name: "tcp connection",
@@ -103,12 +117,19 @@ func TestMachineConnection_Validate(t *testing.T) {
wantErr: false,
},
{
name: "ssh_cli only - valid",
name: "ssh_cli only - valid (backward compat)",
conn: MachineConnection{
SSHCLI: "user@host",
},
wantErr: false,
},
{
name: "ssh_go only - valid",
conn: MachineConnection{
SSHGo: "user@host",
},
wantErr: false,
},
{
name: "tcp only - valid",
conn: MachineConnection{
@@ -141,6 +162,15 @@ func TestMachineConnection_Validate(t *testing.T) {
wantErr: true,
errMsg: "only one connection method allowed",
},
{
name: "ssh and ssh_go - error",
conn: MachineConnection{
SSH: "user@host",
SSHGo: "user@host",
},
wantErr: true,
errMsg: "only one connection method allowed",
},
{
name: "ssh and unix - error",
conn: MachineConnection{
+12 -8
View File
@@ -57,9 +57,9 @@ func connectClusterWithProgress(ctx context.Context, conn config.MachineConnecti
}
func connectCluster(ctx context.Context, conn config.MachineConnection) (*client.Client, error) {
// Determine which SSH type is configured
// Determine which SSH type is configured.
var sshDest config.SSHDestination
var useSSHCLI bool
var useGoSSH bool
// Validate connection configuration early to provide clear error messages.
if err := conn.Validate(); err != nil {
@@ -67,11 +67,15 @@ func connectCluster(ctx context.Context, conn config.MachineConnection) (*client
}
if conn.SSH != "" {
// SSH uses the system ssh CLI command (default).
sshDest = conn.SSH
useSSHCLI = false
} else if conn.SSHCLI != "" {
// SSHCLI is a backward-compatible alias for SSH.
sshDest = conn.SSHCLI
useSSHCLI = true
} else if conn.SSHGo != "" {
// SSHGo uses Go's built-in SSH library.
sshDest = conn.SSHGo
useGoSSH = true
} else if conn.TCP != nil && conn.TCP.IsValid() {
return client.New(ctx, connector.NewTCPConnector(*conn.TCP))
} else if conn.Unix != "" {
@@ -95,11 +99,11 @@ func connectCluster(ctx context.Context, conn config.MachineConnection) (*client
KeyPath: keyPath,
}
// Create appropriate connector based on type
if useSSHCLI {
return client.New(ctx, connector.NewSSHCLIConnector(sshConfig))
// Create appropriate connector based on type.
if useGoSSH {
return client.New(ctx, connector.NewSSHConnector(sshConfig))
}
return client.New(ctx, connector.NewSSHConnector(sshConfig))
return client.New(ctx, connector.NewSSHCLIConnector(sshConfig))
}
// connectModel is a TUI model for connecting to a cluster with a progress spinner.
+5 -5
View File
@@ -23,11 +23,11 @@ const (
)
type RemoteMachine struct {
User string
Host string
Port int
KeyPath string
UseSSHCLI bool // indicates ssh+cli:// should be used
User string
Host string
Port int
KeyPath string
UseSSHGo bool // Use Go's built-in SSH library instead of the system ssh CLI command.
}
func installCmd(user string, version string) string {