Skip to content

Commit 8d3c9dc

Browse files
committed
Merge pull request #402 from dnephin/fig_ports.rebase
Fig port command
2 parents 2827786 + c48ee5c commit 8d3c9dc

9 files changed

Lines changed: 163 additions & 50 deletions

File tree

docs/cli.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ Force stop service containers.
2828

2929
View output from services.
3030

31+
## port
32+
33+
Print the public port for a port binding
34+
3135
## ps
3236

3337
List containers.

fig/cli/errors.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ def __init__(self, msg):
99
def __unicode__(self):
1010
return self.msg
1111

12+
__str__ = __unicode__
13+
1214

1315
class DockerNotFoundMac(UserError):
1416
def __init__(self):

fig/cli/main.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ class TopLevelCommand(Command):
8484
help Get help on a command
8585
kill Kill containers
8686
logs View output from containers
87+
port Print the public port for a port binding
8788
ps List containers
8889
rm Remove stopped containers
8990
run Run a one-off command
@@ -148,6 +149,26 @@ def logs(self, project, options):
148149
print("Attaching to", list_containers(containers))
149150
LogPrinter(containers, attach_params={'logs': True}, monochrome=monochrome).run()
150151

152+
def port(self, project, options):
153+
"""
154+
Print the public port for a port binding.
155+
156+
Usage: port [options] SERVICE PRIVATE_PORT
157+
158+
Options:
159+
--protocol=proto tcp or udp (defaults to tcp)
160+
--index=index index of the container if there are multiple
161+
instances of a service (defaults to 1)
162+
"""
163+
service = project.get_service(options['SERVICE'])
164+
try:
165+
container = service.get_container(number=options.get('--index') or 1)
166+
except ValueError as e:
167+
raise UserError(str(e))
168+
print(container.get_local_port(
169+
options['PRIVATE_PORT'],
170+
protocol=options.get('--protocol') or 'tcp') or '')
171+
151172
def ps(self, project, options):
152173
"""
153174
List containers.

fig/container.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from __future__ import unicode_literals
22
from __future__ import absolute_import
33

4+
from fig.packages import six
5+
46

57
class Container(object):
68
"""
@@ -63,17 +65,20 @@ def number(self):
6365
return None
6466

6567
@property
66-
def human_readable_ports(self):
68+
def ports(self):
6769
self.inspect_if_not_inspected()
68-
if not self.dictionary['NetworkSettings']['Ports']:
69-
return ''
70-
ports = []
71-
for private, public in list(self.dictionary['NetworkSettings']['Ports'].items()):
72-
if public:
73-
ports.append('%s->%s' % (public[0]['HostPort'], private))
74-
else:
75-
ports.append(private)
76-
return ', '.join(ports)
70+
return self.dictionary['NetworkSettings']['Ports'] or {}
71+
72+
@property
73+
def human_readable_ports(self):
74+
def format_port(private, public):
75+
if not public:
76+
return private
77+
return '{HostIp}:{HostPort}->{private}'.format(
78+
private=private, **public[0])
79+
80+
return ', '.join(format_port(*item)
81+
for item in sorted(six.iteritems(self.ports)))
7782

7883
@property
7984
def human_readable_state(self):
@@ -105,6 +110,10 @@ def is_running(self):
105110
self.inspect_if_not_inspected()
106111
return self.dictionary['State']['Running']
107112

113+
def get_local_port(self, port, protocol='tcp'):
114+
port = self.ports.get("%s/%s" % (port, protocol))
115+
return "{HostIp}:{HostPort}".format(**port[0]) if port else None
116+
108117
def start(self, **options):
109118
return self.client.start(self.id, **options)
110119

fig/service.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,22 @@ def has_container(self, container, one_off=False):
7878
name = get_container_name(container)
7979
if not name or not is_valid_name(name, one_off):
8080
return False
81-
project, name, number = parse_name(name)
81+
project, name, _number = parse_name(name)
8282
return project == self.project and name == self.name
8383

84+
def get_container(self, number=1):
85+
"""Return a :class:`fig.container.Container` for this service. The
86+
container must be active, and match `number`.
87+
"""
88+
for container in self.client.containers():
89+
if not self.has_container(container):
90+
continue
91+
_, _, container_number = parse_name(get_container_name(container))
92+
if container_number == number:
93+
return Container.from_ps(self.client, container)
94+
95+
raise ValueError("No container found for %s_%s" % (self.name, number))
96+
8497
def start(self, **options):
8598
for c in self.containers(stopped=True):
8699
self.start_container_if_stopped(c, **options)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
2+
simple:
3+
image: busybox:latest
4+
command: /bin/sleep 300
5+
ports:
6+
- '3000'
7+
- '9999:3001'

tests/integration/cli_test.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from __future__ import absolute_import
2-
from .testcases import DockerClientTestCase
2+
import sys
3+
4+
from fig.packages.six import StringIO
35
from mock import patch
6+
7+
from .testcases import DockerClientTestCase
48
from fig.cli.main import TopLevelCommand
5-
from fig.packages.six import StringIO
6-
import sys
79

810

911
class CLITestCase(DockerClientTestCase):
@@ -213,3 +215,17 @@ def test_scale(self):
213215
self.command.scale(project, {'SERVICE=NUM': ['simple=0', 'another=0']})
214216
self.assertEqual(len(project.get_service('simple').containers()), 0)
215217
self.assertEqual(len(project.get_service('another').containers()), 0)
218+
219+
def test_port(self):
220+
self.command.base_dir = 'tests/fixtures/ports-figfile'
221+
self.command.dispatch(['up', '-d'], None)
222+
container = self.project.get_service('simple').get_container()
223+
224+
@patch('sys.stdout', new_callable=StringIO)
225+
def get_port(number, mock_stdout):
226+
self.command.dispatch(['port', 'simple', str(number)], None)
227+
return mock_stdout.getvalue().rstrip()
228+
229+
self.assertEqual(get_port(3000), container.get_local_port(3000))
230+
self.assertEqual(get_port(3001), "0.0.0.0:9999")
231+
self.assertEqual(get_port(3002), "")

tests/unit/container_test.py

Lines changed: 54 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,28 @@
88

99

1010
class ContainerTest(unittest.TestCase):
11+
12+
13+
def setUp(self):
14+
self.container_dict = {
15+
"Id": "abc",
16+
"Image": "busybox:latest",
17+
"Command": "sleep 300",
18+
"Created": 1387384730,
19+
"Status": "Up 8 seconds",
20+
"Ports": None,
21+
"SizeRw": 0,
22+
"SizeRootFs": 0,
23+
"Names": ["/figtest_db_1"],
24+
"NetworkSettings": {
25+
"Ports": {},
26+
},
27+
}
28+
1129
def test_from_ps(self):
12-
container = Container.from_ps(None, {
13-
"Id":"abc",
14-
"Image":"busybox:latest",
15-
"Command":"sleep 300",
16-
"Created":1387384730,
17-
"Status":"Up 8 seconds",
18-
"Ports":None,
19-
"SizeRw":0,
20-
"SizeRootFs":0,
21-
"Names":["/figtest_db_1"]
22-
}, has_been_inspected=True)
30+
container = Container.from_ps(None,
31+
self.container_dict,
32+
has_been_inspected=True)
2333
self.assertEqual(container.dictionary, {
2434
"Id": "abc",
2535
"Image":"busybox:latest",
@@ -42,35 +52,21 @@ def test_environment(self):
4252
})
4353

4454
def test_number(self):
45-
container = Container.from_ps(None, {
46-
"Id":"abc",
47-
"Image":"busybox:latest",
48-
"Command":"sleep 300",
49-
"Created":1387384730,
50-
"Status":"Up 8 seconds",
51-
"Ports":None,
52-
"SizeRw":0,
53-
"SizeRootFs":0,
54-
"Names":["/figtest_db_1"]
55-
}, has_been_inspected=True)
55+
container = Container.from_ps(None,
56+
self.container_dict,
57+
has_been_inspected=True)
5658
self.assertEqual(container.number, 1)
5759

5860
def test_name(self):
59-
container = Container.from_ps(None, {
60-
"Id":"abc",
61-
"Image":"busybox:latest",
62-
"Command":"sleep 300",
63-
"Names":["/figtest_db_1"]
64-
}, has_been_inspected=True)
61+
container = Container.from_ps(None,
62+
self.container_dict,
63+
has_been_inspected=True)
6564
self.assertEqual(container.name, "figtest_db_1")
6665

6766
def test_name_without_project(self):
68-
container = Container.from_ps(None, {
69-
"Id":"abc",
70-
"Image":"busybox:latest",
71-
"Command":"sleep 300",
72-
"Names":["/figtest_db_1"]
73-
}, has_been_inspected=True)
67+
container = Container.from_ps(None,
68+
self.container_dict,
69+
has_been_inspected=True)
7470
self.assertEqual(container.name_without_project, "db_1")
7571

7672
def test_inspect_if_not_inspected(self):
@@ -85,3 +81,27 @@ def test_inspect_if_not_inspected(self):
8581

8682
container.inspect_if_not_inspected()
8783
self.assertEqual(mock_client.inspect_container.call_count, 1)
84+
85+
def test_human_readable_ports_none(self):
86+
container = Container(None, self.container_dict, has_been_inspected=True)
87+
self.assertEqual(container.human_readable_ports, '')
88+
89+
def test_human_readable_ports_public_and_private(self):
90+
self.container_dict['NetworkSettings']['Ports'].update({
91+
"45454/tcp": [ { "HostIp": "0.0.0.0", "HostPort": "49197" } ],
92+
"45453/tcp": [],
93+
})
94+
container = Container(None, self.container_dict, has_been_inspected=True)
95+
96+
expected = "45453/tcp, 0.0.0.0:49197->45454/tcp"
97+
self.assertEqual(container.human_readable_ports, expected)
98+
99+
def test_get_local_port(self):
100+
self.container_dict['NetworkSettings']['Ports'].update({
101+
"45454/tcp": [ { "HostIp": "0.0.0.0", "HostPort": "49197" } ],
102+
})
103+
container = Container(None, self.container_dict, has_been_inspected=True)
104+
105+
self.assertEqual(
106+
container.get_local_port(45454, protocol='tcp'),
107+
'0.0.0.0:49197')

tests/unit/service_test.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from .. import unittest
66
import mock
77

8+
from fig.packages import docker
9+
810
from fig import Service
911
from fig.service import (
1012
ConfigError,
@@ -97,14 +99,33 @@ def test_split_domainname_both(self):
9799

98100
def test_split_domainname_weird(self):
99101
service = Service('foo',
100-
hostname = 'name.sub',
101-
domainname = 'domain.tld',
102+
hostname='name.sub',
103+
domainname='domain.tld',
102104
)
103105
service.next_container_name = lambda x: 'foo'
104106
opts = service._get_container_create_options({})
105107
self.assertEqual(opts['hostname'], 'name.sub', 'hostname')
106108
self.assertEqual(opts['domainname'], 'domain.tld', 'domainname')
107109

110+
def test_get_container_not_found(self):
111+
mock_client = mock.create_autospec(docker.Client)
112+
mock_client.containers.return_value = []
113+
service = Service('foo', client=mock_client)
114+
115+
self.assertRaises(ValueError, service.get_container)
116+
117+
@mock.patch('fig.service.Container', autospec=True)
118+
def test_get_container(self, mock_container_class):
119+
mock_client = mock.create_autospec(docker.Client)
120+
container_dict = dict(Name='default_foo_2')
121+
mock_client.containers.return_value = [container_dict]
122+
service = Service('foo', client=mock_client)
123+
124+
container = service.get_container(number=2)
125+
self.assertEqual(container, mock_container_class.from_ps.return_value)
126+
mock_container_class.from_ps.assert_called_once_with(
127+
mock_client, container_dict)
128+
108129

109130
class ServiceVolumesTest(unittest.TestCase):
110131

0 commit comments

Comments
 (0)