Skip to content

Fig port command #402

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

Merged
merged 1 commit into from
Sep 5, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ Force stop service containers.

View output from services.

## port

Print the public port for a port binding

## ps

List containers.
Expand Down
2 changes: 2 additions & 0 deletions fig/cli/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ def __init__(self, msg):
def __unicode__(self):
return self.msg

__str__ = __unicode__


class DockerNotFoundMac(UserError):
def __init__(self):
Expand Down
21 changes: 21 additions & 0 deletions fig/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ class TopLevelCommand(Command):
help Get help on a command
kill Kill containers
logs View output from containers
port Print the public port for a port binding
ps List containers
rm Remove stopped containers
run Run a one-off command
Expand Down Expand Up @@ -148,6 +149,26 @@ def logs(self, project, options):
print("Attaching to", list_containers(containers))
LogPrinter(containers, attach_params={'logs': True}, monochrome=monochrome).run()

def port(self, project, options):
"""
Print the public port for a port binding.

Usage: port [options] SERVICE PRIVATE_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['PRIVATE_PORT'],
protocol=options.get('--protocol') or 'tcp') or '')

def ps(self, project, options):
"""
List containers.
Expand Down
29 changes: 19 additions & 10 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,20 @@ 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 '{HostIp}:{HostPort}->{private}'.format(
private=private, **public[0])

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

@property
def human_readable_state(self):
Expand Down Expand Up @@ -105,6 +110,10 @@ def is_running(self):
self.inspect_if_not_inspected()
return self.dictionary['State']['Running']

def get_local_port(self, port, protocol='tcp'):
port = self.ports.get("%s/%s" % (port, protocol))
return "{HostIp}:{HostPort}".format(**port[0]) if port else None

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

Expand Down
15 changes: 14 additions & 1 deletion fig/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,22 @@ def has_container(self, container, one_off=False):
name = get_container_name(container)
if not name or not is_valid_name(name, one_off):
return False
project, name, number = parse_name(name)
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
Copy link

Choose a reason for hiding this comment

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

Is there a reason we're not just using self.containers() here, instead of manually filtering?

_, _, 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):
self.start_container_if_stopped(c, **options)
Expand Down
7 changes: 7 additions & 0 deletions tests/fixtures/ports-figfile/fig.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

simple:
image: busybox:latest
command: /bin/sleep 300
ports:
- '3000'
- '9999:3001'
22 changes: 19 additions & 3 deletions tests/integration/cli_test.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from __future__ import absolute_import
from .testcases import DockerClientTestCase
import sys

from fig.packages.six import StringIO
from mock import patch

from .testcases import DockerClientTestCase
from fig.cli.main import TopLevelCommand
from fig.packages.six import StringIO
import sys


class CLITestCase(DockerClientTestCase):
Expand Down Expand Up @@ -213,3 +215,17 @@ def test_scale(self):
self.command.scale(project, {'SERVICE=NUM': ['simple=0', 'another=0']})
self.assertEqual(len(project.get_service('simple').containers()), 0)
self.assertEqual(len(project.get_service('another').containers()), 0)

def test_port(self):
self.command.base_dir = 'tests/fixtures/ports-figfile'
self.command.dispatch(['up', '-d'], None)
container = self.project.get_service('simple').get_container()

@patch('sys.stdout', new_callable=StringIO)
def get_port(number, mock_stdout):
self.command.dispatch(['port', 'simple', str(number)], None)
return mock_stdout.getvalue().rstrip()

self.assertEqual(get_port(3000), container.get_local_port(3000))
self.assertEqual(get_port(3001), "0.0.0.0:9999")
self.assertEqual(get_port(3002), "")
88 changes: 54 additions & 34 deletions tests/unit/container_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,28 @@


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 @@ -42,35 +52,21 @@ 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):
Expand All @@ -85,3 +81,27 @@ def test_inspect_if_not_inspected(self):

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, 0.0.0.0: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'),
'0.0.0.0:49197')
25 changes: 23 additions & 2 deletions tests/unit/service_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from .. import unittest
import mock

from fig.packages import docker

from fig import Service
from fig.service import (
ConfigError,
Expand Down Expand Up @@ -97,14 +99,33 @@ 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)


class ServiceVolumesTest(unittest.TestCase):

Expand Down