Skip to content

Commit 056dbaa

Browse files
committed
Add a CLI command to view volumes.
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
1 parent 65ae22e commit 056dbaa

File tree

6 files changed

+106
-26
lines changed

6 files changed

+106
-26
lines changed

fig/cli/main.py

+40-23
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import print_function
22
from __future__ import unicode_literals
3+
import itertools
34
import logging
45
import sys
56
import re
@@ -15,7 +16,7 @@
1516
from .command import Command
1617
from .formatter import Formatter
1718
from .log_printer import LogPrinter
18-
from .utils import yesno
19+
from .utils import trim, yesno
1920

2021
from docker.errors import APIError
2122
from .errors import UserError
@@ -95,6 +96,7 @@ class TopLevelCommand(Command):
9596
stop Stop services
9697
restart Restart services
9798
up Create and start containers
99+
volumes List volumes used by containers
98100
99101
"""
100102
def docopt_options(self):
@@ -187,33 +189,23 @@ def ps(self, project, options):
187189
Options:
188190
-q Only display IDs
189191
"""
190-
containers = sorted(
191-
project.containers(service_names=options['SERVICE'], stopped=True) +
192-
project.containers(service_names=options['SERVICE'], one_off=True),
193-
key=attrgetter('name'))
192+
containers = get_sorted_containers(project, options['SERVICE'])
194193

195194
if options['-q']:
196195
for container in containers:
197196
print(container.id)
198-
else:
199-
headers = [
200-
'Name',
201-
'Command',
202-
'State',
203-
'Ports',
197+
return
198+
199+
def build_row(container):
200+
return [
201+
container.name,
202+
trim(container.human_readable_command, 30),
203+
container.human_readable_state,
204+
container.human_readable_ports,
204205
]
205-
rows = []
206-
for container in containers:
207-
command = container.human_readable_command
208-
if len(command) > 30:
209-
command = '%s ...' % command[:26]
210-
rows.append([
211-
container.name,
212-
command,
213-
container.human_readable_state,
214-
container.human_readable_ports,
215-
])
216-
print(Formatter().table(headers, rows))
206+
207+
headers = ['Name', 'Command', 'State', 'Ports']
208+
print(Formatter().table(headers, map(build_row, containers)))
217209

218210
def pull(self, project, options):
219211
"""
@@ -450,6 +442,31 @@ def handler(signal, frame):
450442
print("Gracefully stopping... (press Ctrl+C again to force)")
451443
project.stop(service_names=service_names)
452444

445+
def volumes(self, project, options):
446+
"""
447+
List volumes used by containers.
448+
449+
Usage: volumes [SERVICE...]
450+
"""
451+
containers = get_sorted_containers(project, options['SERVICE'])
452+
453+
def build_row(container):
454+
return [(container.name,) + volume for volume in container.volumes]
455+
456+
headers = ['Name', 'Path', 'Mode', 'Host']
457+
print(Formatter().table(headers, list(flat_map(build_row, containers))))
458+
459+
460+
def get_sorted_containers(project, services):
461+
return sorted(
462+
project.containers(service_names=services, stopped=True) +
463+
project.containers(service_names=services, one_off=True),
464+
key=attrgetter('name'))
465+
466+
467+
def flat_map(func, seq):
468+
return itertools.chain.from_iterable(map(func, seq))
469+
453470

454471
def list_containers(containers):
455472
return ", ".join(c.name for c in containers)

fig/cli/utils.py

+6
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ def prettydate(d):
5353
return '{0} hours ago'.format(s / 3600)
5454

5555

56+
def trim(source, length, extra=' ...'):
57+
if len(source) > length:
58+
return '%s%s' % (source[:length - len(extra)], extra)
59+
return source
60+
61+
5662
def mkdir(path, permissions=0o700):
5763
if not os.path.exists(path):
5864
os.mkdir(path)

fig/container.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
from __future__ import unicode_literals
22
from __future__ import absolute_import
3+
from collections import namedtuple
34

45
import six
56

67

8+
Volume = namedtuple('Volume', 'path mode host')
9+
10+
711
class Container(object):
812
"""
913
Represents a Docker container, constructed from the output of
@@ -66,7 +70,6 @@ def number(self):
6670

6771
@property
6872
def ports(self):
69-
self.inspect_if_not_inspected()
7073
return self.get('NetworkSettings.Ports') or {}
7174

7275
@property
@@ -101,6 +104,19 @@ def environment(self):
101104
def is_running(self):
102105
return self.get('State.Running')
103106

107+
@property
108+
def volumes(self):
109+
def get_mode(is_rw):
110+
if is_rw is None:
111+
return ''
112+
return 'rw' if is_rw else 'ro'
113+
114+
def get_volume(volume_item):
115+
path, host = volume_item
116+
return Volume(path, get_mode(self.get('VolumesRW').get(path)), host)
117+
118+
return map(get_volume, six.iteritems(self.get('Volumes')))
119+
104120
def get(self, key):
105121
"""Return a value from the container or None if the value is not set.
106122
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
one:
3+
image: busybox
4+
volumes:
5+
- '/etc'
6+
- '/home:/home:ro'
7+
8+
two:
9+
image: busybox
10+
volumes_from:
11+
- one

tests/integration/cli_test.py

+10
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,16 @@ def test_ps_alternate_figfile(self, mock_stdout):
6363
self.assertNotIn('multiplefigfiles_another_1', output)
6464
self.assertIn('multiplefigfiles_yetanother_1', output)
6565

66+
@patch('sys.stdout', new_callable=StringIO)
67+
def test_volumes(self, mock_stdout):
68+
self.command.base_dir = 'tests/fixtures/volumes-figfile'
69+
self.command.dispatch(['up', '-d'], None)
70+
self.command.dispatch(['volumes'], None)
71+
72+
output = mock_stdout.getvalue()
73+
self.assertIn('/etc', output)
74+
self.assertIn('/home', output)
75+
6676
@patch('fig.service.log')
6777
def test_pull(self, mock_logging):
6878
self.command.dispatch(['pull'], None)

tests/unit/container_test.py

+22-2
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@
44
import mock
55
import docker
66

7-
from fig.container import Container
7+
from fig.container import Container, Volume
88

99

1010
class ContainerTest(unittest.TestCase):
1111

12-
1312
def setUp(self):
1413
self.container_dict = {
1514
"Id": "abc",
@@ -106,6 +105,27 @@ def test_get_local_port(self):
106105
container.get_local_port(45454, protocol='tcp'),
107106
'0.0.0.0:49197')
108107

108+
def test_volumes(self):
109+
container = Container(None, {
110+
"Volumes": {
111+
"/sys": "/sys",
112+
"/var/lib/docker": "/var/lib/docker",
113+
"/etc": "/var/lib/docker/vfs/dir/531d0515",
114+
},
115+
"VolumesRW": {
116+
"/sys": False,
117+
"/var/lib/docker": True,
118+
"/etc": True,
119+
}
120+
}, has_been_inspected=True)
121+
self.assertEqual(
122+
sorted(container.volumes),
123+
[
124+
Volume('/etc', 'rw', '/var/lib/docker/vfs/dir/531d0515'),
125+
Volume('/sys', 'ro', '/sys'),
126+
Volume('/var/lib/docker', 'rw', '/var/lib/docker'),
127+
])
128+
109129
def test_get(self):
110130
container = Container(None, {
111131
"Status":"Up 8 seconds",

0 commit comments

Comments
 (0)