mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-21 23:01:03 +00:00
feat: support ssh:// scheme for docker client host
Check for the presence of environment variable `DOCKER_HOST`. If nonempty and of scheme `ssh://`, open an SSH tunnel to the specified remote host, forwarding the remote docker socket to a local socket in /tmp. Then, construct the lazydocker client from the local socket. Example usage: `DOCKER_HOST=ssh://my-server lazydocker` Fixes #244, #228, #255
This commit is contained in:
parent
c88fba546f
commit
4e201bb8f4
1 changed files with 111 additions and 1 deletions
|
|
@ -5,10 +5,16 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/acarl005/stripansi"
|
||||
|
|
@ -66,9 +72,113 @@ func (c *DockerCommand) NewCommandObject(obj CommandObject) CommandObject {
|
|||
return defaultObj
|
||||
}
|
||||
|
||||
func handleSSHHosts(c *client.Client) error {
|
||||
ctx := context.Background()
|
||||
u, err := url.Parse(os.Getenv("DOCKER_HOST"))
|
||||
if err != nil {
|
||||
// if no or an invalid docker host is specified, continue nominally
|
||||
return nil
|
||||
}
|
||||
|
||||
// if the docker host scheme is "ssh", forward the docker socket before creating the client
|
||||
if u.Scheme == "ssh" {
|
||||
newDockerHost, err := tunneledDockerHost(ctx, u.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tunnel ssh docker host: %w", err)
|
||||
}
|
||||
return client.WithHost(newDockerHost)(c)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func tunneledDockerHost(ctx context.Context, remoteHost string) (string, error) {
|
||||
socketDir, err := ioutil.TempDir("/tmp", "lazydocker-sshtunnel-")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create ssh tunnel tmp file: %w", err)
|
||||
}
|
||||
localSocket := path.Join(socketDir, "dockerhost.sock")
|
||||
|
||||
err = tunnelSSH(ctx, remoteHost, localSocket)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("tunnel docker host over ssh: %w", err)
|
||||
}
|
||||
|
||||
// set a reasonable timeout, then wait for the socket to dial successfully
|
||||
// before attempting to create a new docker client
|
||||
const socketTunnelTimeout = 8 * time.Second
|
||||
ctx, cancel := context.WithTimeout(ctx, 8*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err = retrySocketDial(ctx, localSocket)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ssh tunneled socket never became available: %w", err)
|
||||
}
|
||||
|
||||
// construct the new DOCKER_HOST url with the proper scheme
|
||||
newDockerHostURL := url.URL{Scheme: "unix", Path: localSocket}
|
||||
return newDockerHostURL.String(), nil
|
||||
}
|
||||
|
||||
// Attempt to dial the socket until it becomes available.
|
||||
// The retry loop will continue until the parent context is canceled.
|
||||
func retrySocketDial(ctx context.Context, socketPath string) error {
|
||||
t := time.NewTicker(1 * time.Second)
|
||||
defer t.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-t.C:
|
||||
}
|
||||
// attempt to dial the socket, exit on success
|
||||
err := tryDial(ctx, socketPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Try to dial the specified unix socket, immediately close the connection if successfully created.
|
||||
func tryDial(ctx context.Context, socketPath string) error {
|
||||
var dialer net.Dialer
|
||||
conn, err := dialer.DialContext(ctx, "unix", socketPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func tunnelSSH(ctx context.Context, host, localSocket string) error {
|
||||
cmd := exec.CommandContext(ctx, "ssh", "-L", localSocket+":/var/run/docker.sock", host, "-N")
|
||||
// when lazydocker exits, kill the SSH tunnel
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
// TODO: ensure we get this behavior on all platforms
|
||||
Pdeathsig: syscall.SIGKILL,
|
||||
}
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build a new docker client from the enviornment.
|
||||
//
|
||||
// Handle special cases including `ssh://` host schemes.
|
||||
func clientBuilder(c *client.Client) error {
|
||||
err := client.FromEnv(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return handleSSHHosts(c)
|
||||
}
|
||||
|
||||
// NewDockerCommand it runs docker commands
|
||||
func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*DockerCommand, error) {
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithVersion(APIVersion))
|
||||
cli, err := client.NewClientWithOpts(clientBuilder, client.WithVersion(APIVersion))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue