mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-23 15:41:02 +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"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/acarl005/stripansi"
|
"github.com/acarl005/stripansi"
|
||||||
|
|
@ -66,9 +72,113 @@ func (c *DockerCommand) NewCommandObject(obj CommandObject) CommandObject {
|
||||||
return defaultObj
|
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
|
// NewDockerCommand it runs docker commands
|
||||||
func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*DockerCommand, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue