Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unix socket #62

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
93f2981
Try switching to running powerstrip on the Docker socket.
Feb 20, 2015
a6afe2b
clarify
Feb 20, 2015
1c75389
spell restarting docker correctly
Feb 20, 2015
06a5e13
note that there are breaking changes and add a header for the UNIX so…
binocarlos Mar 17, 2015
419be89
link to the official docker configure socket instructions and rearran…
binocarlos Mar 17, 2015
ce0d989
note about the default docker options file on ubuntu
binocarlos Mar 17, 2015
fceba23
notes about reconfiguring the docker socket in boot2docker
binocarlos Mar 17, 2015
ef464bf
docker.sock.real -> docker.real.sock
binocarlos Mar 17, 2015
eb4c79c
remove the XXX change this note and added an issue for it
binocarlos Mar 17, 2015
2f0359b
$PWD -> ${PWD}
binocarlos Mar 17, 2015
117dbf3
Add note about boot2docker and docker-machine not currently being sup…
binocarlos Mar 24, 2015
daba983
ignore _trial_temp/
binocarlos Mar 24, 2015
68388e8
note about how to run tests
binocarlos Mar 24, 2015
2b5ee58
tests failing for utils module (create functions from code in powerst…
binocarlos Mar 25, 2015
30b7267
tests passing for the startup function to get the DOCKER_HOST env var…
binocarlos Mar 25, 2015
bdb8ecb
more tests for the DOCKER_HOST utils
binocarlos Mar 25, 2015
dfb2ba7
failing tests for scheme key in dockerAPICredentials
binocarlos Mar 26, 2015
479a406
tests passing for scheme returned from dockerAPICredentials
binocarlos Mar 26, 2015
a77f766
refactor powerstrip.tac based on library functions
binocarlos Mar 26, 2015
856bcd2
add a note about using socat to listen on TCP sockets
binocarlos Mar 26, 2015
48e601a
remove the scheme property because it upsets the creation of the Serv…
binocarlos Mar 26, 2015
a160943
syntax errors - learning python
binocarlos Mar 26, 2015
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,5 @@ docs/_build/

# PyBuilder
target/

_trial_temp/
62 changes: 50 additions & 12 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,42 @@ Try it out

Powerstrip ships as a Docker image, and adapters can be any HTTP endpoint, including other linked Docker containers.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a major change in operation with this release. There should be big red text on here or something.


NOTE: **there are breaking changes in v0.0.2**

Unix Socket
-----------

Powerstrip expects Docker to have been reconfigured to listen on ``/var/run/docker.real.sock``. There are official instructions for doing that .. here: https://docs.docker.com/articles/basics/#bind-docker-to-another-hostport-or-a-unix-socket.

For example, on Ubuntu, the default Docker options are found in ``/etc/default/docker`` which can be edited to say ``DOCKER_OPTS="-H unix:///var/run/docker.real.sock"`` and then run ``sudo service docker restart``.

NOTE: ``boot2docker`` and ``docker-machine`` are not currently supported.

socat
-----

Because powerstrip listens on a Unix Socket (since v0.0.2) - it means that the HTTP api is not exposed over the network. If you need powerstrip to listen on a TCP socket - you can use something like socat. However - it must be noted that it does not work if you run socat inside a container (which is the reason that powerstrip no longers offers an option to listen on a TCP port).

Here is an example of a socat command that will forward traffic on port 2375 to the `/var/run/docker.sock` that powerstrip is listening to:

```bash
$ socat TCP-LISTEN:2375,reuseaddr,fork UNIX-CLIENT:/var/run/docker.sock
```

/var/run volume
---------------

Powerstrip also expects to have a volume for ``/var/run`` on the host bind-mounted to ``/host-var-run`` in the container.

Powerstrip will then create ``/var/run/docker.sock`` from the host's perspective (``/host-var-run/docker.sock`` from inside its container) and normal Docker tools should carry on working as normal.

Example Adapter
---------------

`Slowreq <https://github.com/clusterhq/powerstrip-slowreq>`_ is a trivial Powerstrip adapter (container) which adds a 1 second delay to all create commands.

Try it out like this (assuming logged into an Ubuntu Docker host).

If you are using ``boot2docker``, drop the ``sudo`` and also unset ``DOCKER_TLS_VERIFY``.

.. code:: sh

$ cd ~/
Expand All @@ -86,22 +116,21 @@ If you are using ``boot2docker``, drop the ``sudo`` and also unset ``DOCKER_TLS_
slowreq: http://slowreq/slowreq-adapter
EOF

$ sudo docker run -d --name powerstrip-slowreq \
$ sudo DOCKER_HOST="unix:///var/run/docker.real.sock" \
docker run -d --name powerstrip-slowreq \
--expose 80 \
clusterhq/powerstrip-slowreq:v0.0.1
$ sudo docker run -d --name powerstrip \
-v /var/run/docker.sock:/var/run/docker.sock \
-v $PWD/powerstrip-demo/adapters.yml:/etc/powerstrip/adapters.yml \
$ sudo DOCKER_HOST="unix:///var/run/docker.real.sock" \
docker run -d --name powerstrip \
-v /var/run:/host-var-run \
-v ${PWD}/powerstrip-demo/adapters.yml:/etc/powerstrip/adapters.yml \
--link powerstrip-slowreq:slowreq \
-p 2375:2375 \
clusterhq/powerstrip:v0.0.1
clusterhq/powerstrip:unix-socket

# Note how the second command takes a second longer than the first.
$ time sudo DOCKER_HOST="unix:///var/run/docker.real.sock" \
docker run ubuntu echo hello
$ time sudo docker run ubuntu echo hello
$ time DOCKER_HOST=localhost:2375 docker run ubuntu echo hello

**Warning:** Powerstrip exposes the Docker API unprotected on port 2375.
Only use it in private, secure development environments.

**Issues:** If you are using ``SELinux`` and having some issues, disable it or run the following commands:

Expand Down Expand Up @@ -327,6 +356,7 @@ v0.0.2:

* Add integration tests against real Docker for ``run``, ``build`` and ``pull``, fix various bugs exposed therein.
* In particular, fix docker ``attach``, streaming responses when there are no post-hooks, GET requests, skip pre-hooks with ``application/tar`` handling, stdin handling for ``attach``.
* Powerstrip now listens on a Unix Socket and not TCP - this is to get around proxy problems when using a Docker container to foward the TCP traffic

v0.0.1:

Expand All @@ -340,6 +370,14 @@ Additional Adapter Ideas
* A pre hook for containers => create that will inject ENV variables loaded from `consul <https://github.com/hashicorp/consul>`_ or `etcd <https://github.com/coreos/etcd>`_
* A post hook for containers => {start,stop} that will update `consul <https://github.com/hashicorp/consul>`_ or `etcd <https://github.com/coreos/etcd>`_ with the containers exposed endpoints

Running Tests
=============

To run the test suite do the following commands:

.. code::
sudo TEST_PASSTHRU=1 trial powerstrip.test

License
=======

Expand Down
44 changes: 25 additions & 19 deletions powerstrip.tac
Original file line number Diff line number Diff line change
@@ -1,28 +1,34 @@
import os
import sys
from twisted.application import service, internet
#from twisted.protocols.policies import TrafficLoggingFactory
from urlparse import urlparse

from powerstrip.powerstrip import ServerProtocolFactory
from powerstrip.resources import GetDockerHost,GetDockerAPICredentials

TARGET_DOCKER_SOCKET = "/host-var-run/docker.sock"

application = service.Application("Powerstrip")

DOCKER_HOST = os.environ.get('DOCKER_HOST')
if DOCKER_HOST is None:
# Default to assuming we've got a Docker socket bind-mounted into a
# container we're running in.
DOCKER_HOST = "unix:///var/run/docker.sock"
if "://" not in DOCKER_HOST:
DOCKER_HOST = "tcp://" + DOCKER_HOST
if DOCKER_HOST.startswith("tcp://"):
parsed = urlparse(DOCKER_HOST)
dockerAPI = ServerProtocolFactory(dockerAddr=parsed.hostname,
dockerPort=parsed.port)
elif DOCKER_HOST.startswith("unix://"):
socketPath = DOCKER_HOST[len("unix://"):]
dockerAPI = ServerProtocolFactory(dockerSocket=socketPath)
#logged = TrafficLoggingFactory(dockerAPI, "api-")
dockerServer = internet.TCPServer(2375, dockerAPI, interface='0.0.0.0')
dockerServer.setServiceParent(application)
# we create a connection to the Docker server based on DOCKER_HOST (can be tcp or unix socket)
DOCKER_HOST = GetDockerHost(os.environ.get('DOCKER_HOST'))
dockerAPICredentials = GetDockerAPICredentials(DOCKER_HOST)

# check that /var/run is mounted from the host (so we can write docker.sock to it)
if not os.path.isdir("/host-var-run"):
sys.stderr.write("/var/run must be mounted as /host-var-run in the powerstrip container\n")
sys.exit(1)

print r'export DOCKER_HOST=tcp://localhost:2375'
# check that the docker unix socket that we are trying to connect to actually exists
if dockerAPICredentials['dockerSocket'] and not os.path.exists(dockerAPICredentials["dockerSocket"]):
sys.stderr.write(dockerAPICredentials["dockerSocket"] + " does not exist as a docker unix socket to connect to\n")
sys.exit(1)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not super happy about removing this functionality - some people might be depending on it (for whatever "depending on" means for a hack project).

I feel like we should make the effort to support it still, probably via an environment variable or by implying the user's intention from whether /host-var-run is mounted.

# check that the unix socket we want to listen on does not already exist
if os.path.exists(TARGET_DOCKER_SOCKET):
sys.stderr.write(TARGET_DOCKER_SOCKET + " already exists - we want to listen on this path\n")
sys.exit(1)

dockerAPI = ServerProtocolFactory(**dockerAPICredentials)
dockerServer = internet.UNIXServer(TARGET_DOCKER_SOCKET, dockerAPI, mode=0660)
dockerServer.setServiceParent(application)
38 changes: 38 additions & 0 deletions powerstrip/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
from twisted.internet import reactor
from twisted.internet.task import deferLater
from twisted.web.server import NOT_DONE_YET
import os
from urlparse import urlparse
from powerstrip import ServerProtocolFactory

class BaseProxyResource(proxy.ReverseProxyResource):
def getChild(self, path, request):
Expand All @@ -21,3 +24,38 @@ def run():

class DeleteContainerResource(BaseProxyResource):
pass

def GetDockerHost(DOCKER_HOST=None):
"""
Logic for getting the default value of DOCKER_HOST if its either not given or
only partially given.
The DOCKER_HOST must either start with tcp:// or unix://
If no scheme is provided - we check for a leading slash to determine if its tcp or unix

it is normal to pass the ENV var DOCKER_HOST to this function:

dockerHost = GetDockerHost(os.environ.get('DOCKER_HOST'))
"""

if DOCKER_HOST is None:
# Default to assuming we've got a Docker socket bind-mounted into a
# container we're running in.
DOCKER_HOST = "unix:///host-var-run/docker.real.sock"
if "://" not in DOCKER_HOST:
if DOCKER_HOST.startswith("/"):
DOCKER_HOST = "unix://" + DOCKER_HOST
else:
DOCKER_HOST = "tcp://" + DOCKER_HOST
return DOCKER_HOST

def GetDockerAPICredentials(DOCKER_HOST="unix:///host-var-run/docker.real.sock"):
"""
Logic for getting the arguments to be passed to a ServerProtocolFactory based on the DOCKER_HOST
"""
if DOCKER_HOST.startswith("tcp://"):
parsed = urlparse(DOCKER_HOST)
return dict(dockerAddr=parsed.hostname, dockerPort=parsed.port)
#ServerProtocolFactory(**mydictionary)
elif DOCKER_HOST.startswith("unix://"):
socketPath = DOCKER_HOST[len("unix://"):]
return dict(dockerSocket=socketPath)
80 changes: 80 additions & 0 deletions powerstrip/test/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Copyright ClusterHQ Limited. See LICENSE file for details.
# -*- test-case-name: powerstrip.test.test_utils -*-

from twisted.trial.unittest import TestCase
from ..resources import GetDockerHost,GetDockerAPICredentials

"""
Tests for the utils.
"""

class TestDockerHost(TestCase):

def test_get_default_docker_host(self):
"""
Test that if nothing is supplied we get the default UNIX socket
"""
dockerHost = GetDockerHost()
self.assertEqual(dockerHost, "unix:///host-var-run/docker.real.sock")

def test_get_path_based_docker_host(self):
"""
Check that if a path with no scheme is supplied then we get a unix socket
"""
dockerHost = GetDockerHost('/var/run/my.sock')
self.assertEqual(dockerHost, "unix:///var/run/my.sock")

def test_get_tcp_based_docker_host(self):
"""
Check that if a path with no scheme is supplied then we get a unix socket
"""
dockerHost = GetDockerHost('localhost:2375')
self.assertEqual(dockerHost, "tcp://localhost:2375")

def test_get_path_based_docker_host_unchanged(self):
"""
Check that if a path with no scheme is supplied then we get a unix socket
"""
dockerHost = GetDockerHost('unix:///var/run/yobedisfileyo')
self.assertEqual(dockerHost, "unix:///var/run/yobedisfileyo")

def test_get_tcp_based_docker_host_unchanged(self):
"""
Check that if a path with no scheme is supplied then we get a unix socket
"""
dockerHost = GetDockerHost('tcp://127.0.0.1:2375')
self.assertEqual(dockerHost, "tcp://127.0.0.1:2375")


class TestDockerAPICredentials(TestCase):

def test_get_default_dockerapi_credentials(self):
"""
Test that if nothing is supplied we get the default UNIX socket
"""
dockerAPICredentials = GetDockerAPICredentials()

self.assertEqual(dockerAPICredentials['dockerSocket'], "/host-var-run/docker.real.sock")

self.assertNotIn("dockerAddr", dockerAPICredentials)
self.assertNotIn("dockerPort", dockerAPICredentials)

def test_get_tcp_dockerapi_credentials(self):
"""
Test that if TCP is supplied we get the IP / port returned
"""
dockerAPICredentials = GetDockerAPICredentials('tcp://127.0.0.1:2375')

self.assertEqual(dockerAPICredentials['dockerAddr'], "127.0.0.1")
self.assertEqual(dockerAPICredentials['dockerPort'], 2375)
self.assertNotIn("dockerSocket", dockerAPICredentials)

def test_get_unixsocket_dockerapi_credentials(self):
"""
Test that if UNIX is supplied
"""
dockerAPICredentials = GetDockerAPICredentials('unix:///var/run/yobedisfileyo')

self.assertEqual(dockerAPICredentials['dockerSocket'], "/var/run/yobedisfileyo")
self.assertNotIn("dockerAddr", dockerAPICredentials)
self.assertNotIn("dockerPort", dockerAPICredentials)