Add SSH source support with dialstdio and UI components
- Implement SSH dial via stdio for remote connections - Add sources API and storage layer for managing connection sources - Add SourcePanel and SshFields web components for SSH configuration - Update app structure to support source-based connections - Update handlers and server for new sources endpoint Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
package sshx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestDialAPICommand pins the command the tunnel runs on the source host: it is
|
||||
// the same one `docker -H ssh://…` uses, and the sudo / custom binary settings
|
||||
// have to reach it.
|
||||
func TestDialAPICommand(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
rd *RemoteDocker
|
||||
want string
|
||||
}{
|
||||
{"plain", &RemoteDocker{binary: "docker"}, "docker system dial-stdio"},
|
||||
{"sudo", &RemoteDocker{binary: "docker", sudo: true}, "sudo -n docker system dial-stdio"},
|
||||
{"podman", &RemoteDocker{binary: "podman"}, "podman system dial-stdio"},
|
||||
{"path with a space", &RemoteDocker{binary: "/opt/my docker/bin/docker"}, "'/opt/my docker/bin/docker' system dial-stdio"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := tc.rd.Cmd("system", "dial-stdio"); got != tc.want {
|
||||
t.Fatalf("command = %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestAPIConnSurfacesRemoteStderr covers the failure that would otherwise reach
|
||||
// the Docker client as a bare EOF: the remote docker printing a reason and
|
||||
// exiting.
|
||||
func TestAPIConnSurfacesRemoteStderr(t *testing.T) {
|
||||
errBuf := &syncBuffer{}
|
||||
errBuf.Write([]byte("docker: 'system dial-stdio' is not a docker command\n"))
|
||||
c := &apiConn{
|
||||
stdout: strings.NewReader(""),
|
||||
stdin: nopWriteCloser{io.Discard},
|
||||
stderr: errBuf,
|
||||
}
|
||||
_, err := c.Read(make([]byte, 8))
|
||||
if err == nil {
|
||||
t.Fatal("a closed stream with remote stderr should be an error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "is not a docker command") {
|
||||
t.Fatalf("error = %v, want the remote stderr in it", err)
|
||||
}
|
||||
// Without stderr the plain EOF must survive, or the HTTP transport cannot
|
||||
// tell a finished response from a broken one.
|
||||
quiet := &apiConn{stdout: strings.NewReader(""), stdin: nopWriteCloser{io.Discard}, stderr: &syncBuffer{}}
|
||||
if _, err := quiet.Read(make([]byte, 8)); !errors.Is(err, io.EOF) {
|
||||
t.Fatalf("error = %v, want io.EOF", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIConnDeadlinesAreNoops(t *testing.T) {
|
||||
var c net.Conn = &apiConn{stdout: strings.NewReader(""), stdin: nopWriteCloser{io.Discard}, stderr: &syncBuffer{}}
|
||||
now := time.Now()
|
||||
if err := c.SetDeadline(now); err != nil {
|
||||
t.Fatalf("SetDeadline: %v", err)
|
||||
}
|
||||
if err := c.SetReadDeadline(now); err != nil {
|
||||
t.Fatalf("SetReadDeadline: %v", err)
|
||||
}
|
||||
if err := c.SetWriteDeadline(now); err != nil {
|
||||
t.Fatalf("SetWriteDeadline: %v", err)
|
||||
}
|
||||
if c.RemoteAddr().Network() != "ssh" {
|
||||
t.Fatalf("network = %q, want ssh", c.RemoteAddr().Network())
|
||||
}
|
||||
}
|
||||
|
||||
type nopWriteCloser struct{ io.Writer }
|
||||
|
||||
func (nopWriteCloser) Close() error { return nil }
|
||||
Reference in New Issue
Block a user