Skip to content

Commit

Permalink
Merge pull request #2 from dnephin/fig_ports
Browse files Browse the repository at this point in the history
fig local_port
  • Loading branch information
dnephin committed Aug 12, 2014
2 parents 8a31619 + 2bd26b3 commit 54be28b
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 73 deletions.
20 changes: 20 additions & 0 deletions fig/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,26 @@ def logs(self, project, options):
print("Attaching to", list_containers(containers))
LogPrinter(containers, attach_params={'logs': True}, monochrome=monochrome).run()

def local_port(self, project, options):
"""
Print the local port for a port binding.
Usage: local_port [options] SERVICE PORT
Options:
--protocol=proto tcp or udp (defaults to tcp)
--index=index index of the container if there are multiple
instances of a service (defaults to 1)
"""
service = project.get_service(options['SERVICE'])
try:
container = service.get_container(number=options.get('--index') or 1)
except ValueError as e:
raise UserError(str(e))
print(container.get_local_port(
options['PORT'],
protocol=options.get('--protocol') or 'tcp') or '')

def ps(self, project, options):
"""
List containers.
Expand Down
39 changes: 24 additions & 15 deletions fig/container.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from __future__ import unicode_literals
from __future__ import absolute_import

from fig.packages import six


class Container(object):
"""
Expand Down Expand Up @@ -63,17 +65,19 @@ def number(self):
return None

@property
def human_readable_ports(self):
def ports(self):
self.inspect_if_not_inspected()
if not self.dictionary['NetworkSettings']['Ports']:
return ''
ports = []
for private, public in list(self.dictionary['NetworkSettings']['Ports'].items()):
if public:
ports.append('%s->%s' % (public[0]['HostPort'], private))
else:
ports.append(private)
return ', '.join(ports)
return self.dictionary['NetworkSettings']['Ports'] or {}

@property
def human_readable_ports(self):
def format_port(private, public):
if not public:
return private
return '%s->%s' % (public[0]['HostPort'], private)

return ', '.join(format_port(*item)
for item in sorted(six.iteritems(self.ports)))

@property
def human_readable_state(self):
Expand All @@ -97,17 +101,21 @@ def human_readable_command(self):
@property
def environment(self):
self.inspect_if_not_inspected()
out = {}
for var in self.dictionary.get('Config', {}).get('Env', []):
k, v = var.split('=', 1)
out[k] = v
return out
return dict(var.split("=", 1)
for var in self.dictionary.get('Config', {}).get('Env', []))

@property
def is_running(self):
self.inspect_if_not_inspected()
return self.dictionary['State']['Running']

def get_local_port(self, port, protocol='tcp'):
self.inspect_if_not_inspected()
port = self.ports.get("%s/%s" % (port, protocol))
if not port:
return None
return int(port[0]['HostPort'])

def start(self, **options):
return self.client.start(self.id, **options)

Expand All @@ -132,6 +140,7 @@ def logs(self, *args, **kwargs):

def inspect(self):
self.dictionary = self.client.inspect_container(self.id)
self.has_been_inspected = True
return self.dictionary

def links(self):
Expand Down
12 changes: 6 additions & 6 deletions fig/project.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import unicode_literals
from __future__ import absolute_import
import logging

from .service import Service
from .container import Container
from docker.errors import APIError
Expand Down Expand Up @@ -179,12 +180,11 @@ def remove_stopped(self, service_names=None, **options):
for service in self.get_services(service_names):
service.remove_stopped(**options)

def containers(self, service_names=None, *args, **kwargs):
l = []
for service in self.get_services(service_names):
for container in service.containers(*args, **kwargs):
l.append(container)
return l
def containers(self, service_names=None, stopped=False, one_off=False):
return [Container.from_ps(self.client, container)
for container in self.client.containers(all=stopped)
for service in self.get_services(service_names)
if service.has_container(container, one_off=one_off)]

def _inject_links(self, acc, service):
linked_names = service.get_linked_names()
Expand Down
31 changes: 23 additions & 8 deletions fig/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,30 @@ def __init__(self, name, client=None, project='default', links=None, volumes_fro
self.options = options

def containers(self, stopped=False, one_off=False):
l = []
for container in self.client.containers(all=stopped):
name = get_container_name(container)
if not name or not is_valid_name(name, one_off):
return [Container.from_ps(self.client, container)
for container in self.client.containers(all=stopped)
if self.has_container(container, one_off=one_off)]

def has_container(self, container, one_off=False):
"""Return True if `container` was created to fulfill this service."""
name = get_container_name(container)
if not name or not is_valid_name(name, one_off):
return False
project, name, _number = parse_name(name)
return project == self.project and name == self.name

def get_container(self, number=1):
"""Return a :class:`fig.container.Container` for this service. The
container must be active, and match `number`.
"""
for container in self.client.containers():
if not self.has_container(container):
continue
project, name, number = parse_name(name)
if project == self.project and name == self.name:
l.append(Container.from_ps(self.client, container))
return l
_, _, container_number = parse_name(get_container_name(container))
if container_number == number:
return Container.from_ps(self.client, container)

raise ValueError("No container found for %s_%s" % (self.name, number))

def start(self, **options):
for c in self.containers(stopped=True):
Expand Down
15 changes: 9 additions & 6 deletions tests/integration/project_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,9 @@ def test_project_up_recreates_containers(self):
self.assertEqual(len(project.containers()), 2)

db_container = [c for c in project.containers() if 'db' in c.name][0]
self.assertNotEqual(c.id, old_db_id)
self.assertEqual(c.inspect()['Volumes']['/var/db'], db_volume_path)
self.assertNotEqual(db_container.id, old_db_id)
self.assertEqual(db_container.inspect()['Volumes']['/var/db'],
db_volume_path)

project.kill()
project.remove_stopped()
Expand All @@ -130,8 +131,9 @@ def test_project_up_with_no_recreate_running(self):
self.assertEqual(len(project.containers()), 2)

db_container = [c for c in project.containers() if 'db' in c.name][0]
self.assertEqual(c.id, old_db_id)
self.assertEqual(c.inspect()['Volumes']['/var/db'], db_volume_path)
self.assertEqual(db_container.id, old_db_id)
self.assertEqual(db_container.inspect()['Volumes']['/var/db'],
db_volume_path)

project.kill()
project.remove_stopped()
Expand All @@ -158,8 +160,9 @@ def test_project_up_with_no_recreate_stopped(self):
self.assertEqual(len(new_containers), 2)

db_container = [c for c in new_containers if 'db' in c.name][0]
self.assertEqual(c.id, old_db_id)
self.assertEqual(c.inspect()['Volumes']['/var/db'], db_volume_path)
self.assertEqual(db_container.id, old_db_id)
self.assertEqual(db_container.inspect()['Volumes']['/var/db'],
db_volume_path)

project.kill()
project.remove_stopped()
Expand Down
8 changes: 6 additions & 2 deletions tests/integration/service_test.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from __future__ import unicode_literals
from __future__ import absolute_import
import os

from fig import Service
from fig.service import CannotBeScaledError
from fig.container import Container
from docker.errors import APIError
from .testcases import DockerClientTestCase
import os


class ServiceTest(DockerClientTestCase):
def test_containers(self):
Expand Down Expand Up @@ -143,7 +145,9 @@ def test_recreate_containers(self):

self.assertEqual(len(self.client.containers(all=True)), num_containers_before)
self.assertNotEqual(old_container.id, new_container.id)
self.assertRaises(APIError, lambda: self.client.inspect_container(intermediate_container.id))
self.assertRaises(APIError,
self.client.inspect_container,
intermediate_container.id)

def test_recreate_containers_when_containers_are_stopped(self):
service = self.create_service(
Expand Down
104 changes: 70 additions & 34 deletions tests/unit/container_test.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,35 @@
from __future__ import unicode_literals
from .. import unittest

import mock
from fig.packages import docker

from fig.container import Container


class ContainerTest(unittest.TestCase):


def setUp(self):
self.container_dict = {
"Id": "abc",
"Image": "busybox:latest",
"Command": "sleep 300",
"Created": 1387384730,
"Status": "Up 8 seconds",
"Ports": None,
"SizeRw": 0,
"SizeRootFs": 0,
"Names": ["/figtest_db_1"],
"NetworkSettings": {
"Ports": {},
},
}

def test_from_ps(self):
container = Container.from_ps(None, {
"Id":"abc",
"Image":"busybox:latest",
"Command":"sleep 300",
"Created":1387384730,
"Status":"Up 8 seconds",
"Ports":None,
"SizeRw":0,
"SizeRootFs":0,
"Names":["/figtest_db_1"]
}, has_been_inspected=True)
container = Container.from_ps(None,
self.container_dict,
has_been_inspected=True)
self.assertEqual(container.dictionary, {
"Id": "abc",
"Image":"busybox:latest",
Expand All @@ -37,33 +52,54 @@ def test_environment(self):
})

def test_number(self):
container = Container.from_ps(None, {
"Id":"abc",
"Image":"busybox:latest",
"Command":"sleep 300",
"Created":1387384730,
"Status":"Up 8 seconds",
"Ports":None,
"SizeRw":0,
"SizeRootFs":0,
"Names":["/figtest_db_1"]
}, has_been_inspected=True)
container = Container.from_ps(None,
self.container_dict,
has_been_inspected=True)
self.assertEqual(container.number, 1)

def test_name(self):
container = Container.from_ps(None, {
"Id":"abc",
"Image":"busybox:latest",
"Command":"sleep 300",
"Names":["/figtest_db_1"]
}, has_been_inspected=True)
container = Container.from_ps(None,
self.container_dict,
has_been_inspected=True)
self.assertEqual(container.name, "figtest_db_1")

def test_name_without_project(self):
container = Container.from_ps(None, {
"Id":"abc",
"Image":"busybox:latest",
"Command":"sleep 300",
"Names":["/figtest_db_1"]
}, has_been_inspected=True)
container = Container.from_ps(None,
self.container_dict,
has_been_inspected=True)
self.assertEqual(container.name_without_project, "db_1")

def test_inspect_if_not_inspected(self):
mock_client = mock.create_autospec(docker.Client)
container = Container(mock_client, dict(Id="the_id"))

container.inspect_if_not_inspected()
mock_client.inspect_container.assert_called_once_with("the_id")
self.assertEqual(container.dictionary,
mock_client.inspect_container.return_value)
self.assertTrue(container.has_been_inspected)

container.inspect_if_not_inspected()
self.assertEqual(mock_client.inspect_container.call_count, 1)

def test_human_readable_ports_none(self):
container = Container(None, self.container_dict, has_been_inspected=True)
self.assertEqual(container.human_readable_ports, '')

def test_human_readable_ports_public_and_private(self):
self.container_dict['NetworkSettings']['Ports'].update({
"45454/tcp": [ { "HostIp": "0.0.0.0", "HostPort": "49197" } ],
"45453/tcp": [],
})
container = Container(None, self.container_dict, has_been_inspected=True)

expected = "45453/tcp, 49197->45454/tcp"
self.assertEqual(container.human_readable_ports, expected)

def test_get_local_port(self):
self.container_dict['NetworkSettings']['Ports'].update({
"45454/tcp": [ { "HostIp": "0.0.0.0", "HostPort": "49197" } ],
})
container = Container(None, self.container_dict, has_been_inspected=True)

self.assertEqual(container.get_local_port(45454, protocol='tcp'), 49197)
28 changes: 26 additions & 2 deletions tests/unit/service_test.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
from __future__ import unicode_literals
from __future__ import absolute_import
from .. import unittest

import mock
from fig.packages import docker

from fig import Service
from fig.service import ConfigError, split_port


class ServiceTest(unittest.TestCase):
def test_name_validations(self):
self.assertRaises(ConfigError, lambda: Service(name=''))
Expand Down Expand Up @@ -75,10 +80,29 @@ def test_split_domainname_both(self):

def test_split_domainname_weird(self):
service = Service('foo',
hostname = 'name.sub',
domainname = 'domain.tld',
hostname='name.sub',
domainname='domain.tld',
)
service.next_container_name = lambda x: 'foo'
opts = service._get_container_create_options({})
self.assertEqual(opts['hostname'], 'name.sub', 'hostname')
self.assertEqual(opts['domainname'], 'domain.tld', 'domainname')

def test_get_container_not_found(self):
mock_client = mock.create_autospec(docker.Client)
mock_client.containers.return_value = []
service = Service('foo', client=mock_client)

self.assertRaises(ValueError, service.get_container)

@mock.patch('fig.service.Container', autospec=True)
def test_get_container(self, mock_container_class):
mock_client = mock.create_autospec(docker.Client)
container_dict = dict(Name='default_foo_2')
mock_client.containers.return_value = [container_dict]
service = Service('foo', client=mock_client)

container = service.get_container(number=2)
self.assertEqual(container, mock_container_class.from_ps.return_value)
mock_container_class.from_ps.assert_called_once_with(
mock_client, container_dict)

0 comments on commit 54be28b

Please sign in to comment.