Просмотр исходного кода

Merge pull request #3772 from docker/bump-1.8.0-rc3

Bump 1.8.0-rc3
Joffrey F 9 лет назад
Родитель
Сommit
1bf0cd07de

+ 11 - 1
CHANGELOG.md

@@ -9,11 +9,15 @@ Change log
 - As announced in 1.7.0, `docker-compose rm` now removes containers
   created by `docker-compose run` by default.
 
+- Setting `entrypoint` on a service now empties out any default
+  command that was set on the image (i.e. any `CMD` instruction in the
+  Dockerfile used to build it). This makes it consistent with
+  the `--entrypoint` flag to `docker run`.
+
 New Features
 
 - Added `docker-compose bundle`, a command that builds a bundle file
   to be consumed by the new *Docker Stack* commands in Docker 1.12.
-  This command automatically pushes and pulls images as needed.
 
 - Added `docker-compose push`, a command that pushes service images
   to a registry.
@@ -27,6 +31,9 @@ Bug Fixes
 - Fixed a bug where Compose would erroneously try to read `.env`
   at the project's root when it is a directory.
 
+- `docker-compose run -e VAR` now passes `VAR` through from the shell
+  to the container, as with `docker run -e VAR`.
+
 - Improved config merging when multiple compose files are involved
   for several service sub-keys.
 
@@ -52,6 +59,9 @@ Bug Fixes
 - Fixed a bug where errors during `docker-compose up` would show
   an unrelated stacktrace at the end of the process.
 
+- `docker-compose create` and `docker-compose start` show more
+  descriptive error messages when something goes wrong.
+
 
 1.7.1 (2016-05-04)
 -----------------

+ 1 - 1
compose/__init__.py

@@ -1,4 +1,4 @@
 from __future__ import absolute_import
 from __future__ import unicode_literals
 
-__version__ = '1.8.0-rc2'
+__version__ = '1.8.0-rc3'

+ 18 - 21
compose/bundle.py

@@ -60,7 +60,7 @@ def serialize_bundle(config, image_digests):
     return json.dumps(to_bundle(config, image_digests), indent=2, sort_keys=True)
 
 
-def get_image_digests(project, allow_fetch=False):
+def get_image_digests(project, allow_push=False):
     digests = {}
     needs_push = set()
     needs_pull = set()
@@ -69,7 +69,7 @@ def get_image_digests(project, allow_fetch=False):
         try:
             digests[service.name] = get_image_digest(
                 service,
-                allow_fetch=allow_fetch,
+                allow_push=allow_push,
             )
         except NeedsPush as e:
             needs_push.add(e.image_name)
@@ -82,7 +82,7 @@ def get_image_digests(project, allow_fetch=False):
     return digests
 
 
-def get_image_digest(service, allow_fetch=False):
+def get_image_digest(service, allow_push=False):
     if 'image' not in service.options:
         raise UserError(
             "Service '{s.name}' doesn't define an image tag. An image name is "
@@ -108,27 +108,24 @@ def get_image_digest(service, allow_fetch=False):
         # digests
         return image['RepoDigests'][0]
 
-    if not allow_fetch:
-        if 'build' in service.options:
-            raise NeedsPush(service.image_name)
-        else:
-            raise NeedsPull(service.image_name)
+    if 'build' not in service.options:
+        raise NeedsPull(service.image_name)
 
-    return fetch_image_digest(service)
+    if not allow_push:
+        raise NeedsPush(service.image_name)
 
+    return push_image(service)
 
-def fetch_image_digest(service):
-    if 'build' not in service.options:
-        digest = service.pull()
-    else:
-        try:
-            digest = service.push()
-        except:
-            log.error(
-                "Failed to push image for service '{s.name}'. Please use an "
-                "image tag that can be pushed to a Docker "
-                "registry.".format(s=service))
-            raise
+
+def push_image(service):
+    try:
+        digest = service.push()
+    except:
+        log.error(
+            "Failed to push image for service '{s.name}'. Please use an "
+            "image tag that can be pushed to a Docker "
+            "registry.".format(s=service))
+        raise
 
     if not digest:
         raise ValueError("Failed to get digest for %s" % service.name)

+ 3 - 4
compose/cli/docker_client.py

@@ -10,6 +10,7 @@ from docker.utils import kwargs_from_env
 
 from ..const import HTTP_TIMEOUT
 from .errors import UserError
+from .utils import generate_user_agent
 
 log = logging.getLogger(__name__)
 
@@ -45,10 +46,6 @@ def docker_client(environment, version=None, tls_config=None, host=None,
     Returns a docker-py client configured using environment variables
     according to the same logic as the official Docker client.
     """
-    if 'DOCKER_CLIENT_TIMEOUT' in environment:
-        log.warn("The DOCKER_CLIENT_TIMEOUT environment variable is deprecated.  "
-                 "Please use COMPOSE_HTTP_TIMEOUT instead.")
-
     try:
         kwargs = kwargs_from_env(environment=environment, ssl_version=tls_version)
     except TLSParameterError:
@@ -71,4 +68,6 @@ def docker_client(environment, version=None, tls_config=None, host=None,
     else:
         kwargs['timeout'] = HTTP_TIMEOUT
 
+    kwargs['user_agent'] = generate_user_agent()
+
     return Client(**kwargs)

+ 23 - 14
compose/cli/errors.py

@@ -13,8 +13,8 @@ from requests.exceptions import SSLError
 from requests.packages.urllib3.exceptions import ReadTimeoutError
 
 from ..const import API_VERSION_TO_ENGINE_VERSION
-from ..const import HTTP_TIMEOUT
 from .utils import call_silently
+from .utils import is_docker_for_mac_installed
 from .utils import is_mac
 from .utils import is_ubuntu
 
@@ -46,18 +46,9 @@ def handle_connection_errors(client):
         raise ConnectionError()
     except RequestsConnectionError as e:
         if e.args and isinstance(e.args[0], ReadTimeoutError):
-            log_timeout_error()
+            log_timeout_error(client.timeout)
             raise ConnectionError()
-
-        if call_silently(['which', 'docker']) != 0:
-            if is_mac():
-                exit_with_error(docker_not_found_mac)
-            if is_ubuntu():
-                exit_with_error(docker_not_found_ubuntu)
-            exit_with_error(docker_not_found_generic)
-        if call_silently(['which', 'docker-machine']) == 0:
-            exit_with_error(conn_error_docker_machine)
-        exit_with_error(conn_error_generic.format(url=client.base_url))
+        exit_with_error(get_conn_error_message(client.base_url))
     except APIError as e:
         log_api_error(e, client.api_version)
         raise ConnectionError()
@@ -66,13 +57,13 @@ def handle_connection_errors(client):
         raise ConnectionError()
 
 
-def log_timeout_error():
+def log_timeout_error(timeout):
     log.error(
         "An HTTP request took too long to complete. Retry with --verbose to "
         "obtain debug information.\n"
         "If you encounter this issue regularly because of slow network "
         "conditions, consider setting COMPOSE_HTTP_TIMEOUT to a higher "
-        "value (current value: %s)." % HTTP_TIMEOUT)
+        "value (current value: %s)." % timeout)
 
 
 def log_api_error(e, client_version):
@@ -97,6 +88,20 @@ def exit_with_error(msg):
     raise ConnectionError()
 
 
+def get_conn_error_message(url):
+    if call_silently(['which', 'docker']) != 0:
+        if is_mac():
+            return docker_not_found_mac
+        if is_ubuntu():
+            return docker_not_found_ubuntu
+        return docker_not_found_generic
+    if is_docker_for_mac_installed():
+        return conn_error_docker_for_mac
+    if call_silently(['which', 'docker-machine']) == 0:
+        return conn_error_docker_machine
+    return conn_error_generic.format(url=url)
+
+
 docker_not_found_mac = """
     Couldn't connect to Docker daemon. You might need to install Docker:
 
@@ -122,6 +127,10 @@ conn_error_docker_machine = """
     Couldn't connect to Docker daemon - you might need to run `docker-machine start default`.
 """
 
+conn_error_docker_for_mac = """
+    Couldn't connect to Docker daemon. You might need to start Docker for Mac.
+"""
+
 
 conn_error_generic = """
     Couldn't connect to Docker daemon at {url} - is it running?

+ 32 - 11
compose/cli/main.py

@@ -32,6 +32,7 @@ from ..service import BuildError
 from ..service import ConvergenceStrategy
 from ..service import ImageType
 from ..service import NeedsBuildError
+from ..service import OperationFailedError
 from .command import get_config_from_options
 from .command import project_from_options
 from .docopt_command import DocoptDispatcher
@@ -61,7 +62,8 @@ def main():
     except (KeyboardInterrupt, signals.ShutdownException):
         log.error("Aborting.")
         sys.exit(1)
-    except (UserError, NoSuchService, ConfigurationError, ProjectError) as e:
+    except (UserError, NoSuchService, ConfigurationError,
+            ProjectError, OperationFailedError) as e:
         log.error(e.msg)
         sys.exit(1)
     except BuildError as e:
@@ -221,15 +223,16 @@ class TopLevelCommand(object):
         Generate a Distributed Application Bundle (DAB) from the Compose file.
 
         Images must have digests stored, which requires interaction with a
-        Docker registry. If digests aren't stored for all images, you can pass
-        `--fetch-digests` to automatically fetch them. Images for services
-        with a `build` key will be pushed. Images for services without a
-        `build` key will be pulled.
+        Docker registry. If digests aren't stored for all images, you can fetch
+        them with `docker-compose pull` or `docker-compose push`. To push images
+        automatically when bundling, pass `--push-images`. Only services with
+        a `build` option specified will have their images pushed.
 
         Usage: bundle [options]
 
         Options:
-            --fetch-digests            Automatically fetch image digests if missing
+            --push-images              Automatically push images for any services
+                                       which have a `build` option specified.
 
             -o, --output PATH          Path to write the bundle file to.
                                        Defaults to "<project name>.dab".
@@ -245,7 +248,7 @@ class TopLevelCommand(object):
             try:
                 image_digests = get_image_digests(
                     self.project,
-                    allow_fetch=options['--fetch-digests'],
+                    allow_push=options['--push-images'],
                 )
             except MissingDigests as e:
                 def list_images(images):
@@ -254,12 +257,28 @@ class TopLevelCommand(object):
                 paras = ["Some images are missing digests."]
 
                 if e.needs_push:
-                    paras += ["The following images need to be pushed:", list_images(e.needs_push)]
+                    command_hint = (
+                        "Use `docker-compose push {}` to push them. "
+                        "You can do this automatically with `docker-compose bundle --push-images`."
+                        .format(" ".join(sorted(e.needs_push)))
+                    )
+                    paras += [
+                        "The following images can be pushed:",
+                        list_images(e.needs_push),
+                        command_hint,
+                    ]
 
                 if e.needs_pull:
-                    paras += ["The following images need to be pulled:", list_images(e.needs_pull)]
+                    command_hint = (
+                        "Use `docker-compose pull {}` to pull them. "
+                        .format(" ".join(sorted(e.needs_pull)))
+                    )
 
-                paras.append("If this is OK, run `docker-compose bundle --fetch-digests`.")
+                    paras += [
+                        "The following images need to be pulled:",
+                        list_images(e.needs_pull),
+                        command_hint,
+                    ]
 
                 raise UserError("\n\n".join(paras))
 
@@ -668,8 +687,10 @@ class TopLevelCommand(object):
                 'can not be used togather'
             )
 
-        if options['COMMAND']:
+        if options['COMMAND'] is not None:
             command = [options['COMMAND']] + options['ARGS']
+        elif options['--entrypoint'] is not None:
+            command = []
         else:
             command = service.options.get('command')
 

+ 19 - 0
compose/cli/utils.py

@@ -103,3 +103,22 @@ def get_build_version():
 
     with open(filename) as fh:
         return fh.read().strip()
+
+
+def is_docker_for_mac_installed():
+    return is_mac() and os.path.isdir('/Applications/Docker.app')
+
+
+def generate_user_agent():
+    parts = [
+        "docker-compose/{}".format(compose.__version__),
+        "docker-py/{}".format(docker.__version__),
+    ]
+    try:
+        p_system = platform.system()
+        p_release = platform.release()
+    except IOError:
+        pass
+    else:
+        parts.append("{}/{}".format(p_system, p_release))
+    return " ".join(parts)

+ 1 - 2
compose/const.py

@@ -1,11 +1,10 @@
 from __future__ import absolute_import
 from __future__ import unicode_literals
 
-import os
 import sys
 
 DEFAULT_TIMEOUT = 10
-HTTP_TIMEOUT = int(os.environ.get('DOCKER_CLIENT_TIMEOUT', 60))
+HTTP_TIMEOUT = 60
 IMAGE_EVENTS = ['delete', 'import', 'pull', 'push', 'tag', 'untag']
 IS_WINDOWS_PLATFORM = (sys.platform == "win32")
 LABEL_CONTAINER_NUMBER = 'com.docker.compose.container-number'

+ 7 - 0
compose/errors.py

@@ -0,0 +1,7 @@
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+
+class OperationFailedError(Exception):
+    def __init__(self, reason):
+        self.msg = reason

+ 4 - 0
compose/parallel.py

@@ -12,6 +12,7 @@ from six.moves.queue import Empty
 from six.moves.queue import Queue
 
 from compose.cli.signals import ShutdownException
+from compose.errors import OperationFailedError
 from compose.utils import get_output_stream
 
 
@@ -47,6 +48,9 @@ def parallel_execute(objects, func, get_name, msg, get_deps=None):
         elif isinstance(exception, APIError):
             errors[get_name(obj)] = exception.explanation
             writer.write(get_name(obj), 'error')
+        elif isinstance(exception, OperationFailedError):
+            errors[get_name(obj)] = exception.msg
+            writer.write(get_name(obj), 'error')
         elif isinstance(exception, UpstreamError):
             writer.write(get_name(obj), 'error')
         else:

+ 16 - 0
compose/project.py

@@ -369,6 +369,8 @@ class Project(object):
            detached=False,
            remove_orphans=False):
 
+        warn_for_swarm_mode(self.client)
+
         self.initialize()
         self.find_orphan_containers(remove_orphans)
 
@@ -533,6 +535,20 @@ def get_volumes_from(project, service_dict):
     return [build_volume_from(vf) for vf in volumes_from]
 
 
+def warn_for_swarm_mode(client):
+    info = client.info()
+    if info.get('Swarm', {}).get('LocalNodeState') == 'active':
+        log.warn(
+            "The Docker Engine you're using is running in swarm mode.\n\n"
+            "Compose does not use swarm mode to deploy services to multiple nodes in a swarm. "
+            "All containers will be scheduled on the current node.\n\n"
+            "To deploy your application across the swarm, "
+            "use the bundle feature of the Docker experimental build.\n\n"
+            "More info:\n"
+            "https://docs.docker.com/compose/bundles\n"
+        )
+
+
 class NoSuchService(Exception):
     def __init__(self, name):
         self.name = name

+ 10 - 2
compose/service.py

@@ -27,6 +27,7 @@ from .const import LABEL_PROJECT
 from .const import LABEL_SERVICE
 from .const import LABEL_VERSION
 from .container import Container
+from .errors import OperationFailedError
 from .parallel import parallel_execute
 from .parallel import parallel_start
 from .progress_stream import stream_output
@@ -277,7 +278,11 @@ class Service(object):
         if 'name' in container_options and not quiet:
             log.info("Creating %s" % container_options['name'])
 
-        return Container.create(self.client, **container_options)
+        try:
+            return Container.create(self.client, **container_options)
+        except APIError as ex:
+            raise OperationFailedError("Cannot create container for service %s: %s" %
+                                       (self.name, ex.explanation))
 
     def ensure_image_exists(self, do_build=BuildAction.none):
         if self.can_be_built() and do_build == BuildAction.force:
@@ -447,7 +452,10 @@ class Service(object):
 
     def start_container(self, container):
         self.connect_container_to_networks(container)
-        container.start()
+        try:
+            container.start()
+        except APIError as ex:
+            raise OperationFailedError("Cannot start service %s: %s" % (self.name, ex.explanation))
         return container
 
     def connect_container_to_networks(self, container):

+ 1 - 1
contrib/completion/bash/docker-compose

@@ -117,7 +117,7 @@ _docker_compose_bundle() {
 			;;
 	esac
 
-	COMPREPLY=( $( compgen -W "--help --output -o" -- "$cur" ) )
+	COMPREPLY=( $( compgen -W "--fetch-digests --help --output -o" -- "$cur" ) )
 }
 
 

+ 200 - 0
docs/bundles.md

@@ -0,0 +1,200 @@
+<!--[metadata]>
++++
+title = "Docker Stacks and Distributed Application Bundles"
+description = "Description of Docker and Compose's experimental support for application bundles"
+keywords = ["documentation, docs,  docker, compose, bundles, stacks"]
+advisory = "experimental"
+[menu.main]
+parent="workw_compose"
++++
+<![end-metadata]-->
+
+
+# Docker Stacks and Distributed Application Bundles (experimental)
+
+> **Note**: This is a copy of the [Docker Stacks and Distributed Application
+> Bundles](https://github.com/docker/docker/blob/v1.12.0-rc4/experimental/docker-stacks-and-bundles.md)
+> document in the [docker/docker repo](https://github.com/docker/docker).
+
+## Overview
+
+Docker Stacks and Distributed Application Bundles are experimental features
+introduced in Docker 1.12 and Docker Compose 1.8, alongside the concept of
+swarm mode, and Nodes and Services in the Engine API.
+
+A Dockerfile can be built into an image, and containers can be created from
+that image. Similarly, a docker-compose.yml can be built into a **distributed
+application bundle**, and **stacks** can be created from that bundle. In that
+sense, the bundle is a multi-services distributable image format.
+
+As of Docker 1.12 and Compose 1.8, the features are experimental. Neither
+Docker Engine nor the Docker Registry support distribution of bundles.
+
+## Producing a bundle
+
+The easiest way to produce a bundle is to generate it using `docker-compose`
+from an existing `docker-compose.yml`. Of course, that's just *one* possible way
+to proceed, in the same way that `docker build` isn't the only way to produce a
+Docker image.
+
+From `docker-compose`:
+
+```bash
+$ docker-compose bundle
+WARNING: Unsupported key 'network_mode' in services.nsqd - ignoring
+WARNING: Unsupported key 'links' in services.nsqd - ignoring
+WARNING: Unsupported key 'volumes' in services.nsqd - ignoring
+[...]
+Wrote bundle to vossibility-stack.dab
+```
+
+## Creating a stack from a bundle
+
+A stack is created using the `docker deploy` command:
+
+```bash
+# docker deploy --help
+
+Usage:  docker deploy [OPTIONS] STACK
+
+Create and update a stack
+
+Options:
+      --file   string        Path to a Distributed Application Bundle file (Default: STACK.dab)
+      --help                 Print usage
+      --with-registry-auth   Send registry authentication details to Swarm agents
+```
+
+Let's deploy the stack created before:
+
+```bash
+# docker deploy vossibility-stack
+Loading bundle from vossibility-stack.dab
+Creating service vossibility-stack_elasticsearch
+Creating service vossibility-stack_kibana
+Creating service vossibility-stack_logstash
+Creating service vossibility-stack_lookupd
+Creating service vossibility-stack_nsqd
+Creating service vossibility-stack_vossibility-collector
+```
+
+We can verify that services were correctly created:
+
+```bash
+# docker service ls
+ID            NAME                                     REPLICAS  IMAGE
+COMMAND
+29bv0vnlm903  vossibility-stack_lookupd                1 nsqio/nsq@sha256:eeba05599f31eba418e96e71e0984c3dc96963ceb66924dd37a47bf7ce18a662 /nsqlookupd
+4awt47624qwh  vossibility-stack_nsqd                   1 nsqio/nsq@sha256:eeba05599f31eba418e96e71e0984c3dc96963ceb66924dd37a47bf7ce18a662 /nsqd --data-path=/data --lookupd-tcp-address=lookupd:4160
+4tjx9biia6fs  vossibility-stack_elasticsearch          1 elasticsearch@sha256:12ac7c6af55d001f71800b83ba91a04f716e58d82e748fa6e5a7359eed2301aa
+7563uuzr9eys  vossibility-stack_kibana                 1 kibana@sha256:6995a2d25709a62694a937b8a529ff36da92ebee74bafd7bf00e6caf6db2eb03
+9gc5m4met4he  vossibility-stack_logstash               1 logstash@sha256:2dc8bddd1bb4a5a34e8ebaf73749f6413c101b2edef6617f2f7713926d2141fe logstash -f /etc/logstash/conf.d/logstash.conf
+axqh55ipl40h  vossibility-stack_vossibility-collector  1 icecrime/vossibility-collector@sha256:f03f2977203ba6253988c18d04061c5ec7aab46bca9dfd89a9a1fa4500989fba --config /config/config.toml --debug
+```
+
+## Managing stacks
+
+Stacks are managed using the `docker stack` command:
+
+```bash
+# docker stack --help
+
+Usage:  docker stack COMMAND
+
+Manage Docker stacks
+
+Options:
+      --help   Print usage
+
+Commands:
+  config      Print the stack configuration
+  deploy      Create and update a stack
+  rm          Remove the stack
+  services    List the services in the stack
+  tasks       List the tasks in the stack
+
+Run 'docker stack COMMAND --help' for more information on a command.
+```
+
+## Bundle file format
+
+Distributed application bundles are described in a JSON format. When bundles
+are persisted as files, the file extension is `.dab`.
+
+A bundle has two top-level fields: `version` and `services`. The version used
+by Docker 1.12 tools is `0.1`.
+
+`services` in the bundle are the services that comprise the app. They
+correspond to the new `Service` object introduced in the 1.12 Docker Engine API.
+
+A service has the following fields:
+
+<dl>
+    <dt>
+        Image (required) <code>string</code>
+    </dt>
+    <dd>
+        The image that the service will run. Docker images should be referenced
+        with full content hash to fully specify the deployment artifact for the
+        service. Example:
+        <code>postgres@sha256:e0a230a9f5b4e1b8b03bb3e8cf7322b0e42b7838c5c87f4545edb48f5eb8f077</code>
+    </dd>
+    <dt>
+        Command <code>[]string</code>
+    </dt>
+    <dd>
+        Command to run in service containers.
+    </dd>
+    <dt>
+        Args <code>[]string</code>
+    </dt>
+    <dd>
+        Arguments passed to the service containers.
+    </dd>
+    <dt>
+        Env <code>[]string</code>
+    </dt>
+    <dd>
+        Environment variables.
+    </dd>
+    <dt>
+        Labels <code>map[string]string</code>
+    </dt>
+    <dd>
+        Labels used for setting meta data on services.
+    </dd>
+    <dt>
+        Ports <code>[]Port</code>
+    </dt>
+    <dd>
+        Service ports (composed of <code>Port</code> (<code>int</code>) and
+        <code>Protocol</code> (<code>string</code>). A service description can
+        only specify the container port to be exposed. These ports can be
+        mapped on runtime hosts at the operator's discretion.
+    </dd>
+
+    <dt>
+        WorkingDir <code>string</code>
+    </dt>
+    <dd>
+        Working directory inside the service containers.
+    </dd>
+
+    <dt>
+        User <code>string</code>
+    </dt>
+    <dd>
+        Username or UID (format: <code>&lt;name|uid&gt;[:&lt;group|gid&gt;]</code>).
+    </dd>
+
+    <dt>
+        Networks <code>[]string</code>
+    </dt>
+    <dd>
+        Networks that the service containers should be connected to. An entity
+        deploying a bundle should create networks as needed.
+    </dd>
+</dl>
+
+> **Note:** Some configuration options are not yet supported in the DAB format,
+> including volume mounts.

+ 36 - 7
docs/compose-file.md

@@ -115,22 +115,41 @@ specified.
 
 > [Version 2 file format](#version-2) only.
 
-Add build arguments. You can use either an array or a dictionary. Any
-boolean values; true, false, yes, no, need to be enclosed in quotes to ensure
-they are not converted to True or False by the YML parser.
+Add build arguments, which are environment variables accessible only during the
+build process.
+
+First, specify the arguments in your Dockerfile:
+
+    ARG buildno
+    ARG password
 
-Build arguments with only a key are resolved to their environment value on the
-machine Compose is running on.
+    RUN echo "Build number: $buildno"
+    RUN script-requiring-password.sh "$password"
+
+Then specify the arguments under the `build` key. You can pass either a mapping
+or a list:
 
     build:
+      context: .
       args:
         buildno: 1
-        user: someuser
+        password: secret
 
     build:
+      context: .
       args:
         - buildno=1
-        - user=someuser
+        - password=secret
+
+You can omit the value when specifying a build argument, in which case its value
+at build time is the value in the environment where Compose is running.
+
+    args:
+      - buildno
+      - password
+
+> **Note**: YAML boolean values (`true`, `false`, `yes`, `no`, `on`, `off`) must
+> be enclosed in quotes, so that the parser interprets them as strings.
 
 ### cap_add, cap_drop
 
@@ -274,6 +293,11 @@ beginning with `#` (i.e. comments) are ignored, as are blank lines.
     # Set Rails/Rack environment
     RACK_ENV=development
 
+> **Note:** If your service specifies a [build](#build) option, variables
+> defined in environment files will _not_ be automatically visible during the
+> build. Use the [args](#args) sub-option of `build` to define build-time
+> environment variables.
+
 ### environment
 
 Add environment variables. You can use either an array or a dictionary. Any
@@ -293,6 +317,11 @@ machine Compose is running on, which can be helpful for secret or host-specific
       - SHOW=true
       - SESSION_SECRET
 
+> **Note:** If your service specifies a [build](#build) option, variables
+> defined in `environment` will _not_ be automatically visible during the
+> build. Use the [args](#args) sub-option of `build` to define build-time
+> environment variables.
+
 ### expose
 
 Expose ports without publishing them to the host machine - they'll only be

+ 3 - 3
docs/install.md

@@ -39,7 +39,7 @@ which the release page specifies, in your terminal.
 
      The following is an example command illustrating the format:
 
-        curl -L https://github.com/docker/compose/releases/download/1.8.0-rc2/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
+        curl -L https://github.com/docker/compose/releases/download/1.8.0-rc3/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
 
      If you have problems installing with `curl`, see
      [Alternative Install Options](#alternative-install-options).
@@ -54,7 +54,7 @@ which the release page specifies, in your terminal.
 7. Test the installation.
 
         $ docker-compose --version
-        docker-compose version: 1.8.0-rc2
+        docker-compose version: 1.8.0-rc3
 
 
 ## Alternative install options
@@ -77,7 +77,7 @@ to get started.
 Compose can also be run inside a container, from a small bash script wrapper.
 To install compose as a container run:
 
-    $ curl -L https://github.com/docker/compose/releases/download/1.8.0-rc2/run.sh > /usr/local/bin/docker-compose
+    $ curl -L https://github.com/docker/compose/releases/download/1.8.0-rc3/run.sh > /usr/local/bin/docker-compose
     $ chmod +x /usr/local/bin/docker-compose
 
 ## Master builds

+ 31 - 0
docs/reference/bundle.md

@@ -0,0 +1,31 @@
+<!--[metadata]>
++++
+title = "bundle"
+description = "Create a distributed application bundle from the Compose file."
+keywords = ["fig, composition, compose, docker, orchestration, cli,  bundle"]
+[menu.main]
+identifier="bundle.compose"
+parent = "smn_compose_cli"
++++
+<![end-metadata]-->
+
+# bundle
+
+```
+Usage: bundle [options]
+
+Options:
+    --push-images              Automatically push images for any services
+                               which have a `build` option specified.
+
+    -o, --output PATH          Path to write the bundle file to.
+                               Defaults to "<project name>.dab".
+```
+
+Generate a Distributed Application Bundle (DAB) from the Compose file.
+
+Images must have digests stored, which requires interaction with a
+Docker registry. If digests aren't stored for all images, you can fetch
+them with `docker-compose pull` or `docker-compose push`. To push images
+automatically when bundling, pass `--push-images`. Only services with
+a `build` option specified will have their images pushed.

+ 21 - 0
docs/reference/push.md

@@ -0,0 +1,21 @@
+<!--[metadata]>
++++
+title = "push"
+description = "Pushes service images."
+keywords = ["fig, composition, compose, docker, orchestration, cli,  push"]
+[menu.main]
+identifier="push.compose"
+parent = "smn_compose_cli"
++++
+<![end-metadata]-->
+
+# push
+
+```
+Usage: push [options] [SERVICE...]
+
+Options:
+    --ignore-push-failures  Push what it can and ignores images with push failures.
+```
+
+Pushes images for services.

+ 1 - 1
requirements.txt

@@ -1,7 +1,7 @@
 PyYAML==3.11
 backports.ssl-match-hostname==3.5.0.1; python_version < '3'
 cached-property==1.2.0
-docker-py==1.9.0rc2
+docker-py==1.9.0
 dockerpty==0.4.1
 docopt==0.6.1
 enum34==1.0.4; python_version < '3.4'

+ 1 - 1
script/run/run.sh

@@ -15,7 +15,7 @@
 
 set -e
 
-VERSION="1.8.0-rc2"
+VERSION="1.8.0-rc3"
 IMAGE="docker/compose:$VERSION"
 
 

+ 1 - 1
setup.py

@@ -34,7 +34,7 @@ install_requires = [
     'requests >= 2.6.1, < 2.8',
     'texttable >= 0.8.1, < 0.9',
     'websocket-client >= 0.32.0, < 1.0',
-    'docker-py == 1.9.0rc2',
+    'docker-py >= 1.9.0, < 2.0',
     'dockerpty >= 0.4.1, < 0.5',
     'six >= 1.3.0, < 2',
     'jsonschema >= 2.5.1, < 3',

+ 48 - 11
tests/acceptance/cli_test.py

@@ -4,7 +4,6 @@ from __future__ import unicode_literals
 import datetime
 import json
 import os
-import shlex
 import signal
 import subprocess
 import time
@@ -965,16 +964,54 @@ class CLITestCase(DockerClientTestCase):
             [u'/bin/true'],
         )
 
-    def test_run_service_with_entrypoint_overridden(self):
-        self.base_dir = 'tests/fixtures/dockerfile_with_entrypoint'
-        name = 'service'
-        self.dispatch(['run', '--entrypoint', '/bin/echo', name, 'helloworld'])
-        service = self.project.get_service(name)
-        container = service.containers(stopped=True, one_off=OneOffFilter.only)[0]
-        self.assertEqual(
-            shlex.split(container.human_readable_command),
-            [u'/bin/echo', u'helloworld'],
-        )
+    def test_run_service_with_dockerfile_entrypoint(self):
+        self.base_dir = 'tests/fixtures/entrypoint-dockerfile'
+        self.dispatch(['run', 'test'])
+        container = self.project.containers(stopped=True, one_off=OneOffFilter.only)[0]
+        assert container.get('Config.Entrypoint') == ['printf']
+        assert container.get('Config.Cmd') == ['default', 'args']
+
+    def test_run_service_with_dockerfile_entrypoint_overridden(self):
+        self.base_dir = 'tests/fixtures/entrypoint-dockerfile'
+        self.dispatch(['run', '--entrypoint', 'echo', 'test'])
+        container = self.project.containers(stopped=True, one_off=OneOffFilter.only)[0]
+        assert container.get('Config.Entrypoint') == ['echo']
+        assert not container.get('Config.Cmd')
+
+    def test_run_service_with_dockerfile_entrypoint_and_command_overridden(self):
+        self.base_dir = 'tests/fixtures/entrypoint-dockerfile'
+        self.dispatch(['run', '--entrypoint', 'echo', 'test', 'foo'])
+        container = self.project.containers(stopped=True, one_off=OneOffFilter.only)[0]
+        assert container.get('Config.Entrypoint') == ['echo']
+        assert container.get('Config.Cmd') == ['foo']
+
+    def test_run_service_with_compose_file_entrypoint(self):
+        self.base_dir = 'tests/fixtures/entrypoint-composefile'
+        self.dispatch(['run', 'test'])
+        container = self.project.containers(stopped=True, one_off=OneOffFilter.only)[0]
+        assert container.get('Config.Entrypoint') == ['printf']
+        assert container.get('Config.Cmd') == ['default', 'args']
+
+    def test_run_service_with_compose_file_entrypoint_overridden(self):
+        self.base_dir = 'tests/fixtures/entrypoint-composefile'
+        self.dispatch(['run', '--entrypoint', 'echo', 'test'])
+        container = self.project.containers(stopped=True, one_off=OneOffFilter.only)[0]
+        assert container.get('Config.Entrypoint') == ['echo']
+        assert not container.get('Config.Cmd')
+
+    def test_run_service_with_compose_file_entrypoint_and_command_overridden(self):
+        self.base_dir = 'tests/fixtures/entrypoint-composefile'
+        self.dispatch(['run', '--entrypoint', 'echo', 'test', 'foo'])
+        container = self.project.containers(stopped=True, one_off=OneOffFilter.only)[0]
+        assert container.get('Config.Entrypoint') == ['echo']
+        assert container.get('Config.Cmd') == ['foo']
+
+    def test_run_service_with_compose_file_entrypoint_and_empty_string_command(self):
+        self.base_dir = 'tests/fixtures/entrypoint-composefile'
+        self.dispatch(['run', '--entrypoint', 'echo', 'test', ''])
+        container = self.project.containers(stopped=True, one_off=OneOffFilter.only)[0]
+        assert container.get('Config.Entrypoint') == ['echo']
+        assert container.get('Config.Cmd') == ['']
 
     def test_run_service_with_user_overridden(self):
         self.base_dir = 'tests/fixtures/user-composefile'

+ 0 - 2
tests/fixtures/dockerfile_with_entrypoint/docker-compose.yml

@@ -1,2 +0,0 @@
-service:
-  build: .

+ 6 - 0
tests/fixtures/entrypoint-composefile/docker-compose.yml

@@ -0,0 +1,6 @@
+version: "2"
+services:
+  test:
+    image: busybox
+    entrypoint: printf
+    command: default args

+ 2 - 1
tests/fixtures/dockerfile_with_entrypoint/Dockerfile → tests/fixtures/entrypoint-dockerfile/Dockerfile

@@ -1,3 +1,4 @@
 FROM busybox:latest
 LABEL com.docker.compose.test_image=true
-ENTRYPOINT echo "From prebuilt entrypoint"
+ENTRYPOINT ["printf"]
+CMD ["default", "args"]

+ 4 - 0
tests/fixtures/entrypoint-dockerfile/docker-compose.yml

@@ -0,0 +1,4 @@
+version: "2"
+services:
+  test:
+    build: .

+ 4 - 1
tests/integration/service_test.py

@@ -738,7 +738,10 @@ class ServiceTest(DockerClientTestCase):
 
         self.assertEqual(len(service.containers()), 1)
         self.assertTrue(service.containers()[0].is_running)
-        self.assertIn("ERROR: for composetest_web_2  Boom", mock_stderr.getvalue())
+        self.assertIn(
+            "ERROR: for composetest_web_2  Cannot create container for service web: Boom",
+            mock_stderr.getvalue()
+        )
 
     def test_scale_with_unexpected_exception(self):
         """Test that when scaling if the API returns an error, that is not of type

+ 10 - 24
tests/unit/bundle_test.py

@@ -41,44 +41,30 @@ def test_get_image_digest_no_image(mock_service):
     assert "doesn't define an image tag" in exc.exconly()
 
 
-def test_fetch_image_digest_for_image_with_saved_digest(mock_service):
+def test_push_image_with_saved_digest(mock_service):
+    mock_service.options['build'] = '.'
     mock_service.options['image'] = image_id = 'abcd'
-    mock_service.pull.return_value = expected = 'sha256:thedigest'
+    mock_service.push.return_value = expected = 'sha256:thedigest'
     mock_service.image.return_value = {'RepoDigests': ['digest1']}
 
-    digest = bundle.fetch_image_digest(mock_service)
-    assert digest == image_id + '@' + expected
-
-    mock_service.pull.assert_called_once_with()
-    assert not mock_service.push.called
-    assert not mock_service.client.pull.called
-
-
-def test_fetch_image_digest_for_image(mock_service):
-    mock_service.options['image'] = image_id = 'abcd'
-    mock_service.pull.return_value = expected = 'sha256:thedigest'
-    mock_service.image.return_value = {'RepoDigests': []}
-
-    digest = bundle.fetch_image_digest(mock_service)
+    digest = bundle.push_image(mock_service)
     assert digest == image_id + '@' + expected
 
-    mock_service.pull.assert_called_once_with()
-    assert not mock_service.push.called
-    mock_service.client.pull.assert_called_once_with(digest)
+    mock_service.push.assert_called_once_with()
+    assert not mock_service.client.push.called
 
 
-def test_fetch_image_digest_for_build(mock_service):
+def test_push_image(mock_service):
     mock_service.options['build'] = '.'
     mock_service.options['image'] = image_id = 'abcd'
     mock_service.push.return_value = expected = 'sha256:thedigest'
-    mock_service.image.return_value = {'RepoDigests': ['digest1']}
+    mock_service.image.return_value = {'RepoDigests': []}
 
-    digest = bundle.fetch_image_digest(mock_service)
+    digest = bundle.push_image(mock_service)
     assert digest == image_id + '@' + expected
 
     mock_service.push.assert_called_once_with()
-    assert not mock_service.pull.called
-    assert not mock_service.client.pull.called
+    mock_service.client.pull.assert_called_once_with(digest)
 
 
 def test_to_bundle():

+ 31 - 4
tests/unit/cli/docker_client_test.py

@@ -2,10 +2,13 @@ from __future__ import absolute_import
 from __future__ import unicode_literals
 
 import os
+import platform
 
 import docker
 import pytest
 
+import compose
+from compose.cli import errors
 from compose.cli.docker_client import docker_client
 from compose.cli.docker_client import tls_config_from_options
 from tests import mock
@@ -19,11 +22,35 @@ class DockerClientTestCase(unittest.TestCase):
             del os.environ['HOME']
             docker_client(os.environ)
 
+    @mock.patch.dict(os.environ)
     def test_docker_client_with_custom_timeout(self):
-        timeout = 300
-        with mock.patch('compose.cli.docker_client.HTTP_TIMEOUT', 300):
-            client = docker_client(os.environ)
-            self.assertEqual(client.timeout, int(timeout))
+        os.environ['COMPOSE_HTTP_TIMEOUT'] = '123'
+        client = docker_client(os.environ)
+        assert client.timeout == 123
+
+    @mock.patch.dict(os.environ)
+    def test_custom_timeout_error(self):
+        os.environ['COMPOSE_HTTP_TIMEOUT'] = '123'
+        client = docker_client(os.environ)
+
+        with mock.patch('compose.cli.errors.log') as fake_log:
+            with pytest.raises(errors.ConnectionError):
+                with errors.handle_connection_errors(client):
+                    raise errors.RequestsConnectionError(
+                        errors.ReadTimeoutError(None, None, None))
+
+        assert fake_log.error.call_count == 1
+        assert '123' in fake_log.error.call_args[0][0]
+
+    def test_user_agent(self):
+        client = docker_client(os.environ)
+        expected = "docker-compose/{0} docker-py/{1} {2}/{3}".format(
+            compose.__version__,
+            docker.__version__,
+            platform.system(),
+            platform.release()
+        )
+        self.assertEqual(client.headers['User-Agent'], expected)
 
 
 class TLSConfigTestCase(unittest.TestCase):

+ 32 - 0
tests/unit/project_test.py

@@ -510,3 +510,35 @@ class ProjectTest(unittest.TestCase):
 
         project.down(ImageType.all, True)
         self.mock_client.remove_image.assert_called_once_with("busybox:latest")
+
+    def test_warning_in_swarm_mode(self):
+        self.mock_client.info.return_value = {'Swarm': {'LocalNodeState': 'active'}}
+        project = Project('composetest', [], self.mock_client)
+
+        with mock.patch('compose.project.log') as fake_log:
+            project.up()
+            assert fake_log.warn.call_count == 1
+
+    def test_no_warning_on_stop(self):
+        self.mock_client.info.return_value = {'Swarm': {'LocalNodeState': 'active'}}
+        project = Project('composetest', [], self.mock_client)
+
+        with mock.patch('compose.project.log') as fake_log:
+            project.stop()
+            assert fake_log.warn.call_count == 0
+
+    def test_no_warning_in_normal_mode(self):
+        self.mock_client.info.return_value = {'Swarm': {'LocalNodeState': 'inactive'}}
+        project = Project('composetest', [], self.mock_client)
+
+        with mock.patch('compose.project.log') as fake_log:
+            project.up()
+            assert fake_log.warn.call_count == 0
+
+    def test_no_warning_with_no_swarm_info(self):
+        self.mock_client.info.return_value = {}
+        project = Project('composetest', [], self.mock_client)
+
+        with mock.patch('compose.project.log') as fake_log:
+            project.up()
+            assert fake_log.warn.call_count == 0