mirror of
https://github.com/docker/compose.git
synced 2026-02-14 04:29:29 +08:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d368e2fca9 | ||
|
|
b9c8e3e057 | ||
|
|
5d4210ceb3 | ||
|
|
4a6897ef3b | ||
|
|
fbff8983e4 | ||
|
|
6431d52a2e | ||
|
|
7faba11245 | ||
|
|
4723345473 | ||
|
|
b310516ba7 | ||
|
|
049a10136c | ||
|
|
511a9beede | ||
|
|
a8bc51b229 | ||
|
|
6bad9484be | ||
|
|
d52f73b29a | ||
|
|
edf8f14ac0 | ||
|
|
9faa7e47ed | ||
|
|
f92317c5ee | ||
|
|
fea3bc2eb1 | ||
|
|
2b89494405 | ||
|
|
2bac1c10b0 | ||
|
|
5126649de4 | ||
|
|
690a7d6dd7 | ||
|
|
19c0c9bf06 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
*.egg-info
|
||||
*.pyc
|
||||
/build
|
||||
/dist
|
||||
/docs/_site
|
||||
/docs/.git-gh-pages
|
||||
|
||||
@@ -4,6 +4,9 @@ python:
|
||||
- '2.7'
|
||||
- '3.2'
|
||||
- '3.3'
|
||||
env:
|
||||
- DOCKER_VERSION=0.7.6
|
||||
- DOCKER_VERSION=0.8.0
|
||||
matrix:
|
||||
allow_failures:
|
||||
- python: '3.2'
|
||||
|
||||
15
CHANGES.md
15
CHANGES.md
@@ -1,6 +1,21 @@
|
||||
Change log
|
||||
==========
|
||||
|
||||
0.2.2 (2014-02-17)
|
||||
------------------
|
||||
|
||||
- Resolve dependencies using Cormen/Tarjan topological sort
|
||||
- Fix `fig up` not printing log output
|
||||
- Stop containers in reverse order to starting
|
||||
- Fix scale command not binding ports
|
||||
|
||||
Thanks to @barnybug and @dustinlacewell for their work on this release.
|
||||
|
||||
0.2.1 (2014-02-04)
|
||||
------------------
|
||||
|
||||
- General improvements to error reporting (#77, #79)
|
||||
|
||||
0.2.0 (2014-01-31)
|
||||
------------------
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ Let's get a basic Python web app running on Fig. It assumes a little knowledge o
|
||||
|
||||
First, install Docker. If you're on OS X, you can use [docker-osx](https://github.com/noplay/docker-osx):
|
||||
|
||||
$ curl https://raw.github.com/noplay/docker-osx/master/docker-osx > /usr/local/bin/docker-osx
|
||||
$ curl https://raw.github.com/noplay/docker-osx/0.7.6/docker-osx > /usr/local/bin/docker-osx
|
||||
$ chmod +x /usr/local/bin/docker-osx
|
||||
$ docker-osx shell
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Installing Fig
|
||||
|
||||
First, install Docker. If you're on OS X, you can use [docker-osx](https://github.com/noplay/docker-osx):
|
||||
|
||||
$ curl https://raw.github.com/noplay/docker-osx/master/docker-osx > /usr/local/bin/docker-osx
|
||||
$ curl https://raw.github.com/noplay/docker-osx/0.7.6/docker-osx > /usr/local/bin/docker-osx
|
||||
$ chmod +x /usr/local/bin/docker-osx
|
||||
$ docker-osx shell
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from __future__ import unicode_literals
|
||||
from .service import Service
|
||||
|
||||
__version__ = '0.2.0'
|
||||
__version__ = '0.2.2'
|
||||
|
||||
@@ -7,11 +7,13 @@ import logging
|
||||
import os
|
||||
import re
|
||||
import yaml
|
||||
import six
|
||||
|
||||
from ..project import Project
|
||||
from ..service import ConfigError
|
||||
from .docopt_command import DocoptCommand
|
||||
from .formatter import Formatter
|
||||
from .utils import cached_property, docker_url
|
||||
from .utils import cached_property, docker_url, call_silently, is_mac, is_ubuntu
|
||||
from .errors import UserError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -23,7 +25,29 @@ class Command(DocoptCommand):
|
||||
try:
|
||||
super(Command, self).dispatch(*args, **kwargs)
|
||||
except ConnectionError:
|
||||
raise UserError("""
|
||||
if call_silently(['which', 'docker']) != 0:
|
||||
if is_mac():
|
||||
raise UserError("""
|
||||
Couldn't connect to Docker daemon. You might need to install docker-osx:
|
||||
|
||||
https://github.com/noplay/docker-osx
|
||||
""")
|
||||
elif is_ubuntu():
|
||||
raise UserError("""
|
||||
Couldn't connect to Docker daemon. You might need to install Docker:
|
||||
|
||||
http://docs.docker.io/en/latest/installation/ubuntulinux/
|
||||
""")
|
||||
else:
|
||||
raise UserError("""
|
||||
Couldn't connect to Docker daemon. You might need to install Docker:
|
||||
|
||||
http://docs.docker.io/en/latest/installation/
|
||||
""")
|
||||
elif call_silently(['which', 'docker-osx']) == 0:
|
||||
raise UserError("Couldn't connect to Docker daemon - you might need to run `docker-osx shell`.")
|
||||
else:
|
||||
raise UserError("""
|
||||
Couldn't connect to Docker daemon at %s - is it running?
|
||||
|
||||
If it's at a non-standard location, specify the URL with the DOCKER_HOST environment variable.
|
||||
@@ -47,7 +71,10 @@ If it's at a non-standard location, specify the URL with the DOCKER_HOST environ
|
||||
|
||||
exit(1)
|
||||
|
||||
return Project.from_config(self.project_name, config, self.client)
|
||||
try:
|
||||
return Project.from_config(self.project_name, config, self.client)
|
||||
except ConfigError as e:
|
||||
raise UserError(six.text_type(e))
|
||||
|
||||
@cached_property
|
||||
def project_name(self):
|
||||
|
||||
@@ -289,7 +289,7 @@ class TopLevelCommand(Command):
|
||||
if not detached:
|
||||
to_attach = [c for (s, c) in new]
|
||||
print("Attaching to", list_containers(to_attach))
|
||||
log_printer = LogPrinter(to_attach)
|
||||
log_printer = LogPrinter(to_attach, attach_params={"logs": True})
|
||||
|
||||
for (service, container) in new:
|
||||
service.start_container(container)
|
||||
|
||||
@@ -4,6 +4,8 @@ from __future__ import division
|
||||
import datetime
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import platform
|
||||
from .errors import UserError
|
||||
|
||||
|
||||
@@ -108,3 +110,19 @@ def split_buffer(reader, separator):
|
||||
|
||||
if len(buffered) > 0:
|
||||
yield buffered
|
||||
|
||||
|
||||
def call_silently(*args, **kwargs):
|
||||
"""
|
||||
Like subprocess.call(), but redirects stdout and stderr to /dev/null.
|
||||
"""
|
||||
with open(os.devnull, 'w') as shutup:
|
||||
return subprocess.call(*args, stdout=shutup, stderr=shutup, **kwargs)
|
||||
|
||||
|
||||
def is_mac():
|
||||
return platform.system() == 'Darwin'
|
||||
|
||||
|
||||
def is_ubuntu():
|
||||
return platform.system() == 'Linux' and platform.linux_distribution()[0] == 'Ubuntu'
|
||||
|
||||
@@ -7,31 +7,30 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def sort_service_dicts(services):
|
||||
# Get all services that are dependant on another.
|
||||
dependent_services = [s for s in services if s.get('links')]
|
||||
flatten_links = sum([s['links'] for s in dependent_services], [])
|
||||
# Get all services that are not linked to and don't link to others.
|
||||
non_dependent_sevices = [s for s in services if s['name'] not in flatten_links and not s.get('links')]
|
||||
# Topological sort (Cormen/Tarjan algorithm).
|
||||
unmarked = services[:]
|
||||
temporary_marked = set()
|
||||
sorted_services = []
|
||||
# Topological sort.
|
||||
while dependent_services:
|
||||
n = dependent_services.pop()
|
||||
# Check if a service is dependent on itself, if so raise an error.
|
||||
if n['name'] in n.get('links', []):
|
||||
raise DependencyError('A service can not link to itself: %s' % n['name'])
|
||||
sorted_services.append(n)
|
||||
for l in n['links']:
|
||||
# Get the linked service.
|
||||
linked_service = next(s for s in services if l == s['name'])
|
||||
# Check that there isn't a circular import between services.
|
||||
if n['name'] in linked_service.get('links', []):
|
||||
raise DependencyError('Circular import between %s and %s' % (n['name'], linked_service['name']))
|
||||
# Check the linked service has no links and is not already in the
|
||||
# sorted service list.
|
||||
if not linked_service.get('links') and linked_service not in sorted_services:
|
||||
sorted_services.insert(0, linked_service)
|
||||
return non_dependent_sevices + sorted_services
|
||||
|
||||
def visit(n):
|
||||
if n['name'] in temporary_marked:
|
||||
if n['name'] in n.get('links', []):
|
||||
raise DependencyError('A service can not link to itself: %s' % n['name'])
|
||||
else:
|
||||
raise DependencyError('Circular import between %s' % ' and '.join(temporary_marked))
|
||||
if n in unmarked:
|
||||
temporary_marked.add(n['name'])
|
||||
dependents = [m for m in services if n['name'] in m.get('links', [])]
|
||||
for m in dependents:
|
||||
visit(m)
|
||||
temporary_marked.remove(n['name'])
|
||||
unmarked.remove(n)
|
||||
sorted_services.insert(0, n)
|
||||
|
||||
while unmarked:
|
||||
visit(unmarked[-1])
|
||||
|
||||
return sorted_services
|
||||
|
||||
class Project(object):
|
||||
"""
|
||||
@@ -116,11 +115,11 @@ class Project(object):
|
||||
service.start(**options)
|
||||
|
||||
def stop(self, service_names=None, **options):
|
||||
for service in self.get_services(service_names):
|
||||
for service in reversed(self.get_services(service_names)):
|
||||
service.stop(**options)
|
||||
|
||||
def kill(self, service_names=None, **options):
|
||||
for service in self.get_services(service_names):
|
||||
for service in reversed(self.get_services(service_names)):
|
||||
service.kill(**options)
|
||||
|
||||
def build(self, service_names=None, **options):
|
||||
@@ -156,4 +155,4 @@ class DependencyError(Exception):
|
||||
self.msg = msg
|
||||
|
||||
def __str__(self):
|
||||
return self.msg
|
||||
return self.msg
|
||||
|
||||
@@ -10,6 +10,14 @@ from .container import Container
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
DOCKER_CONFIG_KEYS = ['image', 'command', 'hostname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'volumes_from', 'entrypoint']
|
||||
DOCKER_CONFIG_HINTS = {
|
||||
'link': 'links',
|
||||
'port': 'ports',
|
||||
'volume': 'volumes',
|
||||
}
|
||||
|
||||
|
||||
class BuildError(Exception):
|
||||
pass
|
||||
|
||||
@@ -18,14 +26,27 @@ class CannotBeScaledError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ConfigError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class Service(object):
|
||||
def __init__(self, name, client=None, project='default', links=[], **options):
|
||||
if not re.match('^[a-zA-Z0-9]+$', name):
|
||||
raise ValueError('Invalid name: %s' % name)
|
||||
raise ConfigError('Invalid name: %s' % name)
|
||||
if not re.match('^[a-zA-Z0-9]+$', project):
|
||||
raise ValueError('Invalid project: %s' % project)
|
||||
raise ConfigError('Invalid project: %s' % project)
|
||||
if 'image' in options and 'build' in options:
|
||||
raise ValueError('Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both.' % name)
|
||||
raise ConfigError('Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both.' % name)
|
||||
|
||||
supported_options = DOCKER_CONFIG_KEYS + ['build']
|
||||
|
||||
for k in options:
|
||||
if k not in supported_options:
|
||||
msg = "Unsupported config option for %s service: '%s'" % (name, k)
|
||||
if k in DOCKER_CONFIG_HINTS:
|
||||
msg += " (did you mean '%s'?)" % DOCKER_CONFIG_HINTS[k]
|
||||
raise ConfigError(msg)
|
||||
|
||||
self.name = name
|
||||
self.client = client
|
||||
@@ -90,7 +111,7 @@ class Service(object):
|
||||
while len(running_containers) < desired_num:
|
||||
c = stopped_containers.pop(0)
|
||||
log.info("Starting %s..." % c.name)
|
||||
c.start()
|
||||
self.start_container(c)
|
||||
running_containers.append(c)
|
||||
|
||||
|
||||
@@ -218,8 +239,7 @@ class Service(object):
|
||||
return links
|
||||
|
||||
def _get_container_options(self, override_options, one_off=False):
|
||||
keys = ['image', 'command', 'hostname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'volumes_from', 'entrypoint']
|
||||
container_options = dict((k, self.options[k]) for k in keys if k in self.options)
|
||||
container_options = dict((k, self.options[k]) for k in DOCKER_CONFIG_KEYS if k in self.options)
|
||||
container_options.update(override_options)
|
||||
|
||||
container_options['name'] = self.next_container_name(one_off)
|
||||
|
||||
@@ -7,7 +7,7 @@ sudo sh -c "echo deb http://get.docker.io/ubuntu docker main > /etc/apt/sources.
|
||||
sudo apt-get update
|
||||
echo exit 101 | sudo tee /usr/sbin/policy-rc.d
|
||||
sudo chmod +x /usr/sbin/policy-rc.d
|
||||
sudo apt-get install -qy slirp lxc lxc-docker-0.7.5
|
||||
sudo apt-get install -qy slirp lxc lxc-docker-$DOCKER_VERSION
|
||||
git clone git://github.com/jpetazzo/sekexe
|
||||
python setup.py install
|
||||
pip install -r requirements-dev.txt
|
||||
|
||||
@@ -1,30 +1,34 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from fig import Service
|
||||
from fig.service import CannotBeScaledError
|
||||
from fig.service import CannotBeScaledError, ConfigError
|
||||
from .testcases import DockerClientTestCase
|
||||
|
||||
|
||||
class ServiceTest(DockerClientTestCase):
|
||||
def test_name_validations(self):
|
||||
self.assertRaises(ValueError, lambda: Service(name=''))
|
||||
self.assertRaises(ConfigError, lambda: Service(name=''))
|
||||
|
||||
self.assertRaises(ValueError, lambda: Service(name=' '))
|
||||
self.assertRaises(ValueError, lambda: Service(name='/'))
|
||||
self.assertRaises(ValueError, lambda: Service(name='!'))
|
||||
self.assertRaises(ValueError, lambda: Service(name='\xe2'))
|
||||
self.assertRaises(ValueError, lambda: Service(name='_'))
|
||||
self.assertRaises(ValueError, lambda: Service(name='____'))
|
||||
self.assertRaises(ValueError, lambda: Service(name='foo_bar'))
|
||||
self.assertRaises(ValueError, lambda: Service(name='__foo_bar__'))
|
||||
self.assertRaises(ConfigError, lambda: Service(name=' '))
|
||||
self.assertRaises(ConfigError, lambda: Service(name='/'))
|
||||
self.assertRaises(ConfigError, lambda: Service(name='!'))
|
||||
self.assertRaises(ConfigError, lambda: Service(name='\xe2'))
|
||||
self.assertRaises(ConfigError, lambda: Service(name='_'))
|
||||
self.assertRaises(ConfigError, lambda: Service(name='____'))
|
||||
self.assertRaises(ConfigError, lambda: Service(name='foo_bar'))
|
||||
self.assertRaises(ConfigError, lambda: Service(name='__foo_bar__'))
|
||||
|
||||
Service('a')
|
||||
Service('foo')
|
||||
|
||||
def test_project_validation(self):
|
||||
self.assertRaises(ValueError, lambda: Service(name='foo', project='_'))
|
||||
self.assertRaises(ConfigError, lambda: Service(name='foo', project='_'))
|
||||
Service(name='foo', project='bar')
|
||||
|
||||
def test_config_validation(self):
|
||||
self.assertRaises(ConfigError, lambda: Service(name='foo', port=['8000']))
|
||||
Service(name='foo', ports=['8000'])
|
||||
|
||||
def test_containers(self):
|
||||
foo = self.create_service('foo')
|
||||
bar = self.create_service('bar')
|
||||
@@ -225,5 +229,12 @@ class ServiceTest(DockerClientTestCase):
|
||||
service = self.create_service('web', ports=['8000:8000'])
|
||||
self.assertRaises(CannotBeScaledError, lambda: service.scale(1))
|
||||
|
||||
def test_scale_sets_ports(self):
|
||||
service = self.create_service('web', ports=['8000'])
|
||||
service.scale(2)
|
||||
containers = service.containers()
|
||||
self.assertEqual(len(containers), 2)
|
||||
for container in containers:
|
||||
self.assertEqual(list(container.inspect()['HostConfig']['PortBindings'].keys()), ['8000/tcp'])
|
||||
|
||||
|
||||
|
||||
@@ -44,6 +44,27 @@ class SortServiceTest(unittest.TestCase):
|
||||
self.assertEqual(sorted_services[1]['name'], 'postgres')
|
||||
self.assertEqual(sorted_services[2]['name'], 'web')
|
||||
|
||||
def test_sort_service_dicts_3(self):
|
||||
services = [
|
||||
{
|
||||
'name': 'child'
|
||||
},
|
||||
{
|
||||
'name': 'parent',
|
||||
'links': ['child']
|
||||
},
|
||||
{
|
||||
'links': ['parent'],
|
||||
'name': 'grandparent'
|
||||
},
|
||||
]
|
||||
|
||||
sorted_services = sort_service_dicts(services)
|
||||
self.assertEqual(len(sorted_services), 3)
|
||||
self.assertEqual(sorted_services[0]['name'], 'child')
|
||||
self.assertEqual(sorted_services[1]['name'], 'parent')
|
||||
self.assertEqual(sorted_services[2]['name'], 'grandparent')
|
||||
|
||||
def test_sort_service_dicts_circular_imports(self):
|
||||
services = [
|
||||
{
|
||||
@@ -87,6 +108,30 @@ class SortServiceTest(unittest.TestCase):
|
||||
else:
|
||||
self.fail('Should have thrown an DependencyError')
|
||||
|
||||
def test_sort_service_dicts_circular_imports_3(self):
|
||||
services = [
|
||||
{
|
||||
'links': ['b'],
|
||||
'name': 'a'
|
||||
},
|
||||
{
|
||||
'name': 'b',
|
||||
'links': ['c']
|
||||
},
|
||||
{
|
||||
'name': 'c',
|
||||
'links': ['a']
|
||||
}
|
||||
]
|
||||
|
||||
try:
|
||||
sort_service_dicts(services)
|
||||
except DependencyError as e:
|
||||
self.assertIn('a', e.msg)
|
||||
self.assertIn('b', e.msg)
|
||||
else:
|
||||
self.fail('Should have thrown an DependencyError')
|
||||
|
||||
def test_sort_service_dicts_self_imports(self):
|
||||
services = [
|
||||
{
|
||||
|
||||
@@ -18,7 +18,7 @@ class DockerClientTestCase(unittest.TestCase):
|
||||
self.client.kill(c['Id'])
|
||||
self.client.remove_container(c['Id'])
|
||||
for i in self.client.images():
|
||||
if 'figtest' in i['Tag']:
|
||||
if isinstance(i['Tag'], basestring) and 'figtest' in i['Tag']:
|
||||
self.client.remove_image(i)
|
||||
|
||||
def create_service(self, name, **kwargs):
|
||||
|
||||
Reference in New Issue
Block a user