initial commit v0.80
authorCharles McGarvey <chazmcgarvey@brokenzipper.com>
Sun, 26 Nov 2017 02:54:15 +0000 (19:54 -0700)
committerCharles McGarvey <chazmcgarvey@brokenzipper.com>
Sun, 26 Nov 2017 02:54:15 +0000 (19:54 -0700)
.travis.yml [new file with mode: 0644]
COPYING [new file with mode: 0644]
Makefile [new file with mode: 0644]
README.md [new file with mode: 0644]
docker-connect [new file with mode: 0755]
t/01-basic.sh [new file with mode: 0644]
t/02-socket-errors.sh [new file with mode: 0644]
t/03-options.sh [new file with mode: 0644]
tap.sh [new file with mode: 0644]
unittest.sh [new file with mode: 0644]

diff --git a/.travis.yml b/.travis.yml
new file mode 100644 (file)
index 0000000..d3ed63b
--- /dev/null
@@ -0,0 +1,2 @@
+language: sh
+script: make test V=1
diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..98a904c
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,16 @@
+Copyright 2017 Charles McGarvey
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+associated documentation files (the "Software"), to deal in the Software without restriction,
+including without limitation the rights to use, copy, modify, merge, publish, distribute,
+sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial
+portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
+OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..1077ab5
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,16 @@
+
+PROVE           = prove
+POD2MARKDOWN    = pod2markdown
+
+all:
+
+docs: README.md
+
+test:
+       $(PROVE) --ext sh $(if $(V),-v)
+
+README.md: docker-connect
+       $(POD2MARKDOWN) "$<" "$@"
+
+.PHONY: all docs test
+
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..f157b75
--- /dev/null
+++ b/README.md
@@ -0,0 +1,111 @@
+# NAME
+
+docker-connect - Easily connect to Docker sockets over SSH
+
+# VERSION
+
+Version 0.80
+
+# SYNOPSIS
+
+    docker-connect HOSTNAME [SHELL_ARGS]...
+
+    # launch a new shell wherein docker commands go to staging-01.acme.tld
+    docker-connect staging-01.acme.tld
+
+    # list the docker processes running on staging-01.acme.tld
+    docker-connect staging-01.acme.tld -c 'docker ps'
+
+# DESCRIPTION
+
+This script provides an alternative to Docker Machine for connecting your Docker client to a remote
+Docker daemon. Instead of connecting directly to a Docker daemon listening on an external TCP port,
+this script sets up a connection to the UNIX socket via SSH.
+
+Why?
+
+The main use case for this is when dealing with "permanent" app servers in an environment where you
+have a team of individuals who all need access.
+
+Machine doesn't have a great way to support multiple concurrent users. You can add an existing
+machine to which you have SSH access using the generic driver on your computer, but if your
+colleague does the same then Machine will regenerate the Docker daemon TLS certificates, replacing
+the ones Machine set up for you.
+
+Furthermore, the Docker daemon relies on TLS certificates for client authorization, which is all
+fine and good, but organizations are typically not as prepared to deal with the management of client
+TLS certificates as they are with the management of SSH keys. Worse, the Docker daemon doesn't
+support certificate revocation lists! So if a colleague leaves, you must replace the certificate
+authority and recreate and distribute certificates for each remaining member of the team. Ugh!
+
+Much easier to just use SSH for authorization.
+
+To be clear, this script isn't a full replacement for Docker Machine. For one thing, Machine has
+a lot more features and can actually create machines. This script just assists with a particular
+workflow that is currently underserved by Machine.
+
+# REQUIREMENTS
+
+- a Bourne-compatible, POSIX-compatible shell
+
+    This program is written in shell script.
+
+- [OpenSSH](https://www.openssh.com) 6.7+
+
+    Needed to make the socket connection.
+
+- [Docker](https://www.docker.com) client
+
+    Not technically required, but this program isn't useful without it.
+
+# INSTALL
+
+[![Build Status](https://travis-ci.org/chazmcgarvey/docker-connect.svg?branch=master)](https://travis-ci.org/chazmcgarvey/docker-connect)
+
+To install, just copy `docker-connect` into your `PATH` and make sure it is executable.
+
+    # Assuming you have "$HOME/bin" in your $PATH:
+    cp docker-connect ~/bin/
+    chmod +x ~/bin/docker-connect
+
+# ENVIRONMENT
+
+The following environment variables may affect or will be set by this program:
+
+- `DOCKER_CONNECT_SOCKET`
+
+    The absolute path to the local socket.
+
+- `DOCKER_CONNECT_HOSTNAME`
+
+    The hostname of the remote peer.
+
+- `DOCKER_CONNECT_PID`
+
+    The PID of the SSH process maintaining the connection.
+
+- `DOCKER_HOST`
+
+    The URI of the local socket.
+
+# TIPS
+
+If you run many shells and connections, having the hostname of the host that the Docker client is
+connected to in your prompt may be handy. Try something like this in your local shell config file:
+
+    if [ -n "$DOCKER_CONNECT_HOSTNAME" ]
+    then
+        PS1="[docker:$DOCKER_CONNECT_HOSTNAME] $PS1"
+    fi
+
+# AUTHOR
+
+Charles McGarvey <chazmcgarvey@brokenzipper.com>
+
+# LICENSE
+
+This software is copyright (c) 2017 by Charles McGarvey.
+
+This is free software, licensed under:
+
+    The MIT (X11) License
diff --git a/docker-connect b/docker-connect
new file mode 100755 (executable)
index 0000000..ca2f3f2
--- /dev/null
@@ -0,0 +1,308 @@
+#!/bin/sh
+
+: <<'=cut'
+=pod
+
+=head1 NAME
+
+docker-connect - Easily connect to Docker sockets over SSH
+
+=head1 VERSION
+
+Version 0.80
+
+=head1 SYNOPSIS
+
+    docker-connect HOSTNAME [SHELL_ARGS]...
+
+    # launch a new shell wherein docker commands go to staging-01.acme.tld
+    docker-connect staging-01.acme.tld
+
+    # list the docker processes running on staging-01.acme.tld
+    docker-connect staging-01.acme.tld -c 'docker ps'
+
+=head1 DESCRIPTION
+
+This script provides an alternative to Docker Machine for connecting your Docker client to a remote
+Docker daemon. Instead of connecting directly to a Docker daemon listening on an external TCP port,
+this script sets up a connection to the UNIX socket via SSH.
+
+Why?
+
+The main use case for this is when dealing with "permanent" app servers in an environment where you
+have a team of individuals who all need access.
+
+Machine doesn't have a great way to support multiple concurrent users. You can add an existing
+machine to which you have SSH access using the generic driver on your computer, but if your
+colleague does the same then Machine will regenerate the Docker daemon TLS certificates, replacing
+the ones Machine set up for you.
+
+Furthermore, the Docker daemon relies on TLS certificates for client authorization, which is all
+fine and good, but organizations are typically not as prepared to deal with the management of client
+TLS certificates as they are with the management of SSH keys. Worse, the Docker daemon doesn't
+support certificate revocation lists! So if a colleague leaves, you must replace the certificate
+authority and recreate and distribute certificates for each remaining member of the team. Ugh!
+
+Much easier to just use SSH for authorization.
+
+To be clear, this script isn't a full replacement for Docker Machine. For one thing, Machine has
+a lot more features and can actually create machines. This script just assists with a particular
+workflow that is currently underserved by Machine.
+
+=head1 REQUIREMENTS
+
+=over
+
+=item * a Bourne-compatible, POSIX-compatible shell
+
+This program is written in shell script.
+
+=item * L<OpenSSH|https://www.openssh.com> 6.7+
+
+Needed to make the socket connection.
+
+=item * L<Docker|https://www.docker.com> client
+
+Not technically required, but this program isn't useful without it.
+
+=back
+
+=head1 INSTALL
+
+=for markdown [![Build Status](https://travis-ci.org/chazmcgarvey/docker-connect.svg?branch=master)](https://travis-ci.org/chazmcgarvey/docker-connect)
+
+To install, just copy F<docker-connect> into your C<PATH> and make sure it is executable.
+
+    # Assuming you have "$HOME/bin" in your $PATH:
+    cp docker-connect ~/bin/
+    chmod +x ~/bin/docker-connect
+
+=head1 ENVIRONMENT
+
+The following environment variables may affect or will be set by this program:
+
+=over
+
+=item * C<DOCKER_CONNECT_SOCKET>
+
+The absolute path to the local socket.
+
+=item * C<DOCKER_CONNECT_HOSTNAME>
+
+The hostname of the remote peer.
+
+=item * C<DOCKER_CONNECT_PID>
+
+The PID of the SSH process maintaining the connection.
+
+=item * C<DOCKER_HOST>
+
+The URI of the local socket.
+
+=back
+
+=head1 TIPS
+
+If you run many shells and connections, having the hostname of the host that the Docker client is
+connected to in your prompt may be handy. Try something like this in your local shell config file:
+
+    if [ -n "$DOCKER_CONNECT_HOSTNAME" ]
+    then
+        PS1="[docker:$DOCKER_CONNECT_HOSTNAME] $PS1"
+    fi
+
+=head1 AUTHOR
+
+Charles McGarvey <chazmcgarvey@brokenzipper.com>
+
+=head1 LICENSE
+
+This software is copyright (c) 2017 by Charles McGarvey.
+
+This is free software, licensed under:
+
+    The MIT (X11) License
+
+=cut
+
+set -e
+
+prog=$(basename "$0")
+version="0.80"
+quiet=0
+socket="$DOCKER_CONNECT_SOCKET"
+remote_socket=${REMOTE_SOCKET:-/run/docker.sock}
+timeout=${TIMEOUT:-15}
+
+usage() {
+    cat <<END
+$prog [OPTIONS]... HOSTNAME [SHELL_ARGS]...
+Easily connect to Docker sockets over SSH.
+
+OPTIONS:
+    -h      Show this help info and exit.
+    -q      Be less verbose; can be repeated to enhance effect.
+    -r STR  Specify the absolute path of the remote socket.
+    -s STR  Specify the absolute path of the local socket.
+    -v      Show the program version.
+END
+}
+
+log() {
+    _l=$1
+    shift
+    if [ "$_l" -ge "$quiet" ]
+    then
+        echo >&2 "$prog: $@"
+    fi
+}
+
+while getopts "hqr:s:v" opt
+do
+    case "$opt" in
+        q)
+            quiet=$(expr $quiet + 1)
+            ;;
+        s)
+            socket="$OPTARG"
+            ;;
+        r)
+            remote_socket="$OPTARG"
+            ;;
+        h)
+            usage
+            exit 0
+            ;;
+        v)
+            echo "docker-connect $version"
+            exit 0
+            ;;
+        *)
+            usage
+            exit 1
+            ;;
+    esac
+done
+shift $(expr $OPTIND - 1)
+
+connect=$1
+if [ -z "$connect" ]
+then
+    echo >&2 "Missing HOSTNAME."
+    usage
+    exit 1
+fi
+shift
+
+if [ -z "$socket" ]
+then
+    socket_dir="${TMPDIR:-/tmp}/docker-connect-$(id -u)"
+    mkdir -p "$socket_dir"
+    chmod 0700 "$socket_dir"
+    socket="$socket_dir/docker-$$.sock"
+fi
+
+if [ -S "$socket" ]
+then
+    if [ -n "$DOCKER_CONNECT_HOSTNAME" ]
+    then
+        log 2 "Docker is already connected to $DOCKER_CONNECT_HOSTNAME in this shell."
+        exit 2
+    else
+        log 2 "Docker socket already exists."
+        log 1 "To force a new connection, first remove the file: $socket"
+        exit 3
+    fi
+elif [ -e "$socket" ]
+then
+    log 2 "Cannot create socket because another file is in the way."
+    log 1 "To create a new connection, you may first remove the file: $socket"
+    exit 4
+fi
+
+hostname=
+port=
+user=
+
+if echo "$connect" |grep -q ':'
+then
+    hostname=$(echo "$connect" |cut -d: -f1)
+    port=$(echo "$connect" |cut -d: -f2)
+else
+    hostname="$connect"
+fi
+
+if echo "$hostname" |grep -q '@'
+then
+    user=$(echo "$hostname" |cut -d@ -f1)
+    hostname=$(echo "$hostname" |cut -d@ -f2)
+else
+    user=$(cat ansible.cfg 2>/dev/null |sed -ne 's/^remote_user[[:space:]]*=[[:space:]]*//p')
+fi
+
+ssh_connect="$hostname"
+
+if [ "$user" != "" ]
+then
+    ssh_connect="$user@$ssh_connect"
+fi
+
+if [ "$port" != "" ]
+then
+    ssh_connect="$ssh_connect -p$port"
+fi
+
+${SSH:-ssh} $ssh_connect -L"$socket:$remote_socket" \
+    -oControlPath=none -oConnectTimeout="$timeout" -nNT &
+ssh_pid=$!
+ssh_connected=
+
+handle_noconnect() {
+    log 2 "The connection could not be established."
+    log 1 "Please ensure that you can execute this command successfully:"
+    log 1 "  ${SSH:-ssh} $ssh_connect -oControlPath=none echo OK"
+    exit 5
+}
+
+handle_disconnect() {
+    kill $ssh_pid 2>/dev/null || true
+    rm -f "$socket"
+    log 0 "Disconnected docker from $hostname."
+}
+
+# Wait for the socket connection to be made.
+for i in $(seq 1 "${timeout}0")
+do
+    if [ -S "$socket" ]
+    then
+        ssh_connected=1
+        break
+    fi
+    if ! kill -s 0 $ssh_pid 2>/dev/null
+    then
+        handle_noconnect
+    fi
+    sleep 0.1
+done
+
+if [ -z "$ssh_connected" ]
+then
+    handle_noconnect
+fi
+
+trap handle_disconnect EXIT
+
+export DOCKER_CONNECT_HOSTNAME="$hostname"
+export DOCKER_CONNECT_PID="$ssh_pid"
+export DOCKER_CONNECT_SOCKET="$socket"
+export DOCKER_HOST="unix://$socket"
+
+# Remove incompatible variables set by Docker Machine.
+unset DOCKER_MACHINE_NAME
+unset DOCKER_CERT_PATH
+unset DOCKER_TLS_VERIFY
+
+log 1 "Executing new shell with docker connected to $hostname."
+log 0 "This connection will be terminated when the shell exits."
+${SHELL:-/bin/sh} "$@"
+
diff --git a/t/01-basic.sh b/t/01-basic.sh
new file mode 100644 (file)
index 0000000..223c244
--- /dev/null
@@ -0,0 +1,45 @@
+#!/bin/sh
+
+. ./unittest.sh
+. ./tap.sh
+
+plan 14
+
+cat <<'MOCK' >"$SSH"
+#!/bin/sh
+. ./tap.sh
+next_test_number=1
+note 'ssh args:' "$@"
+is "$1", 'foo',                        'ssh: correct hostname'
+is "$2", "-L$socket:/run/docker.sock", 'ssh: local port forwarding flag'
+is "$3", '-oControlPath=none',         'ssh: disable control path option'
+is "$4", '-oConnectTimeout=15',        'ssh: connection timeout option'
+is "$5", '-nNT',                       'ssh: terminal and other flags'
+perl -MIO::Socket::UNIX \
+    -e 'IO::Socket::UNIX->new(Type => SOCK_STREAM, Local => $ENV{socket}, Listen => 1)'
+MOCK
+chmod +x "$SSH"
+
+cat <<'MOCK' >"$SHELL"
+#!/bin/sh
+. ./tap.sh
+next_test_number=6
+note 'shell args:' "$@"
+is "$1"                        "bar"            "shell: first shell arg is correct"
+is "$2"                        "baz"            "shell: second shell arg is correct"
+is "$DOCKER_HOST"              "unix://$socket" "shell: DOCKER_HOST is correct"
+is "$DOCKER_CONNECT_HOSTNAME"  "foo"            "shell: DOCKER_CONNECT_HOSTNAME is correct"
+is "$DOCKER_CONNECT_SOCKET"    "$socket"        "shell: DOCKER_CONNECT_SOCKET is correct"
+ok '-n "$DOCKER_CONNECT_PID"'  "shell: DOCKER_CONNECT_PID is set"
+ok '-z "$DOCKER_MACHINE_NAME"' "shell: DOCKER_MACHINE_NAME is unset"
+ok '-z "$DOCKER_CERT_PATH"'    "shell: DOCKER_CERT_PATH is unset"
+ok '-z "$DOCKER_TLS_VERIFY"'   "shell: DOCKER_TLS_VERIFY is unset"
+MOCK
+chmod +x "$SHELL"
+
+export DOCKER_MACHINE_NAME="qux"
+export DOCKER_CERT_PATH="/somewhere"
+export DOCKER_TLS_VERIFY=1
+
+./docker-connect -qqq foo bar baz
+
diff --git a/t/02-socket-errors.sh b/t/02-socket-errors.sh
new file mode 100644 (file)
index 0000000..b3c2b3f
--- /dev/null
@@ -0,0 +1,26 @@
+#!/bin/sh
+
+. ./unittest.sh
+. ./tap.sh
+
+plan 3
+
+make_socket "$socket"
+
+DOCKER_CONNECT_HOSTNAME=foo ./docker-connect -qqq foo
+is "$?" 2 'socket already exists and environment is configured'
+
+./docker-connect -qqq foo
+is "$?" 3 'socket already exists'
+
+rm -f "$socket"
+
+cat <<'MOCK' >"$SSH"
+#!/bin/sh
+exit 1
+MOCK
+chmod +x "$SSH"
+
+./docker-connect -qqq foo
+is "$?" 5 'socket connection error'
+
diff --git a/t/03-options.sh b/t/03-options.sh
new file mode 100644 (file)
index 0000000..ac10692
--- /dev/null
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+. ./unittest.sh
+. ./tap.sh
+
+plan 3
+
+./docker-connect -h |grep 'OPTIONS' >/dev/null 2>&1
+is "$?" 0 'the -h flag works'
+
+./docker-connect -v |grep '^docker-connect [[:digit:]]' >/dev/null 2>&1
+is "$?" 0 'the -v flag works'
+
+./docker-connect -Z foo 2>/dev/null
+is "$?" 1 'invalid option correctly fails'
+
diff --git a/tap.sh b/tap.sh
new file mode 100644 (file)
index 0000000..8e0ed88
--- /dev/null
+++ b/tap.sh
@@ -0,0 +1,114 @@
+
+: <<'=cut'
+=pod
+
+=head1 NAME
+
+tap.sh - Useful subset of TAP (Test Anything Protocol) for shell scripts
+
+=head1 SYNOPSIS
+
+    . ./tap.sh
+
+    plan 6
+
+    ok '1 = 1' 'Make sure that one equals one'
+    ok '1 != 2' 'Make sure that one is not two'
+
+    is   2 2 'Two is two'
+    isnt 2 3 'Two is not three'
+
+    pass 'It worked!'
+    fail 'Uh oh'
+
+    diag 'This is a diagnostic message'
+    note - <<NOTE
+    Can also use a heredoc for diag and note
+    NOTE
+
+=head1 SEE ALSO
+
+* https://testanything.org - TAP website
+
+=head1 AUTHOR
+
+Charles McGarvey <chazmcgarvey@brokenzipper.com>
+
+=head1 LICENSE
+
+This software is copyright (c) 2017 by Charles McGarvey.
+
+This is free software, licensed under:
+
+    The MIT (X11) License
+
+=cut
+
+next_test_number=1
+
+plan() {
+    _n=$1; shift
+    echo "1..$_n"
+}
+
+ok() {
+    _t=$1; shift
+    _m=$1; shift
+    if eval "test $_t"; then pass "$_m"; else fail "$_m"; fi
+}
+
+is() {
+    _a=$1; shift
+    _b=$1; shift
+    _m=$1; shift
+    if [ "$_a" = "$_b" ]
+    then
+        pass "$_m"
+    else
+        fail "$_m"
+        note "Expected: $_b" "     Got: $_a"
+    fi
+}
+
+isnt() {
+    _a=$1; shift
+    _b=$1; shift
+    _m=$1; shift
+    if [ "$_a" != "$_b" ]
+    then
+        pass "$_m"
+    else
+        fail "$_m"
+        note "Expected: != $_b" "     Got: $_a"
+    fi
+}
+
+pass() {
+    echo "ok $next_test_number - $@"
+    next_test_number=$(expr $next_test_number + 1)
+}
+
+fail() {
+    echo "not ok $next_test_number - $@"
+    next_test_number=$(expr $next_test_number + 1)
+}
+
+diag() {
+    if [ "$1" != '-' ]
+    then
+        for _m in "$@"
+        do
+            echo "# $_m"
+        done
+    else
+        while read _m
+        do
+            echo "# $_m"
+        done
+    fi
+}
+
+note() {
+    diag "$@"
+}
+
diff --git a/unittest.sh b/unittest.sh
new file mode 100644 (file)
index 0000000..da34030
--- /dev/null
@@ -0,0 +1,32 @@
+
+temp=$(mktemp -d 2>/dev/null || mktemp -d -t 'test')
+
+export DOCKER_CONNECT_UNIT_TEST=1
+export SHELL="$temp/mockshell"
+export SSH="$temp/mockssh"
+export socket="$temp/test.sock"
+export DOCKER_CONNECT_SOCKET="$socket"
+
+cleanup() {
+    rm -rf "$temp"
+}
+
+trap cleanup EXIT
+
+cat <<'MOCK' >"$SSH"
+#!/bin/sh
+perl -MIO::Socket::UNIX \
+    -e 'IO::Socket::UNIX->new(Type => SOCK_STREAM, Local => $ENV{socket}, Listen => 1)'
+MOCK
+chmod +x "$SSH"
+
+cat <<'MOCK' >"$SHELL"
+#!/bin/sh
+MOCK
+chmod +x "$SHELL"
+
+make_socket() {
+    SOCKET=$1 perl -MIO::Socket::UNIX \
+        -e 'IO::Socket::UNIX->new(Type => SOCK_STREAM, Local => $ENV{SOCKET}, Listen => 1)'
+}
+
This page took 0.046224 seconds and 4 git commands to generate.