1
0
Эх сурвалжийг харах

Merge pull request #6835 from docker/bump-1.25.0-rc2

Bump 1.25.0-rc2
Ulysses Souza 6 жил өмнө
parent
commit
2c668e237d
100 өөрчлөгдсөн 891 нэмэгдсэн , 457 устгасан
  1. 1 1
      .circleci/config.yml
  2. 67 3
      CHANGELOG.md
  3. 61 26
      Dockerfile
  4. 0 39
      Dockerfile.armhf
  5. 0 19
      Dockerfile.run
  6. 33 22
      Jenkinsfile
  7. 18 5
      MAINTAINERS
  8. 3 3
      appveyor.yml
  9. 1 1
      compose/__init__.py
  10. 33 16
      compose/bundle.py
  11. 28 8
      compose/cli/command.py
  12. 1 1
      compose/cli/docker_client.py
  13. 49 29
      compose/cli/main.py
  14. 1 1
      compose/cli/utils.py
  15. 42 21
      compose/config/config.py
  16. 9 5
      compose/config/environment.py
  17. 6 6
      compose/config/interpolation.py
  18. 14 6
      compose/config/serialize.py
  19. 3 3
      compose/network.py
  20. 25 17
      compose/project.py
  21. 35 20
      compose/service.py
  22. 2 2
      compose/volume.py
  23. 10 2
      contrib/completion/bash/docker-compose
  24. 1 0
      contrib/completion/fish/docker-compose.fish
  25. 3 0
      contrib/completion/zsh/_docker-compose
  26. 3 3
      contrib/migration/migrate-compose-file-v1-to-v2.py
  27. 20 0
      docker-compose-entrypoint.sh
  28. 2 4
      docs/README.md
  29. 13 0
      pyinstaller/ldd
  30. 1 1
      requirements-build.txt
  31. 2 1
      requirements-dev.txt
  32. 5 5
      requirements.txt
  33. 7 4
      script/build/image
  34. 11 7
      script/build/linux
  35. 33 9
      script/build/linux-entrypoint
  36. 3 2
      script/build/osx
  37. 8 7
      script/build/test-image
  38. 3 3
      script/build/windows.ps1
  39. 1 1
      script/build/write-git-sha
  40. 2 0
      script/release/README.md
  41. 5 3
      script/release/release.py
  42. 1 0
      script/release/release/const.py
  43. 96 31
      script/release/release/images.py
  44. 2 1
      script/release/release/repository.py
  45. 2 2
      script/run/run.sh
  46. 15 9
      script/setup/osx
  47. 2 3
      script/test/all
  48. 0 3
      script/test/ci
  49. 4 3
      script/test/default
  50. 12 12
      setup.py
  51. 98 21
      tests/acceptance/cli_test.py
  52. 2 2
      tests/fixtures/UpperCaseDir/docker-compose.yml
  53. 2 2
      tests/fixtures/abort-on-container-exit-0/docker-compose.yml
  54. 2 2
      tests/fixtures/abort-on-container-exit-1/docker-compose.yml
  55. 1 1
      tests/fixtures/build-args/Dockerfile
  56. 1 1
      tests/fixtures/build-ctx/Dockerfile
  57. 1 1
      tests/fixtures/build-memory/Dockerfile
  58. 1 1
      tests/fixtures/build-multiple-composefile/a/Dockerfile
  59. 1 1
      tests/fixtures/build-multiple-composefile/b/Dockerfile
  60. 4 0
      tests/fixtures/default-env-file/.env2
  61. 1 1
      tests/fixtures/dockerfile-with-volume/Dockerfile
  62. 2 2
      tests/fixtures/duplicate-override-yaml-files/docker-compose.yml
  63. 2 2
      tests/fixtures/echo-services/docker-compose.yml
  64. 1 1
      tests/fixtures/entrypoint-dockerfile/Dockerfile
  65. 2 0
      tests/fixtures/env-file-override/.env.conf
  66. 1 0
      tests/fixtures/env-file-override/.env.override
  67. 6 0
      tests/fixtures/env-file-override/docker-compose.yml
  68. 1 1
      tests/fixtures/environment-composefile/docker-compose.yml
  69. 2 2
      tests/fixtures/exit-code-from/docker-compose.yml
  70. 1 1
      tests/fixtures/expose-composefile/docker-compose.yml
  71. 1 1
      tests/fixtures/images-service-tag/Dockerfile
  72. 2 2
      tests/fixtures/logging-composefile-legacy/docker-compose.yml
  73. 2 2
      tests/fixtures/logging-composefile/docker-compose.yml
  74. 4 4
      tests/fixtures/logs-composefile/docker-compose.yml
  75. 3 3
      tests/fixtures/logs-restart-composefile/docker-compose.yml
  76. 1 1
      tests/fixtures/logs-tail-composefile/docker-compose.yml
  77. 1 1
      tests/fixtures/longer-filename-composefile/docker-compose.yaml
  78. 1 1
      tests/fixtures/multiple-composefiles/compose2.yml
  79. 2 2
      tests/fixtures/multiple-composefiles/docker-compose.yml
  80. 2 2
      tests/fixtures/networks/default-network-config.yml
  81. 2 2
      tests/fixtures/networks/external-default.yml
  82. 3 3
      tests/fixtures/no-links-composefile/docker-compose.yml
  83. 2 2
      tests/fixtures/override-files/docker-compose.yml
  84. 1 1
      tests/fixtures/override-files/extra.yml
  85. 2 2
      tests/fixtures/override-yaml-files/docker-compose.yml
  86. 1 1
      tests/fixtures/ports-composefile-scale/docker-compose.yml
  87. 1 1
      tests/fixtures/ports-composefile/docker-compose.yml
  88. 1 1
      tests/fixtures/ports-composefile/expanded-notation.yml
  89. 1 1
      tests/fixtures/ps-services-filter/docker-compose.yml
  90. 1 1
      tests/fixtures/run-labels/docker-compose.yml
  91. 1 1
      tests/fixtures/run-workdir/docker-compose.yml
  92. 6 2
      tests/fixtures/scale/docker-compose.yml
  93. 1 1
      tests/fixtures/simple-composefile-volume-ready/docker-compose.merge.yml
  94. 1 1
      tests/fixtures/simple-composefile-volume-ready/docker-compose.yml
  95. 1 1
      tests/fixtures/simple-composefile/digest.yml
  96. 1 1
      tests/fixtures/simple-composefile/docker-compose.yml
  97. 1 1
      tests/fixtures/simple-composefile/ignore-pull-failures.yml
  98. 11 0
      tests/fixtures/simple-composefile/pull-with-build.yml
  99. 1 1
      tests/fixtures/simple-failing-dockerfile/Dockerfile
  100. 2 2
      tests/fixtures/sleeps-composefile/docker-compose.yml

+ 1 - 1
.circleci/config.yml

@@ -13,7 +13,7 @@ jobs:
         command: sudo pip install --upgrade tox==2.1.1 virtualenv==16.2.0
     - run:
         name: unit tests
-        command: tox -e py27,py36,py37 -- tests/unit
+        command: tox -e py27,py37 -- tests/unit
 
   build-osx-binary:
     macos:

+ 67 - 3
CHANGELOG.md

@@ -1,14 +1,78 @@
 Change log
 ==========
 
-1.24.1 (2019-06-24)
+1.25.0-rc2 (2019-08-06)
 -------------------
 
+### Features
+
+- Add tag `docker-compose:latest`
+
+- Add `docker-compose:<version>-alpine` image/tag
+
+- Add `docker-compose:<version>-debian` image/tag
+
+- Bumped `docker-py` 4.0.1
+
+- Supports `requests` up to 2.22.0 version
+
+- Drops empty tag on `build:cache_from`
+
+- `Dockerfile` now generates `libmusl` binaries for alpine
+
+- Only pull images that can't be built
+
+- Attribute `scale` can now accept `0` as a value
+
+- Added `--quiet` build flag
+
+- Added `--no-interpolate` to `docker-compose config`
+
+- Bump OpenSSL for macOS build (`1.1.0j` to `1.1.1a`)
+
+- Added `--no-rm` to `build` command
+
+- Added support for `credential_spec`
+
+- Resolve digests without pulling image
+
+- Upgrade `pyyaml` to `4.2b1`
+
+- Lowered severity to `warning` if `down` tries to remove nonexisting image
+
+- Use improved API fields for project events when possible
+
+- Update `setup.py` for modern `pypi/setuptools` and remove `pandoc` dependencies
+
+- Removed `Dockerfile.armhf` which is no longer needed
+
 ### Bugfixes
 
-- Fixed acceptance tests
+- Fixed stdin_open
+
+- Fixed `--remove-orphans` when used with `up --no-start`
+
+- Fixed `docker-compose ps --all`
+
+- Fixed `depends_on` dependency recreation behavior
+
+- Fixed bash completion for `build --memory`
+
+- Fixed misleading warning concerning env vars when performing an `exec` command
+
+- Fixed failure check in parallel_execute_watch
+
+- Fixed race condition after pulling image
+
+- Fixed error on duplicate mount points.
+
+- Fixed merge on networks section
+
+- Always connect Compose container to `stdin`
+
+- Fixed the presentation of failed services on 'docker-compose start' when containers are not available
 
-1.24.0 (2019-03-22)
+1.24.0 (2019-03-28)
 -------------------
 
 ### Features

+ 61 - 26
Dockerfile

@@ -1,36 +1,71 @@
-FROM docker:18.06.1 as docker
-FROM python:3.6
+ARG DOCKER_VERSION=18.09.7
+ARG PYTHON_VERSION=3.7.4
+ARG BUILD_ALPINE_VERSION=3.10
+ARG BUILD_DEBIAN_VERSION=slim-stretch
+ARG RUNTIME_ALPINE_VERSION=3.10.0
+ARG RUNTIME_DEBIAN_VERSION=stretch-20190708-slim
 
-RUN set -ex; \
-    apt-get update -qq; \
-    apt-get install -y \
-        locales \
-        python-dev \
-        git
+ARG BUILD_PLATFORM=alpine
 
-COPY --from=docker /usr/local/bin/docker /usr/local/bin/docker
+FROM docker:${DOCKER_VERSION} AS docker-cli
 
-# Python3 requires a valid locale
-RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && locale-gen
-ENV LANG en_US.UTF-8
+FROM python:${PYTHON_VERSION}-alpine${BUILD_ALPINE_VERSION} AS build-alpine
+RUN apk add --no-cache \
+    bash \
+    build-base \
+    ca-certificates \
+    curl \
+    gcc \
+    git \
+    libc-dev \
+    libffi-dev \
+    libgcc \
+    make \
+    musl-dev \
+    openssl \
+    openssl-dev \
+    python2 \
+    python2-dev \
+    zlib-dev
+ENV BUILD_BOOTLOADER=1
 
-RUN useradd -d /home/user -m -s /bin/bash user
-WORKDIR /code/
+FROM python:${PYTHON_VERSION}-${BUILD_DEBIAN_VERSION} AS build-debian
+RUN apt-get update && apt-get install -y \
+    curl \
+    gcc \
+    git \
+    libc-dev \
+    libgcc-6-dev \
+    make \
+    openssl \
+    python2.7-dev
 
+FROM build-${BUILD_PLATFORM} AS build
+COPY docker-compose-entrypoint.sh /usr/local/bin/
+ENTRYPOINT ["sh", "/usr/local/bin/docker-compose-entrypoint.sh"]
+COPY --from=docker-cli /usr/local/bin/docker /usr/local/bin/docker
+WORKDIR /code/
 # FIXME(chris-crone): virtualenv 16.3.0 breaks build, force 16.2.0 until fixed
 RUN pip install virtualenv==16.2.0
-RUN pip install tox==2.1.1
+RUN pip install tox==2.9.1
 
-ADD requirements.txt /code/
-ADD requirements-dev.txt /code/
-ADD .pre-commit-config.yaml /code/
-ADD setup.py /code/
-ADD tox.ini /code/
-ADD compose /code/compose/
-ADD README.md /code/
+COPY requirements.txt .
+COPY requirements-dev.txt .
+COPY .pre-commit-config.yaml .
+COPY tox.ini .
+COPY setup.py .
+COPY README.md .
+COPY compose compose/
 RUN tox --notest
+COPY . .
+ARG GIT_COMMIT=unknown
+ENV DOCKER_COMPOSE_GITSHA=$GIT_COMMIT
+RUN script/build/linux-entrypoint
 
-ADD . /code/
-RUN chown -R user /code/
-
-ENTRYPOINT ["/code/.tox/py36/bin/docker-compose"]
+FROM alpine:${RUNTIME_ALPINE_VERSION} AS runtime-alpine
+FROM debian:${RUNTIME_DEBIAN_VERSION} AS runtime-debian
+FROM runtime-${BUILD_PLATFORM} AS runtime
+COPY docker-compose-entrypoint.sh /usr/local/bin/
+ENTRYPOINT ["sh", "/usr/local/bin/docker-compose-entrypoint.sh"]
+COPY --from=docker-cli  /usr/local/bin/docker           /usr/local/bin/docker
+COPY --from=build       /usr/local/bin/docker-compose   /usr/local/bin/docker-compose

+ 0 - 39
Dockerfile.armhf

@@ -1,39 +0,0 @@
-FROM python:3.6
-
-RUN set -ex; \
-    apt-get update -qq; \
-    apt-get install -y \
-        locales \
-        curl \
-        python-dev \
-        git
-
-RUN curl -fsSL -o dockerbins.tgz "https://download.docker.com/linux/static/stable/armhf/docker-17.12.0-ce.tgz" && \
-    SHA256=f8de6378dad825b9fd5c3c2f949e791d22f918623c27a72c84fd6975a0e5d0a2; \
-    echo "${SHA256}  dockerbins.tgz" | sha256sum -c - && \
-    tar xvf dockerbins.tgz docker/docker --strip-components 1 && \
-    mv docker /usr/local/bin/docker && \
-    chmod +x /usr/local/bin/docker && \
-    rm dockerbins.tgz
-
-# Python3 requires a valid locale
-RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && locale-gen
-ENV LANG en_US.UTF-8
-
-RUN useradd -d /home/user -m -s /bin/bash user
-WORKDIR /code/
-
-RUN pip install tox==2.1.1
-
-ADD requirements.txt /code/
-ADD requirements-dev.txt /code/
-ADD .pre-commit-config.yaml /code/
-ADD setup.py /code/
-ADD tox.ini /code/
-ADD compose /code/compose/
-RUN tox --notest
-
-ADD . /code/
-RUN chown -R user /code/
-
-ENTRYPOINT ["/code/.tox/py36/bin/docker-compose"]

+ 0 - 19
Dockerfile.run

@@ -1,19 +0,0 @@
-FROM docker:18.06.1 as docker
-FROM alpine:3.8
-
-ENV GLIBC 2.28-r0
-
-RUN apk update && apk add --no-cache openssl ca-certificates curl libgcc && \
-    curl -fsSL -o /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub && \
-    curl -fsSL -o glibc-$GLIBC.apk https://github.com/sgerrand/alpine-pkg-glibc/releases/download/$GLIBC/glibc-$GLIBC.apk && \
-    apk add --no-cache glibc-$GLIBC.apk && \
-    ln -s /lib/libz.so.1 /usr/glibc-compat/lib/ && \
-    ln -s /lib/libc.musl-x86_64.so.1 /usr/glibc-compat/lib && \
-    ln -s /usr/lib/libgcc_s.so.1 /usr/glibc-compat/lib && \
-    rm /etc/apk/keys/sgerrand.rsa.pub glibc-$GLIBC.apk && \
-    apk del curl
-
-COPY --from=docker /usr/local/bin/docker /usr/local/bin/docker
-COPY dist/docker-compose-Linux-x86_64 /usr/local/bin/docker-compose
-
-ENTRYPOINT ["docker-compose"]

+ 33 - 22
Jenkinsfile

@@ -1,29 +1,38 @@
 #!groovy
 
-def image
-
-def buildImage = { ->
+def buildImage = { String baseImage ->
+  def image
   wrappedNode(label: "ubuntu && !zfs", cleanWorkspace: true) {
-    stage("build image") {
+    stage("build image for \"${baseImage}\"") {
       checkout(scm)
-      def imageName = "dockerbuildbot/compose:${gitCommit()}"
+      def imageName = "dockerbuildbot/compose:${baseImage}-${gitCommit()}"
       image = docker.image(imageName)
       try {
         image.pull()
       } catch (Exception exc) {
-        image = docker.build(imageName, ".")
-        image.push()
+        sh """GIT_COMMIT=\$(script/build/write-git-sha) && \\
+            docker build -t ${imageName} \\
+            --target build \\
+            --build-arg BUILD_PLATFORM="${baseImage}" \\
+            --build-arg GIT_COMMIT="${GIT_COMMIT}" \\
+            .\\
+        """
+        sh "docker push ${imageName}"
+        echo "${imageName}"
+        return imageName
       }
     }
   }
+  echo "image.id: ${image.id}"
+  return image.id
 }
 
-def get_versions = { int number ->
+def get_versions = { String imageId, int number ->
   def docker_versions
   wrappedNode(label: "ubuntu && !zfs") {
     def result = sh(script: """docker run --rm \\
         --entrypoint=/code/.tox/py27/bin/python \\
-        ${image.id} \\
+        ${imageId} \\
         /code/script/test/versions.py -n ${number} docker/docker-ce recent
       """, returnStdout: true
     )
@@ -35,9 +44,11 @@ def get_versions = { int number ->
 def runTests = { Map settings ->
   def dockerVersions = settings.get("dockerVersions", null)
   def pythonVersions = settings.get("pythonVersions", null)
+  def baseImage = settings.get("baseImage", null)
+  def imageName = settings.get("image", null)
 
   if (!pythonVersions) {
-    throw new Exception("Need Python versions to test. e.g.: `runTests(pythonVersions: 'py27,py36')`")
+    throw new Exception("Need Python versions to test. e.g.: `runTests(pythonVersions: 'py27,py37')`")
   }
   if (!dockerVersions) {
     throw new Exception("Need Docker versions to test. e.g.: `runTests(dockerVersions: 'all')`")
@@ -45,7 +56,7 @@ def runTests = { Map settings ->
 
   { ->
     wrappedNode(label: "ubuntu && !zfs", cleanWorkspace: true) {
-      stage("test python=${pythonVersions} / docker=${dockerVersions}") {
+      stage("test python=${pythonVersions} / docker=${dockerVersions} / baseImage=${baseImage}") {
         checkout(scm)
         def storageDriver = sh(script: 'docker info | awk -F \': \' \'$1 == "Storage Driver" { print $2; exit }\'', returnStdout: true).trim()
         echo "Using local system's storage driver: ${storageDriver}"
@@ -55,13 +66,13 @@ def runTests = { Map settings ->
           --privileged \\
           --volume="\$(pwd)/.git:/code/.git" \\
           --volume="/var/run/docker.sock:/var/run/docker.sock" \\
-          -e "TAG=${image.id}" \\
+          -e "TAG=${imageName}" \\
           -e "STORAGE_DRIVER=${storageDriver}" \\
           -e "DOCKER_VERSIONS=${dockerVersions}" \\
           -e "BUILD_NUMBER=\$BUILD_TAG" \\
           -e "PY_TEST_VERSIONS=${pythonVersions}" \\
           --entrypoint="script/test/ci" \\
-          ${image.id} \\
+          ${imageName} \\
           --verbose
         """
       }
@@ -69,16 +80,16 @@ def runTests = { Map settings ->
   }
 }
 
-buildImage()
-
 def testMatrix = [failFast: true]
-def docker_versions = get_versions(2)
-
-for (int i = 0; i < docker_versions.length; i++) {
-  def dockerVersion = docker_versions[i]
-  testMatrix["${dockerVersion}_py27"] = runTests([dockerVersions: dockerVersion, pythonVersions: "py27"])
-  testMatrix["${dockerVersion}_py36"] = runTests([dockerVersions: dockerVersion, pythonVersions: "py36"])
-  testMatrix["${dockerVersion}_py37"] = runTests([dockerVersions: dockerVersion, pythonVersions: "py37"])
+def baseImages = ['alpine', 'debian']
+def pythonVersions = ['py27', 'py37']
+baseImages.each { baseImage ->
+  def imageName = buildImage(baseImage)
+  get_versions(imageName, 2).each { dockerVersion ->
+    pythonVersions.each { pyVersion ->
+      testMatrix["${baseImage}_${dockerVersion}_${pyVersion}"] = runTests([baseImage: baseImage, image: imageName, dockerVersions: dockerVersion, pythonVersions: pyVersion])
+    }
+  }
 }
 
 parallel(testMatrix)

+ 18 - 5
MAINTAINERS

@@ -11,9 +11,8 @@
 [Org]
 	[Org."Core maintainers"]
 		people = [
-			"mefyl",
-			"mnottale",
-			"shin-",
+			"rumpl",
+			"ulyssessouza",
 		]
 	[Org.Alumni]
 		people = [
@@ -34,6 +33,10 @@
 			# including muti-file support, variable interpolation, secrets
 			# emulation and many more
 			"dnephin",
+
+			"shin-",
+			"mefyl",
+			"mnottale",
 		]
 
 [people]
@@ -74,7 +77,17 @@
 	Email = "[email protected]"
 	GitHub = "mnowster"
 
-	[People.shin-]
+	[people.rumpl]
+	Name = "Djordje Lukic"
+	Email = "[email protected]"
+	GitHub = "rumpl"
+
+	[people.shin-]
 	Name = "Joffrey F"
-	Email = "[email protected]"
+	Email = "f.joffrey@gmail.com"
 	GitHub = "shin-"
+
+	[people.ulyssessouza]
+	Name = "Ulysses Domiciano Souza"
+	Email = "[email protected]"
+	GitHub = "ulyssessouza"

+ 3 - 3
appveyor.yml

@@ -2,15 +2,15 @@
 version: '{branch}-{build}'
 
 install:
-  - "SET PATH=C:\\Python36-x64;C:\\Python36-x64\\Scripts;%PATH%"
+  - "SET PATH=C:\\Python37-x64;C:\\Python37-x64\\Scripts;%PATH%"
   - "python --version"
-  - "pip install tox==2.9.1 virtualenv==15.1.0"
+  - "pip install tox==2.9.1 virtualenv==16.2.0"
 
 # Build the binary after tests
 build: false
 
 test_script:
-  - "tox -e py27,py36,py37 -- tests/unit"
+  - "tox -e py27,py37 -- tests/unit"
   - ps: ".\\script\\build\\windows.ps1"
 
 artifacts:

+ 1 - 1
compose/__init__.py

@@ -1,4 +1,4 @@
 from __future__ import absolute_import
 from __future__ import unicode_literals
 
-__version__ = '1.24.1'
+__version__ = '1.25.0-rc2'

+ 33 - 16
compose/bundle.py

@@ -95,19 +95,10 @@ def get_image_digest(service, allow_push=False):
     if separator == '@':
         return service.options['image']
 
-    try:
-        image = service.image()
-    except NoSuchImageError:
-        action = 'build' if 'build' in service.options else 'pull'
-        raise UserError(
-            "Image not found for service '{service}'. "
-            "You might need to run `docker-compose {action} {service}`."
-            .format(service=service.name, action=action))
+    digest = get_digest(service)
 
-    if image['RepoDigests']:
-        # TODO: pick a digest based on the image tag if there are multiple
-        # digests
-        return image['RepoDigests'][0]
+    if digest:
+        return digest
 
     if 'build' not in service.options:
         raise NeedsPull(service.image_name, service.name)
@@ -118,6 +109,32 @@ def get_image_digest(service, allow_push=False):
     return push_image(service)
 
 
+def get_digest(service):
+    digest = None
+    try:
+        image = service.image()
+        # TODO: pick a digest based on the image tag if there are multiple
+        # digests
+        if image['RepoDigests']:
+            digest = image['RepoDigests'][0]
+    except NoSuchImageError:
+        try:
+            # Fetch the image digest from the registry
+            distribution = service.get_image_registry_data()
+
+            if distribution['Descriptor']['digest']:
+                digest = '{image_name}@{digest}'.format(
+                    image_name=service.image_name,
+                    digest=distribution['Descriptor']['digest']
+                )
+        except NoSuchImageError:
+            raise UserError(
+                "Digest not found for service '{service}'. "
+                "Repository does not exist or may require 'docker login'"
+                .format(service=service.name))
+    return digest
+
+
 def push_image(service):
     try:
         digest = service.push()
@@ -147,10 +164,10 @@ def push_image(service):
 
 def to_bundle(config, image_digests):
     if config.networks:
-        log.warn("Unsupported top level key 'networks' - ignoring")
+        log.warning("Unsupported top level key 'networks' - ignoring")
 
     if config.volumes:
-        log.warn("Unsupported top level key 'volumes' - ignoring")
+        log.warning("Unsupported top level key 'volumes' - ignoring")
 
     config = denormalize_config(config)
 
@@ -175,7 +192,7 @@ def convert_service_to_bundle(name, service_dict, image_digest):
             continue
 
         if key not in SUPPORTED_KEYS:
-            log.warn("Unsupported key '{}' in services.{} - ignoring".format(key, name))
+            log.warning("Unsupported key '{}' in services.{} - ignoring".format(key, name))
             continue
 
         if key == 'environment':
@@ -222,7 +239,7 @@ def make_service_networks(name, service_dict):
 
     for network_name, network_def in get_network_defs_for_service(service_dict).items():
         for key in network_def.keys():
-            log.warn(
+            log.warning(
                 "Unsupported key '{}' in services.{}.networks.{} - ignoring"
                 .format(key, name, network_name))
 

+ 28 - 8
compose/cli/command.py

@@ -21,10 +21,27 @@ from .utils import get_version_info
 
 log = logging.getLogger(__name__)
 
-
-def project_from_options(project_dir, options):
+SILENT_COMMANDS = {
+    'events',
+    'exec',
+    'kill',
+    'logs',
+    'pause',
+    'ps',
+    'restart',
+    'rm',
+    'start',
+    'stop',
+    'top',
+    'unpause',
+}
+
+
+def project_from_options(project_dir, options, additional_options={}):
     override_dir = options.get('--project-directory')
-    environment = Environment.from_env_file(override_dir or project_dir)
+    environment_file = options.get('--env-file')
+    environment = Environment.from_env_file(override_dir or project_dir, environment_file)
+    environment.silent = options.get('COMMAND', None) in SILENT_COMMANDS
     set_parallel_limit(environment)
 
     host = options.get('--host')
@@ -40,6 +57,7 @@ def project_from_options(project_dir, options):
         environment=environment,
         override_dir=override_dir,
         compatibility=options.get('--compatibility'),
+        interpolate=(not additional_options.get('--no-interpolate'))
     )
 
 
@@ -59,15 +77,17 @@ def set_parallel_limit(environment):
         parallel.GlobalLimit.set_global_limit(parallel_limit)
 
 
-def get_config_from_options(base_dir, options):
+def get_config_from_options(base_dir, options, additional_options={}):
     override_dir = options.get('--project-directory')
-    environment = Environment.from_env_file(override_dir or base_dir)
+    environment_file = options.get('--env-file')
+    environment = Environment.from_env_file(override_dir or base_dir, environment_file)
     config_path = get_config_path_from_options(
         base_dir, options, environment
     )
     return config.load(
         config.find(base_dir, config_path, environment, override_dir),
-        options.get('--compatibility')
+        options.get('--compatibility'),
+        not additional_options.get('--no-interpolate')
     )
 
 
@@ -105,14 +125,14 @@ def get_client(environment, verbose=False, version=None, tls_config=None, host=N
 
 def get_project(project_dir, config_path=None, project_name=None, verbose=False,
                 host=None, tls_config=None, environment=None, override_dir=None,
-                compatibility=False):
+                compatibility=False, interpolate=True):
     if not environment:
         environment = Environment.from_env_file(project_dir)
     config_details = config.find(project_dir, config_path, environment, override_dir)
     project_name = get_project_name(
         config_details.working_dir, project_name, environment
     )
-    config_data = config.load(config_details, compatibility)
+    config_data = config.load(config_details, compatibility, interpolate)
 
     api_version = environment.get(
         'COMPOSE_API_VERSION',

+ 1 - 1
compose/cli/docker_client.py

@@ -31,7 +31,7 @@ def get_tls_version(environment):
 
     tls_attr_name = "PROTOCOL_{}".format(compose_tls_version)
     if not hasattr(ssl, tls_attr_name):
-        log.warn(
+        log.warning(
             'The "{}" protocol is unavailable. You may need to update your '
             'version of Python or OpenSSL. Falling back to TLSv1 (default).'
             .format(compose_tls_version)

+ 49 - 29
compose/cli/main.py

@@ -208,6 +208,7 @@ class TopLevelCommand(object):
                                   (default: the path of the Compose file)
       --compatibility             If set, Compose will attempt to convert keys
                                   in v3 files to their non-Swarm equivalent
+      --env-file PATH             Specify an alternate environment file
 
     Commands:
       build              Build or rebuild services
@@ -246,6 +247,11 @@ class TopLevelCommand(object):
     def project_dir(self):
         return self.toplevel_options.get('--project-directory') or '.'
 
+    @property
+    def toplevel_environment(self):
+        environment_file = self.toplevel_options.get('--env-file')
+        return Environment.from_env_file(self.project_dir, environment_file)
+
     def build(self, options):
         """
         Build or rebuild services.
@@ -260,10 +266,12 @@ class TopLevelCommand(object):
             --compress              Compress the build context using gzip.
             --force-rm              Always remove intermediate containers.
             --no-cache              Do not use cache when building the image.
+            --no-rm                 Do not remove intermediate containers after a successful build.
             --pull                  Always attempt to pull a newer version of the image.
             -m, --memory MEM        Sets memory limit for the build container.
             --build-arg key=val     Set build-time variables for services.
             --parallel              Build images in parallel.
+            -q, --quiet             Don't print anything to STDOUT
         """
         service_names = options['SERVICE']
         build_args = options.get('--build-arg', None)
@@ -273,8 +281,7 @@ class TopLevelCommand(object):
                     '--build-arg is only supported when services are specified for API version < 1.25.'
                     ' Please use a Compose file version > 2.2 or specify which services to build.'
                 )
-            environment = Environment.from_env_file(self.project_dir)
-            build_args = resolve_build_args(build_args, environment)
+            build_args = resolve_build_args(build_args, self.toplevel_environment)
 
         self.project.build(
             service_names=options['SERVICE'],
@@ -282,9 +289,11 @@ class TopLevelCommand(object):
             pull=bool(options.get('--pull', False)),
             force_rm=bool(options.get('--force-rm', False)),
             memory=options.get('--memory'),
+            rm=not bool(options.get('--no-rm', False)),
             build_args=build_args,
             gzip=options.get('--compress', False),
             parallel_build=options.get('--parallel', False),
+            silent=options.get('--quiet', False)
         )
 
     def bundle(self, options):
@@ -327,6 +336,7 @@ class TopLevelCommand(object):
 
         Options:
             --resolve-image-digests  Pin image tags to digests.
+            --no-interpolate         Don't interpolate environment variables
             -q, --quiet              Only validate the configuration, don't print
                                      anything.
             --services               Print the service names, one per line.
@@ -336,11 +346,12 @@ class TopLevelCommand(object):
                                      or use the wildcard symbol to display all services
         """
 
-        compose_config = get_config_from_options('.', self.toplevel_options)
+        additional_options = {'--no-interpolate': options.get('--no-interpolate')}
+        compose_config = get_config_from_options('.', self.toplevel_options, additional_options)
         image_digests = None
 
         if options['--resolve-image-digests']:
-            self.project = project_from_options('.', self.toplevel_options)
+            self.project = project_from_options('.', self.toplevel_options, additional_options)
             with errors.handle_connection_errors(self.project.client):
                 image_digests = image_digests_for_project(self.project)
 
@@ -357,14 +368,14 @@ class TopLevelCommand(object):
 
         if options['--hash'] is not None:
             h = options['--hash']
-            self.project = project_from_options('.', self.toplevel_options)
+            self.project = project_from_options('.', self.toplevel_options, additional_options)
             services = [svc for svc in options['--hash'].split(',')] if h != '*' else None
             with errors.handle_connection_errors(self.project.client):
                 for service in self.project.get_services(services):
                     print('{} {}'.format(service.name, service.config_hash))
             return
 
-        print(serialize_config(compose_config, image_digests))
+        print(serialize_config(compose_config, image_digests, not options['--no-interpolate']))
 
     def create(self, options):
         """
@@ -383,7 +394,7 @@ class TopLevelCommand(object):
         """
         service_names = options['SERVICE']
 
-        log.warn(
+        log.warning(
             'The create command is deprecated. '
             'Use the up command with the --no-start flag instead.'
         )
@@ -422,8 +433,7 @@ class TopLevelCommand(object):
             -t, --timeout TIMEOUT   Specify a shutdown timeout in seconds.
                                     (default: 10)
         """
-        environment = Environment.from_env_file(self.project_dir)
-        ignore_orphans = environment.get_boolean('COMPOSE_IGNORE_ORPHANS')
+        ignore_orphans = self.toplevel_environment.get_boolean('COMPOSE_IGNORE_ORPHANS')
 
         if ignore_orphans and options['--remove-orphans']:
             raise UserError("COMPOSE_IGNORE_ORPHANS and --remove-orphans cannot be combined.")
@@ -480,8 +490,7 @@ class TopLevelCommand(object):
                               not supported in API < 1.25)
             -w, --workdir DIR Path to workdir directory for this command.
         """
-        environment = Environment.from_env_file(self.project_dir)
-        use_cli = not environment.get_boolean('COMPOSE_INTERACTIVE_NO_CLI')
+        use_cli = not self.toplevel_environment.get_boolean('COMPOSE_INTERACTIVE_NO_CLI')
         index = int(options.get('--index'))
         service = self.project.get_service(options['SERVICE'])
         detach = options.get('--detach')
@@ -504,7 +513,7 @@ class TopLevelCommand(object):
         if IS_WINDOWS_PLATFORM or use_cli and not detach:
             sys.exit(call_docker(
                 build_exec_command(options, container.id, command),
-                self.toplevel_options)
+                self.toplevel_options, self.toplevel_environment)
             )
 
         create_exec_options = {
@@ -709,7 +718,8 @@ class TopLevelCommand(object):
 
         if options['--all']:
             containers = sorted(self.project.containers(service_names=options['SERVICE'],
-                                                        one_off=OneOffFilter.include, stopped=True))
+                                                        one_off=OneOffFilter.include, stopped=True),
+                                key=attrgetter('name'))
         else:
             containers = sorted(
                 self.project.containers(service_names=options['SERVICE'], stopped=True) +
@@ -753,7 +763,7 @@ class TopLevelCommand(object):
             --include-deps          Also pull services declared as dependencies
         """
         if options.get('--parallel'):
-            log.warn('--parallel option is deprecated and will be removed in future versions.')
+            log.warning('--parallel option is deprecated and will be removed in future versions.')
         self.project.pull(
             service_names=options['SERVICE'],
             ignore_pull_failures=options.get('--ignore-pull-failures'),
@@ -794,7 +804,7 @@ class TopLevelCommand(object):
             -a, --all     Deprecated - no effect.
         """
         if options.get('--all'):
-            log.warn(
+            log.warning(
                 '--all flag is obsolete. This is now the default behavior '
                 'of `docker-compose rm`'
             )
@@ -872,10 +882,12 @@ class TopLevelCommand(object):
         else:
             command = service.options.get('command')
 
+        options['stdin_open'] = service.options.get('stdin_open', True)
+
         container_options = build_one_off_container_options(options, detach, command)
         run_one_off_container(
             container_options, self.project, service, options,
-            self.toplevel_options, self.project_dir
+            self.toplevel_options, self.toplevel_environment
         )
 
     def scale(self, options):
@@ -904,7 +916,7 @@ class TopLevelCommand(object):
                 'Use the up command with the --scale flag instead.'
             )
         else:
-            log.warn(
+            log.warning(
                 'The scale command is deprecated. '
                 'Use the up command with the --scale flag instead.'
             )
@@ -1050,8 +1062,7 @@ class TopLevelCommand(object):
         if detached and (cascade_stop or exit_value_from):
             raise UserError("--abort-on-container-exit and -d cannot be combined.")
 
-        environment = Environment.from_env_file(self.project_dir)
-        ignore_orphans = environment.get_boolean('COMPOSE_IGNORE_ORPHANS')
+        ignore_orphans = self.toplevel_environment.get_boolean('COMPOSE_IGNORE_ORPHANS')
 
         if ignore_orphans and remove_orphans:
             raise UserError("COMPOSE_IGNORE_ORPHANS and --remove-orphans cannot be combined.")
@@ -1236,7 +1247,7 @@ def exitval_from_opts(options, project):
     exit_value_from = options.get('--exit-code-from')
     if exit_value_from:
         if not options.get('--abort-on-container-exit'):
-            log.warn('using --exit-code-from implies --abort-on-container-exit')
+            log.warning('using --exit-code-from implies --abort-on-container-exit')
             options['--abort-on-container-exit'] = True
         if exit_value_from not in [s.name for s in project.get_services()]:
             log.error('No service named "%s" was found in your compose file.',
@@ -1271,7 +1282,7 @@ def build_one_off_container_options(options, detach, command):
     container_options = {
         'command': command,
         'tty': not (detach or options['-T'] or not sys.stdin.isatty()),
-        'stdin_open': not detach,
+        'stdin_open': options.get('stdin_open'),
         'detach': detach,
     }
 
@@ -1314,7 +1325,7 @@ def build_one_off_container_options(options, detach, command):
 
 
 def run_one_off_container(container_options, project, service, options, toplevel_options,
-                          project_dir='.'):
+                          toplevel_environment):
     if not options['--no-deps']:
         deps = service.get_dependency_names()
         if deps:
@@ -1343,8 +1354,7 @@ def run_one_off_container(container_options, project, service, options, toplevel
         if options['--rm']:
             project.client.remove_container(container.id, force=True, v=True)
 
-    environment = Environment.from_env_file(project_dir)
-    use_cli = not environment.get_boolean('COMPOSE_INTERACTIVE_NO_CLI')
+    use_cli = not toplevel_environment.get_boolean('COMPOSE_INTERACTIVE_NO_CLI')
 
     signals.set_signal_handler_to_shutdown()
     signals.set_signal_handler_to_hang_up()
@@ -1353,8 +1363,8 @@ def run_one_off_container(container_options, project, service, options, toplevel
             if IS_WINDOWS_PLATFORM or use_cli:
                 service.connect_container_to_networks(container, use_network_aliases)
                 exit_code = call_docker(
-                    ["start", "--attach", "--interactive", container.id],
-                    toplevel_options
+                    get_docker_start_call(container_options, container.id),
+                    toplevel_options, toplevel_environment
                 )
             else:
                 operation = RunOperation(
@@ -1380,6 +1390,16 @@ def run_one_off_container(container_options, project, service, options, toplevel
     sys.exit(exit_code)
 
 
+def get_docker_start_call(container_options, container_id):
+    docker_call = ["start"]
+    if not container_options.get('detach'):
+        docker_call.append("--attach")
+    if container_options.get('stdin_open'):
+        docker_call.append("--interactive")
+    docker_call.append(container_id)
+    return docker_call
+
+
 def log_printer_from_project(
     project,
     containers,
@@ -1434,7 +1454,7 @@ def exit_if(condition, message, exit_code):
         raise SystemExit(exit_code)
 
 
-def call_docker(args, dockeropts):
+def call_docker(args, dockeropts, environment):
     executable_path = find_executable('docker')
     if not executable_path:
         raise UserError(errors.docker_not_found_msg("Couldn't find `docker` binary."))
@@ -1464,7 +1484,7 @@ def call_docker(args, dockeropts):
     args = [executable_path] + tls_options + args
     log.debug(" ".join(map(pipes.quote, args)))
 
-    return subprocess.call(args)
+    return subprocess.call(args, env=environment)
 
 
 def parse_scale_args(options):
@@ -1565,7 +1585,7 @@ def warn_for_swarm_mode(client):
             # UCP does multi-node scheduling with traditional Compose files.
             return
 
-        log.warn(
+        log.warning(
             "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"

+ 1 - 1
compose/cli/utils.py

@@ -137,7 +137,7 @@ def human_readable_file_size(size):
     if order >= len(suffixes):
         order = len(suffixes) - 1
 
-    return '{0:.3g} {1}'.format(
+    return '{0:.4g} {1}'.format(
         size / float(1 << (order * 10)),
         suffixes[order]
     )

+ 42 - 21
compose/config/config.py

@@ -198,9 +198,9 @@ class ConfigFile(namedtuple('_ConfigFile', 'filename config')):
         version = self.config['version']
 
         if isinstance(version, dict):
-            log.warn('Unexpected type for "version" key in "{}". Assuming '
-                     '"version" is the name of a service, and defaulting to '
-                     'Compose file version 1.'.format(self.filename))
+            log.warning('Unexpected type for "version" key in "{}". Assuming '
+                        '"version" is the name of a service, and defaulting to '
+                        'Compose file version 1.'.format(self.filename))
             return V1
 
         if not isinstance(version, six.string_types):
@@ -318,8 +318,8 @@ def get_default_config_files(base_dir):
     winner = candidates[0]
 
     if len(candidates) > 1:
-        log.warn("Found multiple config files with supported names: %s", ", ".join(candidates))
-        log.warn("Using %s\n", winner)
+        log.warning("Found multiple config files with supported names: %s", ", ".join(candidates))
+        log.warning("Using %s\n", winner)
 
     return [os.path.join(path, winner)] + get_default_override_file(path)
 
@@ -362,7 +362,7 @@ def check_swarm_only_config(service_dicts, compatibility=False):
     def check_swarm_only_key(service_dicts, key):
         services = [s for s in service_dicts if s.get(key)]
         if services:
-            log.warn(
+            log.warning(
                 warning_template.format(
                     services=", ".join(sorted(s['name'] for s in services)),
                     key=key
@@ -373,7 +373,7 @@ def check_swarm_only_config(service_dicts, compatibility=False):
     check_swarm_only_key(service_dicts, 'configs')
 
 
-def load(config_details, compatibility=False):
+def load(config_details, compatibility=False, interpolate=True):
     """Load the configuration from a working directory and a list of
     configuration files.  Files are loaded in order, and merged on top
     of each other to create the final configuration.
@@ -383,7 +383,7 @@ def load(config_details, compatibility=False):
     validate_config_version(config_details.config_files)
 
     processed_files = [
-        process_config_file(config_file, config_details.environment)
+        process_config_file(config_file, config_details.environment, interpolate=interpolate)
         for config_file in config_details.config_files
     ]
     config_details = config_details._replace(config_files=processed_files)
@@ -505,7 +505,6 @@ def load_services(config_details, config_file, compatibility=False):
 
 
 def interpolate_config_section(config_file, config, section, environment):
-    validate_config_section(config_file.filename, config, section)
     return interpolate_environment_variables(
         config_file.version,
         config,
@@ -514,38 +513,60 @@ def interpolate_config_section(config_file, config, section, environment):
     )
 
 
-def process_config_file(config_file, environment, service_name=None):
-    services = interpolate_config_section(
+def process_config_section(config_file, config, section, environment, interpolate):
+    validate_config_section(config_file.filename, config, section)
+    if interpolate:
+        return interpolate_environment_variables(
+            config_file.version,
+            config,
+            section,
+            environment
+            )
+    else:
+        return config
+
+
+def process_config_file(config_file, environment, service_name=None, interpolate=True):
+    services = process_config_section(
         config_file,
         config_file.get_service_dicts(),
         'service',
-        environment)
+        environment,
+        interpolate,
+    )
 
     if config_file.version > V1:
         processed_config = dict(config_file.config)
         processed_config['services'] = services
-        processed_config['volumes'] = interpolate_config_section(
+        processed_config['volumes'] = process_config_section(
             config_file,
             config_file.get_volumes(),
             'volume',
-            environment)
-        processed_config['networks'] = interpolate_config_section(
+            environment,
+            interpolate,
+        )
+        processed_config['networks'] = process_config_section(
             config_file,
             config_file.get_networks(),
             'network',
-            environment)
+            environment,
+            interpolate,
+        )
         if config_file.version >= const.COMPOSEFILE_V3_1:
-            processed_config['secrets'] = interpolate_config_section(
+            processed_config['secrets'] = process_config_section(
                 config_file,
                 config_file.get_secrets(),
                 'secret',
-                environment)
+                environment,
+                interpolate,
+            )
         if config_file.version >= const.COMPOSEFILE_V3_3:
-            processed_config['configs'] = interpolate_config_section(
+            processed_config['configs'] = process_config_section(
                 config_file,
                 config_file.get_configs(),
                 'config',
-                environment
+                environment,
+                interpolate,
             )
     else:
         processed_config = services
@@ -900,7 +921,7 @@ def finalize_service(service_config, service_names, version, environment, compat
             service_dict
         )
         if ignored_keys:
-            log.warn(
+            log.warning(
                 'The following deploy sub-keys are not supported in compatibility mode and have'
                 ' been ignored: {}'.format(', '.join(ignored_keys))
             )

+ 9 - 5
compose/config/environment.py

@@ -26,7 +26,7 @@ def split_env(env):
         key = env
     if re.search(r'\s', key):
         raise ConfigurationError(
-            "environment variable name '{}' may not contains whitespace.".format(key)
+            "environment variable name '{}' may not contain whitespace.".format(key)
         )
     return key, value
 
@@ -56,14 +56,18 @@ class Environment(dict):
     def __init__(self, *args, **kwargs):
         super(Environment, self).__init__(*args, **kwargs)
         self.missing_keys = []
+        self.silent = False
 
     @classmethod
-    def from_env_file(cls, base_dir):
+    def from_env_file(cls, base_dir, env_file=None):
         def _initialize():
             result = cls()
             if base_dir is None:
                 return result
-            env_file_path = os.path.join(base_dir, '.env')
+            if env_file:
+                env_file_path = os.path.join(base_dir, env_file)
+            else:
+                env_file_path = os.path.join(base_dir, '.env')
             try:
                 return cls(env_vars_from_file(env_file_path))
             except EnvFileNotFound:
@@ -95,8 +99,8 @@ class Environment(dict):
                     return super(Environment, self).__getitem__(key.upper())
                 except KeyError:
                     pass
-            if key not in self.missing_keys:
-                log.warn(
+            if not self.silent and key not in self.missing_keys:
+                log.warning(
                     "The {} variable is not set. Defaulting to a blank string."
                     .format(key)
                 )

+ 6 - 6
compose/config/interpolation.py

@@ -64,12 +64,12 @@ def interpolate_value(name, config_key, value, section, interpolator):
                 string=e.string))
     except UnsetRequiredSubstitution as e:
         raise ConfigurationError(
-            'Missing mandatory value for "{config_key}" option in {section} "{name}": {err}'.format(
-                config_key=config_key,
-                name=name,
-                section=section,
-                err=e.err
-            )
+            'Missing mandatory value for "{config_key}" option interpolating {value} '
+            'in {section} "{name}": {err}'.format(config_key=config_key,
+                                                  value=value,
+                                                  name=name,
+                                                  section=section,
+                                                  err=e.err)
         )
 
 

+ 14 - 6
compose/config/serialize.py

@@ -24,14 +24,12 @@ def serialize_dict_type(dumper, data):
 
 
 def serialize_string(dumper, data):
-    """ Ensure boolean-like strings are quoted in the output and escape $ characters """
+    """ Ensure boolean-like strings are quoted in the output """
     representer = dumper.represent_str if six.PY3 else dumper.represent_unicode
 
     if isinstance(data, six.binary_type):
         data = data.decode('utf-8')
 
-    data = data.replace('$', '$$')
-
     if data.lower() in ('y', 'n', 'yes', 'no', 'on', 'off', 'true', 'false'):
         # Empirically only y/n appears to be an issue, but this might change
         # depending on which PyYaml version is being used. Err on safe side.
@@ -39,6 +37,12 @@ def serialize_string(dumper, data):
     return representer(data)
 
 
+def serialize_string_escape_dollar(dumper, data):
+    """ Ensure boolean-like strings are quoted in the output and escape $ characters """
+    data = data.replace('$', '$$')
+    return serialize_string(dumper, data)
+
+
 yaml.SafeDumper.add_representer(types.MountSpec, serialize_dict_type)
 yaml.SafeDumper.add_representer(types.VolumeFromSpec, serialize_config_type)
 yaml.SafeDumper.add_representer(types.VolumeSpec, serialize_config_type)
@@ -46,8 +50,6 @@ yaml.SafeDumper.add_representer(types.SecurityOpt, serialize_config_type)
 yaml.SafeDumper.add_representer(types.ServiceSecret, serialize_dict_type)
 yaml.SafeDumper.add_representer(types.ServiceConfig, serialize_dict_type)
 yaml.SafeDumper.add_representer(types.ServicePort, serialize_dict_type)
-yaml.SafeDumper.add_representer(str, serialize_string)
-yaml.SafeDumper.add_representer(six.text_type, serialize_string)
 
 
 def denormalize_config(config, image_digests=None):
@@ -93,7 +95,13 @@ def v3_introduced_name_key(key):
     return V3_5
 
 
-def serialize_config(config, image_digests=None):
+def serialize_config(config, image_digests=None, escape_dollar=True):
+    if escape_dollar:
+        yaml.SafeDumper.add_representer(str, serialize_string_escape_dollar)
+        yaml.SafeDumper.add_representer(six.text_type, serialize_string_escape_dollar)
+    else:
+        yaml.SafeDumper.add_representer(str, serialize_string)
+        yaml.SafeDumper.add_representer(six.text_type, serialize_string)
     return yaml.safe_dump(
         denormalize_config(config, image_digests),
         default_flow_style=False,

+ 3 - 3
compose/network.py

@@ -231,7 +231,7 @@ def check_remote_network_config(remote, local):
         if k.startswith('com.docker.'):  # We are only interested in user-specified labels
             continue
         if remote_labels.get(k) != local_labels.get(k):
-            log.warn(
+            log.warning(
                 'Network {}: label "{}" has changed. It may need to be'
                 ' recreated.'.format(local.true_name, k)
             )
@@ -276,7 +276,7 @@ class ProjectNetworks(object):
         }
         unused = set(networks) - set(service_networks) - {'default'}
         if unused:
-            log.warn(
+            log.warning(
                 "Some networks were defined but are not used by any service: "
                 "{}".format(", ".join(unused)))
         return cls(service_networks, use_networking)
@@ -288,7 +288,7 @@ class ProjectNetworks(object):
             try:
                 network.remove()
             except NotFound:
-                log.warn("Network %s not found.", network.true_name)
+                log.warning("Network %s not found.", network.true_name)
 
     def initialize(self):
         if not self.use_networking:

+ 25 - 17
compose/project.py

@@ -355,18 +355,17 @@ class Project(object):
         return containers
 
     def build(self, service_names=None, no_cache=False, pull=False, force_rm=False, memory=None,
-              build_args=None, gzip=False, parallel_build=False):
+              build_args=None, gzip=False, parallel_build=False, rm=True, silent=False):
 
         services = []
         for service in self.get_services(service_names):
             if service.can_be_built():
                 services.append(service)
-            else:
+            elif not silent:
                 log.info('%s uses an image, skipping' % service.name)
 
         def build_service(service):
-            service.build(no_cache, pull, force_rm, memory, build_args, gzip)
-
+            service.build(no_cache, pull, force_rm, memory, build_args, gzip, rm, silent)
         if parallel_build:
             _, errors = parallel.parallel_execute(
                 services,
@@ -587,8 +586,10 @@ class Project(object):
                           ", ".join(updated_dependencies))
                 containers_stopped = any(
                     service.containers(stopped=True, filters={'status': ['created', 'exited']}))
-                has_links = any(c.get('HostConfig.Links') for c in service.containers())
-                if always_recreate_deps or containers_stopped or not has_links:
+                service_has_links = any(service.get_link_names())
+                container_has_links = any(c.get('HostConfig.Links') for c in service.containers())
+                should_recreate_for_links = service_has_links ^ container_has_links
+                if always_recreate_deps or containers_stopped or should_recreate_for_links:
                     plan = service.convergence_plan(ConvergenceStrategy.always)
                 else:
                     plan = service.convergence_plan(strategy)
@@ -602,6 +603,9 @@ class Project(object):
     def pull(self, service_names=None, ignore_pull_failures=False, parallel_pull=False, silent=False,
              include_deps=False):
         services = self.get_services(service_names, include_deps)
+        images_to_build = {service.image_name for service in services if service.can_be_built()}
+        services_to_pull = [service for service in services if service.image_name not in images_to_build]
+
         msg = not silent and 'Pulling' or None
 
         if parallel_pull:
@@ -627,7 +631,7 @@ class Project(object):
                     )
 
             _, errors = parallel.parallel_execute(
-                services,
+                services_to_pull,
                 pull_service,
                 operator.attrgetter('name'),
                 msg,
@@ -640,7 +644,7 @@ class Project(object):
                 raise ProjectError(combined_errors)
 
         else:
-            for service in services:
+            for service in services_to_pull:
                 service.pull(ignore_pull_failures, silent=silent)
 
     def push(self, service_names=None, ignore_push_failures=False):
@@ -686,7 +690,7 @@ class Project(object):
 
     def find_orphan_containers(self, remove_orphans):
         def _find():
-            containers = self._labeled_containers()
+            containers = set(self._labeled_containers() + self._labeled_containers(stopped=True))
             for ctnr in containers:
                 service_name = ctnr.labels.get(LABEL_SERVICE)
                 if service_name not in self.service_names:
@@ -697,7 +701,10 @@ class Project(object):
         if remove_orphans:
             for ctnr in orphans:
                 log.info('Removing orphan container "{0}"'.format(ctnr.name))
-                ctnr.kill()
+                try:
+                    ctnr.kill()
+                except APIError:
+                    pass
                 ctnr.remove(force=True)
         else:
             log.warning(
@@ -725,10 +732,11 @@ class Project(object):
 
     def build_container_operation_with_timeout_func(self, operation, options):
         def container_operation_with_timeout(container):
-            if options.get('timeout') is None:
+            _options = options.copy()
+            if _options.get('timeout') is None:
                 service = self.get_service(container.service)
-                options['timeout'] = service.stop_timeout(None)
-            return getattr(container, operation)(**options)
+                _options['timeout'] = service.stop_timeout(None)
+            return getattr(container, operation)(**_options)
         return container_operation_with_timeout
 
 
@@ -771,13 +779,13 @@ def get_secrets(service, service_secrets, secret_defs):
                 .format(service=service, secret=secret.source))
 
         if secret_def.get('external'):
-            log.warn("Service \"{service}\" uses secret \"{secret}\" which is external. "
-                     "External secrets are not available to containers created by "
-                     "docker-compose.".format(service=service, secret=secret.source))
+            log.warning("Service \"{service}\" uses secret \"{secret}\" which is external. "
+                        "External secrets are not available to containers created by "
+                        "docker-compose.".format(service=service, secret=secret.source))
             continue
 
         if secret.uid or secret.gid or secret.mode:
-            log.warn(
+            log.warning(
                 "Service \"{service}\" uses secret \"{secret}\" with uid, "
                 "gid, or mode. These fields are not supported by this "
                 "implementation of the Compose file".format(

+ 35 - 20
compose/service.py

@@ -59,7 +59,6 @@ from .utils import parse_seconds_float
 from .utils import truncate_id
 from .utils import unique_everseen
 
-
 log = logging.getLogger(__name__)
 
 
@@ -177,7 +176,7 @@ class Service(object):
         network_mode=None,
         networks=None,
         secrets=None,
-        scale=None,
+        scale=1,
         pid_mode=None,
         default_platform=None,
         **options
@@ -192,7 +191,7 @@ class Service(object):
         self.pid_mode = pid_mode or PidMode(None)
         self.networks = networks or {}
         self.secrets = secrets or []
-        self.scale_num = scale or 1
+        self.scale_num = scale
         self.default_platform = default_platform
         self.options = options
 
@@ -241,15 +240,15 @@ class Service(object):
 
     def show_scale_warnings(self, desired_num):
         if self.custom_container_name and desired_num > 1:
-            log.warn('The "%s" service is using the custom container name "%s". '
-                     'Docker requires each container to have a unique name. '
-                     'Remove the custom name to scale the service.'
-                     % (self.name, self.custom_container_name))
+            log.warning('The "%s" service is using the custom container name "%s". '
+                        'Docker requires each container to have a unique name. '
+                        'Remove the custom name to scale the service.'
+                        % (self.name, self.custom_container_name))
 
         if self.specifies_host_port() and desired_num > 1:
-            log.warn('The "%s" service specifies a port on the host. If multiple containers '
-                     'for this service are created on a single host, the port will clash.'
-                     % self.name)
+            log.warning('The "%s" service specifies a port on the host. If multiple containers '
+                        'for this service are created on a single host, the port will clash.'
+                        % self.name)
 
     def scale(self, desired_num, timeout=None):
         """
@@ -358,11 +357,17 @@ class Service(object):
             raise NeedsBuildError(self)
 
         self.build()
-        log.warn(
+        log.warning(
             "Image for service {} was built because it did not already exist. To "
             "rebuild this image you must use `docker-compose build` or "
             "`docker-compose up --build`.".format(self.name))
 
+    def get_image_registry_data(self):
+        try:
+            return self.client.inspect_distribution(self.image_name)
+        except APIError:
+            raise NoSuchImageError("Image '{}' not found".format(self.image_name))
+
     def image(self):
         try:
             return self.client.inspect_image(self.image_name)
@@ -680,6 +685,7 @@ class Service(object):
             'links': self.get_link_names(),
             'net': self.network_mode.id,
             'networks': self.networks,
+            'secrets': self.secrets,
             'volumes_from': [
                 (v.source.name, v.mode)
                 for v in self.volumes_from if isinstance(v.source, Service)
@@ -1043,8 +1049,11 @@ class Service(object):
         return [build_spec(secret) for secret in self.secrets]
 
     def build(self, no_cache=False, pull=False, force_rm=False, memory=None, build_args_override=None,
-              gzip=False):
-        log.info('Building %s' % self.name)
+              gzip=False, rm=True, silent=False):
+        output_stream = open(os.devnull, 'w')
+        if not silent:
+            output_stream = sys.stdout
+            log.info('Building %s' % self.name)
 
         build_opts = self.options.get('build', {})
 
@@ -1064,12 +1073,12 @@ class Service(object):
         build_output = self.client.build(
             path=path,
             tag=self.image_name,
-            rm=True,
+            rm=rm,
             forcerm=force_rm,
             pull=pull,
             nocache=no_cache,
             dockerfile=build_opts.get('dockerfile', None),
-            cache_from=build_opts.get('cache_from', None),
+            cache_from=self.get_cache_from(build_opts),
             labels=build_opts.get('labels', None),
             buildargs=build_args,
             network_mode=build_opts.get('network', None),
@@ -1085,7 +1094,7 @@ class Service(object):
         )
 
         try:
-            all_events = list(stream_output(build_output, sys.stdout))
+            all_events = list(stream_output(build_output, output_stream))
         except StreamOutputError as e:
             raise BuildError(self, six.text_type(e))
 
@@ -1107,6 +1116,12 @@ class Service(object):
 
         return image_id
 
+    def get_cache_from(self, build_opts):
+        cache_from = build_opts.get('cache_from', None)
+        if cache_from is not None:
+            cache_from = [tag for tag in cache_from if tag]
+        return cache_from
+
     def can_be_built(self):
         return 'build' in self.options
 
@@ -1316,7 +1331,7 @@ class ServicePidMode(PidMode):
         if containers:
             return 'container:' + containers[0].id
 
-        log.warn(
+        log.warning(
             "Service %s is trying to use reuse the PID namespace "
             "of another service that is not running." % (self.service_name)
         )
@@ -1379,8 +1394,8 @@ class ServiceNetworkMode(object):
         if containers:
             return 'container:' + containers[0].id
 
-        log.warn("Service %s is trying to use reuse the network stack "
-                 "of another service that is not running." % (self.id))
+        log.warning("Service %s is trying to use reuse the network stack "
+                    "of another service that is not running." % (self.id))
         return None
 
 
@@ -1531,7 +1546,7 @@ def warn_on_masked_volume(volumes_option, container_volumes, service):
             volume.internal in container_volumes and
             container_volumes.get(volume.internal) != volume.external
         ):
-            log.warn((
+            log.warning((
                 "Service \"{service}\" is using volume \"{volume}\" from the "
                 "previous container. Host mapping \"{host_path}\" has no effect. "
                 "Remove the existing containers (with `docker-compose rm {service}`) "

+ 2 - 2
compose/volume.py

@@ -127,7 +127,7 @@ class ProjectVolumes(object):
             try:
                 volume.remove()
             except NotFound:
-                log.warn("Volume %s not found.", volume.true_name)
+                log.warning("Volume %s not found.", volume.true_name)
 
     def initialize(self):
         try:
@@ -209,7 +209,7 @@ def check_remote_volume_config(remote, local):
         if k.startswith('com.docker.'):  # We are only interested in user-specified labels
             continue
         if remote_labels.get(k) != local_labels.get(k):
-            log.warn(
+            log.warning(
                 'Volume {}: label "{}" has changed. It may need to be'
                 ' recreated.'.format(local.name, k)
             )

+ 10 - 2
contrib/completion/bash/docker-compose

@@ -110,11 +110,14 @@ _docker_compose_build() {
 			__docker_compose_nospace
 			return
 			;;
+		--memory|-m)
+			return
+			;;
 	esac
 
 	case "$cur" in
 		-*)
-			COMPREPLY=( $( compgen -W "--build-arg --compress --force-rm --help --memory --no-cache --pull --parallel" -- "$cur" ) )
+			COMPREPLY=( $( compgen -W "--build-arg --compress --force-rm --help --memory -m --no-cache --no-rm --pull --parallel -q --quiet" -- "$cur" ) )
 			;;
 		*)
 			__docker_compose_complete_services --filter source=build
@@ -147,7 +150,7 @@ _docker_compose_config() {
 			;;
 	esac
 
-	COMPREPLY=( $( compgen -W "--hash --help --quiet -q --resolve-image-digests --services --volumes" -- "$cur" ) )
+	COMPREPLY=( $( compgen -W "--hash --help --no-interpolate --quiet -q --resolve-image-digests --services --volumes" -- "$cur" ) )
 }
 
 
@@ -181,6 +184,10 @@ _docker_compose_docker_compose() {
 			_filedir -d
 			return
 			;;
+		--env-file)
+			_filedir
+			return
+			;;
 		$(__docker_compose_to_extglob "$daemon_options_with_args") )
 			return
 			;;
@@ -609,6 +616,7 @@ _docker_compose() {
 		--tlsverify
 	"
 	local daemon_options_with_args="
+		--env-file
 		--file -f
 		--host -H
 		--project-directory

+ 1 - 0
contrib/completion/fish/docker-compose.fish

@@ -12,6 +12,7 @@ end
 
 complete -c docker-compose -s f -l file -r                -d 'Specify an alternate compose file'
 complete -c docker-compose -s p -l project-name -x        -d 'Specify an alternate project name'
+complete -c docker-compose -l env-file -r                 -d 'Specify an alternate environment file (default: .env)'
 complete -c docker-compose -l verbose                     -d 'Show more output'
 complete -c docker-compose -s H -l host -x                -d 'Daemon socket to connect to'
 complete -c docker-compose -l tls                         -d 'Use TLS; implied by --tlsverify'

+ 3 - 0
contrib/completion/zsh/_docker-compose

@@ -113,6 +113,7 @@ __docker-compose_subcommand() {
                 $opts_help \
                 "*--build-arg=[Set build-time variables for one service.]:<varname>=<value>: " \
                 '--force-rm[Always remove intermediate containers.]' \
+                '(--quiet -q)'{--quiet,-q}'[Curb build output]' \
                 '(--memory -m)'{--memory,-m}'[Memory limit for the build container.]' \
                 '--no-cache[Do not use cache when building the image.]' \
                 '--pull[Always attempt to pull a newer version of the image.]' \
@@ -340,6 +341,7 @@ _docker-compose() {
         '(- :)'{-h,--help}'[Get help]' \
         '*'{-f,--file}"[${file_description}]:file:_files -g '*.yml'" \
         '(-p --project-name)'{-p,--project-name}'[Specify an alternate project name (default: directory name)]:project name:' \
+        '--env-file[Specify an alternate environment file (default: .env)]:env-file:_files' \
         "--compatibility[If set, Compose will attempt to convert keys in v3 files to their non-Swarm equivalent]" \
         '(- :)'{-v,--version}'[Print version and exit]' \
         '--verbose[Show more output]' \
@@ -358,6 +360,7 @@ _docker-compose() {
     local -a relevant_compose_flags relevant_compose_repeatable_flags relevant_docker_flags compose_options docker_options
 
     relevant_compose_flags=(
+        "--env-file"
         "--file" "-f"
         "--host" "-H"
         "--project-name" "-p"

+ 3 - 3
contrib/migration/migrate-compose-file-v1-to-v2.py

@@ -44,7 +44,7 @@ def warn_for_links(name, service):
     links = service.get('links')
     if links:
         example_service = links[0].partition(':')[0]
-        log.warn(
+        log.warning(
             "Service {name} has links, which no longer create environment "
             "variables such as {example_service_upper}_PORT. "
             "If you are using those in your application code, you should "
@@ -57,7 +57,7 @@ def warn_for_links(name, service):
 def warn_for_external_links(name, service):
     external_links = service.get('external_links')
     if external_links:
-        log.warn(
+        log.warning(
             "Service {name} has external_links: {ext}, which now work "
             "slightly differently. In particular, two containers must be "
             "connected to at least one network in common in order to "
@@ -107,7 +107,7 @@ def rewrite_volumes_from(service, service_names):
 def create_volumes_section(data):
     named_volumes = get_named_volumes(data['services'])
     if named_volumes:
-        log.warn(
+        log.warning(
             "Named volumes ({names}) must be explicitly declared. Creating a "
             "'volumes' section with declarations.\n\n"
             "For backwards-compatibility, they've been declared as external. "

+ 20 - 0
docker-compose-entrypoint.sh

@@ -0,0 +1,20 @@
+#!/bin/sh
+set -e
+
+# first arg is `-f` or `--some-option`
+if [ "${1#-}" != "$1" ]; then
+	set -- docker-compose "$@"
+fi
+
+# if our command is a valid Docker subcommand, let's invoke it through Docker instead
+# (this allows for "docker run docker ps", etc)
+if docker-compose help "$1" > /dev/null 2>&1; then
+	set -- docker-compose "$@"
+fi
+
+# if we have "--link some-docker:docker" and not DOCKER_HOST, let's set DOCKER_HOST automatically
+if [ -z "$DOCKER_HOST" -a "$DOCKER_PORT_2375_TCP" ]; then
+	export DOCKER_HOST='tcp://docker:2375'
+fi
+
+exec "$@"

+ 2 - 4
docs/README.md

@@ -6,11 +6,9 @@ The documentation for Compose has been merged into
 The docs for Compose are now here:
 https://github.com/docker/docker.github.io/tree/master/compose
 
-Please submit pull requests for unpublished features on the `vnext-compose` branch (https://github.com/docker/docker.github.io/tree/vnext-compose).
+Please submit pull requests for unreleased features/changes on the `master` branch (https://github.com/docker/docker.github.io/tree/master), please prefix the PR title with `[WIP]` to indicate that it relates to an unreleased change.
 
-If you submit a PR to this codebase that has a docs impact, create a second docs PR on `docker.github.io`. Use the docs PR template provided (coming soon - watch this space).
-
-PRs for typos, additional information, etc. for already-published features should be labeled as `okay-to-publish` (we are still settling on a naming convention, will provide a label soon). You can submit these PRs either to `vnext-compose` or directly to `master` on `docker.github.io`
+If you submit a PR to this codebase that has a docs impact, create a second docs PR on `docker.github.io`. Use the docs PR template provided.
 
 As always, the docs remain open-source and we appreciate your feedback and
 pull requests!

+ 13 - 0
pyinstaller/ldd

@@ -0,0 +1,13 @@
+#!/bin/sh
+
+# From http://wiki.musl-libc.org/wiki/FAQ#Q:_where_is_ldd_.3F
+#
+#     Musl's dynlinker comes with ldd functionality built in. just create a
+#     symlink from ld-musl-$ARCH.so to /bin/ldd. If the dynlinker was started
+#     as "ldd", it will detect that and print the appropriate DSO information.
+#
+# Instead, this string replaced "ldd" with the package so that pyinstaller
+# can find the actual lib.
+exec /usr/bin/ldd "$@" | \
+    sed -r 's/([^[:space:]]+) => ldd/\1 => \/lib\/\1/g' | \
+    sed -r 's/ldd \(.*\)//g'

+ 1 - 1
requirements-build.txt

@@ -1 +1 @@
-pyinstaller==3.3.1
+pyinstaller==3.4

+ 2 - 1
requirements-dev.txt

@@ -1,5 +1,6 @@
 coverage==4.4.2
+ddt==1.2.0
 flake8==3.5.0
-mock==2.0.0
+mock==3.0.5
 pytest==3.6.3
 pytest-cov==2.5.1

+ 5 - 5
requirements.txt

@@ -3,7 +3,7 @@ cached-property==1.3.0
 certifi==2017.4.17
 chardet==3.0.4
 colorama==0.4.0; sys_platform == 'win32'
-docker==3.7.3
+docker==4.0.1
 docker-pycreds==0.4.0
 dockerpty==0.4.1
 docopt==0.6.2
@@ -17,8 +17,8 @@ pypiwin32==219; sys_platform == 'win32' and python_version < '3.6'
 pypiwin32==223; sys_platform == 'win32' and python_version >= '3.6'
 PySocks==1.6.7
 PyYAML==4.2b1
-requests==2.20.0
+requests==2.22.0
 six==1.10.0
-texttable==0.9.1
-urllib3==1.21.1; python_version == '3.3'
-websocket-client==0.56.0
+texttable==1.6.2
+urllib3==1.24.2; python_version == '3.3'
+websocket-client==0.32.0

+ 7 - 4
script/build/image

@@ -7,11 +7,14 @@ if [ -z "$1" ]; then
     exit 1
 fi
 
-TAG=$1
+TAG="$1"
 
 VERSION="$(python setup.py --version)"
 
-./script/build/write-git-sha
+DOCKER_COMPOSE_GITSHA="$(script/build/write-git-sha)"
+echo "${DOCKER_COMPOSE_GITSHA}" > compose/GITSHA
 python setup.py sdist bdist_wheel
-./script/build/linux
-docker build -t docker/compose:$TAG -f Dockerfile.run .
+
+docker build \
+    --build-arg GIT_COMMIT="${DOCKER_COMPOSE_GITSHA}" \
+    -t "${TAG}" .

+ 11 - 7
script/build/linux

@@ -4,10 +4,14 @@ set -ex
 
 ./script/clean
 
-TAG="docker-compose"
-docker build -t "$TAG" .
-docker run \
-    --rm --entrypoint="script/build/linux-entrypoint" \
-    -v $(pwd)/dist:/code/dist \
-    -v $(pwd)/.git:/code/.git \
-    "$TAG"
+DOCKER_COMPOSE_GITSHA="$(script/build/write-git-sha)"
+TAG="docker/compose:tmp-glibc-linux-binary-${DOCKER_COMPOSE_GITSHA}"
+
+docker build -t "${TAG}" . \
+       --build-arg BUILD_PLATFORM=debian \
+       --build-arg GIT_COMMIT="${DOCKER_COMPOSE_GITSHA}"
+TMP_CONTAINER=$(docker create "${TAG}")
+mkdir -p dist
+docker cp "${TMP_CONTAINER}":/usr/local/bin/docker-compose dist/docker-compose-Linux-x86_64
+docker container rm -f "${TMP_CONTAINER}"
+docker image rm -f "${TAG}"

+ 33 - 9
script/build/linux-entrypoint

@@ -2,14 +2,38 @@
 
 set -ex
 
-TARGET=dist/docker-compose-$(uname -s)-$(uname -m)
-VENV=/code/.tox/py36
+CODE_PATH=/code
+VENV="${CODE_PATH}"/.tox/py37
 
-mkdir -p `pwd`/dist
-chmod 777 `pwd`/dist
+cd "${CODE_PATH}"
+mkdir -p dist
+chmod 777 dist
 
-$VENV/bin/pip install -q -r requirements-build.txt
-./script/build/write-git-sha
-su -c "$VENV/bin/pyinstaller docker-compose.spec" user
-mv dist/docker-compose $TARGET
-$TARGET version
+"${VENV}"/bin/pip3 install -q -r requirements-build.txt
+
+# TODO(ulyssessouza) To check if really needed
+if [ -z "${DOCKER_COMPOSE_GITSHA}" ]; then
+    DOCKER_COMPOSE_GITSHA="$(script/build/write-git-sha)"
+fi
+echo "${DOCKER_COMPOSE_GITSHA}" > compose/GITSHA
+
+export PATH="${CODE_PATH}/pyinstaller:${PATH}"
+
+if [ ! -z "${BUILD_BOOTLOADER}" ]; then
+    # Build bootloader for alpine
+    git clone --single-branch --branch master https://github.com/pyinstaller/pyinstaller.git /tmp/pyinstaller
+    cd /tmp/pyinstaller/bootloader
+    git checkout v3.4
+    "${VENV}"/bin/python3 ./waf configure --no-lsb all
+    "${VENV}"/bin/pip3 install ..
+    cd "${CODE_PATH}"
+    rm -Rf /tmp/pyinstaller
+else
+    echo "NOT compiling bootloader!!!"
+fi
+
+"${VENV}"/bin/pyinstaller --exclude-module pycrypto --exclude-module PyInstaller docker-compose.spec
+ls -la dist/
+ldd dist/docker-compose
+mv dist/docker-compose /usr/local/bin
+docker-compose version

+ 3 - 2
script/build/osx

@@ -5,11 +5,12 @@ TOOLCHAIN_PATH="$(realpath $(dirname $0)/../../build/toolchain)"
 
 rm -rf venv
 
-virtualenv -p ${TOOLCHAIN_PATH}/bin/python3 venv
+virtualenv -p "${TOOLCHAIN_PATH}"/bin/python3 venv
 venv/bin/pip install -r requirements.txt
 venv/bin/pip install -r requirements-build.txt
 venv/bin/pip install --no-deps .
-./script/build/write-git-sha
+DOCKER_COMPOSE_GITSHA="$(script/build/write-git-sha)"
+echo "${DOCKER_COMPOSE_GITSHA}" > compose/GITSHA
 venv/bin/pyinstaller docker-compose.spec
 mv dist/docker-compose dist/docker-compose-Darwin-x86_64
 dist/docker-compose-Darwin-x86_64 version

+ 8 - 7
script/build/test-image

@@ -7,11 +7,12 @@ if [ -z "$1" ]; then
     exit 1
 fi
 
-TAG=$1
+TAG="$1"
+IMAGE="docker/compose-tests"
 
-docker build -t docker-compose-tests:tmp .
-ctnr_id=$(docker create --entrypoint=tox docker-compose-tests:tmp)
-docker commit $ctnr_id docker/compose-tests:latest
-docker tag docker/compose-tests:latest docker/compose-tests:$TAG
-docker rm -f $ctnr_id
-docker rmi -f docker-compose-tests:tmp
+DOCKER_COMPOSE_GITSHA="$(script/build/write-git-sha)"
+docker build -t "${IMAGE}:${TAG}" . \
+       --target build \
+       --build-arg BUILD_PLATFORM="debian" \
+       --build-arg GIT_COMMIT="${DOCKER_COMPOSE_GITSHA}"
+docker tag "${IMAGE}":"${TAG}" "${IMAGE}":latest

+ 3 - 3
script/build/windows.ps1

@@ -6,17 +6,17 @@
 #
 #        http://git-scm.com/download/win
 #
-# 2. Install Python 3.6.4:
+# 2. Install Python 3.7.2:
 #
 #        https://www.python.org/downloads/
 #
-# 3. Append ";C:\Python36;C:\Python36\Scripts" to the "Path" environment variable:
+# 3. Append ";C:\Python37;C:\Python37\Scripts" to the "Path" environment variable:
 #
 #        https://www.microsoft.com/resources/documentation/windows/xp/all/proddocs/en-us/sysdm_advancd_environmnt_addchange_variable.mspx?mfr=true
 #
 # 4. In Powershell, run the following commands:
 #
-#        $ pip install 'virtualenv>=15.1.0'
+#        $ pip install 'virtualenv==16.2.0'
 #        $ Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
 #
 # 5. Clone the repository:

+ 1 - 1
script/build/write-git-sha

@@ -9,4 +9,4 @@ if [[ "${?}" != "0" ]]; then
     echo "Couldn't get revision of the git repository. Setting to 'unknown' instead"
     DOCKER_COMPOSE_GITSHA="unknown"
 fi
-echo "${DOCKER_COMPOSE_GITSHA}" > compose/GITSHA
+echo "${DOCKER_COMPOSE_GITSHA}"

+ 2 - 0
script/release/README.md

@@ -192,6 +192,8 @@ be handled manually by the operator:
     - Bump the version in `compose/__init__.py` to the *next* minor version
       number with `dev` appended. For example, if you just released `1.4.0`,
       update it to `1.5.0dev`
+    - Update compose_version in [github.com/docker/docker.github.io/blob/master/_config.yml](https://github.com/docker/docker.github.io/blob/master/_config.yml) and [github.com/docker/docker.github.io/blob/master/_config_authoring.yml](https://github.com/docker/docker.github.io/blob/master/_config_authoring.yml)
+    - Update the release note in [github.com/docker/docker.github.io](https://github.com/docker/docker.github.io/blob/master/release-notes/docker-compose.md)
 
 ## Advanced options
 

+ 5 - 3
script/release/release.py

@@ -15,6 +15,7 @@ from release.const import NAME
 from release.const import REPO_ROOT
 from release.downloader import BinaryDownloader
 from release.images import ImageManager
+from release.images import is_tag_latest
 from release.pypi import check_pypirc
 from release.pypi import pypi_upload
 from release.repository import delete_assets
@@ -204,7 +205,7 @@ def resume(args):
         delete_assets(gh_release)
         upload_assets(gh_release, files)
         img_manager = ImageManager(args.release)
-        img_manager.build_images(repository, files)
+        img_manager.build_images(repository)
     except ScriptError as e:
         print(e)
         return 1
@@ -244,7 +245,7 @@ def start(args):
         gh_release = create_release_draft(repository, args.release, pr_data, files)
         upload_assets(gh_release, files)
         img_manager = ImageManager(args.release)
-        img_manager.build_images(repository, files)
+        img_manager.build_images(repository)
     except ScriptError as e:
         print(e)
         return 1
@@ -258,7 +259,8 @@ def finalize(args):
     try:
         check_pypirc()
         repository = Repository(REPO_ROOT, args.repo)
-        img_manager = ImageManager(args.release)
+        tag_as_latest = is_tag_latest(args.release)
+        img_manager = ImageManager(args.release, tag_as_latest)
         pr_data = repository.find_release_pr(args.release)
         if not pr_data:
             raise ScriptError('No PR found for {}'.format(args.release))

+ 1 - 0
script/release/release/const.py

@@ -6,4 +6,5 @@ import os
 
 REPO_ROOT = os.path.join(os.path.dirname(__file__), '..', '..', '..')
 NAME = 'docker/compose'
+COMPOSE_TESTS_IMAGE_BASE_NAME = NAME + '-tests'
 BINTRAY_ORG = 'docker-compose'

+ 96 - 31
script/release/release/images.py

@@ -5,18 +5,36 @@ from __future__ import unicode_literals
 import base64
 import json
 import os
-import shutil
 
 import docker
+from enum import Enum
 
+from .const import NAME
 from .const import REPO_ROOT
 from .utils import ScriptError
+from .utils import yesno
+from script.release.release.const import COMPOSE_TESTS_IMAGE_BASE_NAME
+
+
+class Platform(Enum):
+    ALPINE = 'alpine'
+    DEBIAN = 'debian'
+
+    def __str__(self):
+        return self.value
+
+
+# Checks if this version respects the GA version format ('x.y.z') and not an RC
+def is_tag_latest(version):
+    ga_version = all(n.isdigit() for n in version.split('.')) and version.count('.') == 2
+    return ga_version and yesno('Should this release be tagged as \"latest\"? [Y/n]: ', default=True)
 
 
 class ImageManager(object):
-    def __init__(self, version):
+    def __init__(self, version, latest=False):
         self.docker_client = docker.APIClient(**docker.utils.kwargs_from_env())
         self.version = version
+        self.latest = latest
         if 'HUB_CREDENTIALS' in os.environ:
             print('HUB_CREDENTIALS found in environment, issuing login')
             credentials = json.loads(base64.urlsafe_b64decode(os.environ['HUB_CREDENTIALS']))
@@ -24,16 +42,36 @@ class ImageManager(object):
                 username=credentials['Username'], password=credentials['Password']
             )
 
-    def build_images(self, repository, files):
-        print("Building release images...")
-        repository.write_git_sha()
-        distdir = os.path.join(REPO_ROOT, 'dist')
-        os.makedirs(distdir, exist_ok=True)
-        shutil.copy(files['docker-compose-Linux-x86_64'][0], distdir)
-        os.chmod(os.path.join(distdir, 'docker-compose-Linux-x86_64'), 0o755)
-        print('Building docker/compose image')
+    def _tag(self, image, existing_tag, new_tag):
+        existing_repo_tag = '{image}:{tag}'.format(image=image, tag=existing_tag)
+        new_repo_tag = '{image}:{tag}'.format(image=image, tag=new_tag)
+        self.docker_client.tag(existing_repo_tag, new_repo_tag)
+
+    def get_full_version(self, platform=None):
+        return self.version + '-' + platform.__str__() if platform else self.version
+
+    def get_runtime_image_tag(self, tag):
+        return '{image_base_image}:{tag}'.format(
+            image_base_image=NAME,
+            tag=self.get_full_version(tag)
+        )
+
+    def build_runtime_image(self, repository, platform):
+        git_sha = repository.write_git_sha()
+        compose_image_base_name = NAME
+        print('Building {image} image ({platform} based)'.format(
+            image=compose_image_base_name,
+            platform=platform
+        ))
+        full_version = self.get_full_version(platform)
+        build_tag = self.get_runtime_image_tag(platform)
         logstream = self.docker_client.build(
-            REPO_ROOT, tag='docker/compose:{}'.format(self.version), dockerfile='Dockerfile.run',
+            REPO_ROOT,
+            tag=build_tag,
+            buildargs={
+                'BUILD_PLATFORM': platform.value,
+                'GIT_COMMIT': git_sha,
+            },
             decode=True
         )
         for chunk in logstream:
@@ -42,9 +80,33 @@ class ImageManager(object):
             if 'stream' in chunk:
                 print(chunk['stream'], end='')
 
-        print('Building test image (for UCP e2e)')
+        if platform == Platform.ALPINE:
+            self._tag(compose_image_base_name, full_version, self.version)
+        if self.latest:
+            self._tag(compose_image_base_name, full_version, platform)
+            if platform == Platform.ALPINE:
+                self._tag(compose_image_base_name, full_version, 'latest')
+
+    def get_ucp_test_image_tag(self, tag=None):
+        return '{image}:{tag}'.format(
+            image=COMPOSE_TESTS_IMAGE_BASE_NAME,
+            tag=tag or self.version
+        )
+
+    # Used for producing a test image for UCP
+    def build_ucp_test_image(self, repository):
+        print('Building test image (debian based for UCP e2e)')
+        git_sha = repository.write_git_sha()
+        ucp_test_image_tag = self.get_ucp_test_image_tag()
         logstream = self.docker_client.build(
-            REPO_ROOT, tag='docker-compose-tests:tmp', decode=True
+            REPO_ROOT,
+            tag=ucp_test_image_tag,
+            target='build',
+            buildargs={
+                'BUILD_PLATFORM': Platform.DEBIAN.value,
+                'GIT_COMMIT': git_sha,
+            },
+            decode=True
         )
         for chunk in logstream:
             if 'error' in chunk:
@@ -52,26 +114,15 @@ class ImageManager(object):
             if 'stream' in chunk:
                 print(chunk['stream'], end='')
 
-        container = self.docker_client.create_container(
-            'docker-compose-tests:tmp', entrypoint='tox'
-        )
-        self.docker_client.commit(container, 'docker/compose-tests', 'latest')
-        self.docker_client.tag(
-            'docker/compose-tests:latest', 'docker/compose-tests:{}'.format(self.version)
-        )
-        self.docker_client.remove_container(container, force=True)
-        self.docker_client.remove_image('docker-compose-tests:tmp', force=True)
+        self._tag(COMPOSE_TESTS_IMAGE_BASE_NAME, self.version, 'latest')
 
-    @property
-    def image_names(self):
-        return [
-            'docker/compose-tests:latest',
-            'docker/compose-tests:{}'.format(self.version),
-            'docker/compose:{}'.format(self.version)
-        ]
+    def build_images(self, repository):
+        self.build_runtime_image(repository, Platform.ALPINE)
+        self.build_runtime_image(repository, Platform.DEBIAN)
+        self.build_ucp_test_image(repository)
 
     def check_images(self):
-        for name in self.image_names:
+        for name in self.get_images_to_push():
             try:
                 self.docker_client.inspect_image(name)
             except docker.errors.ImageNotFound:
@@ -79,8 +130,22 @@ class ImageManager(object):
                 return False
         return True
 
+    def get_images_to_push(self):
+        tags_to_push = {
+            "{}:{}".format(NAME, self.version),
+            self.get_runtime_image_tag(Platform.ALPINE),
+            self.get_runtime_image_tag(Platform.DEBIAN),
+            self.get_ucp_test_image_tag(),
+            self.get_ucp_test_image_tag('latest'),
+        }
+        if is_tag_latest(self.version):
+            tags_to_push.add("{}:latest".format(NAME))
+        return tags_to_push
+
     def push_images(self):
-        for name in self.image_names:
+        tags_to_push = self.get_images_to_push()
+        print('Build tags to push {}'.format(tags_to_push))
+        for name in tags_to_push:
             print('Pushing {} to Docker Hub'.format(name))
             logstream = self.docker_client.push(name, stream=True, decode=True)
             for chunk in logstream:

+ 2 - 1
script/release/release/repository.py

@@ -175,6 +175,7 @@ class Repository(object):
     def write_git_sha(self):
         with open(os.path.join(REPO_ROOT, 'compose', 'GITSHA'), 'w') as f:
             f.write(self.git_repo.head.commit.hexsha[:7])
+        return self.git_repo.head.commit.hexsha[:7]
 
     def cherry_pick_prs(self, release_branch, ids):
         if not ids:
@@ -219,7 +220,7 @@ def get_contributors(pr_data):
     commits = pr_data.get_commits()
     authors = {}
     for commit in commits:
-        if not commit.author:
+        if not commit or not commit.author or not commit.author.login:
             continue
         author = commit.author.login
         authors[author] = authors.get(author, 0) + 1

+ 2 - 2
script/run/run.sh

@@ -15,7 +15,7 @@
 
 set -e
 
-VERSION="1.24.1"
+VERSION="1.25.0-rc2"
 IMAGE="docker/compose:$VERSION"
 
 
@@ -48,7 +48,7 @@ fi
 
 # Only allocate tty if we detect one
 if [ -t 0 -a -t 1 ]; then
-        DOCKER_RUN_OPTIONS="$DOCKER_RUN_OPTIONS -t"
+    DOCKER_RUN_OPTIONS="$DOCKER_RUN_OPTIONS -t"
 fi
 
 # Always set -i to support piped and terminal input in run/exec

+ 15 - 9
script/setup/osx

@@ -13,13 +13,13 @@ if ! [ ${DEPLOYMENT_TARGET} == "$(macos_version)" ]; then
   SDK_SHA1=dd228a335194e3392f1904ce49aff1b1da26ca62
 fi
 
-OPENSSL_VERSION=1.1.0j
+OPENSSL_VERSION=1.1.1c
 OPENSSL_URL=https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz
-OPENSSL_SHA1=dcad1efbacd9a4ed67d4514470af12bbe2a1d60a
+OPENSSL_SHA1=71b830a077276cbeccc994369538617a21bee808
 
-PYTHON_VERSION=3.6.8
+PYTHON_VERSION=3.7.4
 PYTHON_URL=https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz
-PYTHON_SHA1=09fcc4edaef0915b4dedbfb462f1cd15f82d3a6f
+PYTHON_SHA1=fb1d764be8a9dcd40f2f152a610a0ab04e0d0ed3
 
 #
 # Install prerequisites.
@@ -36,7 +36,7 @@ if ! [ -x "$(command -v python3)" ]; then
   brew install python3
 fi
 if ! [ -x "$(command -v virtualenv)" ]; then
-  pip install virtualenv
+  pip install virtualenv==16.2.0
 fi
 
 #
@@ -50,7 +50,7 @@ mkdir -p ${TOOLCHAIN_PATH}
 #
 # Set macOS SDK.
 #
-if [ ${SDK_FETCH} ]; then
+if [[ ${SDK_FETCH} && ! -f ${TOOLCHAIN_PATH}/MacOSX${DEPLOYMENT_TARGET}.sdk/SDKSettings.plist ]]; then
   SDK_PATH=${TOOLCHAIN_PATH}/MacOSX${DEPLOYMENT_TARGET}.sdk
   fetch_tarball ${SDK_URL} ${SDK_PATH} ${SDK_SHA1}
 else
@@ -61,7 +61,7 @@ fi
 # Build OpenSSL.
 #
 OPENSSL_SRC_PATH=${TOOLCHAIN_PATH}/openssl-${OPENSSL_VERSION}
-if ! [ -f ${TOOLCHAIN_PATH}/bin/openssl ]; then
+if ! [[ $(${TOOLCHAIN_PATH}/bin/openssl version) == *"${OPENSSL_VERSION}"* ]]; then
   rm -rf ${OPENSSL_SRC_PATH}
   fetch_tarball ${OPENSSL_URL} ${OPENSSL_SRC_PATH} ${OPENSSL_SHA1}
   (
@@ -77,7 +77,7 @@ fi
 # Build Python.
 #
 PYTHON_SRC_PATH=${TOOLCHAIN_PATH}/Python-${PYTHON_VERSION}
-if ! [ -f ${TOOLCHAIN_PATH}/bin/python3 ]; then
+if ! [[ $(${TOOLCHAIN_PATH}/bin/python3 --version) == *"${PYTHON_VERSION}"* ]]; then
   rm -rf ${PYTHON_SRC_PATH}
   fetch_tarball ${PYTHON_URL} ${PYTHON_SRC_PATH} ${PYTHON_SHA1}
   (
@@ -87,9 +87,10 @@ if ! [ -f ${TOOLCHAIN_PATH}/bin/python3 ]; then
       --datarootdir=${TOOLCHAIN_PATH}/share \
       --datadir=${TOOLCHAIN_PATH}/share \
       --enable-framework=${TOOLCHAIN_PATH}/Frameworks \
+      --with-openssl=${TOOLCHAIN_PATH} \
       MACOSX_DEPLOYMENT_TARGET=${DEPLOYMENT_TARGET} \
       CFLAGS="-isysroot ${SDK_PATH} -I${TOOLCHAIN_PATH}/include" \
-      CPPFLAGS="-I${SDK_PATH}/usr/include -I${TOOLCHAIN_PATH}include" \
+      CPPFLAGS="-I${SDK_PATH}/usr/include -I${TOOLCHAIN_PATH}/include" \
       LDFLAGS="-isysroot ${SDK_PATH} -L ${TOOLCHAIN_PATH}/lib"
     make -j 4
     make install PYTHONAPPSDIR=${TOOLCHAIN_PATH}
@@ -97,6 +98,11 @@ if ! [ -f ${TOOLCHAIN_PATH}/bin/python3 ]; then
   )
 fi
 
+#
+# Smoke test built Python.
+#
+openssl_version ${TOOLCHAIN_PATH}
+
 echo ""
 echo "*** Targeting macOS: ${DEPLOYMENT_TARGET}"
 echo "*** Using SDK ${SDK_PATH}"

+ 2 - 3
script/test/all

@@ -8,8 +8,7 @@ set -e
 docker run --rm \
   --tty \
   ${GIT_VOLUME} \
-  --entrypoint="tox" \
-  "$TAG" -e pre-commit
+  "$TAG" tox -e pre-commit
 
 get_versions="docker run --rm
     --entrypoint=/code/.tox/py27/bin/python
@@ -24,7 +23,7 @@ fi
 
 
 BUILD_NUMBER=${BUILD_NUMBER-$USER}
-PY_TEST_VERSIONS=${PY_TEST_VERSIONS:-py27,py36}
+PY_TEST_VERSIONS=${PY_TEST_VERSIONS:-py27,py37}
 
 for version in $DOCKER_VERSIONS; do
   >&2 echo "Running tests against Docker $version"

+ 0 - 3
script/test/ci

@@ -20,6 +20,3 @@ export DOCKER_DAEMON_ARGS="--storage-driver=$STORAGE_DRIVER"
 
 GIT_VOLUME="--volumes-from=$(hostname)"
 . script/test/all
-
->&2 echo "Building Linux binary"
-. script/build/linux-entrypoint

+ 4 - 3
script/test/default

@@ -3,17 +3,18 @@
 
 set -ex
 
-TAG="docker-compose:$(git rev-parse --short HEAD)"
+TAG="docker-compose:alpine-$(git rev-parse --short HEAD)"
 
 # By default use the Dockerfile, but can be overridden to use an alternative file
-# e.g DOCKERFILE=Dockerfile.armhf script/test/default
+# e.g DOCKERFILE=Dockerfile.s390x script/test/default
 DOCKERFILE="${DOCKERFILE:-Dockerfile}"
+DOCKER_BUILD_TARGET="${DOCKER_BUILD_TARGET:-build}"
 
 rm -rf coverage-html
 # Create the host directory so it's owned by $USER
 mkdir -p coverage-html
 
-docker build -f ${DOCKERFILE} -t "$TAG" .
+docker build -f "${DOCKERFILE}" -t "${TAG}" --target "${DOCKER_BUILD_TARGET}" .
 
 GIT_VOLUME="--volume=$(pwd)/.git:/code/.git"
 . script/test/all

+ 12 - 12
setup.py

@@ -31,31 +31,31 @@ def find_version(*file_paths):
 
 install_requires = [
     'cached-property >= 1.2.0, < 2',
-    'docopt >= 0.6.1, < 0.7',
-    'PyYAML >= 3.10, < 4.3',
-    'requests >= 2.6.1, != 2.11.0, != 2.12.2, != 2.18.0, < 2.21',
-    'texttable >= 0.9.0, < 0.10',
-    'websocket-client >= 0.32.0, < 1.0',
-    'docker[ssh] >= 3.7.0, < 4.0',
-    'dockerpty >= 0.4.1, < 0.5',
+    'docopt >= 0.6.1, < 1',
+    'PyYAML >= 3.10, < 5',
+    'requests >= 2.20.0, < 3',
+    'texttable >= 0.9.0, < 2',
+    'websocket-client >= 0.32.0, < 1',
+    'docker[ssh] >= 3.7.0, < 5',
+    'dockerpty >= 0.4.1, < 1',
     'six >= 1.3.0, < 2',
     'jsonschema >= 2.5.1, < 3',
 ]
 
 
 tests_require = [
-    'pytest',
+    'pytest < 6',
 ]
 
 
 if sys.version_info[:2] < (3, 4):
-    tests_require.append('mock >= 1.0.1')
+    tests_require.append('mock >= 1.0.1, < 4')
 
 extras_require = {
     ':python_version < "3.4"': ['enum34 >= 1.0.4, < 2'],
-    ':python_version < "3.5"': ['backports.ssl_match_hostname >= 3.5'],
-    ':python_version < "3.3"': ['ipaddress >= 1.0.16'],
-    ':sys_platform == "win32"': ['colorama >= 0.4, < 0.5'],
+    ':python_version < "3.5"': ['backports.ssl_match_hostname >= 3.5, < 4'],
+    ':python_version < "3.3"': ['ipaddress >= 1.0.16, < 2'],
+    ':sys_platform == "win32"': ['colorama >= 0.4, < 1'],
     'socks': ['PySocks >= 1.5.6, != 1.5.7, < 2'],
 }
 

+ 98 - 21
tests/acceptance/cli_test.py

@@ -11,6 +11,7 @@ import subprocess
 import time
 from collections import Counter
 from collections import namedtuple
+from functools import reduce
 from operator import attrgetter
 
 import pytest
@@ -19,6 +20,7 @@ import yaml
 from docker import errors
 
 from .. import mock
+from ..helpers import BUSYBOX_IMAGE_WITH_TAG
 from ..helpers import create_host_file
 from compose.cli.command import get_project
 from compose.config.errors import DuplicateOverrideFileFound
@@ -62,6 +64,12 @@ def wait_on_process(proc, returncode=0):
     return ProcessResult(stdout.decode('utf-8'), stderr.decode('utf-8'))
 
 
+def dispatch(base_dir, options, project_options=None, returncode=0):
+    project_options = project_options or []
+    proc = start_process(base_dir, project_options + options)
+    return wait_on_process(proc, returncode=returncode)
+
+
 def wait_on_condition(condition, delay=0.1, timeout=40):
     start_time = time.time()
     while not condition():
@@ -149,9 +157,7 @@ class CLITestCase(DockerClientTestCase):
         return self._project
 
     def dispatch(self, options, project_options=None, returncode=0):
-        project_options = project_options or []
-        proc = start_process(self.base_dir, project_options + options)
-        return wait_on_process(proc, returncode=returncode)
+        return dispatch(self.base_dir, options, project_options, returncode)
 
     def execute(self, container, cmd):
         # Remove once Hijack and CloseNotifier sign a peace treaty
@@ -170,6 +176,13 @@ class CLITestCase(DockerClientTestCase):
         # Prevent tearDown from trying to create a project
         self.base_dir = None
 
+    def test_quiet_build(self):
+        self.base_dir = 'tests/fixtures/build-args'
+        result = self.dispatch(['build'], None)
+        quietResult = self.dispatch(['build', '-q'], None)
+        assert result.stdout != ""
+        assert quietResult.stdout == ""
+
     def test_help_nonexistent(self):
         self.base_dir = 'tests/fixtures/no-composefile'
         result = self.dispatch(['help', 'foobar'], returncode=1)
@@ -258,7 +271,7 @@ class CLITestCase(DockerClientTestCase):
                     'volumes_from': ['service:other:rw'],
                 },
                 'other': {
-                    'image': 'busybox:latest',
+                    'image': BUSYBOX_IMAGE_WITH_TAG,
                     'command': 'top',
                     'volumes': ['/data'],
                 },
@@ -324,6 +337,21 @@ class CLITestCase(DockerClientTestCase):
             'version': '2.4'
         }
 
+    def test_config_with_env_file(self):
+        self.base_dir = 'tests/fixtures/default-env-file'
+        result = self.dispatch(['--env-file', '.env2', 'config'])
+        json_result = yaml.load(result.stdout)
+        assert json_result == {
+            'services': {
+                'web': {
+                    'command': 'false',
+                    'image': 'alpine:latest',
+                    'ports': ['5644/tcp', '9998/tcp']
+                }
+            },
+            'version': '2.4'
+        }
+
     def test_config_with_dot_env_and_override_dir(self):
         self.base_dir = 'tests/fixtures/default-env-file'
         result = self.dispatch(['--project-directory', 'alt/', 'config'])
@@ -616,7 +644,7 @@ class CLITestCase(DockerClientTestCase):
     def test_pull_with_digest(self):
         result = self.dispatch(['-f', 'digest.yml', 'pull', '--no-parallel'])
 
-        assert 'Pulling simple (busybox:latest)...' in result.stderr
+        assert 'Pulling simple ({})...'.format(BUSYBOX_IMAGE_WITH_TAG) in result.stderr
         assert ('Pulling digest (busybox@'
                 'sha256:38a203e1986cf79639cfb9b2e1d6e773de84002feea2d4eb006b520'
                 '04ee8502d)...') in result.stderr
@@ -627,12 +655,19 @@ class CLITestCase(DockerClientTestCase):
             'pull', '--ignore-pull-failures', '--no-parallel']
         )
 
-        assert 'Pulling simple (busybox:latest)...' in result.stderr
+        assert 'Pulling simple ({})...'.format(BUSYBOX_IMAGE_WITH_TAG) in result.stderr
         assert 'Pulling another (nonexisting-image:latest)...' in result.stderr
         assert ('repository nonexisting-image not found' in result.stderr or
                 'image library/nonexisting-image:latest not found' in result.stderr or
                 'pull access denied for nonexisting-image' in result.stderr)
 
+    def test_pull_with_build(self):
+        result = self.dispatch(['-f', 'pull-with-build.yml', 'pull'])
+
+        assert 'Pulling simple' not in result.stderr
+        assert 'Pulling from_simple' not in result.stderr
+        assert 'Pulling another ...' in result.stderr
+
     def test_pull_with_quiet(self):
         assert self.dispatch(['pull', '--quiet']).stderr == ''
         assert self.dispatch(['pull', '--quiet']).stdout == ''
@@ -747,6 +782,27 @@ class CLITestCase(DockerClientTestCase):
         ]
         assert not containers
 
+    @pytest.mark.xfail(True, reason='Flaky on local')
+    def test_build_rm(self):
+        containers = [
+            Container.from_ps(self.project.client, c)
+            for c in self.project.client.containers(all=True)
+        ]
+
+        assert not containers
+
+        self.base_dir = 'tests/fixtures/simple-dockerfile'
+        self.dispatch(['build', '--no-rm', 'simple'], returncode=0)
+
+        containers = [
+            Container.from_ps(self.project.client, c)
+            for c in self.project.client.containers(all=True)
+        ]
+        assert containers
+
+        for c in self.project.client.containers(all=True):
+            self.addCleanup(self.project.client.remove_container, c, force=True)
+
     def test_build_shm_size_build_option(self):
         pull_busybox(self.client)
         self.base_dir = 'tests/fixtures/build-shm-size'
@@ -1108,6 +1164,22 @@ class CLITestCase(DockerClientTestCase):
         ]
         assert len(remote_volumes) > 0
 
+    @v2_only()
+    def test_up_no_start_remove_orphans(self):
+        self.base_dir = 'tests/fixtures/v2-simple'
+        self.dispatch(['up', '--no-start'], None)
+
+        services = self.project.get_services()
+
+        stopped = reduce((lambda prev, next: prev.containers(
+            stopped=True) + next.containers(stopped=True)), services)
+        assert len(stopped) == 2
+
+        self.dispatch(['-f', 'one-container.yml', 'up', '--no-start', '--remove-orphans'], None)
+        stopped2 = reduce((lambda prev, next: prev.containers(
+            stopped=True) + next.containers(stopped=True)), services)
+        assert len(stopped2) == 1
+
     @v2_only()
     def test_up_no_ansi(self):
         self.base_dir = 'tests/fixtures/v2-simple'
@@ -1380,7 +1452,7 @@ class CLITestCase(DockerClientTestCase):
             if v['Name'].split('/')[-1].startswith('{}_'.format(self.project.name))
         ]
 
-        assert set([v['Name'].split('/')[-1] for v in volumes]) == set([volume_with_label])
+        assert set([v['Name'].split('/')[-1] for v in volumes]) == {volume_with_label}
         assert 'label_key' in volumes[0]['Labels']
         assert volumes[0]['Labels']['label_key'] == 'label_val'
 
@@ -2045,7 +2117,7 @@ class CLITestCase(DockerClientTestCase):
             for _, config in networks.items():
                 # TODO: once we drop support for API <1.24, this can be changed to:
                 # assert config['Aliases'] == [container.short_id]
-                aliases = set(config['Aliases'] or []) - set([container.short_id])
+                aliases = set(config['Aliases'] or []) - {container.short_id}
                 assert not aliases
 
     @v2_only()
@@ -2065,7 +2137,7 @@ class CLITestCase(DockerClientTestCase):
         for _, config in networks.items():
             # TODO: once we drop support for API <1.24, this can be changed to:
             # assert config['Aliases'] == [container.short_id]
-            aliases = set(config['Aliases'] or []) - set([container.short_id])
+            aliases = set(config['Aliases'] or []) - {container.short_id}
             assert not aliases
 
         assert self.lookup(container, 'app')
@@ -2301,6 +2373,7 @@ class CLITestCase(DockerClientTestCase):
         assert 'another' in result.stdout
         assert 'exited with code 0' in result.stdout
 
+    @pytest.mark.skip(reason="race condition between up and logs")
     def test_logs_follow_logs_from_new_containers(self):
         self.base_dir = 'tests/fixtures/logs-composefile'
         self.dispatch(['up', '-d', 'simple'])
@@ -2327,6 +2400,7 @@ class CLITestCase(DockerClientTestCase):
         assert '{} exited with code 0'.format(another_name) in result.stdout
         assert '{} exited with code 137'.format(simple_name) in result.stdout
 
+    @pytest.mark.skip(reason="race condition between up and logs")
     def test_logs_follow_logs_from_restarted_containers(self):
         self.base_dir = 'tests/fixtures/logs-restart-composefile'
         proc = start_process(self.base_dir, ['up'])
@@ -2347,6 +2421,7 @@ class CLITestCase(DockerClientTestCase):
         ) == 3
         assert result.stdout.count('world') == 3
 
+    @pytest.mark.skip(reason="race condition between up and logs")
     def test_logs_default(self):
         self.base_dir = 'tests/fixtures/logs-composefile'
         self.dispatch(['up', '-d'])
@@ -2473,10 +2548,12 @@ class CLITestCase(DockerClientTestCase):
         self.dispatch(['up', '-d'])
         assert len(project.get_service('web').containers()) == 2
         assert len(project.get_service('db').containers()) == 1
+        assert len(project.get_service('worker').containers()) == 0
 
-        self.dispatch(['up', '-d', '--scale', 'web=3'])
+        self.dispatch(['up', '-d', '--scale', 'web=3', '--scale', 'worker=1'])
         assert len(project.get_service('web').containers()) == 3
         assert len(project.get_service('db').containers()) == 1
+        assert len(project.get_service('worker').containers()) == 1
 
     def test_up_scale_scale_down(self):
         self.base_dir = 'tests/fixtures/scale'
@@ -2485,22 +2562,26 @@ class CLITestCase(DockerClientTestCase):
         self.dispatch(['up', '-d'])
         assert len(project.get_service('web').containers()) == 2
         assert len(project.get_service('db').containers()) == 1
+        assert len(project.get_service('worker').containers()) == 0
 
         self.dispatch(['up', '-d', '--scale', 'web=1'])
         assert len(project.get_service('web').containers()) == 1
         assert len(project.get_service('db').containers()) == 1
+        assert len(project.get_service('worker').containers()) == 0
 
     def test_up_scale_reset(self):
         self.base_dir = 'tests/fixtures/scale'
         project = self.project
 
-        self.dispatch(['up', '-d', '--scale', 'web=3', '--scale', 'db=3'])
+        self.dispatch(['up', '-d', '--scale', 'web=3', '--scale', 'db=3', '--scale', 'worker=3'])
         assert len(project.get_service('web').containers()) == 3
         assert len(project.get_service('db').containers()) == 3
+        assert len(project.get_service('worker').containers()) == 3
 
         self.dispatch(['up', '-d'])
         assert len(project.get_service('web').containers()) == 2
         assert len(project.get_service('db').containers()) == 1
+        assert len(project.get_service('worker').containers()) == 0
 
     def test_up_scale_to_zero(self):
         self.base_dir = 'tests/fixtures/scale'
@@ -2509,10 +2590,12 @@ class CLITestCase(DockerClientTestCase):
         self.dispatch(['up', '-d'])
         assert len(project.get_service('web').containers()) == 2
         assert len(project.get_service('db').containers()) == 1
+        assert len(project.get_service('worker').containers()) == 0
 
-        self.dispatch(['up', '-d', '--scale', 'web=0', '--scale', 'db=0'])
+        self.dispatch(['up', '-d', '--scale', 'web=0', '--scale', 'db=0', '--scale', 'worker=0'])
         assert len(project.get_service('web').containers()) == 0
         assert len(project.get_service('db').containers()) == 0
+        assert len(project.get_service('worker').containers()) == 0
 
     def test_port(self):
         self.base_dir = 'tests/fixtures/ports-composefile'
@@ -2664,7 +2747,7 @@ class CLITestCase(DockerClientTestCase):
         self.base_dir = 'tests/fixtures/extends'
         self.dispatch(['up', '-d'], None)
 
-        assert set([s.name for s in self.project.services]) == set(['mydb', 'myweb'])
+        assert set([s.name for s in self.project.services]) == {'mydb', 'myweb'}
 
         # Sort by name so we get [db, web]
         containers = sorted(
@@ -2676,15 +2759,9 @@ class CLITestCase(DockerClientTestCase):
         web = containers[1]
         db_name = containers[0].name_without_project
 
-        assert set(get_links(web)) == set(
-            ['db', db_name, 'extends_{}'.format(db_name)]
-        )
+        assert set(get_links(web)) == {'db', db_name, 'extends_{}'.format(db_name)}
 
-        expected_env = set([
-            "FOO=1",
-            "BAR=2",
-            "BAZ=2",
-        ])
+        expected_env = {"FOO=1", "BAR=2", "BAZ=2"}
         assert expected_env <= set(web.get('Config.Env'))
 
     def test_top_services_not_running(self):

+ 2 - 2
tests/fixtures/UpperCaseDir/docker-compose.yml

@@ -1,6 +1,6 @@
 simple:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: top
 another:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: top

+ 2 - 2
tests/fixtures/abort-on-container-exit-0/docker-compose.yml

@@ -1,6 +1,6 @@
 simple:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: top
 another:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: ls .

+ 2 - 2
tests/fixtures/abort-on-container-exit-1/docker-compose.yml

@@ -1,6 +1,6 @@
 simple:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: top
 another:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: ls /thecakeisalie

+ 1 - 1
tests/fixtures/build-args/Dockerfile

@@ -1,4 +1,4 @@
-FROM busybox:latest
+FROM busybox:1.31.0-uclibc
 LABEL com.docker.compose.test_image=true
 ARG favorite_th_character
 RUN echo "Favorite Touhou Character: ${favorite_th_character}"

+ 1 - 1
tests/fixtures/build-ctx/Dockerfile

@@ -1,3 +1,3 @@
-FROM busybox:latest
+FROM busybox:1.31.0-uclibc
 LABEL com.docker.compose.test_image=true
 CMD echo "success"

+ 1 - 1
tests/fixtures/build-memory/Dockerfile

@@ -1,4 +1,4 @@
-FROM busybox
+FROM busybox:1.31.0-uclibc
 
 # Report the memory (through the size of the group memory)
 RUN echo "memory:" $(cat /sys/fs/cgroup/memory/memory.limit_in_bytes)

+ 1 - 1
tests/fixtures/build-multiple-composefile/a/Dockerfile

@@ -1,4 +1,4 @@
 
-FROM busybox:latest
+FROM busybox:1.31.0-uclibc
 RUN  echo a
 CMD  top

+ 1 - 1
tests/fixtures/build-multiple-composefile/b/Dockerfile

@@ -1,4 +1,4 @@
 
-FROM busybox:latest
+FROM busybox:1.31.0-uclibc
 RUN  echo b
 CMD  top

+ 4 - 0
tests/fixtures/default-env-file/.env2

@@ -0,0 +1,4 @@
+IMAGE=alpine:latest
+COMMAND=false
+PORT1=5644
+PORT2=9998

+ 1 - 1
tests/fixtures/dockerfile-with-volume/Dockerfile

@@ -1,4 +1,4 @@
-FROM busybox:latest
+FROM busybox:1.31.0-uclibc
 LABEL com.docker.compose.test_image=true
 VOLUME /data
 CMD top

+ 2 - 2
tests/fixtures/duplicate-override-yaml-files/docker-compose.yml

@@ -1,10 +1,10 @@
 
 web:
-    image: busybox:latest
+    image: busybox:1.31.0-uclibc
     command: "sleep 100"
     links:
         - db
 
 db:
-    image: busybox:latest
+    image: busybox:1.31.0-uclibc
     command: "sleep 200"

+ 2 - 2
tests/fixtures/echo-services/docker-compose.yml

@@ -1,6 +1,6 @@
 simple:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: echo simple
 another:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: echo another

+ 1 - 1
tests/fixtures/entrypoint-dockerfile/Dockerfile

@@ -1,4 +1,4 @@
-FROM busybox:latest
+FROM busybox:1.31.0-uclibc
 LABEL com.docker.compose.test_image=true
 ENTRYPOINT ["printf"]
 CMD ["default", "args"]

+ 2 - 0
tests/fixtures/env-file-override/.env.conf

@@ -0,0 +1,2 @@
+WHEREAMI
+DEFAULT_CONF_LOADED=true

+ 1 - 0
tests/fixtures/env-file-override/.env.override

@@ -0,0 +1 @@
+WHEREAMI=override

+ 6 - 0
tests/fixtures/env-file-override/docker-compose.yml

@@ -0,0 +1,6 @@
+version: '3.7'
+services:
+  test:
+    image: busybox
+    env_file: .env.conf
+    entrypoint: env

+ 1 - 1
tests/fixtures/environment-composefile/docker-compose.yml

@@ -1,5 +1,5 @@
 service:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: top
 
   environment:

+ 2 - 2
tests/fixtures/exit-code-from/docker-compose.yml

@@ -1,6 +1,6 @@
 simple:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: sh -c "echo hello && tail -f /dev/null"
 another:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: /bin/false

+ 1 - 1
tests/fixtures/expose-composefile/docker-compose.yml

@@ -1,6 +1,6 @@
 
 simple:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: top
   expose:
     - '3000'

+ 1 - 1
tests/fixtures/images-service-tag/Dockerfile

@@ -1,2 +1,2 @@
-FROM busybox:latest
+FROM busybox:1.31.0-uclibc
 RUN touch /foo

+ 2 - 2
tests/fixtures/logging-composefile-legacy/docker-compose.yml

@@ -1,9 +1,9 @@
 simple:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: top
   log_driver: "none"
 another:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: top
   log_driver: "json-file"
   log_opt:

+ 2 - 2
tests/fixtures/logging-composefile/docker-compose.yml

@@ -1,12 +1,12 @@
 version: "2"
 services:
   simple:
-    image: busybox:latest
+    image: busybox:1.31.0-uclibc
     command: top
     logging:
       driver: "none"
   another:
-    image: busybox:latest
+    image: busybox:1.31.0-uclibc
     command: top
     logging:
       driver: "json-file"

+ 4 - 4
tests/fixtures/logs-composefile/docker-compose.yml

@@ -1,6 +1,6 @@
 simple:
-  image: busybox:latest
-  command: sh -c "echo hello && tail -f /dev/null"
+  image: busybox:1.31.0-uclibc
+  command: sh -c "sleep 1 && echo hello && tail -f /dev/null"
 another:
-  image: busybox:latest
-  command: sh -c "echo test"
+  image: busybox:1.31.0-uclibc
+  command: sh -c "sleep 1 && echo test"

+ 3 - 3
tests/fixtures/logs-restart-composefile/docker-compose.yml

@@ -1,7 +1,7 @@
 simple:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: sh -c "echo hello && tail -f /dev/null"
 another:
-  image: busybox:latest
-  command: sh -c "sleep 0.5 && echo world && /bin/false"
+  image: busybox:1.31.0-uclibc
+  command: sh -c "sleep 2 && echo world && /bin/false"
   restart: "on-failure:2"

+ 1 - 1
tests/fixtures/logs-tail-composefile/docker-compose.yml

@@ -1,3 +1,3 @@
 simple:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: sh -c "echo w && echo x && echo y && echo z"

+ 1 - 1
tests/fixtures/longer-filename-composefile/docker-compose.yaml

@@ -1,3 +1,3 @@
 definedinyamlnotyml:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: top

+ 1 - 1
tests/fixtures/multiple-composefiles/compose2.yml

@@ -1,3 +1,3 @@
 yetanother:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: top

+ 2 - 2
tests/fixtures/multiple-composefiles/docker-compose.yml

@@ -1,6 +1,6 @@
 simple:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: top
 another:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: top

+ 2 - 2
tests/fixtures/networks/default-network-config.yml

@@ -1,10 +1,10 @@
 version: "2"
 services:
   simple:
-    image: busybox:latest
+    image: busybox:1.31.0-uclibc
     command: top
   another:
-    image: busybox:latest
+    image: busybox:1.31.0-uclibc
     command: top
 networks:
   default:

+ 2 - 2
tests/fixtures/networks/external-default.yml

@@ -1,10 +1,10 @@
 version: "2"
 services:
   simple:
-    image: busybox:latest
+    image: busybox:1.31.0-uclibc
     command: top
   another:
-    image: busybox:latest
+    image: busybox:1.31.0-uclibc
     command: top
 networks:
   default:

+ 3 - 3
tests/fixtures/no-links-composefile/docker-compose.yml

@@ -1,9 +1,9 @@
 db:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: top
 web:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: top
 console:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: top

+ 2 - 2
tests/fixtures/override-files/docker-compose.yml

@@ -1,10 +1,10 @@
 version: '2.2'
 services:
   web:
-    image: busybox:latest
+    image: busybox:1.31.0-uclibc
     command: "sleep 200"
     depends_on:
         - db
   db:
-    image: busybox:latest
+    image: busybox:1.31.0-uclibc
     command: "sleep 200"

+ 1 - 1
tests/fixtures/override-files/extra.yml

@@ -6,5 +6,5 @@ services:
         - other
 
   other:
-    image: busybox:latest
+    image: busybox:1.31.0-uclibc
     command: "top"

+ 2 - 2
tests/fixtures/override-yaml-files/docker-compose.yml

@@ -1,10 +1,10 @@
 
 web:
-    image: busybox:latest
+    image: busybox:1.31.0-uclibc
     command: "sleep 100"
     links:
         - db
 
 db:
-    image: busybox:latest
+    image: busybox:1.31.0-uclibc
     command: "sleep 200"

+ 1 - 1
tests/fixtures/ports-composefile-scale/docker-compose.yml

@@ -1,6 +1,6 @@
 
 simple:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: /bin/sleep 300
   ports:
     - '3000'

+ 1 - 1
tests/fixtures/ports-composefile/docker-compose.yml

@@ -1,6 +1,6 @@
 
 simple:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: top
   ports:
     - '3000'

+ 1 - 1
tests/fixtures/ports-composefile/expanded-notation.yml

@@ -1,7 +1,7 @@
 version: '3.2'
 services:
     simple:
-      image: busybox:latest
+      image: busybox:1.31.0-uclibc
       command: top
       ports:
         - target: 3000

+ 1 - 1
tests/fixtures/ps-services-filter/docker-compose.yml

@@ -1,5 +1,5 @@
 with_image:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: top
 with_build:
   build: ../build-ctx/

+ 1 - 1
tests/fixtures/run-labels/docker-compose.yml

@@ -1,5 +1,5 @@
 service:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: top
 
   labels:

+ 1 - 1
tests/fixtures/run-workdir/docker-compose.yml

@@ -1,4 +1,4 @@
 service:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   working_dir: /etc
   command: /bin/true

+ 6 - 2
tests/fixtures/scale/docker-compose.yml

@@ -5,5 +5,9 @@ services:
         command: top
         scale: 2
     db:
-      image: busybox
-      command: top
+        image: busybox
+        command: top
+    worker:
+        image: busybox
+        command: top
+        scale: 0

+ 1 - 1
tests/fixtures/simple-composefile-volume-ready/docker-compose.merge.yml

@@ -1,7 +1,7 @@
 version: '2.2'
 services:
   simple:
-    image: busybox:latest
+    image: busybox:1.31.0-uclibc
     volumes:
       - datastore:/data1
 

+ 1 - 1
tests/fixtures/simple-composefile-volume-ready/docker-compose.yml

@@ -1,2 +1,2 @@
 simple:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc

+ 1 - 1
tests/fixtures/simple-composefile/digest.yml

@@ -1,5 +1,5 @@
 simple:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: top
 digest:
   image: busybox@sha256:38a203e1986cf79639cfb9b2e1d6e773de84002feea2d4eb006b52004ee8502d

+ 1 - 1
tests/fixtures/simple-composefile/docker-compose.yml

@@ -2,5 +2,5 @@ simple:
   image: busybox:1.27.2
   command: top
 another:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: top

+ 1 - 1
tests/fixtures/simple-composefile/ignore-pull-failures.yml

@@ -1,5 +1,5 @@
 simple:
-  image: busybox:latest
+  image: busybox:1.31.0-uclibc
   command: top
 another:
   image: nonexisting-image:latest

+ 11 - 0
tests/fixtures/simple-composefile/pull-with-build.yml

@@ -0,0 +1,11 @@
+version: "3"
+services:
+  build_simple:
+    image: simple
+    build: .
+    command: top
+  from_simple:
+    image: simple
+  another:
+    image: busybox:1.31.0-uclibc
+    command: top

+ 1 - 1
tests/fixtures/simple-failing-dockerfile/Dockerfile

@@ -1,4 +1,4 @@
-FROM busybox:latest
+FROM busybox:1.31.0-uclibc
 LABEL com.docker.compose.test_image=true
 LABEL com.docker.compose.test_failing_image=true
 # With the following label the container wil be cleaned up automatically

+ 2 - 2
tests/fixtures/sleeps-composefile/docker-compose.yml

@@ -3,8 +3,8 @@ version: "2"
 
 services:
   simple:
-    image: busybox:latest
+    image: busybox:1.31.0-uclibc
     command: sleep 200
   another:
-    image: busybox:latest
+    image: busybox:1.31.0-uclibc
     command: sleep 200

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно