mirror of https://github.com/docker/docs.git
commit
1bf0cd07de
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -9,11 +9,15 @@ Change log
|
||||||
- As announced in 1.7.0, `docker-compose rm` now removes containers
|
- As announced in 1.7.0, `docker-compose rm` now removes containers
|
||||||
created by `docker-compose run` by default.
|
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
|
New Features
|
||||||
|
|
||||||
- Added `docker-compose bundle`, a command that builds a bundle file
|
- Added `docker-compose bundle`, a command that builds a bundle file
|
||||||
to be consumed by the new *Docker Stack* commands in Docker 1.12.
|
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
|
- Added `docker-compose push`, a command that pushes service images
|
||||||
to a registry.
|
to a registry.
|
||||||
|
@ -27,6 +31,9 @@ Bug Fixes
|
||||||
- Fixed a bug where Compose would erroneously try to read `.env`
|
- Fixed a bug where Compose would erroneously try to read `.env`
|
||||||
at the project's root when it is a directory.
|
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
|
- Improved config merging when multiple compose files are involved
|
||||||
for several service sub-keys.
|
for several service sub-keys.
|
||||||
|
|
||||||
|
@ -52,6 +59,9 @@ Bug Fixes
|
||||||
- Fixed a bug where errors during `docker-compose up` would show
|
- Fixed a bug where errors during `docker-compose up` would show
|
||||||
an unrelated stacktrace at the end of the process.
|
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.7.1 (2016-05-04)
|
||||||
-----------------
|
-----------------
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
__version__ = '1.8.0-rc2'
|
__version__ = '1.8.0-rc3'
|
||||||
|
|
|
@ -60,7 +60,7 @@ def serialize_bundle(config, image_digests):
|
||||||
return json.dumps(to_bundle(config, image_digests), indent=2, sort_keys=True)
|
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 = {}
|
digests = {}
|
||||||
needs_push = set()
|
needs_push = set()
|
||||||
needs_pull = set()
|
needs_pull = set()
|
||||||
|
@ -69,7 +69,7 @@ def get_image_digests(project, allow_fetch=False):
|
||||||
try:
|
try:
|
||||||
digests[service.name] = get_image_digest(
|
digests[service.name] = get_image_digest(
|
||||||
service,
|
service,
|
||||||
allow_fetch=allow_fetch,
|
allow_push=allow_push,
|
||||||
)
|
)
|
||||||
except NeedsPush as e:
|
except NeedsPush as e:
|
||||||
needs_push.add(e.image_name)
|
needs_push.add(e.image_name)
|
||||||
|
@ -82,7 +82,7 @@ def get_image_digests(project, allow_fetch=False):
|
||||||
return digests
|
return digests
|
||||||
|
|
||||||
|
|
||||||
def get_image_digest(service, allow_fetch=False):
|
def get_image_digest(service, allow_push=False):
|
||||||
if 'image' not in service.options:
|
if 'image' not in service.options:
|
||||||
raise UserError(
|
raise UserError(
|
||||||
"Service '{s.name}' doesn't define an image tag. An image name is "
|
"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
|
# digests
|
||||||
return image['RepoDigests'][0]
|
return image['RepoDigests'][0]
|
||||||
|
|
||||||
if not allow_fetch:
|
|
||||||
if 'build' in service.options:
|
|
||||||
raise NeedsPush(service.image_name)
|
|
||||||
else:
|
|
||||||
raise NeedsPull(service.image_name)
|
|
||||||
|
|
||||||
return fetch_image_digest(service)
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_image_digest(service):
|
|
||||||
if 'build' not in service.options:
|
if 'build' not in service.options:
|
||||||
digest = service.pull()
|
raise NeedsPull(service.image_name)
|
||||||
else:
|
|
||||||
try:
|
if not allow_push:
|
||||||
digest = service.push()
|
raise NeedsPush(service.image_name)
|
||||||
except:
|
|
||||||
log.error(
|
return push_image(service)
|
||||||
"Failed to push image for service '{s.name}'. Please use an "
|
|
||||||
"image tag that can be pushed to a Docker "
|
|
||||||
"registry.".format(s=service))
|
def push_image(service):
|
||||||
raise
|
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:
|
if not digest:
|
||||||
raise ValueError("Failed to get digest for %s" % service.name)
|
raise ValueError("Failed to get digest for %s" % service.name)
|
||||||
|
|
|
@ -10,6 +10,7 @@ from docker.utils import kwargs_from_env
|
||||||
|
|
||||||
from ..const import HTTP_TIMEOUT
|
from ..const import HTTP_TIMEOUT
|
||||||
from .errors import UserError
|
from .errors import UserError
|
||||||
|
from .utils import generate_user_agent
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
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
|
Returns a docker-py client configured using environment variables
|
||||||
according to the same logic as the official Docker client.
|
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:
|
try:
|
||||||
kwargs = kwargs_from_env(environment=environment, ssl_version=tls_version)
|
kwargs = kwargs_from_env(environment=environment, ssl_version=tls_version)
|
||||||
except TLSParameterError:
|
except TLSParameterError:
|
||||||
|
@ -71,4 +68,6 @@ def docker_client(environment, version=None, tls_config=None, host=None,
|
||||||
else:
|
else:
|
||||||
kwargs['timeout'] = HTTP_TIMEOUT
|
kwargs['timeout'] = HTTP_TIMEOUT
|
||||||
|
|
||||||
|
kwargs['user_agent'] = generate_user_agent()
|
||||||
|
|
||||||
return Client(**kwargs)
|
return Client(**kwargs)
|
||||||
|
|
|
@ -13,8 +13,8 @@ from requests.exceptions import SSLError
|
||||||
from requests.packages.urllib3.exceptions import ReadTimeoutError
|
from requests.packages.urllib3.exceptions import ReadTimeoutError
|
||||||
|
|
||||||
from ..const import API_VERSION_TO_ENGINE_VERSION
|
from ..const import API_VERSION_TO_ENGINE_VERSION
|
||||||
from ..const import HTTP_TIMEOUT
|
|
||||||
from .utils import call_silently
|
from .utils import call_silently
|
||||||
|
from .utils import is_docker_for_mac_installed
|
||||||
from .utils import is_mac
|
from .utils import is_mac
|
||||||
from .utils import is_ubuntu
|
from .utils import is_ubuntu
|
||||||
|
|
||||||
|
@ -46,18 +46,9 @@ def handle_connection_errors(client):
|
||||||
raise ConnectionError()
|
raise ConnectionError()
|
||||||
except RequestsConnectionError as e:
|
except RequestsConnectionError as e:
|
||||||
if e.args and isinstance(e.args[0], ReadTimeoutError):
|
if e.args and isinstance(e.args[0], ReadTimeoutError):
|
||||||
log_timeout_error()
|
log_timeout_error(client.timeout)
|
||||||
raise ConnectionError()
|
raise ConnectionError()
|
||||||
|
exit_with_error(get_conn_error_message(client.base_url))
|
||||||
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))
|
|
||||||
except APIError as e:
|
except APIError as e:
|
||||||
log_api_error(e, client.api_version)
|
log_api_error(e, client.api_version)
|
||||||
raise ConnectionError()
|
raise ConnectionError()
|
||||||
|
@ -66,13 +57,13 @@ def handle_connection_errors(client):
|
||||||
raise ConnectionError()
|
raise ConnectionError()
|
||||||
|
|
||||||
|
|
||||||
def log_timeout_error():
|
def log_timeout_error(timeout):
|
||||||
log.error(
|
log.error(
|
||||||
"An HTTP request took too long to complete. Retry with --verbose to "
|
"An HTTP request took too long to complete. Retry with --verbose to "
|
||||||
"obtain debug information.\n"
|
"obtain debug information.\n"
|
||||||
"If you encounter this issue regularly because of slow network "
|
"If you encounter this issue regularly because of slow network "
|
||||||
"conditions, consider setting COMPOSE_HTTP_TIMEOUT to a higher "
|
"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):
|
def log_api_error(e, client_version):
|
||||||
|
@ -97,6 +88,20 @@ def exit_with_error(msg):
|
||||||
raise ConnectionError()
|
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 = """
|
docker_not_found_mac = """
|
||||||
Couldn't connect to Docker daemon. You might need to install Docker:
|
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`.
|
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 = """
|
conn_error_generic = """
|
||||||
Couldn't connect to Docker daemon at {url} - is it running?
|
Couldn't connect to Docker daemon at {url} - is it running?
|
||||||
|
|
|
@ -32,6 +32,7 @@ from ..service import BuildError
|
||||||
from ..service import ConvergenceStrategy
|
from ..service import ConvergenceStrategy
|
||||||
from ..service import ImageType
|
from ..service import ImageType
|
||||||
from ..service import NeedsBuildError
|
from ..service import NeedsBuildError
|
||||||
|
from ..service import OperationFailedError
|
||||||
from .command import get_config_from_options
|
from .command import get_config_from_options
|
||||||
from .command import project_from_options
|
from .command import project_from_options
|
||||||
from .docopt_command import DocoptDispatcher
|
from .docopt_command import DocoptDispatcher
|
||||||
|
@ -61,7 +62,8 @@ def main():
|
||||||
except (KeyboardInterrupt, signals.ShutdownException):
|
except (KeyboardInterrupt, signals.ShutdownException):
|
||||||
log.error("Aborting.")
|
log.error("Aborting.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except (UserError, NoSuchService, ConfigurationError, ProjectError) as e:
|
except (UserError, NoSuchService, ConfigurationError,
|
||||||
|
ProjectError, OperationFailedError) as e:
|
||||||
log.error(e.msg)
|
log.error(e.msg)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except BuildError as e:
|
except BuildError as e:
|
||||||
|
@ -221,15 +223,16 @@ class TopLevelCommand(object):
|
||||||
Generate a Distributed Application Bundle (DAB) from the Compose file.
|
Generate a Distributed Application Bundle (DAB) from the Compose file.
|
||||||
|
|
||||||
Images must have digests stored, which requires interaction with a
|
Images must have digests stored, which requires interaction with a
|
||||||
Docker registry. If digests aren't stored for all images, you can pass
|
Docker registry. If digests aren't stored for all images, you can fetch
|
||||||
`--fetch-digests` to automatically fetch them. Images for services
|
them with `docker-compose pull` or `docker-compose push`. To push images
|
||||||
with a `build` key will be pushed. Images for services without a
|
automatically when bundling, pass `--push-images`. Only services with
|
||||||
`build` key will be pulled.
|
a `build` option specified will have their images pushed.
|
||||||
|
|
||||||
Usage: bundle [options]
|
Usage: bundle [options]
|
||||||
|
|
||||||
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.
|
-o, --output PATH Path to write the bundle file to.
|
||||||
Defaults to "<project name>.dab".
|
Defaults to "<project name>.dab".
|
||||||
|
@ -245,7 +248,7 @@ class TopLevelCommand(object):
|
||||||
try:
|
try:
|
||||||
image_digests = get_image_digests(
|
image_digests = get_image_digests(
|
||||||
self.project,
|
self.project,
|
||||||
allow_fetch=options['--fetch-digests'],
|
allow_push=options['--push-images'],
|
||||||
)
|
)
|
||||||
except MissingDigests as e:
|
except MissingDigests as e:
|
||||||
def list_images(images):
|
def list_images(images):
|
||||||
|
@ -254,12 +257,28 @@ class TopLevelCommand(object):
|
||||||
paras = ["Some images are missing digests."]
|
paras = ["Some images are missing digests."]
|
||||||
|
|
||||||
if e.needs_push:
|
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:
|
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))
|
raise UserError("\n\n".join(paras))
|
||||||
|
|
||||||
|
@ -668,8 +687,10 @@ class TopLevelCommand(object):
|
||||||
'can not be used togather'
|
'can not be used togather'
|
||||||
)
|
)
|
||||||
|
|
||||||
if options['COMMAND']:
|
if options['COMMAND'] is not None:
|
||||||
command = [options['COMMAND']] + options['ARGS']
|
command = [options['COMMAND']] + options['ARGS']
|
||||||
|
elif options['--entrypoint'] is not None:
|
||||||
|
command = []
|
||||||
else:
|
else:
|
||||||
command = service.options.get('command')
|
command = service.options.get('command')
|
||||||
|
|
||||||
|
|
|
@ -103,3 +103,22 @@ def get_build_version():
|
||||||
|
|
||||||
with open(filename) as fh:
|
with open(filename) as fh:
|
||||||
return fh.read().strip()
|
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,11 +1,10 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
DEFAULT_TIMEOUT = 10
|
DEFAULT_TIMEOUT = 10
|
||||||
HTTP_TIMEOUT = int(os.environ.get('DOCKER_CLIENT_TIMEOUT', 60))
|
HTTP_TIMEOUT = 60
|
||||||
IMAGE_EVENTS = ['delete', 'import', 'pull', 'push', 'tag', 'untag']
|
IMAGE_EVENTS = ['delete', 'import', 'pull', 'push', 'tag', 'untag']
|
||||||
IS_WINDOWS_PLATFORM = (sys.platform == "win32")
|
IS_WINDOWS_PLATFORM = (sys.platform == "win32")
|
||||||
LABEL_CONTAINER_NUMBER = 'com.docker.compose.container-number'
|
LABEL_CONTAINER_NUMBER = 'com.docker.compose.container-number'
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
|
||||||
|
class OperationFailedError(Exception):
|
||||||
|
def __init__(self, reason):
|
||||||
|
self.msg = reason
|
|
@ -12,6 +12,7 @@ from six.moves.queue import Empty
|
||||||
from six.moves.queue import Queue
|
from six.moves.queue import Queue
|
||||||
|
|
||||||
from compose.cli.signals import ShutdownException
|
from compose.cli.signals import ShutdownException
|
||||||
|
from compose.errors import OperationFailedError
|
||||||
from compose.utils import get_output_stream
|
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):
|
elif isinstance(exception, APIError):
|
||||||
errors[get_name(obj)] = exception.explanation
|
errors[get_name(obj)] = exception.explanation
|
||||||
writer.write(get_name(obj), 'error')
|
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):
|
elif isinstance(exception, UpstreamError):
|
||||||
writer.write(get_name(obj), 'error')
|
writer.write(get_name(obj), 'error')
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -369,6 +369,8 @@ class Project(object):
|
||||||
detached=False,
|
detached=False,
|
||||||
remove_orphans=False):
|
remove_orphans=False):
|
||||||
|
|
||||||
|
warn_for_swarm_mode(self.client)
|
||||||
|
|
||||||
self.initialize()
|
self.initialize()
|
||||||
self.find_orphan_containers(remove_orphans)
|
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]
|
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):
|
class NoSuchService(Exception):
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
|
@ -27,6 +27,7 @@ from .const import LABEL_PROJECT
|
||||||
from .const import LABEL_SERVICE
|
from .const import LABEL_SERVICE
|
||||||
from .const import LABEL_VERSION
|
from .const import LABEL_VERSION
|
||||||
from .container import Container
|
from .container import Container
|
||||||
|
from .errors import OperationFailedError
|
||||||
from .parallel import parallel_execute
|
from .parallel import parallel_execute
|
||||||
from .parallel import parallel_start
|
from .parallel import parallel_start
|
||||||
from .progress_stream import stream_output
|
from .progress_stream import stream_output
|
||||||
|
@ -277,7 +278,11 @@ class Service(object):
|
||||||
if 'name' in container_options and not quiet:
|
if 'name' in container_options and not quiet:
|
||||||
log.info("Creating %s" % container_options['name'])
|
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):
|
def ensure_image_exists(self, do_build=BuildAction.none):
|
||||||
if self.can_be_built() and do_build == BuildAction.force:
|
if self.can_be_built() and do_build == BuildAction.force:
|
||||||
|
@ -447,7 +452,10 @@ class Service(object):
|
||||||
|
|
||||||
def start_container(self, container):
|
def start_container(self, container):
|
||||||
self.connect_container_to_networks(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
|
return container
|
||||||
|
|
||||||
def connect_container_to_networks(self, container):
|
def connect_container_to_networks(self, container):
|
||||||
|
|
|
@ -117,7 +117,7 @@ _docker_compose_bundle() {
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
COMPREPLY=( $( compgen -W "--help --output -o" -- "$cur" ) )
|
COMPREPLY=( $( compgen -W "--fetch-digests --help --output -o" -- "$cur" ) )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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><name|uid>[:<group|gid>]</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.
|
|
@ -115,22 +115,41 @@ specified.
|
||||||
|
|
||||||
> [Version 2 file format](#version-2) only.
|
> [Version 2 file format](#version-2) only.
|
||||||
|
|
||||||
Add build arguments. You can use either an array or a dictionary. Any
|
Add build arguments, which are environment variables accessible only during the
|
||||||
boolean values; true, false, yes, no, need to be enclosed in quotes to ensure
|
build process.
|
||||||
they are not converted to True or False by the YML parser.
|
|
||||||
|
|
||||||
Build arguments with only a key are resolved to their environment value on the
|
First, specify the arguments in your Dockerfile:
|
||||||
machine Compose is running on.
|
|
||||||
|
ARG buildno
|
||||||
|
ARG password
|
||||||
|
|
||||||
|
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:
|
build:
|
||||||
|
context: .
|
||||||
args:
|
args:
|
||||||
buildno: 1
|
buildno: 1
|
||||||
user: someuser
|
password: secret
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
context: .
|
||||||
args:
|
args:
|
||||||
- buildno=1
|
- 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
|
### cap_add, cap_drop
|
||||||
|
|
||||||
|
@ -274,6 +293,11 @@ beginning with `#` (i.e. comments) are ignored, as are blank lines.
|
||||||
# Set Rails/Rack environment
|
# Set Rails/Rack environment
|
||||||
RACK_ENV=development
|
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
|
### environment
|
||||||
|
|
||||||
Add environment variables. You can use either an array or a dictionary. Any
|
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
|
- SHOW=true
|
||||||
- SESSION_SECRET
|
- 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
|
||||||
|
|
||||||
Expose ports without publishing them to the host machine - they'll only be
|
Expose ports without publishing them to the host machine - they'll only be
|
||||||
|
|
|
@ -39,7 +39,7 @@ which the release page specifies, in your terminal.
|
||||||
|
|
||||||
The following is an example command illustrating the format:
|
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
|
If you have problems installing with `curl`, see
|
||||||
[Alternative Install Options](#alternative-install-options).
|
[Alternative Install Options](#alternative-install-options).
|
||||||
|
@ -54,7 +54,7 @@ which the release page specifies, in your terminal.
|
||||||
7. Test the installation.
|
7. Test the installation.
|
||||||
|
|
||||||
$ docker-compose --version
|
$ docker-compose --version
|
||||||
docker-compose version: 1.8.0-rc2
|
docker-compose version: 1.8.0-rc3
|
||||||
|
|
||||||
|
|
||||||
## Alternative install options
|
## Alternative install options
|
||||||
|
@ -77,7 +77,7 @@ to get started.
|
||||||
Compose can also be run inside a container, from a small bash script wrapper.
|
Compose can also be run inside a container, from a small bash script wrapper.
|
||||||
To install compose as a container run:
|
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
|
$ chmod +x /usr/local/bin/docker-compose
|
||||||
|
|
||||||
## Master builds
|
## Master builds
|
||||||
|
|
|
@ -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.
|
|
@ -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,7 +1,7 @@
|
||||||
PyYAML==3.11
|
PyYAML==3.11
|
||||||
backports.ssl-match-hostname==3.5.0.1; python_version < '3'
|
backports.ssl-match-hostname==3.5.0.1; python_version < '3'
|
||||||
cached-property==1.2.0
|
cached-property==1.2.0
|
||||||
docker-py==1.9.0rc2
|
docker-py==1.9.0
|
||||||
dockerpty==0.4.1
|
dockerpty==0.4.1
|
||||||
docopt==0.6.1
|
docopt==0.6.1
|
||||||
enum34==1.0.4; python_version < '3.4'
|
enum34==1.0.4; python_version < '3.4'
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
VERSION="1.8.0-rc2"
|
VERSION="1.8.0-rc3"
|
||||||
IMAGE="docker/compose:$VERSION"
|
IMAGE="docker/compose:$VERSION"
|
||||||
|
|
||||||
|
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -34,7 +34,7 @@ install_requires = [
|
||||||
'requests >= 2.6.1, < 2.8',
|
'requests >= 2.6.1, < 2.8',
|
||||||
'texttable >= 0.8.1, < 0.9',
|
'texttable >= 0.8.1, < 0.9',
|
||||||
'websocket-client >= 0.32.0, < 1.0',
|
'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',
|
'dockerpty >= 0.4.1, < 0.5',
|
||||||
'six >= 1.3.0, < 2',
|
'six >= 1.3.0, < 2',
|
||||||
'jsonschema >= 2.5.1, < 3',
|
'jsonschema >= 2.5.1, < 3',
|
||||||
|
|
|
@ -4,7 +4,6 @@ from __future__ import unicode_literals
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import shlex
|
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
|
@ -965,16 +964,54 @@ class CLITestCase(DockerClientTestCase):
|
||||||
[u'/bin/true'],
|
[u'/bin/true'],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_run_service_with_entrypoint_overridden(self):
|
def test_run_service_with_dockerfile_entrypoint(self):
|
||||||
self.base_dir = 'tests/fixtures/dockerfile_with_entrypoint'
|
self.base_dir = 'tests/fixtures/entrypoint-dockerfile'
|
||||||
name = 'service'
|
self.dispatch(['run', 'test'])
|
||||||
self.dispatch(['run', '--entrypoint', '/bin/echo', name, 'helloworld'])
|
container = self.project.containers(stopped=True, one_off=OneOffFilter.only)[0]
|
||||||
service = self.project.get_service(name)
|
assert container.get('Config.Entrypoint') == ['printf']
|
||||||
container = service.containers(stopped=True, one_off=OneOffFilter.only)[0]
|
assert container.get('Config.Cmd') == ['default', 'args']
|
||||||
self.assertEqual(
|
|
||||||
shlex.split(container.human_readable_command),
|
def test_run_service_with_dockerfile_entrypoint_overridden(self):
|
||||||
[u'/bin/echo', u'helloworld'],
|
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):
|
def test_run_service_with_user_overridden(self):
|
||||||
self.base_dir = 'tests/fixtures/user-composefile'
|
self.base_dir = 'tests/fixtures/user-composefile'
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
service:
|
|
||||||
build: .
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
version: "2"
|
||||||
|
services:
|
||||||
|
test:
|
||||||
|
image: busybox
|
||||||
|
entrypoint: printf
|
||||||
|
command: default args
|
|
@ -1,3 +1,4 @@
|
||||||
FROM busybox:latest
|
FROM busybox:latest
|
||||||
LABEL com.docker.compose.test_image=true
|
LABEL com.docker.compose.test_image=true
|
||||||
ENTRYPOINT echo "From prebuilt entrypoint"
|
ENTRYPOINT ["printf"]
|
||||||
|
CMD ["default", "args"]
|
|
@ -0,0 +1,4 @@
|
||||||
|
version: "2"
|
||||||
|
services:
|
||||||
|
test:
|
||||||
|
build: .
|
|
@ -738,7 +738,10 @@ class ServiceTest(DockerClientTestCase):
|
||||||
|
|
||||||
self.assertEqual(len(service.containers()), 1)
|
self.assertEqual(len(service.containers()), 1)
|
||||||
self.assertTrue(service.containers()[0].is_running)
|
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):
|
def test_scale_with_unexpected_exception(self):
|
||||||
"""Test that when scaling if the API returns an error, that is not of type
|
"""Test that when scaling if the API returns an error, that is not of type
|
||||||
|
|
|
@ -41,44 +41,30 @@ def test_get_image_digest_no_image(mock_service):
|
||||||
assert "doesn't define an image tag" in exc.exconly()
|
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['image'] = image_id = 'abcd'
|
|
||||||
mock_service.pull.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)
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def test_fetch_image_digest_for_build(mock_service):
|
|
||||||
mock_service.options['build'] = '.'
|
mock_service.options['build'] = '.'
|
||||||
mock_service.options['image'] = image_id = 'abcd'
|
mock_service.options['image'] = image_id = 'abcd'
|
||||||
mock_service.push.return_value = expected = 'sha256:thedigest'
|
mock_service.push.return_value = expected = 'sha256:thedigest'
|
||||||
mock_service.image.return_value = {'RepoDigests': ['digest1']}
|
mock_service.image.return_value = {'RepoDigests': ['digest1']}
|
||||||
|
|
||||||
digest = bundle.fetch_image_digest(mock_service)
|
digest = bundle.push_image(mock_service)
|
||||||
assert digest == image_id + '@' + expected
|
assert digest == image_id + '@' + expected
|
||||||
|
|
||||||
mock_service.push.assert_called_once_with()
|
mock_service.push.assert_called_once_with()
|
||||||
assert not mock_service.pull.called
|
assert not mock_service.client.push.called
|
||||||
assert not mock_service.client.pull.called
|
|
||||||
|
|
||||||
|
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': []}
|
||||||
|
|
||||||
|
digest = bundle.push_image(mock_service)
|
||||||
|
assert digest == image_id + '@' + expected
|
||||||
|
|
||||||
|
mock_service.push.assert_called_once_with()
|
||||||
|
mock_service.client.pull.assert_called_once_with(digest)
|
||||||
|
|
||||||
|
|
||||||
def test_to_bundle():
|
def test_to_bundle():
|
||||||
|
|
|
@ -2,10 +2,13 @@ from __future__ import absolute_import
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
|
|
||||||
import docker
|
import docker
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
import compose
|
||||||
|
from compose.cli import errors
|
||||||
from compose.cli.docker_client import docker_client
|
from compose.cli.docker_client import docker_client
|
||||||
from compose.cli.docker_client import tls_config_from_options
|
from compose.cli.docker_client import tls_config_from_options
|
||||||
from tests import mock
|
from tests import mock
|
||||||
|
@ -19,11 +22,35 @@ class DockerClientTestCase(unittest.TestCase):
|
||||||
del os.environ['HOME']
|
del os.environ['HOME']
|
||||||
docker_client(os.environ)
|
docker_client(os.environ)
|
||||||
|
|
||||||
|
@mock.patch.dict(os.environ)
|
||||||
def test_docker_client_with_custom_timeout(self):
|
def test_docker_client_with_custom_timeout(self):
|
||||||
timeout = 300
|
os.environ['COMPOSE_HTTP_TIMEOUT'] = '123'
|
||||||
with mock.patch('compose.cli.docker_client.HTTP_TIMEOUT', 300):
|
client = docker_client(os.environ)
|
||||||
client = docker_client(os.environ)
|
assert client.timeout == 123
|
||||||
self.assertEqual(client.timeout, int(timeout))
|
|
||||||
|
@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):
|
class TLSConfigTestCase(unittest.TestCase):
|
||||||
|
|
|
@ -510,3 +510,35 @@ class ProjectTest(unittest.TestCase):
|
||||||
|
|
||||||
project.down(ImageType.all, True)
|
project.down(ImageType.all, True)
|
||||||
self.mock_client.remove_image.assert_called_once_with("busybox:latest")
|
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
|
||||||
|
|
Loading…
Reference in New Issue