Add enum34 and use it to create a ConvergenceStrategy enum.

Signed-off-by: Daniel Nephin <dnephin@gmail.com>
This commit is contained in:
Daniel Nephin 2015-09-02 11:07:59 -04:00 committed by Daniel Nephin
parent d4372bc98f
commit 0484e22a84
11 changed files with 105 additions and 77 deletions

View File

@ -19,6 +19,7 @@ from ..progress_stream import StreamOutputError
from ..project import ConfigurationError from ..project import ConfigurationError
from ..project import NoSuchService from ..project import NoSuchService
from ..service import BuildError from ..service import BuildError
from ..service import ConvergenceStrategy
from ..service import NeedsBuildError from ..service import NeedsBuildError
from .command import Command from .command import Command
from .docopt_command import NoSuchCommand from .docopt_command import NoSuchCommand
@ -332,7 +333,7 @@ class TopLevelCommand(Command):
project.up( project.up(
service_names=deps, service_names=deps,
start_deps=True, start_deps=True,
allow_recreate=False, strategy=ConvergenceStrategy.never,
) )
tty = True tty = True
@ -515,29 +516,20 @@ class TopLevelCommand(Command):
if options['--allow-insecure-ssl']: if options['--allow-insecure-ssl']:
log.warn(INSECURE_SSL_WARNING) log.warn(INSECURE_SSL_WARNING)
detached = options['-d']
monochrome = options['--no-color'] monochrome = options['--no-color']
start_deps = not options['--no-deps'] start_deps = not options['--no-deps']
allow_recreate = not options['--no-recreate']
force_recreate = options['--force-recreate']
service_names = options['SERVICE'] service_names = options['SERVICE']
timeout = int(options.get('--timeout') or DEFAULT_TIMEOUT) timeout = int(options.get('--timeout') or DEFAULT_TIMEOUT)
if force_recreate and not allow_recreate:
raise UserError("--force-recreate and --no-recreate cannot be combined.")
to_attach = project.up( to_attach = project.up(
service_names=service_names, service_names=service_names,
start_deps=start_deps, start_deps=start_deps,
allow_recreate=allow_recreate, strategy=convergence_strategy_from_opts(options),
force_recreate=force_recreate,
do_build=not options['--no-build'], do_build=not options['--no-build'],
timeout=timeout timeout=timeout
) )
if not detached: if not options['-d']:
log_printer = build_log_printer(to_attach, service_names, monochrome) log_printer = build_log_printer(to_attach, service_names, monochrome)
attach_to_logs(project, log_printer, service_names, timeout) attach_to_logs(project, log_printer, service_names, timeout)
@ -582,6 +574,21 @@ class TopLevelCommand(Command):
print(get_version_info('full')) print(get_version_info('full'))
def convergence_strategy_from_opts(options):
no_recreate = options['--no-recreate']
force_recreate = options['--force-recreate']
if force_recreate and no_recreate:
raise UserError("--force-recreate and --no-recreate cannot be combined.")
if force_recreate:
return ConvergenceStrategy.always
if no_recreate:
return ConvergenceStrategy.never
return ConvergenceStrategy.changed
def build_log_printer(containers, service_names, monochrome): def build_log_printer(containers, service_names, monochrome):
if service_names: if service_names:
containers = [c for c in containers if c.service in service_names] containers = [c for c in containers if c.service in service_names]

View File

@ -15,6 +15,7 @@ from .const import LABEL_SERVICE
from .container import Container from .container import Container
from .legacy import check_for_legacy_containers from .legacy import check_for_legacy_containers
from .service import ContainerNet from .service import ContainerNet
from .service import ConvergenceStrategy
from .service import Net from .service import Net
from .service import Service from .service import Service
from .service import ServiceNet from .service import ServiceNet
@ -266,24 +267,16 @@ class Project(object):
def up(self, def up(self,
service_names=None, service_names=None,
start_deps=True, start_deps=True,
allow_recreate=True, strategy=ConvergenceStrategy.changed,
force_recreate=False,
do_build=True, do_build=True,
timeout=DEFAULT_TIMEOUT): timeout=DEFAULT_TIMEOUT):
if force_recreate and not allow_recreate:
raise ValueError("force_recreate and allow_recreate are in conflict")
services = self.get_services(service_names, include_deps=start_deps) services = self.get_services(service_names, include_deps=start_deps)
for service in services: for service in services:
service.remove_duplicate_containers() service.remove_duplicate_containers()
plans = self._get_convergence_plans( plans = self._get_convergence_plans(services, strategy)
services,
allow_recreate=allow_recreate,
force_recreate=force_recreate,
)
return [ return [
container container
@ -295,11 +288,7 @@ class Project(object):
) )
] ]
def _get_convergence_plans(self, def _get_convergence_plans(self, services, strategy):
services,
allow_recreate=True,
force_recreate=False):
plans = {} plans = {}
for service in services: for service in services:
@ -310,20 +299,13 @@ class Project(object):
and plans[name].action == 'recreate' and plans[name].action == 'recreate'
] ]
if updated_dependencies and allow_recreate: if updated_dependencies and strategy.allows_recreate:
log.debug( log.debug('%s has upstream changes (%s)',
'%s has upstream changes (%s)', service.name,
service.name, ", ".join(updated_dependencies), ", ".join(updated_dependencies))
) plan = service.convergence_plan(ConvergenceStrategy.always)
plan = service.convergence_plan(
allow_recreate=allow_recreate,
force_recreate=True,
)
else: else:
plan = service.convergence_plan( plan = service.convergence_plan(strategy)
allow_recreate=allow_recreate,
force_recreate=force_recreate,
)
plans[service.name] = plan plans[service.name] = plan

View File

@ -8,6 +8,7 @@ import sys
from collections import namedtuple from collections import namedtuple
from operator import attrgetter from operator import attrgetter
import enum
import six import six
from docker.errors import APIError from docker.errors import APIError
from docker.utils import create_host_config from docker.utils import create_host_config
@ -86,6 +87,20 @@ ServiceName = namedtuple('ServiceName', 'project service number')
ConvergencePlan = namedtuple('ConvergencePlan', 'action containers') ConvergencePlan = namedtuple('ConvergencePlan', 'action containers')
@enum.unique
class ConvergenceStrategy(enum.Enum):
"""Enumeration for all possible convergence strategies. Values refer to
when containers should be recreated.
"""
changed = 1
always = 2
never = 3
@property
def allows_recreate(self):
return self is not type(self).never
class Service(object): class Service(object):
def __init__( def __init__(
self, self,
@ -326,22 +341,19 @@ class Service(object):
else: else:
return self.options['image'] return self.options['image']
def convergence_plan(self, def convergence_plan(self, strategy=ConvergenceStrategy.changed):
allow_recreate=True,
force_recreate=False):
if force_recreate and not allow_recreate:
raise ValueError("force_recreate and allow_recreate are in conflict")
containers = self.containers(stopped=True) containers = self.containers(stopped=True)
if not containers: if not containers:
return ConvergencePlan('create', []) return ConvergencePlan('create', [])
if not allow_recreate: if strategy is ConvergenceStrategy.never:
return ConvergencePlan('start', containers) return ConvergencePlan('start', containers)
if force_recreate or self._containers_have_diverged(containers): if (
strategy is ConvergenceStrategy.always or
self._containers_have_diverged(containers)
):
return ConvergencePlan('recreate', containers) return ConvergencePlan('recreate', containers)
stopped = [c for c in containers if not c.is_running] stopped = [c for c in containers if not c.is_running]

View File

@ -206,7 +206,7 @@ At this point, you have seen the basics of how Compose works.
## Release Notes ## Release Notes
To see a detailed list of changes for past and current releases of Docker To see a detailed list of changes for past and current releases of Docker
Compose, please refer to the [CHANGELOG](https://github.com/docker/compose/blob/master/CHANGELOG.md). Compose, please refer to the [CHANGELOG](https://github.com/docker/compose/blob/master/CHANGELOG.md).
## Getting help ## Getting help

View File

@ -2,6 +2,7 @@ PyYAML==3.10
docker-py==1.3.1 docker-py==1.3.1
dockerpty==0.3.4 dockerpty==0.3.4
docopt==0.6.1 docopt==0.6.1
enum34==1.0.4
jsonschema==2.5.1 jsonschema==2.5.1
requests==2.7.0 requests==2.7.0
six==1.7.3 six==1.7.3

View File

@ -45,8 +45,9 @@ tests_require = [
] ]
if sys.version_info[:1] < (3,): if sys.version_info[:2] < (3, 4):
tests_require.append('mock >= 1.0.1') tests_require.append('mock >= 1.0.1')
install_requires.append('enum34 >= 1.0.4, < 2')
setup( setup(

View File

@ -223,7 +223,7 @@ class CLITestCase(DockerClientTestCase):
self.assertTrue(config['AttachStdin']) self.assertTrue(config['AttachStdin'])
@mock.patch('dockerpty.start') @mock.patch('dockerpty.start')
def test_run_service_with_links(self, __): def test_run_service_with_links(self, _):
self.command.base_dir = 'tests/fixtures/links-composefile' self.command.base_dir = 'tests/fixtures/links-composefile'
self.command.dispatch(['run', 'web', '/bin/true'], None) self.command.dispatch(['run', 'web', '/bin/true'], None)
db = self.project.get_service('db') db = self.project.get_service('db')
@ -232,14 +232,14 @@ class CLITestCase(DockerClientTestCase):
self.assertEqual(len(console.containers()), 0) self.assertEqual(len(console.containers()), 0)
@mock.patch('dockerpty.start') @mock.patch('dockerpty.start')
def test_run_with_no_deps(self, __): def test_run_with_no_deps(self, _):
self.command.base_dir = 'tests/fixtures/links-composefile' self.command.base_dir = 'tests/fixtures/links-composefile'
self.command.dispatch(['run', '--no-deps', 'web', '/bin/true'], None) self.command.dispatch(['run', '--no-deps', 'web', '/bin/true'], None)
db = self.project.get_service('db') db = self.project.get_service('db')
self.assertEqual(len(db.containers()), 0) self.assertEqual(len(db.containers()), 0)
@mock.patch('dockerpty.start') @mock.patch('dockerpty.start')
def test_run_does_not_recreate_linked_containers(self, __): def test_run_does_not_recreate_linked_containers(self, _):
self.command.base_dir = 'tests/fixtures/links-composefile' self.command.base_dir = 'tests/fixtures/links-composefile'
self.command.dispatch(['up', '-d', 'db'], None) self.command.dispatch(['up', '-d', 'db'], None)
db = self.project.get_service('db') db = self.project.get_service('db')

View File

@ -5,6 +5,7 @@ from compose import config
from compose.const import LABEL_PROJECT from compose.const import LABEL_PROJECT
from compose.container import Container from compose.container import Container
from compose.project import Project from compose.project import Project
from compose.service import ConvergenceStrategy
def build_service_dicts(service_config): def build_service_dicts(service_config):
@ -224,7 +225,7 @@ class ProjectTest(DockerClientTestCase):
old_db_id = project.containers()[0].id old_db_id = project.containers()[0].id
db_volume_path = project.containers()[0].get('Volumes./etc') db_volume_path = project.containers()[0].get('Volumes./etc')
project.up(force_recreate=True) project.up(strategy=ConvergenceStrategy.always)
self.assertEqual(len(project.containers()), 2) self.assertEqual(len(project.containers()), 2)
db_container = [c for c in project.containers() if 'db' in c.name][0] db_container = [c for c in project.containers() if 'db' in c.name][0]
@ -243,7 +244,7 @@ class ProjectTest(DockerClientTestCase):
old_db_id = project.containers()[0].id old_db_id = project.containers()[0].id
db_volume_path = project.containers()[0].inspect()['Volumes']['/var/db'] db_volume_path = project.containers()[0].inspect()['Volumes']['/var/db']
project.up(allow_recreate=False) project.up(strategy=ConvergenceStrategy.never)
self.assertEqual(len(project.containers()), 2) self.assertEqual(len(project.containers()), 2)
db_container = [c for c in project.containers() if 'db' in c.name][0] db_container = [c for c in project.containers() if 'db' in c.name][0]
@ -267,7 +268,7 @@ class ProjectTest(DockerClientTestCase):
old_db_id = old_containers[0].id old_db_id = old_containers[0].id
db_volume_path = old_containers[0].inspect()['Volumes']['/var/db'] db_volume_path = old_containers[0].inspect()['Volumes']['/var/db']
project.up(allow_recreate=False) project.up(strategy=ConvergenceStrategy.never)
new_containers = project.containers(stopped=True) new_containers = project.containers(stopped=True)
self.assertEqual(len(new_containers), 2) self.assertEqual(len(new_containers), 2)

View File

@ -4,6 +4,7 @@ from __future__ import unicode_literals
from .. import mock from .. import mock
from .testcases import DockerClientTestCase from .testcases import DockerClientTestCase
from compose.project import Project from compose.project import Project
from compose.service import ConvergenceStrategy
class ResilienceTest(DockerClientTestCase): class ResilienceTest(DockerClientTestCase):
@ -16,14 +17,14 @@ class ResilienceTest(DockerClientTestCase):
self.host_path = container.get('Volumes')['/var/db'] self.host_path = container.get('Volumes')['/var/db']
def test_successful_recreate(self): def test_successful_recreate(self):
self.project.up(force_recreate=True) self.project.up(strategy=ConvergenceStrategy.always)
container = self.db.containers()[0] container = self.db.containers()[0]
self.assertEqual(container.get('Volumes')['/var/db'], self.host_path) self.assertEqual(container.get('Volumes')['/var/db'], self.host_path)
def test_create_failure(self): def test_create_failure(self):
with mock.patch('compose.service.Service.create_container', crash): with mock.patch('compose.service.Service.create_container', crash):
with self.assertRaises(Crash): with self.assertRaises(Crash):
self.project.up(force_recreate=True) self.project.up(strategy=ConvergenceStrategy.always)
self.project.up() self.project.up()
container = self.db.containers()[0] container = self.db.containers()[0]
@ -32,7 +33,7 @@ class ResilienceTest(DockerClientTestCase):
def test_start_failure(self): def test_start_failure(self):
with mock.patch('compose.service.Service.start_container', crash): with mock.patch('compose.service.Service.start_container', crash):
with self.assertRaises(Crash): with self.assertRaises(Crash):
self.project.up(force_recreate=True) self.project.up(strategy=ConvergenceStrategy.always)
self.project.up() self.project.up()
container = self.db.containers()[0] container = self.db.containers()[0]

View File

@ -12,6 +12,7 @@ from .testcases import DockerClientTestCase
from compose import config from compose import config
from compose.const import LABEL_CONFIG_HASH from compose.const import LABEL_CONFIG_HASH
from compose.project import Project from compose.project import Project
from compose.service import ConvergenceStrategy
class ProjectTestCase(DockerClientTestCase): class ProjectTestCase(DockerClientTestCase):
@ -151,7 +152,9 @@ class ProjectWithDependenciesTest(ProjectTestCase):
old_containers = self.run_up(self.cfg) old_containers = self.run_up(self.cfg)
self.cfg['db']['environment'] = {'NEW_VAR': '1'} self.cfg['db']['environment'] = {'NEW_VAR': '1'}
new_containers = self.run_up(self.cfg, allow_recreate=False) new_containers = self.run_up(
self.cfg,
strategy=ConvergenceStrategy.never)
self.assertEqual(new_containers - old_containers, set()) self.assertEqual(new_containers - old_containers, set())
@ -175,23 +178,11 @@ class ProjectWithDependenciesTest(ProjectTestCase):
def converge(service, def converge(service,
allow_recreate=True, strategy=ConvergenceStrategy.changed,
force_recreate=False,
do_build=True): do_build=True):
""" """Create a converge plan from a strategy and execute the plan."""
If a container for this service doesn't exist, create and start one. If there are plan = service.convergence_plan(strategy)
any, stop them, create+start new ones, and remove the old containers. return service.execute_convergence_plan(plan, do_build=do_build, timeout=1)
"""
plan = service.convergence_plan(
allow_recreate=allow_recreate,
force_recreate=force_recreate,
)
return service.execute_convergence_plan(
plan,
do_build=do_build,
timeout=1,
)
class ServiceStateTest(DockerClientTestCase): class ServiceStateTest(DockerClientTestCase):

View File

@ -1,10 +1,13 @@
from __future__ import absolute_import from __future__ import absolute_import
from compose import container from compose import container
from compose.cli.errors import UserError
from compose.cli.log_printer import LogPrinter from compose.cli.log_printer import LogPrinter
from compose.cli.main import attach_to_logs from compose.cli.main import attach_to_logs
from compose.cli.main import build_log_printer from compose.cli.main import build_log_printer
from compose.cli.main import convergence_strategy_from_opts
from compose.project import Project from compose.project import Project
from compose.service import ConvergenceStrategy
from tests import mock from tests import mock
from tests import unittest from tests import unittest
@ -55,3 +58,32 @@ class CLIMainTestCase(unittest.TestCase):
project.stop.assert_called_once_with( project.stop.assert_called_once_with(
service_names=service_names, service_names=service_names,
timeout=timeout) timeout=timeout)
class ConvergeStrategyFromOptsTestCase(unittest.TestCase):
def test_invalid_opts(self):
options = {'--force-recreate': True, '--no-recreate': True}
with self.assertRaises(UserError):
convergence_strategy_from_opts(options)
def test_always(self):
options = {'--force-recreate': True, '--no-recreate': False}
self.assertEqual(
convergence_strategy_from_opts(options),
ConvergenceStrategy.always
)
def test_never(self):
options = {'--force-recreate': False, '--no-recreate': True}
self.assertEqual(
convergence_strategy_from_opts(options),
ConvergenceStrategy.never
)
def test_changed(self):
options = {'--force-recreate': False, '--no-recreate': False}
self.assertEqual(
convergence_strategy_from_opts(options),
ConvergenceStrategy.changed
)