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

Add a links argument to GearClient.add (fixes #150) #183

Merged
merged 41 commits into from
Jul 2, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
1bb4da1
A skeleton test
wallrj Jun 26, 2014
79724bb
Dockerfile to run nc against a fixed local port
wallrj Jun 26, 2014
d5c78a5
A helper to build the Dockerfile for the test
wallrj Jun 26, 2014
40f2223
merge forward
wallrj Jun 27, 2014
d30af41
twiddling
wallrj Jun 27, 2014
4c512ad
gear and systemd react badly to long names starting with _
wallrj Jun 27, 2014
6feeeed
Merge remote-tracking branch 'origin/master' into gear-links-150
wallrj Jun 30, 2014
6fb2e80
Committing something ugly that sort of works
wallrj Jun 30, 2014
ca758f3
Disable the docker image cleanup step which was failing intermittentl…
wallrj Jun 30, 2014
c897ee8
Start of including IP address as part of PortMap
adamtheturtle Jun 30, 2014
7501e18
Merge branch 'gear-links-150' of github.com:clusterhq/flocker into ge…
adamtheturtle Jun 30, 2014
4417166
Fixed merge conflict
adamtheturtle Jun 30, 2014
a81fddb
Continuation of adding internal_address to PortMap
adamtheturtle Jun 30, 2014
3c3a612
Account for PortMap changes
adamtheturtle Jun 30, 2014
157d1b0
Changed long lines for flake8
adamtheturtle Jun 30, 2014
af93846
Don't bother trying to cleanup the image.
wallrj Jun 30, 2014
a65032d
Ok I realise now that when you specify HostAddress 127.0.0.1 it actua…
wallrj Jun 30, 2014
8277080
Attempt to connect in a loop which allows us to remove all the sleeps
wallrj Jun 30, 2014
b03d285
use twisted to capture the lines sent from the container
wallrj Jun 30, 2014
1104bf7
remove some of the changes associated with the earlier PortMap.extern…
wallrj Jun 30, 2014
6a07879
Move DockerImageBuilder to testtools and document it.
wallrj Jun 30, 2014
2c5be53
Merge remote-tracking branch 'origin/master' into gear-links-150
wallrj Jun 30, 2014
92f3d0c
Merge remote-tracking branch 'origin/master' into gear-links-150
wallrj Jun 30, 2014
b19f6fc
Fixed linting errors
adamtheturtle Jul 1, 2014
f731c5b
Merge remote-tracking branch 'origin/master' into gear-links-150
adamtheturtle Jul 1, 2014
c444ef8
Grammar fix
adamtheturtle Jul 1, 2014
42ead91
Merge remote-tracking branch 'origin/master' into gear-links-150
wallrj Jul 2, 2014
f67ec33
Use clusterhq for maintainer details.
wallrj Jul 2, 2014
55e7211
Make the Docker run script timeout after a period of time.
wallrj Jul 2, 2014
d86ea75
The Dockerfile can be a template
wallrj Jul 2, 2014
26f1fa7
Implement a Dockerfile template feature which allows the host, port a…
wallrj Jul 2, 2014
1d2ff08
full stop
wallrj Jul 2, 2014
1e168f6
Remove unnecessary brackets
wallrj Jul 2, 2014
a9f21a1
Highlight Deferred etc in docstring. Add return docs
wallrj Jul 2, 2014
bbd97ca
A note about --force-rm
wallrj Jul 2, 2014
10dd47a
Document the motivation for ProtocolPoppingFactory
wallrj Jul 2, 2014
ecacef8
A note about localhost links
wallrj Jul 2, 2014
808920d
A note about the behaviour of geard loopback link forwarding behaviour.
wallrj Jul 2, 2014
dd5f3d6
A note about ugly (but useful) debug output
wallrj Jul 2, 2014
ef7d327
Remove unused DockerImage.remove method
wallrj Jul 2, 2014
31cf086
flake8 cleanup
wallrj Jul 2, 2014
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
4 changes: 4 additions & 0 deletions flocker/node/functional/docker/Dockerfile.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
FROM busybox
MAINTAINER ClusterHQ <[email protected]>
ADD . /
CMD ["/bin/sh", "-e", "run.sh", "{host}", "{port}", "{bytes}", "{timeout}"]
56 changes: 56 additions & 0 deletions flocker/node/functional/docker/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/bin/sh
set -e
help() {
cat <<EOF
Usage: run.sh [options] HOST PORT BYTES TIMEOUT

Send BYTES to HOST:PORT using a TCP connection.

Retry until a connection can be established or until the TIMEOUT period is
Copy link
Contributor

Choose a reason for hiding this comment

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

How is this better than just relying on trial's timeout?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It means that the error will be clearer if the test does fail and it means that the gear unit will fail in a timely manner and with a meaningful log entry if the test fails to clean it up for some reason.

reached.

This is the init script for the Docker container described in the neighbouring
Dockerfile.

Options:
--help: Print help.

EOF
}

while [[ $# -gt 0 ]]; do
case "$1" in
-h | --help)
help
exit 0
;;
--)
shift
break
;;
-*)
help >&2
echo "ERROR: Unknown option: $1" >&2
exit 1
;;
*)
break
;;
esac
done

HOST=${1:?"Error: Missing parameter 1:HOST"}
Copy link
Contributor

Choose a reason for hiding this comment

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

❓ In this case I guess I'm also missing parameters 2, 3, 4, right?

PORT=${2:?"Error: Missing parameter 2:PORT"}
BYTES=${3:?"Error: Missing parameter 3:BYTES"}
TIMEOUT=${4:?"Error: Missing parameter 3:TIMEOUT"}

start_time=$(date +"%s")
# Attempt to connect
# NB nc -w 10 means connection timeout after 10s
while ! echo -n "${BYTES}" | nc -w 10 "${HOST}" "${PORT}"; do
usleep 100000
if test "$(date +'%s')" -gt "$((start_time+${TIMEOUT}))"; then
echo "ERROR: unable to connect to after ${TIMEOUT} seconds." >&2
break
fi
done
82 changes: 75 additions & 7 deletions flocker/node/functional/test_gear.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@

from twisted.trial.unittest import TestCase
from twisted.python.procutils import which
from twisted.python.filepath import FilePath
from twisted.internet.defer import succeed
from twisted.internet.error import ConnectionRefusedError
from twisted.internet.endpoints import TCP4ServerEndpoint
from twisted.internet import reactor

from treq import request, content

from ...testtools import loop_until, find_free_port
from ...testtools import (
loop_until, find_free_port, make_capture_protocol,
ProtocolPoppingFactory, DockerImageBuilder)

from ..test.test_gear import make_igearclient_tests, random_name
from ..gear import GearClient, GearError, GEAR_PORT, PortMap

Expand Down Expand Up @@ -53,22 +59,31 @@ class GearClientTests(TestCase):
def setUp(self):
pass

def start_container(self, name, ports=None):
def start_container(self, unit_name,
image_name=u"openshift/busybox-http-app",
ports=None, links=None):
"""Start a unit and wait until it's up and running.

:param unicode name: The name of the unit.
:param unicode unit_name: See ``IGearClient.add``.
:param unicode image_name: See ``IGearClient.add``.
:param list ports: See ``IGearClient.add``.
:param list links: See ``IGearClient.add``.

:return: ``Deferred`` that fires with the ``GearClient`` when the unit
is running.
"""
client = GearClient("127.0.0.1")
d = client.add(name, u"openshift/busybox-http-app", ports=ports)
self.addCleanup(client.remove, name)
d = client.add(
unit_name=unit_name,
image_name=image_name,
ports=ports,
links=links,
)
self.addCleanup(client.remove, unit_name)

def is_started(units):
return [unit for unit in units if
(unit.name == name and
(unit.name == unit_name and
unit.activation_state == u"active")]

def check_if_started():
Expand Down Expand Up @@ -218,7 +233,8 @@ def test_add_with_port(self):
external_port = find_free_port()[1]
name = random_name()
d = self.start_container(
name, ports=[PortMap(internal=8080, external=external_port)])
name, ports=[PortMap(internal_port=8080,
external_port=external_port)])

d.addCallback(
lambda ignored: self.request_until_response(external_port))
Expand All @@ -229,3 +245,55 @@ def started(response):
return d
d.addCallback(started)
return d

def test_add_with_links(self):
Copy link
Contributor

Choose a reason for hiding this comment

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

This has lots of output in trial

Sending build context to Docker daemon 3.584 kB
Sending build context to Docker daemon 
Step 0 : FROM busybox
 ---> a9eb17255234
Step 1 : MAINTAINER Hybrid Logic <[email protected]>
 ---> Using cache
 ---> 89a17e0c1408
Step 2 : ADD . /
 ---> Using cache
 ---> bc57ffdd885f
Step 3 : CMD ["/bin/sh",  "-e", "run.sh"]
 ---> Using cache
 ---> a6a3d93b8df5
Successfully built a6a3d93b8df5

Also, after running this test (I think, maybe my modifications changed something) I have flocker/send_xxx_to_31337 visible when I run docker images. This should be removed (probably with the unused remove method below. This can be another issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added a comment and a link to #171

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was originally calling DockerImage.remove as a cleanup callback, but it was intermittently failing if the gear unit cleanup callback hadn't finished in time. That's set up in start_container but seems to return before gear has actually finished removing the Docker containers associated with the unit.
I've raised a ticket about possibly doing a global cleanup from tox before running the tests. See #195

"""
``GearClient.add`` accepts a links argument which sets up links between
container local ports and host local ports.
"""
internal_port = 31337
Copy link
Contributor

Choose a reason for hiding this comment

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

A comment that this is the same as the port in run.sh would make this more clear.
Perhaps a temporary Dockerfile could be made here using these variables.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I altered things so that the destination port can be supplied in the test.

expected_bytes = b'foo bar baz'
image_name = b'flocker/send_bytes_to'
# Create a Docker image
image = DockerImageBuilder(
source_dir=FilePath(
os.path.join(os.path.dirname(__file__), 'docker')),
tag=image_name,
working_dir=FilePath(self.mktemp())
)
image.build(
dockerfile_variables=dict(
host=b'127.0.0.1',
port=internal_port,
bytes=expected_bytes,
timeout=30
)
)

# This is the target of the proxy which will be created.
server = TCP4ServerEndpoint(reactor, 0)
capture_finished, protocol = make_capture_protocol()

def check_data(data):
self.assertEqual(expected_bytes, data)
capture_finished.addCallback(check_data)

factory = ProtocolPoppingFactory(protocols=[protocol])
d = server.listen(factory)

def start_container(port):
self.addCleanup(port.stopListening)
host_port = port.getHost().port
return self.start_container(
unit_name=random_name(),
image_name=image_name,
links=[PortMap(internal_port=internal_port,
external_port=host_port)]
)
d.addCallback(start_container)

def started(ignored):
return capture_finished
d.addCallback(started)

return d
56 changes: 47 additions & 9 deletions flocker/node/gear.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class Unit(object):
class IGearClient(Interface):
"""A client for the geard HTTP API."""

def add(unit_name, image_name, ports=None):
def add(unit_name, image_name, ports=None, links=None):
"""Install and start a new unit.

:param unicode unit_name: The name of the unit to create.
Expand All @@ -55,6 +55,9 @@ def add(unit_name, image_name, ports=None):
the container to ports exposed on the host. Default ``None`` means
that no port mappings will be configured for this unit.

:param list links: A list of ``PortMap``\ s mapping ports forwarded
from the container to ports on the host.

:return: ``Deferred`` that fires on success, or errbacks with
:class:`AlreadyExists` if a unit by that name already exists.
"""
Expand Down Expand Up @@ -133,6 +136,7 @@ def _request(self, method, path, data=None):
url = self._base_url + path
if data is not None:
data = json.dumps(data)

return request(method, url, data=data, persistent=False)

def _ensure_ok(self, response):
Expand All @@ -154,14 +158,45 @@ def _ensure_ok(self, response):
d.addCallback(lambda data: fail(GearError(response.code, data)))
return d

def add(self, unit_name, image_name, ports=None):
def add(self, unit_name, image_name, ports=None, links=None):
"""
See ``IGearClient.add`` for base documentation.

Gear `NetworkLinks` are currently fixed to destination localhost. This
allows us to control the actual target of the link using proxy / nat
rules on the host machine without having to restart the gear unit.

XXX: If gear allowed us to reconfigure links this wouldn't be
necessary. See https://github.com/openshift/geard/issues/223

XXX: As long as we need to set the target as 127.0.0.1 its also worth
noting that gear will actually route the traffic to a non-loopback
address on the host. So if your service or NAT rule on the host is
configured for 127.0.0.1 only, it won't receive any traffic. See
https://github.com/openshift/geard/issues/224
"""
if ports is None:
ports = []

data = {u"Image": image_name, u"Started": True, u'Ports': []}
if links is None:
Copy link
Contributor

Choose a reason for hiding this comment

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

Should links not just default to []?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you use mutable values as argument defaults there's a danger that they get mutated inside the function. Then the next time the function is called, the default will be the mutated value.

links = []

data = {
u"Image": image_name, u"Started": True, u'Ports': [],
u'NetworkLinks': []}

for port in ports:
data['Ports'].append(
{u'Internal': port.internal, u'External': port.external})
{u'Internal': port.internal_port,
u'External': port.external_port})

for link in links:
data['NetworkLinks'].append(
{u'FromHost': u'127.0.0.1',
u'FromPort': link.internal_port,
u'ToHost': u'127.0.0.1',
Copy link
Contributor

Choose a reason for hiding this comment

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

Comment here about why the behaviour when using 127.0.0.1 might not be as expected

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I created openshift/geard#223 and openshift/geard#224 to track these issues.

u'ToPort': link.external_port}
)

checked = self.exists(unit_name)
checked.addCallback(
Expand Down Expand Up @@ -212,15 +247,18 @@ class FakeGearClient(object):
def __init__(self):
self._units = {}

def add(self, unit_name, image_name, ports=None):
def add(self, unit_name, image_name, ports=None, links=None):
if ports is None:
ports = []
if links is None:
links = []
if unit_name in self._units:
return fail(AlreadyExists(unit_name))
self._units[unit_name] = {
'unit_name': unit_name,
'image_name': image_name,
'ports': ports
'ports': ports,
'links': links,
}
return succeed(None)

Expand All @@ -239,12 +277,12 @@ def list(self):
return succeed(result)


@attributes(['internal', 'external'])
@attributes(['internal_port', 'external_port'],)
class PortMap(object):
"""
A record representing the mapping between a port exposed internally by a
docker container and the corresponding external port on the host.

:ivar int internal: The port number exposed by the container.
:ivar int external: The port number exposed by the host
:ivar int internal_port: The port number exposed by the container.
:ivar int external_port: The port number exposed by the host.
"""
20 changes: 10 additions & 10 deletions flocker/node/test/test_gear.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ class PortMapInitTests(
make_with_init_tests(
record_type=PortMap,
kwargs=dict(
internal=1234,
external=5678,
internal_port=5678,
external_port=910,
)
)
):
Expand All @@ -162,8 +162,8 @@ def test_repr(self):
``PortMap.__repr__`` shows the internal and external ports.
"""
self.assertEqual(
"<PortMap(internal=1234, external=5678)>",
repr(PortMap(internal=1234, external=5678))
"<PortMap(internal_port=5678, external_port=910)>",
repr(PortMap(internal_port=5678, external_port=910))
)

def test_equal(self):
Expand All @@ -172,16 +172,16 @@ def test_equal(self):
equal.
"""
self.assertEqual(
PortMap(internal=1234, external=5678),
PortMap(internal=1234, external=5678)
PortMap(internal_port=5678, external_port=910),
PortMap(internal_port=5678, external_port=910),
)

def test_not_equal(self):
"""
``PortMap`` instances with the different internal and external ports
do not compare equal.
``PortMap`` instances with the different internal and external ports do
not compare equal.
"""
self.assertNotEqual(
PortMap(internal=5678, external=1234),
PortMap(internal=1234, external=5678)
PortMap(internal_port=5678, external_port=910),
PortMap(internal_port=1516, external_port=1718)
)
Copy link
Contributor

Choose a reason for hiding this comment

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

❓ Test that if just the internal ports are the same, or just the external ports are the same, PortMap instances do not compare equal.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See the comment in the testcase docstring about implementing more complete equality tests.

Loading