瀏覽代碼

Add --build flag for up and create

Also adds a warning when up builds an image without the --build flag
so that users know it wont happen on the next up.

Signed-off-by: Daniel Nephin <[email protected]>
Daniel Nephin 9 年之前
父節點
當前提交
b7fb3a6d9b
共有 4 個文件被更改,包括 89 次插入20 次删除
  1. 20 4
      compose/cli/main.py
  2. 8 2
      compose/project.py
  3. 27 10
      compose/service.py
  4. 34 4
      tests/unit/service_test.py

+ 20 - 4
compose/cli/main.py

@@ -25,6 +25,7 @@ from ..const import HTTP_TIMEOUT
 from ..const import IS_WINDOWS_PLATFORM
 from ..progress_stream import StreamOutputError
 from ..project import NoSuchService
+from ..service import BuildAction
 from ..service import BuildError
 from ..service import ConvergenceStrategy
 from ..service import ImageType
@@ -249,14 +250,15 @@ class TopLevelCommand(DocoptCommand):
                                    image haven't changed. Incompatible with --no-recreate.
             --no-recreate          If containers already exist, don't recreate them.
                                    Incompatible with --force-recreate.
-            --no-build             Don't build an image, even if it's missing
+            --no-build             Don't build an image, even if it's missing.
+            --build                Build images before creating containers.
         """
         service_names = options['SERVICE']
 
         project.create(
             service_names=service_names,
             strategy=convergence_strategy_from_opts(options),
-            do_build=not options['--no-build']
+            do_build=build_action_from_opts(options),
         )
 
     def down(self, project, options):
@@ -699,7 +701,8 @@ class TopLevelCommand(DocoptCommand):
                                        Incompatible with --no-recreate.
             --no-recreate              If containers already exist, don't recreate them.
                                        Incompatible with --force-recreate.
-            --no-build                 Don't build an image, even if it's missing
+            --no-build                 Don't build an image, even if it's missing.
+            --build                    Build images before starting containers.
             --abort-on-container-exit  Stops all containers if any container was stopped.
                                        Incompatible with -d.
             -t, --timeout TIMEOUT      Use this timeout in seconds for container shutdown
@@ -721,7 +724,7 @@ class TopLevelCommand(DocoptCommand):
                 service_names=service_names,
                 start_deps=start_deps,
                 strategy=convergence_strategy_from_opts(options),
-                do_build=not options['--no-build'],
+                do_build=build_action_from_opts(options),
                 timeout=timeout,
                 detached=detached)
 
@@ -775,6 +778,19 @@ def image_type_from_opt(flag, value):
         raise UserError("%s flag must be one of: all, local" % flag)
 
 
+def build_action_from_opts(options):
+    if options['--build'] and options['--no-build']:
+        raise UserError("--build and --no-build can not be combined.")
+
+    if options['--build']:
+        return BuildAction.force
+
+    if options['--no-build']:
+        return BuildAction.skip
+
+    return BuildAction.none
+
+
 def run_one_off_container(container_options, project, service, options):
     if not options['--no-deps']:
         deps = service.get_dependency_names()

+ 8 - 2
compose/project.py

@@ -21,6 +21,7 @@ from .container import Container
 from .network import build_networks
 from .network import get_networks
 from .network import ProjectNetworks
+from .service import BuildAction
 from .service import ContainerNetworkMode
 from .service import ConvergenceStrategy
 from .service import NetworkMode
@@ -249,7 +250,12 @@ class Project(object):
             else:
                 log.info('%s uses an image, skipping' % service.name)
 
-    def create(self, service_names=None, strategy=ConvergenceStrategy.changed, do_build=True):
+    def create(
+        self,
+        service_names=None,
+        strategy=ConvergenceStrategy.changed,
+        do_build=BuildAction.none,
+    ):
         services = self.get_services_without_duplicate(service_names, include_deps=True)
 
         plans = self._get_convergence_plans(services, strategy)
@@ -298,7 +304,7 @@ class Project(object):
            service_names=None,
            start_deps=True,
            strategy=ConvergenceStrategy.changed,
-           do_build=True,
+           do_build=BuildAction.none,
            timeout=DEFAULT_TIMEOUT,
            detached=False):
 

+ 27 - 10
compose/service.py

@@ -104,6 +104,14 @@ class ImageType(enum.Enum):
     all = 2
 
 
[email protected]
+class BuildAction(enum.Enum):
+    """Enumeration for the possible build actions."""
+    none = 0
+    force = 1
+    skip = 2
+
+
 class Service(object):
     def __init__(
         self,
@@ -243,7 +251,7 @@ class Service(object):
 
     def create_container(self,
                          one_off=False,
-                         do_build=True,
+                         do_build=BuildAction.none,
                          previous_container=None,
                          number=None,
                          quiet=False,
@@ -266,20 +274,29 @@ class Service(object):
 
         return Container.create(self.client, **container_options)
 
-    def ensure_image_exists(self, do_build=True):
+    def ensure_image_exists(self, do_build=BuildAction.none):
+        if self.can_be_built() and do_build == BuildAction.force:
+            self.build()
+            return
+
         try:
             self.image()
             return
         except NoSuchImageError:
             pass
 
-        if self.can_be_built():
-            if do_build:
-                self.build()
-            else:
-                raise NeedsBuildError(self)
-        else:
+        if not self.can_be_built():
             self.pull()
+            return
+
+        if do_build == BuildAction.skip:
+            raise NeedsBuildError(self)
+
+        self.build()
+        log.warn(
+            "Image for service {} was build because it was not found. To "
+            "rebuild this image you must use the `build` command or the "
+            "--build flag.".format(self.name))
 
     def image(self):
         try:
@@ -343,7 +360,7 @@ class Service(object):
 
     def execute_convergence_plan(self,
                                  plan,
-                                 do_build=True,
+                                 do_build=BuildAction.none,
                                  timeout=DEFAULT_TIMEOUT,
                                  detached=False,
                                  start=True):
@@ -392,7 +409,7 @@ class Service(object):
     def recreate_container(
             self,
             container,
-            do_build=False,
+            do_build=BuildAction.none,
             timeout=DEFAULT_TIMEOUT,
             attach_logs=False,
             start_new_container=True):

+ 34 - 4
tests/unit/service_test.py

@@ -2,6 +2,7 @@ from __future__ import absolute_import
 from __future__ import unicode_literals
 
 import docker
+import pytest
 from docker.errors import APIError
 
 from .. import mock
@@ -15,6 +16,7 @@ from compose.const import LABEL_SERVICE
 from compose.container import Container
 from compose.service import build_ulimits
 from compose.service import build_volume_binding
+from compose.service import BuildAction
 from compose.service import ContainerNetworkMode
 from compose.service import get_container_data_volumes
 from compose.service import ImageType
@@ -427,7 +429,12 @@ class ServiceTest(unittest.TestCase):
             '{"stream": "Successfully built abcd"}',
         ]
 
-        service.create_container(do_build=True)
+        with mock.patch('compose.service.log', autospec=True) as mock_log:
+            service.create_container(do_build=BuildAction.none)
+            assert mock_log.warn.called
+            _, args, _ = mock_log.warn.mock_calls[0]
+            assert 'was build because it was not found' in args[0]
+
         self.mock_client.build.assert_called_once_with(
             tag='default_foo',
             dockerfile=None,
@@ -444,14 +451,37 @@ class ServiceTest(unittest.TestCase):
         service = Service('foo', client=self.mock_client, build={'context': '.'})
         self.mock_client.inspect_image.return_value = {'Id': 'abc123'}
 
-        service.create_container(do_build=False)
+        service.create_container(do_build=BuildAction.skip)
         self.assertFalse(self.mock_client.build.called)
 
     def test_create_container_no_build_but_needs_build(self):
         service = Service('foo', client=self.mock_client, build={'context': '.'})
         self.mock_client.inspect_image.side_effect = NoSuchImageError
-        with self.assertRaises(NeedsBuildError):
-            service.create_container(do_build=False)
+        with pytest.raises(NeedsBuildError):
+            service.create_container(do_build=BuildAction.skip)
+
+    def test_create_container_force_build(self):
+        service = Service('foo', client=self.mock_client, build={'context': '.'})
+        self.mock_client.inspect_image.return_value = {'Id': 'abc123'}
+        self.mock_client.build.return_value = [
+            '{"stream": "Successfully built abcd"}',
+        ]
+
+        with mock.patch('compose.service.log', autospec=True) as mock_log:
+            service.create_container(do_build=BuildAction.force)
+
+        assert not mock_log.warn.called
+        self.mock_client.build.assert_called_once_with(
+            tag='default_foo',
+            dockerfile=None,
+            stream=True,
+            path='.',
+            pull=False,
+            forcerm=False,
+            nocache=False,
+            rm=True,
+            buildargs=None,
+        )
 
     def test_build_does_not_pull(self):
         self.mock_client.build.return_value = [