feat: add CLI connector for unix domain socket (#186)

* feat: Add cli connector for unix domain socket

* Update cmd/uncloud/main.go

---------

Co-authored-by: Pasha Sviderski <me@psviderski.name>
This commit is contained in:
Justin Bradford
2025-11-25 21:45:07 +10:00
committed by GitHub
co-authored by Pasha Sviderski
parent e87207eae3
commit 91a09d5d37
5 changed files with 85 additions and 11 deletions
+6 -5
View File
@@ -58,11 +58,12 @@ func main() {
conn = &config.MachineConnection{ conn = &config.MachineConnection{
SSHCLI: config.SSHDestination(dest), SSHCLI: config.SSHDestination(dest),
} }
} else { } else if strings.HasPrefix(opts.connect, "unix://") {
dest := opts.connect conn = &config.MachineConnection{
if strings.HasPrefix(dest, "ssh://") { Unix: opts.connect[len("unix://"):],
dest = dest[len("ssh://"):]
} }
} else {
dest := strings.TrimPrefix(opts.connect, "ssh://")
conn = &config.MachineConnection{ conn = &config.MachineConnection{
SSH: config.SSHDestination(dest), SSH: config.SSHDestination(dest),
} }
@@ -81,7 +82,7 @@ func main() {
cmd.PersistentFlags().StringVar(&opts.connect, "connect", "", cmd.PersistentFlags().StringVar(&opts.connect, "connect", "",
"Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]\n"+ "Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]\n"+
"Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], or tcp://host:port") "Format: [ssh://]user@host[:port], ssh+cli://user@host[:port], tcp://host:port, or unix:///path/to/uncloud.sock")
cmd.PersistentFlags().StringVar(&opts.configPath, "uncloud-config", "~/.config/uncloud/config.yaml", cmd.PersistentFlags().StringVar(&opts.configPath, "uncloud-config", "~/.config/uncloud/config.yaml",
"Path to the Uncloud configuration file. [$UNCLOUD_CONFIG]") "Path to the Uncloud configuration file. [$UNCLOUD_CONFIG]")
_ = cmd.MarkPersistentFlagFilename("uncloud-config", "yaml", "yml") _ = cmd.MarkPersistentFlagFilename("uncloud-config", "yaml", "yml")
+9 -2
View File
@@ -23,6 +23,8 @@ type MachineConnection struct {
// TCP is the address and port of the machine's API server. // 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. // The pointer is used to omit the field when not set. Otherwise, yaml marshalling includes an empty object.
TCP *netip.AddrPort `yaml:"tcp,omitempty"` TCP *netip.AddrPort `yaml:"tcp,omitempty"`
// Unix is the path to the machine's API unix socket.
Unix string `yaml:"unix,omitempty"`
Host string `yaml:"host,omitempty"` Host string `yaml:"host,omitempty"`
PublicKey secret.Secret `yaml:"public_key,omitempty"` PublicKey secret.Secret `yaml:"public_key,omitempty"`
MachineID string `yaml:"machine_id,omitempty"` MachineID string `yaml:"machine_id,omitempty"`
@@ -35,6 +37,8 @@ func (c MachineConnection) String() string {
return "ssh+cli://" + string(c.SSHCLI) return "ssh+cli://" + string(c.SSHCLI)
} else if c.TCP != nil && c.TCP.IsValid() { } else if c.TCP != nil && c.TCP.IsValid() {
return fmt.Sprintf("tcp://%s", c.TCP) return fmt.Sprintf("tcp://%s", c.TCP)
} else if c.Unix != "" {
return fmt.Sprintf("unix://%s", c.Unix)
} }
return "unknown connection" return "unknown connection"
} }
@@ -50,12 +54,15 @@ func (c *MachineConnection) Validate() error {
if c.TCP != nil && c.TCP.IsValid() { if c.TCP != nil && c.TCP.IsValid() {
setCount++ setCount++
} }
if c.Unix != "" {
setCount++
}
if setCount == 0 { if setCount == 0 {
return errors.New("no connection method specified (ssh, ssh_cli, or tcp required)") return errors.New("no connection method specified (ssh, ssh_cli, tcp, or unix required)")
} }
if setCount > 1 { if setCount > 1 {
return errors.New("only one connection method allowed per connection (ssh, ssh_cli, or tcp)") return errors.New("only one connection method allowed per connection (ssh, ssh_cli, tcp, or unix)")
} }
return nil return nil
+23
View File
@@ -53,6 +53,13 @@ func TestMachineConnection_String(t *testing.T) {
}, },
want: "tcp://10.0.0.1:8080", want: "tcp://10.0.0.1:8080",
}, },
{
name: "unix connection",
conn: MachineConnection{
Unix: "/run/uncloud/uncloud.sock",
},
want: "unix:///run/uncloud/uncloud.sock",
},
{ {
name: "no connection", name: "no connection",
conn: MachineConnection{}, conn: MachineConnection{},
@@ -112,6 +119,13 @@ func TestMachineConnection_Validate(t *testing.T) {
}, },
wantErr: false, wantErr: false,
}, },
{
name: "unix only - valid",
conn: MachineConnection{
Unix: "/path/to/socket",
},
wantErr: false,
},
{ {
name: "no connection method - error", name: "no connection method - error",
conn: MachineConnection{}, conn: MachineConnection{},
@@ -127,6 +141,15 @@ func TestMachineConnection_Validate(t *testing.T) {
wantErr: true, wantErr: true,
errMsg: "only one connection method allowed", errMsg: "only one connection method allowed",
}, },
{
name: "ssh and unix - error",
conn: MachineConnection{
SSH: "user@host",
Unix: "/path/to/socket",
},
wantErr: true,
errMsg: "only one connection method allowed",
},
{ {
name: "ssh and tcp - error", name: "ssh and tcp - error",
conn: MachineConnection{ conn: MachineConnection{
+2
View File
@@ -73,6 +73,8 @@ func connectCluster(ctx context.Context, conn config.MachineConnection) (*client
useSSHCLI = true useSSHCLI = true
} else if conn.TCP != nil && conn.TCP.IsValid() { } else if conn.TCP != nil && conn.TCP.IsValid() {
return client.New(ctx, connector.NewTCPConnector(*conn.TCP)) return client.New(ctx, connector.NewTCPConnector(*conn.TCP))
} else if conn.Unix != "" {
return client.New(ctx, connector.NewUnixConnector(conn.Unix))
} else { } else {
return nil, errors.New("connection configuration is invalid") return nil, errors.New("connection configuration is invalid")
} }
+41
View File
@@ -0,0 +1,41 @@
package connector
import (
"context"
"fmt"
"golang.org/x/net/proxy"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
// UnixConnector establishes a connection to the machine API through a unix domain socket.
type UnixConnector struct {
socketPath string
}
func NewUnixConnector(socketPath string) *UnixConnector {
return &UnixConnector{socketPath: socketPath}
}
func (c *UnixConnector) Connect(_ context.Context) (*grpc.ClientConn, error) {
// gRPC uses "unix:path" syntax for unix sockets.
target := "unix:" + c.socketPath
conn, err := grpc.NewClient(
target,
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
return nil, fmt.Errorf("create machine API client: %w", err)
}
return conn, nil
}
func (c *UnixConnector) Dialer() (proxy.ContextDialer, error) {
return nil, fmt.Errorf("proxy connections are not supported over a unix connection")
}
func (c *UnixConnector) Close() error {
return nil
}