diff --git a/plum/cli/main.py b/plum/cli/main.py index f4ba57666..56ef3e579 100644 --- a/plum/cli/main.py +++ b/plum/cli/main.py @@ -78,7 +78,7 @@ class TopLevelCommand(Command): Usage: ps """ for container in self._get_containers(all=False): - print get_container_name(container) + print container.name def run(self, options): """ @@ -126,4 +126,4 @@ class TopLevelCommand(Command): LogPrinter(client=self.client).attach(containers) def _get_containers(self, all): - return [c for s in self.service_collection for c in s.get_containers(all=all)] + return [c for s in self.service_collection for c in s.containers(all=all)] diff --git a/plum/container.py b/plum/container.py new file mode 100644 index 000000000..9ce6509df --- /dev/null +++ b/plum/container.py @@ -0,0 +1,86 @@ + + +class Container(object): + """ + Represents a Docker container, constructed from the output of + GET /containers/:id:/json. + """ + def __init__(self, client, dictionary, has_been_inspected=False): + self.client = client + self.dictionary = dictionary + self.has_been_inspected = has_been_inspected + + @classmethod + def from_ps(cls, client, dictionary, **kwargs): + """ + Construct a container object from the output of GET /containers/json. + """ + new_dictionary = { + 'ID': dictionary['Id'], + 'Image': dictionary['Image'], + } + for name in dictionary.get('Names', []): + if len(name.split('/')) == 2: + new_dictionary['Name'] = name + return cls(client, new_dictionary, **kwargs) + + @classmethod + def from_id(cls, client, id): + return cls(client, client.inspect_container(id)) + + @classmethod + def create(cls, client, **options): + response = client.create_container(**options) + return cls.from_id(client, response['Id']) + + @property + def id(self): + return self.dictionary['ID'] + + @property + def name(self): + return self.dictionary['Name'] + + @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 + + def start(self, **options): + return self.client.start(self.id, **options) + + def stop(self): + return self.client.stop(self.id) + + def kill(self): + return self.client.kill(self.id) + + def remove(self): + return self.client.remove_container(self.id) + + def inspect_if_not_inspected(self): + if not self.has_been_inspected: + self.inspect() + + def wait(self): + return self.client.wait(self.id) + + def logs(self, *args, **kwargs): + return self.client.logs(self.id, *args, **kwargs) + + def inspect(self): + self.dictionary = self.client.inspect_container(self.id) + return self.dictionary + + def links(self): + links = [] + for container in self.client.containers(): + for name in container['Names']: + bits = name.split('/') + if len(bits) > 2 and bits[1] == self.name[1:]: + links.append(bits[2]) + return links diff --git a/plum/service.py b/plum/service.py index b3fadbd9a..296470511 100644 --- a/plum/service.py +++ b/plum/service.py @@ -1,6 +1,7 @@ from docker.client import APIError import logging import re +from .container import Container log = logging.getLogger(__name__) @@ -21,28 +22,26 @@ class Service(object): self.links = links or [] self.options = options - @property - def containers(self): - return list(self.get_containers(all=True)) - - def get_containers(self, all): + def containers(self, all=False): + l = [] for container in self.client.containers(all=all): name = get_container_name(container) if is_valid_name(name) and parse_name(name)[0] == self.name: - yield container + l.append(Container.from_ps(self.client, container)) + return l def start(self): - if len(self.containers) == 0: + if len(self.containers()) == 0: return self.start_container() def stop(self): self.scale(0) def scale(self, num): - while len(self.containers) < num: + while len(self.containers()) < num: self.start_container() - while len(self.containers) > num: + while len(self.containers()) > num: self.stop_container() def create_container(self, **override_options): @@ -52,12 +51,12 @@ class Service(object): """ container_options = self._get_container_options(override_options) try: - return self.client.create_container(**container_options) + return Container.create(self.client, **container_options) except APIError, e: if e.response.status_code == 404 and e.explanation and 'No such image' in e.explanation: log.info('Pulling image %s...' % container_options['image']) self.client.pull(container_options['image']) - return self.client.create_container(**container_options) + return Container.create(self.client, **container_options) raise def start_container(self, container=None, **override_options): @@ -71,39 +70,32 @@ class Service(object): port_bindings[int(internal_port)] = int(external_port) else: port_bindings[int(port)] = None - log.info("Starting %s..." % container['Id']) - self.client.start( - container['Id'], + log.info("Starting %s..." % container.name) + container.start( links=self._get_links(), port_bindings=port_bindings, ) return container def stop_container(self): - container = self.containers[-1] - log.info("Stopping and removing %s..." % get_container_name(container)) - self.client.kill(container) - self.client.remove_container(container) + container = self.containers()[-1] + log.info("Stopping and removing %s..." % container.name) + container.kill() + container.remove() def next_container_number(self): - numbers = [parse_name(get_container_name(c))[1] for c in self.containers] + numbers = [parse_name(c.name)[1] for c in self.containers(all=True)] if len(numbers) == 0: return 1 else: return max(numbers) + 1 - def get_names(self): - return [get_container_name(c) for c in self.containers] - - def inspect(self): - return [self.client.inspect_container(c['Id']) for c in self.containers] - def _get_links(self): links = {} for service in self.links: - for name in service.get_names(): - links[name] = name + for container in service.containers(): + links[container.name[1:]] = container.name[1:] return links def _get_container_options(self, override_options): diff --git a/tests/container_test.py b/tests/container_test.py new file mode 100644 index 000000000..8628b04d8 --- /dev/null +++ b/tests/container_test.py @@ -0,0 +1,36 @@ +from .testcases import DockerClientTestCase +from plum.container import Container + +class ContainerTest(DockerClientTestCase): + def test_from_ps(self): + container = Container.from_ps(self.client, { + "Id":"abc", + "Image":"ubuntu:12.04", + "Command":"sleep 300", + "Created":1387384730, + "Status":"Up 8 seconds", + "Ports":None, + "SizeRw":0, + "SizeRootFs":0, + "Names":["/db_1"] + }, has_been_inspected=True) + self.assertEqual(container.dictionary, { + "ID": "abc", + "Image":"ubuntu:12.04", + "Name": "/db_1", + }) + + def test_environment(self): + container = Container(self.client, { + 'ID': 'abc', + 'Config': { + 'Env': [ + 'FOO=BAR', + 'BAZ=DOGE', + ] + } + }, has_been_inspected=True) + self.assertEqual(container.environment, { + 'FOO': 'BAR', + 'BAZ': 'DOGE', + }) diff --git a/tests/service_collection_test.py b/tests/service_collection_test.py index 1c764ecac..7dbffdcee 100644 --- a/tests/service_collection_test.py +++ b/tests/service_collection_test.py @@ -50,13 +50,13 @@ class ServiceCollectionTest(DockerClientTestCase): collection.start() - self.assertEqual(len(collection[0].containers), 1) - self.assertEqual(len(collection[1].containers), 1) + self.assertEqual(len(collection[0].containers()), 1) + self.assertEqual(len(collection[1].containers()), 1) collection.stop() - self.assertEqual(len(collection[0].containers), 0) - self.assertEqual(len(collection[1].containers), 0) + self.assertEqual(len(collection[0].containers()), 0) + self.assertEqual(len(collection[1].containers()), 0) diff --git a/tests/service_test.py b/tests/service_test.py index 321f550bb..62d53e563 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -24,57 +24,57 @@ class ServiceTest(DockerClientTestCase): foo.start() - self.assertEqual(len(foo.containers), 1) - self.assertEqual(foo.containers[0]['Names'], ['/foo_1']) - self.assertEqual(len(bar.containers), 0) + self.assertEqual(len(foo.containers()), 1) + self.assertEqual(foo.containers()[0].name, '/foo_1') + self.assertEqual(len(bar.containers()), 0) bar.scale(2) - self.assertEqual(len(foo.containers), 1) - self.assertEqual(len(bar.containers), 2) + self.assertEqual(len(foo.containers()), 1) + self.assertEqual(len(bar.containers()), 2) - names = [c['Names'] for c in bar.containers] - self.assertIn(['/bar_1'], names) - self.assertIn(['/bar_2'], names) + names = [c.name for c in bar.containers()] + self.assertIn('/bar_1', names) + self.assertIn('/bar_2', names) def test_up_scale_down(self): service = self.create_service('scaling_test') - self.assertEqual(len(service.containers), 0) + self.assertEqual(len(service.containers()), 0) service.start() - self.assertEqual(len(service.containers), 1) + self.assertEqual(len(service.containers()), 1) service.start() - self.assertEqual(len(service.containers), 1) + self.assertEqual(len(service.containers()), 1) service.scale(2) - self.assertEqual(len(service.containers), 2) + self.assertEqual(len(service.containers()), 2) service.scale(1) - self.assertEqual(len(service.containers), 1) + self.assertEqual(len(service.containers()), 1) service.stop() - self.assertEqual(len(service.containers), 0) + self.assertEqual(len(service.containers()), 0) service.stop() - self.assertEqual(len(service.containers), 0) + self.assertEqual(len(service.containers()), 0) def test_start_container_passes_through_options(self): db = self.create_service('db') db.start_container(environment={'FOO': 'BAR'}) - self.assertEqual(db.inspect()[0]['Config']['Env'], ['FOO=BAR']) + self.assertEqual(db.containers()[0].environment['FOO'], 'BAR') def test_start_container_inherits_options_from_constructor(self): db = self.create_service('db', environment={'FOO': 'BAR'}) db.start_container() - self.assertEqual(db.inspect()[0]['Config']['Env'], ['FOO=BAR']) + self.assertEqual(db.containers()[0].environment['FOO'], 'BAR') def test_start_container_creates_links(self): db = self.create_service('db') web = self.create_service('web', links=[db]) db.start_container() web.start_container() - self.assertIn('/web_1/db_1', db.containers[0]['Names']) + self.assertIn('db_1', web.containers()[0].links()) db.stop() web.stop() @@ -85,20 +85,18 @@ class ServiceTest(DockerClientTestCase): build='tests/fixtures/simple-dockerfile', ) container = service.start() - self.client.wait(container) - self.assertIn('success', self.client.logs(container)) + container.wait() + self.assertIn('success', container.logs()) def test_start_container_creates_ports(self): service = self.create_service('web', ports=[8000]) - service.start_container() - container = service.inspect()[0] + container = service.start_container().inspect() self.assertIn('8000/tcp', container['HostConfig']['PortBindings']) self.assertNotEqual(container['HostConfig']['PortBindings']['8000/tcp'][0]['HostPort'], '8000') def test_start_container_creates_fixed_external_ports(self): service = self.create_service('web', ports=['8000:8000']) - service.start_container() - container = service.inspect()[0] + container = service.start_container().inspect() self.assertIn('8000/tcp', container['HostConfig']['PortBindings']) self.assertEqual(container['HostConfig']['PortBindings']['8000/tcp'][0]['HostPort'], '8000')