diff --git a/.circleci/config.yml b/.circleci/config.yml index 08f8c42c3..906b1c0dc 100644 --- a/.circleci/config.yml +++ b/.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: diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f777c6c5..89f028812 100644 --- a/CHANGELOG.md +++ b/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 -1.24.0 (2019-03-22) +- 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-28) ------------------- ### Features diff --git a/Dockerfile b/Dockerfile index c5e7c815a..ed9d74e5e 100644 --- a/Dockerfile +++ b/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 +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 diff --git a/Dockerfile.armhf b/Dockerfile.armhf deleted file mode 100644 index ee2ce8941..000000000 --- a/Dockerfile.armhf +++ /dev/null @@ -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"] diff --git a/Dockerfile.run b/Dockerfile.run deleted file mode 100644 index ccc86ea96..000000000 --- a/Dockerfile.run +++ /dev/null @@ -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"] diff --git a/Jenkinsfile b/Jenkinsfile index 04f5cfbda..4de276ada 100644 --- a/Jenkinsfile +++ b/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) diff --git a/MAINTAINERS b/MAINTAINERS index 7aedd46e9..5d4bd6a63 100644 --- a/MAINTAINERS +++ b/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 = "mazz@houseofmnowster.com" GitHub = "mnowster" - [People.shin-] + [people.rumpl] + Name = "Djordje Lukic" + Email = "djordje.lukic@docker.com" + GitHub = "rumpl" + + [people.shin-] Name = "Joffrey F" - Email = "joffrey@docker.com" + Email = "f.joffrey@gmail.com" GitHub = "shin-" + + [people.ulyssessouza] + Name = "Ulysses Domiciano Souza" + Email = "ulysses.souza@docker.com" + GitHub = "ulyssessouza" diff --git a/appveyor.yml b/appveyor.yml index da80d01d9..04a40e9c2 100644 --- a/appveyor.yml +++ b/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: diff --git a/compose/__init__.py b/compose/__init__.py index 6a40e150f..df0fd3fbd 100644 --- a/compose/__init__.py +++ b/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' diff --git a/compose/bundle.py b/compose/bundle.py index 937a3708a..77cb37aa9 100644 --- a/compose/bundle.py +++ b/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)) diff --git a/compose/cli/command.py b/compose/cli/command.py index 339a65c53..2f38fe5af 100644 --- a/compose/cli/command.py +++ b/compose/cli/command.py @@ -21,10 +21,27 @@ from .utils import get_version_info log = logging.getLogger(__name__) +SILENT_COMMANDS = { + 'events', + 'exec', + 'kill', + 'logs', + 'pause', + 'ps', + 'restart', + 'rm', + 'start', + 'stop', + 'top', + 'unpause', +} -def project_from_options(project_dir, options): + +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', diff --git a/compose/cli/docker_client.py b/compose/cli/docker_client.py index a01704fd2..a57a69b50 100644 --- a/compose/cli/docker_client.py +++ b/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) diff --git a/compose/cli/main.py b/compose/cli/main.py index 789601792..477b57b52 100644 --- a/compose/cli/main.py +++ b/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" diff --git a/compose/cli/utils.py b/compose/cli/utils.py index 4cc055cc9..bd06beef8 100644 --- a/compose/cli/utils.py +++ b/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] ) diff --git a/compose/config/config.py b/compose/config/config.py index f3142d80a..5202d0025 100644 --- a/compose/config/config.py +++ b/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)) ) diff --git a/compose/config/environment.py b/compose/config/environment.py index bd52758f2..696356f32 100644 --- a/compose/config/environment.py +++ b/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) ) diff --git a/compose/config/interpolation.py b/compose/config/interpolation.py index 0f878be14..18be8562c 100644 --- a/compose/config/interpolation.py +++ b/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) ) diff --git a/compose/config/serialize.py b/compose/config/serialize.py index 8cb8a2808..5776ce957 100644 --- a/compose/config/serialize.py +++ b/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, diff --git a/compose/network.py b/compose/network.py index 2491a5989..e0d711ff7 100644 --- a/compose/network.py +++ b/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: diff --git a/compose/project.py b/compose/project.py index a7f2aa057..a608ffd71 100644 --- a/compose/project.py +++ b/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( diff --git a/compose/service.py b/compose/service.py index 8c6702f1e..0db35438d 100644 --- a/compose/service.py +++ b/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}`) " diff --git a/compose/volume.py b/compose/volume.py index 60c1e0fe8..b02fc5d80 100644 --- a/compose/volume.py +++ b/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) ) diff --git a/contrib/completion/bash/docker-compose b/contrib/completion/bash/docker-compose index 2add0c9cd..6dc47799d 100644 --- a/contrib/completion/bash/docker-compose +++ b/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 diff --git a/contrib/completion/fish/docker-compose.fish b/contrib/completion/fish/docker-compose.fish index 69ecc5056..0566e16ae 100644 --- a/contrib/completion/fish/docker-compose.fish +++ b/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' diff --git a/contrib/completion/zsh/_docker-compose b/contrib/completion/zsh/_docker-compose index d25256c14..faf405988 100755 --- a/contrib/completion/zsh/_docker-compose +++ b/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" diff --git a/contrib/migration/migrate-compose-file-v1-to-v2.py b/contrib/migration/migrate-compose-file-v1-to-v2.py index c1785b0da..274b499b9 100755 --- a/contrib/migration/migrate-compose-file-v1-to-v2.py +++ b/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. " diff --git a/docker-compose-entrypoint.sh b/docker-compose-entrypoint.sh new file mode 100755 index 000000000..84436fa07 --- /dev/null +++ b/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 "$@" diff --git a/docs/README.md b/docs/README.md index 50c91d207..accc7c23e 100644 --- a/docs/README.md +++ b/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! diff --git a/pyinstaller/ldd b/pyinstaller/ldd new file mode 100755 index 000000000..3f10ad275 --- /dev/null +++ b/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' diff --git a/requirements-build.txt b/requirements-build.txt index e5a77e794..9161fadf9 100644 --- a/requirements-build.txt +++ b/requirements-build.txt @@ -1 +1 @@ -pyinstaller==3.3.1 +pyinstaller==3.4 diff --git a/requirements-dev.txt b/requirements-dev.txt index bfb941152..27b71a268 100644 --- a/requirements-dev.txt +++ b/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 diff --git a/requirements.txt b/requirements.txt index 6007ee3ff..e5b6883e9 100644 --- a/requirements.txt +++ b/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 diff --git a/script/build/image b/script/build/image index a3198c99f..fb3f856ee 100755 --- a/script/build/image +++ b/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}" . diff --git a/script/build/linux b/script/build/linux index 056940ad0..28065da08 100755 --- a/script/build/linux +++ b/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}" diff --git a/script/build/linux-entrypoint b/script/build/linux-entrypoint index 0e3c7ec1e..1c5438d8e 100755 --- a/script/build/linux-entrypoint +++ b/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 diff --git a/script/build/osx b/script/build/osx index c62b27024..529914586 100755 --- a/script/build/osx +++ b/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 diff --git a/script/build/test-image b/script/build/test-image index a2eb62cdf..4964a5f9d 100755 --- a/script/build/test-image +++ b/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 diff --git a/script/build/windows.ps1 b/script/build/windows.ps1 index 41dc51e31..4c7a8bed5 100644 --- a/script/build/windows.ps1 +++ b/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: diff --git a/script/build/write-git-sha b/script/build/write-git-sha index be87f5058..cac4b6fd3 100755 --- a/script/build/write-git-sha +++ b/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}" diff --git a/script/release/README.md b/script/release/README.md index 0c6f12cbe..97168d376 100644 --- a/script/release/README.md +++ b/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 diff --git a/script/release/release.py b/script/release/release.py index 9db1a49d9..a9c05eb78 100755 --- a/script/release/release.py +++ b/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)) diff --git a/script/release/release/const.py b/script/release/release/const.py index 5a72bde41..52458ea14 100644 --- a/script/release/release/const.py +++ b/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' diff --git a/script/release/release/images.py b/script/release/release/images.py index df6eeda4f..17d572df3 100644 --- a/script/release/release/images.py +++ b/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: diff --git a/script/release/release/repository.py b/script/release/release/repository.py index bb8f4fbeb..a0281eaa3 100644 --- a/script/release/release/repository.py +++ b/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 diff --git a/script/run/run.sh b/script/run/run.sh index 4881adc38..8756ae34a 100755 --- a/script/run/run.sh +++ b/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 diff --git a/script/setup/osx b/script/setup/osx index 1b546816d..69280f8a2 100755 --- a/script/setup/osx +++ b/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}" diff --git a/script/test/all b/script/test/all index e48f73bba..f929a57ee 100755 --- a/script/test/all +++ b/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" diff --git a/script/test/ci b/script/test/ci index 8d3aa56cb..bbcedac47 100755 --- a/script/test/ci +++ b/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 diff --git a/script/test/default b/script/test/default index cbb6a67cb..4f307f2e9 100755 --- a/script/test/default +++ b/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 diff --git a/setup.py b/setup.py index 8371cc756..a4020df46 100644 --- a/setup.py +++ b/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'], } diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 9ed257369..77b46c279 100644 --- a/tests/acceptance/cli_test.py +++ b/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): diff --git a/tests/fixtures/UpperCaseDir/docker-compose.yml b/tests/fixtures/UpperCaseDir/docker-compose.yml index b25beaf4b..09cc9519f 100644 --- a/tests/fixtures/UpperCaseDir/docker-compose.yml +++ b/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 diff --git a/tests/fixtures/abort-on-container-exit-0/docker-compose.yml b/tests/fixtures/abort-on-container-exit-0/docker-compose.yml index ce41697bc..77307ef29 100644 --- a/tests/fixtures/abort-on-container-exit-0/docker-compose.yml +++ b/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 . diff --git a/tests/fixtures/abort-on-container-exit-1/docker-compose.yml b/tests/fixtures/abort-on-container-exit-1/docker-compose.yml index 7ec9b7e11..23290964e 100644 --- a/tests/fixtures/abort-on-container-exit-1/docker-compose.yml +++ b/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 diff --git a/tests/fixtures/build-args/Dockerfile b/tests/fixtures/build-args/Dockerfile index 93ebcb9cd..d1534068a 100644 --- a/tests/fixtures/build-args/Dockerfile +++ b/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}" diff --git a/tests/fixtures/build-ctx/Dockerfile b/tests/fixtures/build-ctx/Dockerfile index dd864b838..4acac9c7a 100644 --- a/tests/fixtures/build-ctx/Dockerfile +++ b/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" diff --git a/tests/fixtures/build-memory/Dockerfile b/tests/fixtures/build-memory/Dockerfile index b27349b96..076b84d77 100644 --- a/tests/fixtures/build-memory/Dockerfile +++ b/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) diff --git a/tests/fixtures/build-multiple-composefile/a/Dockerfile b/tests/fixtures/build-multiple-composefile/a/Dockerfile index 2ba45ce55..52ed15ec6 100644 --- a/tests/fixtures/build-multiple-composefile/a/Dockerfile +++ b/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 diff --git a/tests/fixtures/build-multiple-composefile/b/Dockerfile b/tests/fixtures/build-multiple-composefile/b/Dockerfile index e282e8bbf..932d851d9 100644 --- a/tests/fixtures/build-multiple-composefile/b/Dockerfile +++ b/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 diff --git a/tests/fixtures/default-env-file/.env2 b/tests/fixtures/default-env-file/.env2 new file mode 100644 index 000000000..d754523fc --- /dev/null +++ b/tests/fixtures/default-env-file/.env2 @@ -0,0 +1,4 @@ +IMAGE=alpine:latest +COMMAND=false +PORT1=5644 +PORT2=9998 diff --git a/tests/fixtures/dockerfile-with-volume/Dockerfile b/tests/fixtures/dockerfile-with-volume/Dockerfile index 0d376ec48..f38e1d579 100644 --- a/tests/fixtures/dockerfile-with-volume/Dockerfile +++ b/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 diff --git a/tests/fixtures/duplicate-override-yaml-files/docker-compose.yml b/tests/fixtures/duplicate-override-yaml-files/docker-compose.yml index 5f2909d69..6880435b3 100644 --- a/tests/fixtures/duplicate-override-yaml-files/docker-compose.yml +++ b/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" diff --git a/tests/fixtures/echo-services/docker-compose.yml b/tests/fixtures/echo-services/docker-compose.yml index 8014f3d91..75fc45d95 100644 --- a/tests/fixtures/echo-services/docker-compose.yml +++ b/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 diff --git a/tests/fixtures/entrypoint-dockerfile/Dockerfile b/tests/fixtures/entrypoint-dockerfile/Dockerfile index 49f4416c8..30ec50bac 100644 --- a/tests/fixtures/entrypoint-dockerfile/Dockerfile +++ b/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"] diff --git a/tests/fixtures/env-file-override/.env.conf b/tests/fixtures/env-file-override/.env.conf new file mode 100644 index 000000000..90b8b495a --- /dev/null +++ b/tests/fixtures/env-file-override/.env.conf @@ -0,0 +1,2 @@ +WHEREAMI +DEFAULT_CONF_LOADED=true diff --git a/tests/fixtures/env-file-override/.env.override b/tests/fixtures/env-file-override/.env.override new file mode 100644 index 000000000..398fa51b3 --- /dev/null +++ b/tests/fixtures/env-file-override/.env.override @@ -0,0 +1 @@ +WHEREAMI=override diff --git a/tests/fixtures/env-file-override/docker-compose.yml b/tests/fixtures/env-file-override/docker-compose.yml new file mode 100644 index 000000000..fdae6d826 --- /dev/null +++ b/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 diff --git a/tests/fixtures/environment-composefile/docker-compose.yml b/tests/fixtures/environment-composefile/docker-compose.yml index 9d99fee08..5650c7c8e 100644 --- a/tests/fixtures/environment-composefile/docker-compose.yml +++ b/tests/fixtures/environment-composefile/docker-compose.yml @@ -1,5 +1,5 @@ service: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top environment: diff --git a/tests/fixtures/exit-code-from/docker-compose.yml b/tests/fixtures/exit-code-from/docker-compose.yml index 687e78b97..c38bd549b 100644 --- a/tests/fixtures/exit-code-from/docker-compose.yml +++ b/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 diff --git a/tests/fixtures/expose-composefile/docker-compose.yml b/tests/fixtures/expose-composefile/docker-compose.yml index d14a468de..c2a3dc424 100644 --- a/tests/fixtures/expose-composefile/docker-compose.yml +++ b/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' diff --git a/tests/fixtures/images-service-tag/Dockerfile b/tests/fixtures/images-service-tag/Dockerfile index 145e0202f..1e1a1b2e0 100644 --- a/tests/fixtures/images-service-tag/Dockerfile +++ b/tests/fixtures/images-service-tag/Dockerfile @@ -1,2 +1,2 @@ -FROM busybox:latest +FROM busybox:1.31.0-uclibc RUN touch /foo diff --git a/tests/fixtures/logging-composefile-legacy/docker-compose.yml b/tests/fixtures/logging-composefile-legacy/docker-compose.yml index ee9941079..efac1d6a3 100644 --- a/tests/fixtures/logging-composefile-legacy/docker-compose.yml +++ b/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: diff --git a/tests/fixtures/logging-composefile/docker-compose.yml b/tests/fixtures/logging-composefile/docker-compose.yml index 466d13e5b..ac231b898 100644 --- a/tests/fixtures/logging-composefile/docker-compose.yml +++ b/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" diff --git a/tests/fixtures/logs-composefile/docker-compose.yml b/tests/fixtures/logs-composefile/docker-compose.yml index b719c91e0..3ffaa9849 100644 --- a/tests/fixtures/logs-composefile/docker-compose.yml +++ b/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" diff --git a/tests/fixtures/logs-restart-composefile/docker-compose.yml b/tests/fixtures/logs-restart-composefile/docker-compose.yml index c662a1e71..2179d54de 100644 --- a/tests/fixtures/logs-restart-composefile/docker-compose.yml +++ b/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" diff --git a/tests/fixtures/logs-tail-composefile/docker-compose.yml b/tests/fixtures/logs-tail-composefile/docker-compose.yml index b70d0cc63..18dad986e 100644 --- a/tests/fixtures/logs-tail-composefile/docker-compose.yml +++ b/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" diff --git a/tests/fixtures/longer-filename-composefile/docker-compose.yaml b/tests/fixtures/longer-filename-composefile/docker-compose.yaml index a4eba2d05..5dadce44a 100644 --- a/tests/fixtures/longer-filename-composefile/docker-compose.yaml +++ b/tests/fixtures/longer-filename-composefile/docker-compose.yaml @@ -1,3 +1,3 @@ definedinyamlnotyml: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top diff --git a/tests/fixtures/multiple-composefiles/compose2.yml b/tests/fixtures/multiple-composefiles/compose2.yml index 568033804..530d92df6 100644 --- a/tests/fixtures/multiple-composefiles/compose2.yml +++ b/tests/fixtures/multiple-composefiles/compose2.yml @@ -1,3 +1,3 @@ yetanother: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top diff --git a/tests/fixtures/multiple-composefiles/docker-compose.yml b/tests/fixtures/multiple-composefiles/docker-compose.yml index b25beaf4b..09cc9519f 100644 --- a/tests/fixtures/multiple-composefiles/docker-compose.yml +++ b/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 diff --git a/tests/fixtures/networks/default-network-config.yml b/tests/fixtures/networks/default-network-config.yml index 4bd0989b7..556ca9805 100644 --- a/tests/fixtures/networks/default-network-config.yml +++ b/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: diff --git a/tests/fixtures/networks/external-default.yml b/tests/fixtures/networks/external-default.yml index 5c9426b84..42a395656 100644 --- a/tests/fixtures/networks/external-default.yml +++ b/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: diff --git a/tests/fixtures/no-links-composefile/docker-compose.yml b/tests/fixtures/no-links-composefile/docker-compose.yml index 75a6a085c..54936f306 100644 --- a/tests/fixtures/no-links-composefile/docker-compose.yml +++ b/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 diff --git a/tests/fixtures/override-files/docker-compose.yml b/tests/fixtures/override-files/docker-compose.yml index 6c3d4e172..0119ec738 100644 --- a/tests/fixtures/override-files/docker-compose.yml +++ b/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" diff --git a/tests/fixtures/override-files/extra.yml b/tests/fixtures/override-files/extra.yml index 492c37952..d03c5096d 100644 --- a/tests/fixtures/override-files/extra.yml +++ b/tests/fixtures/override-files/extra.yml @@ -6,5 +6,5 @@ services: - other other: - image: busybox:latest + image: busybox:1.31.0-uclibc command: "top" diff --git a/tests/fixtures/override-yaml-files/docker-compose.yml b/tests/fixtures/override-yaml-files/docker-compose.yml index 5f2909d69..6880435b3 100644 --- a/tests/fixtures/override-yaml-files/docker-compose.yml +++ b/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" diff --git a/tests/fixtures/ports-composefile-scale/docker-compose.yml b/tests/fixtures/ports-composefile-scale/docker-compose.yml index 1a2bb485b..bdd39cef3 100644 --- a/tests/fixtures/ports-composefile-scale/docker-compose.yml +++ b/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' diff --git a/tests/fixtures/ports-composefile/docker-compose.yml b/tests/fixtures/ports-composefile/docker-compose.yml index c213068de..f49870278 100644 --- a/tests/fixtures/ports-composefile/docker-compose.yml +++ b/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' diff --git a/tests/fixtures/ports-composefile/expanded-notation.yml b/tests/fixtures/ports-composefile/expanded-notation.yml index 09a7a2bf9..6510e4281 100644 --- a/tests/fixtures/ports-composefile/expanded-notation.yml +++ b/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 diff --git a/tests/fixtures/ps-services-filter/docker-compose.yml b/tests/fixtures/ps-services-filter/docker-compose.yml index 3d8609373..180f515aa 100644 --- a/tests/fixtures/ps-services-filter/docker-compose.yml +++ b/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/ diff --git a/tests/fixtures/run-labels/docker-compose.yml b/tests/fixtures/run-labels/docker-compose.yml index e8cd50065..e3b237fd5 100644 --- a/tests/fixtures/run-labels/docker-compose.yml +++ b/tests/fixtures/run-labels/docker-compose.yml @@ -1,5 +1,5 @@ service: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top labels: diff --git a/tests/fixtures/run-workdir/docker-compose.yml b/tests/fixtures/run-workdir/docker-compose.yml index dc3ea86a0..9d092a55f 100644 --- a/tests/fixtures/run-workdir/docker-compose.yml +++ b/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 diff --git a/tests/fixtures/scale/docker-compose.yml b/tests/fixtures/scale/docker-compose.yml index a0d3b771f..53ae1342d 100644 --- a/tests/fixtures/scale/docker-compose.yml +++ b/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 diff --git a/tests/fixtures/simple-composefile-volume-ready/docker-compose.merge.yml b/tests/fixtures/simple-composefile-volume-ready/docker-compose.merge.yml index fe7171516..45b626d02 100644 --- a/tests/fixtures/simple-composefile-volume-ready/docker-compose.merge.yml +++ b/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 diff --git a/tests/fixtures/simple-composefile-volume-ready/docker-compose.yml b/tests/fixtures/simple-composefile-volume-ready/docker-compose.yml index 98a7d23b7..088d71c99 100644 --- a/tests/fixtures/simple-composefile-volume-ready/docker-compose.yml +++ b/tests/fixtures/simple-composefile-volume-ready/docker-compose.yml @@ -1,2 +1,2 @@ simple: - image: busybox:latest + image: busybox:1.31.0-uclibc diff --git a/tests/fixtures/simple-composefile/digest.yml b/tests/fixtures/simple-composefile/digest.yml index 08f1d993e..79f043baa 100644 --- a/tests/fixtures/simple-composefile/digest.yml +++ b/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 diff --git a/tests/fixtures/simple-composefile/docker-compose.yml b/tests/fixtures/simple-composefile/docker-compose.yml index e86d3fc80..b66a06527 100644 --- a/tests/fixtures/simple-composefile/docker-compose.yml +++ b/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 diff --git a/tests/fixtures/simple-composefile/ignore-pull-failures.yml b/tests/fixtures/simple-composefile/ignore-pull-failures.yml index a28f79223..7e7d560da 100644 --- a/tests/fixtures/simple-composefile/ignore-pull-failures.yml +++ b/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 diff --git a/tests/fixtures/simple-composefile/pull-with-build.yml b/tests/fixtures/simple-composefile/pull-with-build.yml new file mode 100644 index 000000000..3bff35c51 --- /dev/null +++ b/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 diff --git a/tests/fixtures/simple-failing-dockerfile/Dockerfile b/tests/fixtures/simple-failing-dockerfile/Dockerfile index c2d06b167..205021a23 100644 --- a/tests/fixtures/simple-failing-dockerfile/Dockerfile +++ b/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 diff --git a/tests/fixtures/sleeps-composefile/docker-compose.yml b/tests/fixtures/sleeps-composefile/docker-compose.yml index 7c8d84f8d..26feb502a 100644 --- a/tests/fixtures/sleeps-composefile/docker-compose.yml +++ b/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 diff --git a/tests/fixtures/stop-signal-composefile/docker-compose.yml b/tests/fixtures/stop-signal-composefile/docker-compose.yml index 04f58aa98..9f99b0c75 100644 --- a/tests/fixtures/stop-signal-composefile/docker-compose.yml +++ b/tests/fixtures/stop-signal-composefile/docker-compose.yml @@ -1,5 +1,5 @@ simple: - image: busybox:latest + image: busybox:1.31.0-uclibc command: - sh - '-c' diff --git a/tests/fixtures/tagless-image/Dockerfile b/tests/fixtures/tagless-image/Dockerfile index 567410552..923055551 100644 --- a/tests/fixtures/tagless-image/Dockerfile +++ b/tests/fixtures/tagless-image/Dockerfile @@ -1,2 +1,2 @@ -FROM busybox:latest +FROM busybox:1.31.0-uclibc RUN touch /blah diff --git a/tests/fixtures/top/docker-compose.yml b/tests/fixtures/top/docker-compose.yml index d632a836e..36a3917d7 100644 --- a/tests/fixtures/top/docker-compose.yml +++ b/tests/fixtures/top/docker-compose.yml @@ -1,6 +1,6 @@ service_a: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top service_b: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top diff --git a/tests/fixtures/unicode-environment/docker-compose.yml b/tests/fixtures/unicode-environment/docker-compose.yml index a41af4f07..307678cd0 100644 --- a/tests/fixtures/unicode-environment/docker-compose.yml +++ b/tests/fixtures/unicode-environment/docker-compose.yml @@ -1,7 +1,7 @@ version: '2' services: simple: - image: busybox:latest + image: busybox:1.31.0-uclibc command: sh -c 'echo $$FOO' environment: FOO: ${BAR} diff --git a/tests/fixtures/user-composefile/docker-compose.yml b/tests/fixtures/user-composefile/docker-compose.yml index 3eb7d3977..11283d9d9 100644 --- a/tests/fixtures/user-composefile/docker-compose.yml +++ b/tests/fixtures/user-composefile/docker-compose.yml @@ -1,4 +1,4 @@ service: - image: busybox:latest + image: busybox:1.31.0-uclibc user: notauser command: id diff --git a/tests/fixtures/v2-dependencies/docker-compose.yml b/tests/fixtures/v2-dependencies/docker-compose.yml index 2e14b94bb..45ec8501e 100644 --- a/tests/fixtures/v2-dependencies/docker-compose.yml +++ b/tests/fixtures/v2-dependencies/docker-compose.yml @@ -1,13 +1,13 @@ version: "2.0" services: db: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top web: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top depends_on: - db console: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top diff --git a/tests/fixtures/v2-full/Dockerfile b/tests/fixtures/v2-full/Dockerfile index 51ed0d907..6fa7a726c 100644 --- a/tests/fixtures/v2-full/Dockerfile +++ b/tests/fixtures/v2-full/Dockerfile @@ -1,4 +1,4 @@ -FROM busybox:latest +FROM busybox:1.31.0-uclibc RUN echo something CMD top diff --git a/tests/fixtures/v2-full/docker-compose.yml b/tests/fixtures/v2-full/docker-compose.yml index a973dd0cf..20c14f0f7 100644 --- a/tests/fixtures/v2-full/docker-compose.yml +++ b/tests/fixtures/v2-full/docker-compose.yml @@ -18,7 +18,7 @@ services: - other other: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top volumes: - /data diff --git a/tests/fixtures/v2-simple/links-invalid.yml b/tests/fixtures/v2-simple/links-invalid.yml index 481aa4045..a88eb1d52 100644 --- a/tests/fixtures/v2-simple/links-invalid.yml +++ b/tests/fixtures/v2-simple/links-invalid.yml @@ -1,10 +1,10 @@ version: "2" services: simple: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top links: - another another: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top diff --git a/tests/fixtures/v2-simple/one-container.yml b/tests/fixtures/v2-simple/one-container.yml new file mode 100644 index 000000000..2d5c2ca66 --- /dev/null +++ b/tests/fixtures/v2-simple/one-container.yml @@ -0,0 +1,5 @@ +version: "2" +services: + simple: + image: busybox:1.31.0-uclibc + command: top diff --git a/tests/helpers.py b/tests/helpers.py index dd1299811..327715ee2 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -7,6 +7,10 @@ from compose.config.config import ConfigDetails from compose.config.config import ConfigFile from compose.config.config import load +BUSYBOX_IMAGE_NAME = 'busybox' +BUSYBOX_DEFAULT_TAG = '1.31.0-uclibc' +BUSYBOX_IMAGE_WITH_TAG = '{}:{}'.format(BUSYBOX_IMAGE_NAME, BUSYBOX_DEFAULT_TAG) + def build_config(contents, **kwargs): return load(build_config_details(contents, **kwargs)) @@ -22,7 +26,7 @@ def build_config_details(contents, working_dir='working_dir', filename='filename def create_custom_host_file(client, filename, content): dirname = os.path.dirname(filename) container = client.create_container( - 'busybox:latest', + BUSYBOX_IMAGE_WITH_TAG, ['sh', '-c', 'echo -n "{}" > {}'.format(content, filename)], volumes={dirname: {}}, host_config=client.create_host_config( diff --git a/tests/integration/environment_test.py b/tests/integration/environment_test.py new file mode 100644 index 000000000..671e65318 --- /dev/null +++ b/tests/integration/environment_test.py @@ -0,0 +1,70 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import tempfile + +from ddt import data +from ddt import ddt + +from .. import mock +from ..acceptance.cli_test import dispatch +from compose.cli.command import get_project +from compose.cli.command import project_from_options +from compose.config.environment import Environment +from tests.integration.testcases import DockerClientTestCase + + +@ddt +class EnvironmentTest(DockerClientTestCase): + @classmethod + def setUpClass(cls): + super(EnvironmentTest, cls).setUpClass() + cls.compose_file = tempfile.NamedTemporaryFile(mode='w+b') + cls.compose_file.write(bytes("""version: '3.2' +services: + svc: + image: busybox:1.31.0-uclibc + environment: + TEST_VARIABLE: ${TEST_VARIABLE}""", encoding='utf-8')) + cls.compose_file.flush() + + @classmethod + def tearDownClass(cls): + super(EnvironmentTest, cls).tearDownClass() + cls.compose_file.close() + + @data('events', + 'exec', + 'kill', + 'logs', + 'pause', + 'ps', + 'restart', + 'rm', + 'start', + 'stop', + 'top', + 'unpause') + def _test_no_warning_on_missing_host_environment_var_on_silent_commands(self, cmd): + options = {'COMMAND': cmd, '--file': [EnvironmentTest.compose_file.name]} + with mock.patch('compose.config.environment.log') as fake_log: + # Note that the warning silencing and the env variables check is + # done in `project_from_options` + # So no need to have a proper options map, the `COMMAND` key is enough + project_from_options('.', options) + assert fake_log.warn.call_count == 0 + + +class EnvironmentOverrideFileTest(DockerClientTestCase): + def test_env_file_override(self): + base_dir = 'tests/fixtures/env-file-override' + dispatch(base_dir, ['--env-file', '.env.override', 'up']) + project = get_project(project_dir=base_dir, + config_path=['docker-compose.yml'], + environment=Environment.from_env_file(base_dir, '.env.override'), + override_dir=base_dir) + containers = project.containers(stopped=True) + assert len(containers) == 1 + assert "WHEREAMI=override" in containers[0].get('Config.Env') + assert "DEFAULT_CONF_LOADED=true" in containers[0].get('Config.Env') + dispatch(base_dir, ['--env-file', '.env.override', 'down'], None) diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index 57f3b7074..4c88f3d6b 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -1,6 +1,7 @@ from __future__ import absolute_import from __future__ import unicode_literals +import copy import json import os import random @@ -14,6 +15,7 @@ from docker.errors import NotFound from .. import mock from ..helpers import build_config as load_config +from ..helpers import BUSYBOX_IMAGE_WITH_TAG from ..helpers import create_host_file from .testcases import DockerClientTestCase from .testcases import SWARM_SKIP_CONTAINERS_ALL @@ -103,7 +105,7 @@ class ProjectTest(DockerClientTestCase): self.create_service('extra').create_container() project = Project('composetest', [web, db], self.client) - assert set(project.containers(stopped=True)) == set([web_1, db_1]) + assert set(project.containers(stopped=True)) == {web_1, db_1} def test_parallel_pull_with_no_image(self): config_data = build_config( @@ -127,11 +129,11 @@ class ProjectTest(DockerClientTestCase): name='composetest', config_data=load_config({ 'data': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'volumes': ['/var/data'], }, 'db': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'volumes_from': ['data'], }, }), @@ -144,7 +146,7 @@ class ProjectTest(DockerClientTestCase): def test_volumes_from_container(self): data_container = Container.create( self.client, - image='busybox:latest', + image=BUSYBOX_IMAGE_WITH_TAG, volumes=['/var/data'], name='composetest_data_container', labels={LABEL_PROJECT: 'composetest'}, @@ -154,7 +156,7 @@ class ProjectTest(DockerClientTestCase): name='composetest', config_data=load_config({ 'db': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'volumes_from': ['composetest_data_container'], }, }), @@ -173,11 +175,11 @@ class ProjectTest(DockerClientTestCase): 'version': str(V2_0), 'services': { 'net': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': ["top"] }, 'web': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'network_mode': 'service:net', 'command': ["top"] }, @@ -201,7 +203,7 @@ class ProjectTest(DockerClientTestCase): 'version': str(V2_0), 'services': { 'web': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'network_mode': 'container:composetest_net_container' }, }, @@ -216,7 +218,7 @@ class ProjectTest(DockerClientTestCase): net_container = Container.create( self.client, - image='busybox:latest', + image=BUSYBOX_IMAGE_WITH_TAG, name='composetest_net_container', command='top', labels={LABEL_PROJECT: 'composetest'}, @@ -236,11 +238,11 @@ class ProjectTest(DockerClientTestCase): name='composetest', config_data=load_config({ 'net': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': ["top"] }, 'web': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'net': 'container:net', 'command': ["top"] }, @@ -261,7 +263,7 @@ class ProjectTest(DockerClientTestCase): name='composetest', config_data=load_config({ 'web': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'net': 'container:composetest_net_container' }, }), @@ -275,7 +277,7 @@ class ProjectTest(DockerClientTestCase): net_container = Container.create( self.client, - image='busybox:latest', + image=BUSYBOX_IMAGE_WITH_TAG, name='composetest_net_container', command='top', labels={LABEL_PROJECT: 'composetest'}, @@ -304,24 +306,20 @@ class ProjectTest(DockerClientTestCase): db_container = db.create_container() project.start(service_names=['web']) - assert set(c.name for c in project.containers() if c.is_running) == set( - [web_container_1.name, web_container_2.name] - ) + assert set(c.name for c in project.containers() if c.is_running) == { + web_container_1.name, web_container_2.name} project.start() - assert set(c.name for c in project.containers() if c.is_running) == set( - [web_container_1.name, web_container_2.name, db_container.name] - ) + assert set(c.name for c in project.containers() if c.is_running) == { + web_container_1.name, web_container_2.name, db_container.name} project.pause(service_names=['web']) - assert set([c.name for c in project.containers() if c.is_paused]) == set( - [web_container_1.name, web_container_2.name] - ) + assert set([c.name for c in project.containers() if c.is_paused]) == { + web_container_1.name, web_container_2.name} project.pause() - assert set([c.name for c in project.containers() if c.is_paused]) == set( - [web_container_1.name, web_container_2.name, db_container.name] - ) + assert set([c.name for c in project.containers() if c.is_paused]) == { + web_container_1.name, web_container_2.name, db_container.name} project.unpause(service_names=['db']) assert len([c.name for c in project.containers() if c.is_paused]) == 2 @@ -330,7 +328,7 @@ class ProjectTest(DockerClientTestCase): assert len([c.name for c in project.containers() if c.is_paused]) == 0 project.stop(service_names=['web'], timeout=1) - assert set(c.name for c in project.containers() if c.is_running) == set([db_container.name]) + assert set(c.name for c in project.containers() if c.is_running) == {db_container.name} project.kill(service_names=['db']) assert len([c for c in project.containers() if c.is_running]) == 0 @@ -552,20 +550,20 @@ class ProjectTest(DockerClientTestCase): name='composetest', config_data=load_config({ 'console': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': ["top"], }, 'data': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': ["top"] }, 'db': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': ["top"], 'volumes_from': ['data'], }, 'web': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': ["top"], 'links': ['db'], }, @@ -587,20 +585,20 @@ class ProjectTest(DockerClientTestCase): name='composetest', config_data=load_config({ 'console': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': ["top"], }, 'data': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': ["top"] }, 'db': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': ["top"], 'volumes_from': ['data'], }, 'web': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': ["top"], 'links': ['db'], }, @@ -626,7 +624,7 @@ class ProjectTest(DockerClientTestCase): 'version': '2.1', 'services': { 'foo': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'tmpfs': ['/dev/shm'], 'volumes': ['/dev/shm'] } @@ -667,7 +665,7 @@ class ProjectTest(DockerClientTestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'networks': { 'foo': None, @@ -712,7 +710,7 @@ class ProjectTest(DockerClientTestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'networks': {'front': None}, }], networks={ @@ -772,7 +770,7 @@ class ProjectTest(DockerClientTestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'networks': {'front': None}, }], networks={ @@ -807,7 +805,7 @@ class ProjectTest(DockerClientTestCase): version=V2_1, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'networks': { 'static_test': { @@ -859,7 +857,7 @@ class ProjectTest(DockerClientTestCase): version=V2_3, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'networks': { 'n1': { 'priority': p1, @@ -922,7 +920,7 @@ class ProjectTest(DockerClientTestCase): version=V2_1, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'networks': { 'static_test': { @@ -965,7 +963,7 @@ class ProjectTest(DockerClientTestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'networks': { 'static_test': { 'ipv4_address': '172.16.100.100', @@ -1001,7 +999,7 @@ class ProjectTest(DockerClientTestCase): version=V2_1, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'networks': { 'linklocaltest': { 'link_local_ips': ['169.254.8.8'] @@ -1038,7 +1036,7 @@ class ProjectTest(DockerClientTestCase): 'name': 'web', 'volumes': [VolumeSpec.parse('foo:/container-path')], 'networks': {'foo': {}}, - 'image': 'busybox:latest' + 'image': BUSYBOX_IMAGE_WITH_TAG }], networks={ 'foo': { @@ -1074,7 +1072,7 @@ class ProjectTest(DockerClientTestCase): version=V2_1, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'isolation': 'default' }], ) @@ -1094,7 +1092,7 @@ class ProjectTest(DockerClientTestCase): version=V2_1, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'isolation': 'foobar' }], ) @@ -1114,7 +1112,7 @@ class ProjectTest(DockerClientTestCase): version=V2_3, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'runtime': 'runc' }], ) @@ -1134,7 +1132,7 @@ class ProjectTest(DockerClientTestCase): version=V2_3, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'runtime': 'foobar' }], ) @@ -1154,7 +1152,7 @@ class ProjectTest(DockerClientTestCase): version=V2_3, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'runtime': 'nvidia' }], ) @@ -1174,7 +1172,7 @@ class ProjectTest(DockerClientTestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'networks': {'internal': None}, }], networks={ @@ -1203,7 +1201,7 @@ class ProjectTest(DockerClientTestCase): version=V2_1, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'networks': {network_name: None} }], networks={ @@ -1236,7 +1234,7 @@ class ProjectTest(DockerClientTestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top' }], volumes={vol_name: {'driver': 'local'}}, @@ -1263,7 +1261,7 @@ class ProjectTest(DockerClientTestCase): version=V2_1, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'volumes': [VolumeSpec.parse('{}:/data'.format(volume_name))] }], volumes={ @@ -1302,9 +1300,9 @@ class ProjectTest(DockerClientTestCase): { 'version': str(V2_0), 'services': { - 'simple': {'image': 'busybox:latest', 'command': 'top'}, + 'simple': {'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top'}, 'another': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'logging': { 'driver': "json-file", @@ -1355,7 +1353,7 @@ class ProjectTest(DockerClientTestCase): 'version': str(V2_0), 'services': { 'simple': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'ports': ['1234:1234'] }, @@ -1389,7 +1387,7 @@ class ProjectTest(DockerClientTestCase): version=V2_2, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'scale': 3 }] @@ -1419,7 +1417,7 @@ class ProjectTest(DockerClientTestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top' }], volumes={vol_name: {}}, @@ -1443,7 +1441,7 @@ class ProjectTest(DockerClientTestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top' }], volumes={vol_name: {}}, @@ -1467,7 +1465,7 @@ class ProjectTest(DockerClientTestCase): version=V3_1, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'cat /run/secrets/special', 'secrets': [ types.ServiceSecret.parse({'source': 'super', 'target': 'special'}), @@ -1496,6 +1494,60 @@ class ProjectTest(DockerClientTestCase): output = container.logs() assert output == b"This is the secret\n" + @v3_only() + def test_project_up_with_added_secrets(self): + node = create_host_file(self.client, os.path.abspath('tests/fixtures/secrets/default')) + + config_input1 = { + 'version': V3_1, + 'services': [ + { + 'name': 'web', + 'image': BUSYBOX_IMAGE_WITH_TAG, + 'command': 'cat /run/secrets/special', + 'environment': ['constraint:node=={}'.format(node if node is not None else '')] + } + + ], + 'secrets': { + 'super': { + 'file': os.path.abspath('tests/fixtures/secrets/default') + } + } + } + config_input2 = copy.deepcopy(config_input1) + # Add the secret + config_input2['services'][0]['secrets'] = [ + types.ServiceSecret.parse({'source': 'super', 'target': 'special'}) + ] + + config_data1 = build_config(**config_input1) + config_data2 = build_config(**config_input2) + + # First up with non-secret + project = Project.from_config( + client=self.client, + name='composetest', + config_data=config_data1, + ) + project.up() + + # Then up with secret + project = Project.from_config( + client=self.client, + name='composetest', + config_data=config_data2, + ) + project.up() + project.stop() + + containers = project.containers(stopped=True) + assert len(containers) == 1 + container, = containers + + output = container.logs() + assert output == b"This is the secret\n" + @v2_only() def test_initialize_volumes_invalid_volume_driver(self): vol_name = '{0:x}'.format(random.getrandbits(32)) @@ -1504,7 +1556,7 @@ class ProjectTest(DockerClientTestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top' }], volumes={vol_name: {'driver': 'foobar'}}, @@ -1527,7 +1579,7 @@ class ProjectTest(DockerClientTestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top' }], volumes={vol_name: {'driver': 'local'}}, @@ -1569,7 +1621,7 @@ class ProjectTest(DockerClientTestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top' }], volumes={ @@ -1611,7 +1663,7 @@ class ProjectTest(DockerClientTestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top' }], volumes={vol_name: {'driver': 'local'}}, @@ -1650,7 +1702,7 @@ class ProjectTest(DockerClientTestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top' }], volumes={ @@ -1674,7 +1726,7 @@ class ProjectTest(DockerClientTestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top' }], volumes={ @@ -1702,7 +1754,7 @@ class ProjectTest(DockerClientTestCase): 'version': str(V2_0), 'services': { 'simple': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'volumes': ['{0}:/data'.format(vol_name)] }, @@ -1731,7 +1783,7 @@ class ProjectTest(DockerClientTestCase): def test_project_up_orphans(self): config_dict = { 'service1': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', } } @@ -1768,7 +1820,7 @@ class ProjectTest(DockerClientTestCase): def test_project_up_ignore_orphans(self): config_dict = { 'service1': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', } } @@ -1796,7 +1848,7 @@ class ProjectTest(DockerClientTestCase): 'version': '2.1', 'services': { 'svc1': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'healthcheck': { 'test': 'exit 0', @@ -1806,7 +1858,7 @@ class ProjectTest(DockerClientTestCase): }, }, 'svc2': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'depends_on': { 'svc1': {'condition': 'service_healthy'}, @@ -1833,7 +1885,7 @@ class ProjectTest(DockerClientTestCase): 'version': '2.1', 'services': { 'svc1': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'healthcheck': { 'test': 'exit 1', @@ -1843,7 +1895,7 @@ class ProjectTest(DockerClientTestCase): }, }, 'svc2': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'depends_on': { 'svc1': {'condition': 'service_healthy'}, @@ -1872,14 +1924,14 @@ class ProjectTest(DockerClientTestCase): 'version': '2.1', 'services': { 'svc1': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'healthcheck': { 'disable': True }, }, 'svc2': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'depends_on': { 'svc1': {'condition': 'service_healthy'}, @@ -1916,7 +1968,7 @@ class ProjectTest(DockerClientTestCase): 'version': '2.3', 'services': { 'svc1': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'security_opt': ['seccomp:"{}"'.format(profile_path)] } @@ -1940,7 +1992,7 @@ class ProjectTest(DockerClientTestCase): 'version': '2.3', 'services': { 'svc1': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'ls', 'volumes': ['foo:/foo:rw'], 'networks': ['bar'], diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 000f6838c..9750f581c 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -15,6 +15,7 @@ from six import StringIO from six import text_type from .. import mock +from ..helpers import BUSYBOX_IMAGE_WITH_TAG from .testcases import docker_client from .testcases import DockerClientTestCase from .testcases import get_links @@ -373,7 +374,7 @@ class ServiceTest(DockerClientTestCase): self.client.create_volume(volume_name) service = Service('db', client=client, volumes=[ MountSpec(type='volume', source=volume_name, target=container_path) - ], image='busybox:latest', command=['top'], project='composetest') + ], image=BUSYBOX_IMAGE_WITH_TAG, command=['top'], project='composetest') container = service.create_container() service.start_container(container) mount = container.get_mount(container_path) @@ -388,7 +389,7 @@ class ServiceTest(DockerClientTestCase): container_path = '/container-tmpfs' service = Service('db', client=client, volumes=[ MountSpec(type='tmpfs', target=container_path) - ], image='busybox:latest', command=['top'], project='composetest') + ], image=BUSYBOX_IMAGE_WITH_TAG, command=['top'], project='composetest') container = service.create_container() service.start_container(container) mount = container.get_mount(container_path) @@ -474,7 +475,7 @@ class ServiceTest(DockerClientTestCase): volume_container_1 = volume_service.create_container() volume_container_2 = Container.create( self.client, - image='busybox:latest', + image=BUSYBOX_IMAGE_WITH_TAG, command=["top"], labels={LABEL_PROJECT: 'composetest'}, host_config={}, @@ -695,8 +696,8 @@ class ServiceTest(DockerClientTestCase): new_container, = service.execute_convergence_plan( ConvergencePlan('recreate', [old_container])) - mock_log.warn.assert_called_once_with(mock.ANY) - _, args, kwargs = mock_log.warn.mock_calls[0] + mock_log.warning.assert_called_once_with(mock.ANY) + _, args, kwargs = mock_log.warning.mock_calls[0] assert "Service \"db\" is using volume \"/data\" from the previous container" in args[0] assert [mount['Destination'] for mount in new_container.get('Mounts')] == ['/data'] @@ -1232,9 +1233,8 @@ class ServiceTest(DockerClientTestCase): # }) def test_create_with_image_id(self): - # Get image id for the current busybox:latest pull_busybox(self.client) - image_id = self.client.inspect_image('busybox:latest')['Id'][:12] + image_id = self.client.inspect_image(BUSYBOX_IMAGE_WITH_TAG)['Id'][:12] service = self.create_service('foo', image=image_id) service.create_container() @@ -1382,7 +1382,7 @@ class ServiceTest(DockerClientTestCase): with pytest.raises(OperationFailedError): service.scale(3) - captured_output = mock_log.warn.call_args[0][0] + captured_output = mock_log.warning.call_args[0][0] assert len(service.containers()) == 1 assert "Remove the custom name to scale the service." in captured_output diff --git a/tests/integration/state_test.py b/tests/integration/state_test.py index b7d38a4ba..714945ee5 100644 --- a/tests/integration/state_test.py +++ b/tests/integration/state_test.py @@ -5,9 +5,12 @@ by `docker-compose up`. from __future__ import absolute_import from __future__ import unicode_literals +import copy + import py from docker.errors import ImageNotFound +from ..helpers import BUSYBOX_IMAGE_WITH_TAG from .testcases import DockerClientTestCase from .testcases import get_links from .testcases import no_cluster @@ -40,8 +43,8 @@ class BasicProjectTest(ProjectTestCase): super(BasicProjectTest, self).setUp() self.cfg = { - 'db': {'image': 'busybox:latest', 'command': 'top'}, - 'web': {'image': 'busybox:latest', 'command': 'top'}, + 'db': {'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top'}, + 'web': {'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top'}, } def test_no_change(self): @@ -97,16 +100,16 @@ class ProjectWithDependenciesTest(ProjectTestCase): self.cfg = { 'db': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'tail -f /dev/null', }, 'web': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'tail -f /dev/null', 'links': ['db'], }, 'nginx': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'tail -f /dev/null', 'links': ['web'], }, @@ -171,7 +174,7 @@ class ProjectWithDependenciesTest(ProjectTestCase): def test_service_removed_while_down(self): next_cfg = { 'web': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'tail -f /dev/null', }, 'nginx': self.cfg['nginx'], @@ -209,6 +212,143 @@ class ProjectWithDependenciesTest(ProjectTestCase): } +class ProjectWithDependsOnDependenciesTest(ProjectTestCase): + def setUp(self): + super(ProjectWithDependsOnDependenciesTest, self).setUp() + + self.cfg = { + 'version': '2', + 'services': { + 'db': { + 'image': BUSYBOX_IMAGE_WITH_TAG, + 'command': 'tail -f /dev/null', + }, + 'web': { + 'image': BUSYBOX_IMAGE_WITH_TAG, + 'command': 'tail -f /dev/null', + 'depends_on': ['db'], + }, + 'nginx': { + 'image': BUSYBOX_IMAGE_WITH_TAG, + 'command': 'tail -f /dev/null', + 'depends_on': ['web'], + }, + } + } + + def test_up(self): + local_cfg = copy.deepcopy(self.cfg) + containers = self.run_up(local_cfg) + assert set(c.service for c in containers) == set(['db', 'web', 'nginx']) + + def test_change_leaf(self): + local_cfg = copy.deepcopy(self.cfg) + old_containers = self.run_up(local_cfg) + + local_cfg['services']['nginx']['environment'] = {'NEW_VAR': '1'} + new_containers = self.run_up(local_cfg) + + assert set(c.service for c in new_containers - old_containers) == set(['nginx']) + + def test_change_middle(self): + local_cfg = copy.deepcopy(self.cfg) + old_containers = self.run_up(local_cfg) + + local_cfg['services']['web']['environment'] = {'NEW_VAR': '1'} + new_containers = self.run_up(local_cfg) + + assert set(c.service for c in new_containers - old_containers) == set(['web']) + + def test_change_middle_always_recreate_deps(self): + local_cfg = copy.deepcopy(self.cfg) + old_containers = self.run_up(local_cfg, always_recreate_deps=True) + + local_cfg['services']['web']['environment'] = {'NEW_VAR': '1'} + new_containers = self.run_up(local_cfg, always_recreate_deps=True) + + assert set(c.service for c in new_containers - old_containers) == set(['web', 'nginx']) + + def test_change_root(self): + local_cfg = copy.deepcopy(self.cfg) + old_containers = self.run_up(local_cfg) + + local_cfg['services']['db']['environment'] = {'NEW_VAR': '1'} + new_containers = self.run_up(local_cfg) + + assert set(c.service for c in new_containers - old_containers) == set(['db']) + + def test_change_root_always_recreate_deps(self): + local_cfg = copy.deepcopy(self.cfg) + old_containers = self.run_up(local_cfg, always_recreate_deps=True) + + local_cfg['services']['db']['environment'] = {'NEW_VAR': '1'} + new_containers = self.run_up(local_cfg, always_recreate_deps=True) + + assert set(c.service for c in new_containers - old_containers) == set(['db', 'web', 'nginx']) + + def test_change_root_no_recreate(self): + local_cfg = copy.deepcopy(self.cfg) + old_containers = self.run_up(local_cfg) + + local_cfg['services']['db']['environment'] = {'NEW_VAR': '1'} + new_containers = self.run_up( + local_cfg, + strategy=ConvergenceStrategy.never) + + assert new_containers - old_containers == set() + + def test_service_removed_while_down(self): + local_cfg = copy.deepcopy(self.cfg) + next_cfg = copy.deepcopy(self.cfg) + del next_cfg['services']['db'] + del next_cfg['services']['web']['depends_on'] + + containers = self.run_up(local_cfg) + assert set(c.service for c in containers) == set(['db', 'web', 'nginx']) + + project = self.make_project(local_cfg) + project.stop(timeout=1) + + next_containers = self.run_up(next_cfg) + assert set(c.service for c in next_containers) == set(['web', 'nginx']) + + def test_service_removed_while_up(self): + local_cfg = copy.deepcopy(self.cfg) + containers = self.run_up(local_cfg) + assert set(c.service for c in containers) == set(['db', 'web', 'nginx']) + + del local_cfg['services']['db'] + del local_cfg['services']['web']['depends_on'] + + containers = self.run_up(local_cfg) + assert set(c.service for c in containers) == set(['web', 'nginx']) + + def test_dependency_removed(self): + local_cfg = copy.deepcopy(self.cfg) + next_cfg = copy.deepcopy(self.cfg) + del next_cfg['services']['nginx']['depends_on'] + + containers = self.run_up(local_cfg, service_names=['nginx']) + assert set(c.service for c in containers) == set(['db', 'web', 'nginx']) + + project = self.make_project(local_cfg) + project.stop(timeout=1) + + next_containers = self.run_up(next_cfg, service_names=['nginx']) + assert set(c.service for c in next_containers if c.is_running) == set(['nginx']) + + def test_dependency_added(self): + local_cfg = copy.deepcopy(self.cfg) + + del local_cfg['services']['nginx']['depends_on'] + containers = self.run_up(local_cfg, service_names=['nginx']) + assert set(c.service for c in containers) == set(['nginx']) + + local_cfg['services']['nginx']['depends_on'] = ['db'] + containers = self.run_up(local_cfg, service_names=['nginx']) + assert set(c.service for c in containers) == set(['nginx', 'db']) + + class ServiceStateTest(DockerClientTestCase): """Test cases for Service.convergence_plan.""" @@ -246,7 +386,7 @@ class ServiceStateTest(DockerClientTestCase): assert ('recreate', [container]) == web.convergence_plan() def test_trigger_recreate_with_nonexistent_image_tag(self): - web = self.create_service('web', image="busybox:latest") + web = self.create_service('web', image=BUSYBOX_IMAGE_WITH_TAG) container = web.create_container() web = self.create_service('web', image="nonexistent-image") diff --git a/tests/integration/testcases.py b/tests/integration/testcases.py index cfdf22f7e..fe70d1f72 100644 --- a/tests/integration/testcases.py +++ b/tests/integration/testcases.py @@ -9,6 +9,7 @@ from docker.errors import APIError from docker.utils import version_lt from .. import unittest +from ..helpers import BUSYBOX_IMAGE_WITH_TAG from compose.cli.docker_client import docker_client from compose.config.config import resolve_environment from compose.config.environment import Environment @@ -32,7 +33,7 @@ SWARM_ASSUME_MULTINODE = os.environ.get('SWARM_ASSUME_MULTINODE', '0') != '0' def pull_busybox(client): - client.pull('busybox:latest', stream=False) + client.pull(BUSYBOX_IMAGE_WITH_TAG, stream=False) def get_links(container): @@ -123,7 +124,7 @@ class DockerClientTestCase(unittest.TestCase): def create_service(self, name, **kwargs): if 'image' not in kwargs and 'build' not in kwargs: - kwargs['image'] = 'busybox:latest' + kwargs['image'] = BUSYBOX_IMAGE_WITH_TAG if 'command' not in kwargs: kwargs['command'] = ["top"] diff --git a/tests/unit/bundle_test.py b/tests/unit/bundle_test.py index 88f75405a..8faebb7f1 100644 --- a/tests/unit/bundle_test.py +++ b/tests/unit/bundle_test.py @@ -10,6 +10,7 @@ from compose import service from compose.cli.errors import UserError from compose.config.config import Config from compose.const import COMPOSEFILE_V2_0 as V2_0 +from compose.service import NoSuchImageError @pytest.fixture @@ -35,6 +36,16 @@ def test_get_image_digest_image_uses_digest(mock_service): assert not mock_service.image.called +def test_get_image_digest_from_repository(mock_service): + mock_service.options['image'] = 'abcd' + mock_service.image_name = 'abcd' + mock_service.image.side_effect = NoSuchImageError(None) + mock_service.get_image_registry_data.return_value = {'Descriptor': {'digest': 'digest'}} + + digest = bundle.get_image_digest(mock_service) + assert digest == 'abcd@digest' + + def test_get_image_digest_no_image(mock_service): with pytest.raises(UserError) as exc: bundle.get_image_digest(service.Service(name='theservice')) @@ -83,7 +94,7 @@ def test_to_bundle(): configs={} ) - with mock.patch('compose.bundle.log.warn', autospec=True) as mock_log: + with mock.patch('compose.bundle.log.warning', autospec=True) as mock_log: output = bundle.to_bundle(config, image_digests) assert mock_log.mock_calls == [ @@ -117,7 +128,7 @@ def test_convert_service_to_bundle(): 'privileged': True, } - with mock.patch('compose.bundle.log.warn', autospec=True) as mock_log: + with mock.patch('compose.bundle.log.warning', autospec=True) as mock_log: config = bundle.convert_service_to_bundle(name, service_dict, image_digest) mock_log.assert_called_once_with( @@ -166,7 +177,7 @@ def test_make_service_networks_default(): name = 'theservice' service_dict = {} - with mock.patch('compose.bundle.log.warn', autospec=True) as mock_log: + with mock.patch('compose.bundle.log.warning', autospec=True) as mock_log: networks = bundle.make_service_networks(name, service_dict) assert not mock_log.called @@ -184,7 +195,7 @@ def test_make_service_networks(): }, } - with mock.patch('compose.bundle.log.warn', autospec=True) as mock_log: + with mock.patch('compose.bundle.log.warning', autospec=True) as mock_log: networks = bundle.make_service_networks(name, service_dict) mock_log.assert_called_once_with( diff --git a/tests/unit/cli/docker_client_test.py b/tests/unit/cli/docker_client_test.py index be91ea31d..772c136ee 100644 --- a/tests/unit/cli/docker_client_test.py +++ b/tests/unit/cli/docker_client_test.py @@ -247,5 +247,5 @@ class TestGetTlsVersion(object): environment = {'COMPOSE_TLS_VERSION': 'TLSv5_5'} with mock.patch('compose.cli.docker_client.log') as mock_log: tls_version = get_tls_version(environment) - mock_log.warn.assert_called_once_with(mock.ANY) + mock_log.warning.assert_called_once_with(mock.ANY) assert tls_version is None diff --git a/tests/unit/cli/main_test.py b/tests/unit/cli/main_test.py index 2e97f2c87..aadb9d459 100644 --- a/tests/unit/cli/main_test.py +++ b/tests/unit/cli/main_test.py @@ -9,9 +9,11 @@ import pytest from compose import container from compose.cli.errors import UserError from compose.cli.formatter import ConsoleWarningFormatter +from compose.cli.main import build_one_off_container_options from compose.cli.main import call_docker from compose.cli.main import convergence_strategy_from_opts from compose.cli.main import filter_containers_to_service_names +from compose.cli.main import get_docker_start_call from compose.cli.main import setup_console_handler from compose.cli.main import warn_for_swarm_mode from compose.service import ConvergenceStrategy @@ -63,7 +65,65 @@ class TestCLIMainTestCase(object): with mock.patch('compose.cli.main.log') as fake_log: warn_for_swarm_mode(mock_client) - assert fake_log.warn.call_count == 1 + assert fake_log.warning.call_count == 1 + + def test_build_one_off_container_options(self): + command = 'build myservice' + detach = False + options = { + '-e': ['MYVAR=MYVALUE'], + '-T': True, + '--label': ['MYLABEL'], + '--entrypoint': 'bash', + '--user': 'MYUSER', + '--service-ports': [], + '--publish': '', + '--name': 'MYNAME', + '--workdir': '.', + '--volume': [], + 'stdin_open': False, + } + + expected_container_options = { + 'command': command, + 'tty': False, + 'stdin_open': False, + 'detach': detach, + 'entrypoint': 'bash', + 'environment': {'MYVAR': 'MYVALUE'}, + 'labels': {'MYLABEL': ''}, + 'name': 'MYNAME', + 'ports': [], + 'restart': None, + 'user': 'MYUSER', + 'working_dir': '.', + } + + container_options = build_one_off_container_options(options, detach, command) + assert container_options == expected_container_options + + def test_get_docker_start_call(self): + container_id = 'my_container_id' + + mock_container_options = {'detach': False, 'stdin_open': True} + expected_docker_start_call = ['start', '--attach', '--interactive', container_id] + docker_start_call = get_docker_start_call(mock_container_options, container_id) + assert expected_docker_start_call == docker_start_call + + mock_container_options = {'detach': False, 'stdin_open': False} + expected_docker_start_call = ['start', '--attach', container_id] + docker_start_call = get_docker_start_call(mock_container_options, container_id) + assert expected_docker_start_call == docker_start_call + + mock_container_options = {'detach': True, 'stdin_open': True} + expected_docker_start_call = ['start', '--interactive', container_id] + docker_start_call = get_docker_start_call(mock_container_options, container_id) + assert expected_docker_start_call == docker_start_call + + mock_container_options = {'detach': True, 'stdin_open': False} + expected_docker_start_call = ['start', container_id] + docker_start_call = get_docker_start_call(mock_container_options, container_id) + assert expected_docker_start_call == docker_start_call class TestSetupConsoleHandlerTestCase(object): @@ -123,13 +183,13 @@ def mock_find_executable(exe): class TestCallDocker(object): def test_simple_no_options(self): with mock.patch('subprocess.call') as fake_call: - call_docker(['ps'], {}) + call_docker(['ps'], {}, {}) assert fake_call.call_args[0][0] == ['docker', 'ps'] def test_simple_tls_option(self): with mock.patch('subprocess.call') as fake_call: - call_docker(['ps'], {'--tls': True}) + call_docker(['ps'], {'--tls': True}, {}) assert fake_call.call_args[0][0] == ['docker', '--tls', 'ps'] @@ -140,7 +200,7 @@ class TestCallDocker(object): '--tlscacert': './ca.pem', '--tlscert': './cert.pem', '--tlskey': './key.pem', - }) + }, {}) assert fake_call.call_args[0][0] == [ 'docker', '--tls', '--tlscacert', './ca.pem', '--tlscert', @@ -149,7 +209,7 @@ class TestCallDocker(object): def test_with_host_option(self): with mock.patch('subprocess.call') as fake_call: - call_docker(['ps'], {'--host': 'tcp://mydocker.net:2333'}) + call_docker(['ps'], {'--host': 'tcp://mydocker.net:2333'}, {}) assert fake_call.call_args[0][0] == [ 'docker', '--host', 'tcp://mydocker.net:2333', 'ps' @@ -157,7 +217,7 @@ class TestCallDocker(object): def test_with_http_host(self): with mock.patch('subprocess.call') as fake_call: - call_docker(['ps'], {'--host': 'http://mydocker.net:2333'}) + call_docker(['ps'], {'--host': 'http://mydocker.net:2333'}, {}) assert fake_call.call_args[0][0] == [ 'docker', '--host', 'tcp://mydocker.net:2333', 'ps', @@ -165,8 +225,17 @@ class TestCallDocker(object): def test_with_host_option_shorthand_equal(self): with mock.patch('subprocess.call') as fake_call: - call_docker(['ps'], {'--host': '=tcp://mydocker.net:2333'}) + call_docker(['ps'], {'--host': '=tcp://mydocker.net:2333'}, {}) assert fake_call.call_args[0][0] == [ 'docker', '--host', 'tcp://mydocker.net:2333', 'ps' ] + + def test_with_env(self): + with mock.patch('subprocess.call') as fake_call: + call_docker(['ps'], {}, {'DOCKER_HOST': 'tcp://mydocker.net:2333'}) + + assert fake_call.call_args[0][0] == [ + 'docker', 'ps' + ] + assert fake_call.call_args[1]['env'] == {'DOCKER_HOST': 'tcp://mydocker.net:2333'} diff --git a/tests/unit/cli/utils_test.py b/tests/unit/cli/utils_test.py index 26524ff37..b340fb947 100644 --- a/tests/unit/cli/utils_test.py +++ b/tests/unit/cli/utils_test.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import unittest +from compose.cli.utils import human_readable_file_size from compose.utils import unquote_path @@ -21,3 +22,23 @@ class UnquotePathTest(unittest.TestCase): assert unquote_path('""hello""') == '"hello"' assert unquote_path('"hel"lo"') == 'hel"lo' assert unquote_path('"hello""') == 'hello"' + + +class HumanReadableFileSizeTest(unittest.TestCase): + def test_100b(self): + assert human_readable_file_size(100) == '100 B' + + def test_1kb(self): + assert human_readable_file_size(1024) == '1 kB' + + def test_1023b(self): + assert human_readable_file_size(1023) == '1023 B' + + def test_units(self): + assert human_readable_file_size((2 ** 10) ** 0) == '1 B' + assert human_readable_file_size((2 ** 10) ** 1) == '1 kB' + assert human_readable_file_size((2 ** 10) ** 2) == '1 MB' + assert human_readable_file_size((2 ** 10) ** 3) == '1 GB' + assert human_readable_file_size((2 ** 10) ** 4) == '1 TB' + assert human_readable_file_size((2 ** 10) ** 5) == '1 PB' + assert human_readable_file_size((2 ** 10) ** 6) == '1 EB' diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 50d8e13a8..b583422f5 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -15,6 +15,7 @@ import pytest import yaml from ...helpers import build_config_details +from ...helpers import BUSYBOX_IMAGE_WITH_TAG from compose.config import config from compose.config import types from compose.config.config import resolve_build_args @@ -329,7 +330,7 @@ class ConfigTest(unittest.TestCase): ) assert 'Unexpected type for "version" key in "filename.yml"' \ - in mock_logging.warn.call_args[0][0] + in mock_logging.warning.call_args[0][0] service_dicts = config_data.services assert service_sort(service_dicts) == service_sort([ @@ -343,7 +344,7 @@ class ConfigTest(unittest.TestCase): with pytest.raises(ConfigurationError): config.load( build_config_details( - {'web': 'busybox:latest'}, + {'web': BUSYBOX_IMAGE_WITH_TAG}, 'working_dir', 'filename.yml' ) @@ -353,7 +354,7 @@ class ConfigTest(unittest.TestCase): with pytest.raises(ConfigurationError): config.load( build_config_details( - {'version': '2', 'services': {'web': 'busybox:latest'}}, + {'version': '2', 'services': {'web': BUSYBOX_IMAGE_WITH_TAG}}, 'working_dir', 'filename.yml' ) @@ -364,7 +365,7 @@ class ConfigTest(unittest.TestCase): config.load( build_config_details({ 'version': '2', - 'services': {'web': 'busybox:latest'}, + 'services': {'web': BUSYBOX_IMAGE_WITH_TAG}, 'networks': { 'invalid': {'foo', 'bar'} } @@ -613,6 +614,25 @@ class ConfigTest(unittest.TestCase): excinfo.exconly() ) + def test_config_integer_service_name_raise_validation_error_v2_when_no_interpolate(self): + with pytest.raises(ConfigurationError) as excinfo: + config.load( + build_config_details( + { + 'version': '2', + 'services': {1: {'image': 'busybox'}} + }, + 'working_dir', + 'filename.yml' + ), + interpolate=False + ) + + assert ( + "In file 'filename.yml', the service name 1 must be a quoted string, i.e. '1'." in + excinfo.exconly() + ) + def test_config_integer_service_property_raise_validation_error(self): with pytest.raises(ConfigurationError) as excinfo: config.load( @@ -828,15 +848,15 @@ class ConfigTest(unittest.TestCase): def test_load_sorts_in_dependency_order(self): config_details = build_config_details({ 'web': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'links': ['db'], }, 'db': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'volumes_from': ['volume:ro'] }, 'volume': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'volumes': ['/tmp'], } }) @@ -1261,7 +1281,7 @@ class ConfigTest(unittest.TestCase): 'version': '2', 'services': { 'web': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'volumes': ['data0028:/data:ro'], }, }, @@ -1277,7 +1297,7 @@ class ConfigTest(unittest.TestCase): 'version': '2', 'services': { 'web': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'volumes': ['./data0028:/data:ro'], }, }, @@ -1293,7 +1313,7 @@ class ConfigTest(unittest.TestCase): 'base.yaml', { 'web': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'volumes': ['data0028:/data:ro'], }, } @@ -1310,7 +1330,7 @@ class ConfigTest(unittest.TestCase): 'version': '2.3', 'services': { 'web': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'volumes': [ { 'target': '/anonymous', 'type': 'volume' @@ -1355,7 +1375,7 @@ class ConfigTest(unittest.TestCase): 'version': '3.4', 'services': { 'web': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'volumes': [ {'type': 'bind', 'source': './web', 'target': '/web'}, ], @@ -1377,7 +1397,7 @@ class ConfigTest(unittest.TestCase): 'version': '3.4', 'services': { 'web': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'volumes': [ {'type': 'bind', 'source': '~/web', 'target': '/web'}, ], @@ -2274,7 +2294,7 @@ class ConfigTest(unittest.TestCase): def test_merge_mixed_ports(self): base = { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'ports': [ { @@ -2291,7 +2311,7 @@ class ConfigTest(unittest.TestCase): actual = config.merge_service_dicts(base, override, V3_1) assert actual == { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'ports': [types.ServicePort('1245', '1245', 'udp', None, None)] } @@ -3465,6 +3485,25 @@ class InterpolationTest(unittest.TestCase): 'command': 'true' } + @mock.patch.dict(os.environ) + def test_config_file_with_options_environment_file(self): + project_dir = 'tests/fixtures/default-env-file' + service_dicts = config.load( + config.find( + project_dir, None, Environment.from_env_file(project_dir, '.env2') + ) + ).services + + assert service_dicts[0] == { + 'name': 'web', + 'image': 'alpine:latest', + 'ports': [ + types.ServicePort.parse('5644')[0], + types.ServicePort.parse('9998')[0] + ], + 'command': 'false' + } + @mock.patch.dict(os.environ) def test_config_file_with_environment_variable(self): project_dir = 'tests/fixtures/environment-interpolation' @@ -3532,8 +3571,8 @@ class InterpolationTest(unittest.TestCase): with mock.patch('compose.config.environment.log') as log: config.load(config_details) - assert 2 == log.warn.call_count - warnings = sorted(args[0][0] for args in log.warn.call_args_list) + assert 2 == log.warning.call_count + warnings = sorted(args[0][0] for args in log.warning.call_args_list) assert 'BAR' in warnings[0] assert 'FOO' in warnings[1] @@ -3563,8 +3602,8 @@ class InterpolationTest(unittest.TestCase): with mock.patch('compose.config.config.log') as log: config.load(config_details, compatibility=True) - assert log.warn.call_count == 1 - warn_message = log.warn.call_args[0][0] + assert log.warning.call_count == 1 + warn_message = log.warning.call_args[0][0] assert warn_message.startswith( 'The following deploy sub-keys are not supported in compatibility mode' ) @@ -3603,7 +3642,7 @@ class InterpolationTest(unittest.TestCase): with mock.patch('compose.config.config.log') as log: cfg = config.load(config_details, compatibility=True) - assert log.warn.call_count == 0 + assert log.warning.call_count == 0 service_dict = cfg.services[0] assert service_dict == { @@ -3783,35 +3822,35 @@ class MergePathMappingTest(object): {self.config_name: ['/foo:/code', '/data']}, {}, DEFAULT_VERSION) - assert set(service_dict[self.config_name]) == set(['/foo:/code', '/data']) + assert set(service_dict[self.config_name]) == {'/foo:/code', '/data'} def test_no_base(self): service_dict = config.merge_service_dicts( {}, {self.config_name: ['/bar:/code']}, DEFAULT_VERSION) - assert set(service_dict[self.config_name]) == set(['/bar:/code']) + assert set(service_dict[self.config_name]) == {'/bar:/code'} def test_override_explicit_path(self): service_dict = config.merge_service_dicts( {self.config_name: ['/foo:/code', '/data']}, {self.config_name: ['/bar:/code']}, DEFAULT_VERSION) - assert set(service_dict[self.config_name]) == set(['/bar:/code', '/data']) + assert set(service_dict[self.config_name]) == {'/bar:/code', '/data'} def test_add_explicit_path(self): service_dict = config.merge_service_dicts( {self.config_name: ['/foo:/code', '/data']}, {self.config_name: ['/bar:/code', '/quux:/data']}, DEFAULT_VERSION) - assert set(service_dict[self.config_name]) == set(['/bar:/code', '/quux:/data']) + assert set(service_dict[self.config_name]) == {'/bar:/code', '/quux:/data'} def test_remove_explicit_path(self): service_dict = config.merge_service_dicts( {self.config_name: ['/foo:/code', '/quux:/data']}, {self.config_name: ['/bar:/code', '/data']}, DEFAULT_VERSION) - assert set(service_dict[self.config_name]) == set(['/bar:/code', '/data']) + assert set(service_dict[self.config_name]) == {'/bar:/code', '/data'} class MergeVolumesTest(unittest.TestCase, MergePathMappingTest): @@ -4015,28 +4054,28 @@ class MergeStringsOrListsTest(unittest.TestCase): {'dns': '8.8.8.8'}, {}, DEFAULT_VERSION) - assert set(service_dict['dns']) == set(['8.8.8.8']) + assert set(service_dict['dns']) == {'8.8.8.8'} def test_no_base(self): service_dict = config.merge_service_dicts( {}, {'dns': '8.8.8.8'}, DEFAULT_VERSION) - assert set(service_dict['dns']) == set(['8.8.8.8']) + assert set(service_dict['dns']) == {'8.8.8.8'} def test_add_string(self): service_dict = config.merge_service_dicts( {'dns': ['8.8.8.8']}, {'dns': '9.9.9.9'}, DEFAULT_VERSION) - assert set(service_dict['dns']) == set(['8.8.8.8', '9.9.9.9']) + assert set(service_dict['dns']) == {'8.8.8.8', '9.9.9.9'} def test_add_list(self): service_dict = config.merge_service_dicts( {'dns': '8.8.8.8'}, {'dns': ['9.9.9.9']}, DEFAULT_VERSION) - assert set(service_dict['dns']) == set(['8.8.8.8', '9.9.9.9']) + assert set(service_dict['dns']) == {'8.8.8.8', '9.9.9.9'} class MergeLabelsTest(unittest.TestCase): @@ -4108,7 +4147,7 @@ class MergeBuildTest(unittest.TestCase): assert result['context'] == override['context'] assert result['dockerfile'] == override['dockerfile'] assert result['args'] == {'x': '12', 'y': '2'} - assert set(result['cache_from']) == set(['ubuntu', 'debian']) + assert set(result['cache_from']) == {'ubuntu', 'debian'} assert result['labels'] == override['labels'] def test_empty_override(self): @@ -4312,7 +4351,7 @@ class EnvTest(unittest.TestCase): "tests/fixtures/env", ) ).services[0] - assert set(service_dict['volumes']) == set([VolumeSpec.parse('/tmp:/host/tmp')]) + assert set(service_dict['volumes']) == {VolumeSpec.parse('/tmp:/host/tmp')} service_dict = config.load( build_config_details( @@ -4320,7 +4359,7 @@ class EnvTest(unittest.TestCase): "tests/fixtures/env", ) ).services[0] - assert set(service_dict['volumes']) == set([VolumeSpec.parse('/opt/tmp:/opt/host/tmp')]) + assert set(service_dict['volumes']) == {VolumeSpec.parse('/opt/tmp:/opt/host/tmp')} def load_from_filename(filename, override_dir=None): @@ -5327,6 +5366,28 @@ class SerializeTest(unittest.TestCase): assert serialized_service['command'] == 'echo $$FOO' assert serialized_service['entrypoint'][0] == '$$SHELL' + def test_serialize_escape_dont_interpolate(self): + cfg = { + 'version': '2.2', + 'services': { + 'web': { + 'image': 'busybox', + 'command': 'echo $FOO', + 'environment': { + 'CURRENCY': '$' + }, + 'entrypoint': ['$SHELL', '-c'], + } + } + } + config_dict = config.load(build_config_details(cfg), interpolate=False) + + serialized_config = yaml.load(serialize_config(config_dict, escape_dollar=False)) + serialized_service = serialized_config['services']['web'] + assert serialized_service['environment']['CURRENCY'] == '$' + assert serialized_service['command'] == 'echo $FOO' + assert serialized_service['entrypoint'][0] == '$SHELL' + def test_serialize_unicode_values(self): cfg = { 'version': '2.3', diff --git a/tests/unit/container_test.py b/tests/unit/container_test.py index fde17847a..626b466d4 100644 --- a/tests/unit/container_test.py +++ b/tests/unit/container_test.py @@ -5,6 +5,7 @@ import docker from .. import mock from .. import unittest +from ..helpers import BUSYBOX_IMAGE_WITH_TAG from compose.const import LABEL_ONE_OFF from compose.const import LABEL_SLUG from compose.container import Container @@ -17,7 +18,7 @@ class ContainerTest(unittest.TestCase): self.container_id = "abcabcabcbabc12345" self.container_dict = { "Id": self.container_id, - "Image": "busybox:latest", + "Image": BUSYBOX_IMAGE_WITH_TAG, "Command": "top", "Created": 1387384730, "Status": "Up 8 seconds", @@ -43,7 +44,7 @@ class ContainerTest(unittest.TestCase): has_been_inspected=True) assert container.dictionary == { "Id": self.container_id, - "Image": "busybox:latest", + "Image": BUSYBOX_IMAGE_WITH_TAG, "Name": "/composetest_db_1", } @@ -58,7 +59,7 @@ class ContainerTest(unittest.TestCase): has_been_inspected=True) assert container.dictionary == { "Id": self.container_id, - "Image": "busybox:latest", + "Image": BUSYBOX_IMAGE_WITH_TAG, "Name": "/composetest_db_1", } diff --git a/tests/unit/network_test.py b/tests/unit/network_test.py index d7ffa2894..82cfb3be2 100644 --- a/tests/unit/network_test.py +++ b/tests/unit/network_test.py @@ -165,6 +165,6 @@ class NetworkTest(unittest.TestCase): with mock.patch('compose.network.log') as mock_log: check_remote_network_config(remote, net) - mock_log.warn.assert_called_once_with(mock.ANY) - _, args, kwargs = mock_log.warn.mock_calls[0] + mock_log.warning.assert_called_once_with(mock.ANY) + _, args, kwargs = mock_log.warning.mock_calls[0] assert 'label "com.project.touhou.character" has changed' in args[0] diff --git a/tests/unit/project_test.py b/tests/unit/project_test.py index 4aea91a0d..93a9aa292 100644 --- a/tests/unit/project_test.py +++ b/tests/unit/project_test.py @@ -10,11 +10,14 @@ from docker.errors import NotFound from .. import mock from .. import unittest +from ..helpers import BUSYBOX_IMAGE_WITH_TAG from compose.config.config import Config from compose.config.types import VolumeFromSpec from compose.const import COMPOSEFILE_V1 as V1 from compose.const import COMPOSEFILE_V2_0 as V2_0 from compose.const import COMPOSEFILE_V2_4 as V2_4 +from compose.const import COMPOSEFILE_V3_7 as V3_7 +from compose.const import DEFAULT_TIMEOUT from compose.const import LABEL_SERVICE from compose.container import Container from compose.errors import OperationFailedError @@ -37,11 +40,11 @@ class ProjectTest(unittest.TestCase): services=[ { 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, }, { 'name': 'db', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, }, ], networks=None, @@ -56,9 +59,9 @@ class ProjectTest(unittest.TestCase): ) assert len(project.services) == 2 assert project.get_service('web').name == 'web' - assert project.get_service('web').options['image'] == 'busybox:latest' + assert project.get_service('web').options['image'] == BUSYBOX_IMAGE_WITH_TAG assert project.get_service('db').name == 'db' - assert project.get_service('db').options['image'] == 'busybox:latest' + assert project.get_service('db').options['image'] == BUSYBOX_IMAGE_WITH_TAG assert not project.networks.use_networking @mock.patch('compose.network.Network.true_name', lambda n: n.full_name) @@ -68,11 +71,11 @@ class ProjectTest(unittest.TestCase): services=[ { 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, }, { 'name': 'db', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, }, ], networks=None, @@ -89,7 +92,7 @@ class ProjectTest(unittest.TestCase): project='composetest', name='web', client=None, - image="busybox:latest", + image=BUSYBOX_IMAGE_WITH_TAG, ) project = Project('test', [web], None) assert project.get_service('web') == web @@ -174,7 +177,7 @@ class ProjectTest(unittest.TestCase): version=V2_0, services=[{ 'name': 'test', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'volumes_from': [VolumeFromSpec('aaa', 'rw', 'container')] }], networks=None, @@ -192,7 +195,7 @@ class ProjectTest(unittest.TestCase): "Name": container_name, "Names": [container_name], "Id": container_name, - "Image": 'busybox:latest' + "Image": BUSYBOX_IMAGE_WITH_TAG } ] project = Project.from_config( @@ -203,11 +206,11 @@ class ProjectTest(unittest.TestCase): services=[ { 'name': 'vol', - 'image': 'busybox:latest' + 'image': BUSYBOX_IMAGE_WITH_TAG }, { 'name': 'test', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'volumes_from': [VolumeFromSpec('vol', 'rw', 'service')] } ], @@ -231,11 +234,11 @@ class ProjectTest(unittest.TestCase): services=[ { 'name': 'vol', - 'image': 'busybox:latest' + 'image': BUSYBOX_IMAGE_WITH_TAG }, { 'name': 'test', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'volumes_from': [VolumeFromSpec('vol', 'rw', 'service')] } ], @@ -541,7 +544,7 @@ class ProjectTest(unittest.TestCase): services=[ { 'name': 'test', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, } ], networks=None, @@ -566,7 +569,7 @@ class ProjectTest(unittest.TestCase): services=[ { 'name': 'test', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'network_mode': 'container:aaa' }, ], @@ -586,7 +589,7 @@ class ProjectTest(unittest.TestCase): "Name": container_name, "Names": [container_name], "Id": container_name, - "Image": 'busybox:latest' + "Image": BUSYBOX_IMAGE_WITH_TAG } ] project = Project.from_config( @@ -597,11 +600,11 @@ class ProjectTest(unittest.TestCase): services=[ { 'name': 'aaa', - 'image': 'busybox:latest' + 'image': BUSYBOX_IMAGE_WITH_TAG }, { 'name': 'test', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'network_mode': 'service:aaa' }, ], @@ -624,7 +627,7 @@ class ProjectTest(unittest.TestCase): services=[ { 'name': 'foo', - 'image': 'busybox:latest' + 'image': BUSYBOX_IMAGE_WITH_TAG }, ], networks=None, @@ -645,7 +648,7 @@ class ProjectTest(unittest.TestCase): services=[ { 'name': 'foo', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'networks': {'custom': None} }, ], @@ -660,9 +663,9 @@ class ProjectTest(unittest.TestCase): def test_container_without_name(self): self.mock_client.containers.return_value = [ - {'Image': 'busybox:latest', 'Id': '1', 'Name': '1'}, - {'Image': 'busybox:latest', 'Id': '2', 'Name': None}, - {'Image': 'busybox:latest', 'Id': '3'}, + {'Image': BUSYBOX_IMAGE_WITH_TAG, 'Id': '1', 'Name': '1'}, + {'Image': BUSYBOX_IMAGE_WITH_TAG, 'Id': '2', 'Name': None}, + {'Image': BUSYBOX_IMAGE_WITH_TAG, 'Id': '3'}, ] self.mock_client.inspect_container.return_value = { 'Id': '1', @@ -679,7 +682,7 @@ class ProjectTest(unittest.TestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, }], networks=None, volumes=None, @@ -697,7 +700,7 @@ class ProjectTest(unittest.TestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, }], networks={'default': {}}, volumes={'data': {}}, @@ -709,7 +712,7 @@ class ProjectTest(unittest.TestCase): self.mock_client.remove_volume.side_effect = NotFound(None, None, 'oops') project.down(ImageType.all, True) - self.mock_client.remove_image.assert_called_once_with("busybox:latest") + self.mock_client.remove_image.assert_called_once_with(BUSYBOX_IMAGE_WITH_TAG) def test_no_warning_on_stop(self): self.mock_client.info.return_value = {'Swarm': {'LocalNodeState': 'active'}} @@ -742,7 +745,7 @@ class ProjectTest(unittest.TestCase): def test_project_platform_value(self): service_config = { 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, } config_data = Config( version=V2_4, services=[service_config], networks={}, volumes={}, secrets=None, configs=None @@ -765,6 +768,34 @@ class ProjectTest(unittest.TestCase): ) assert project.get_service('web').platform == 'linux/s390x' + def test_build_container_operation_with_timeout_func_does_not_mutate_options_with_timeout(self): + config_data = Config( + version=V3_7, + services=[ + {'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG}, + {'name': 'db', 'image': BUSYBOX_IMAGE_WITH_TAG, 'stop_grace_period': '1s'}, + ], + networks={}, volumes={}, secrets=None, configs=None, + ) + + project = Project.from_config(name='test', client=self.mock_client, config_data=config_data) + + stop_op = project.build_container_operation_with_timeout_func('stop', options={}) + + web_container = mock.create_autospec(Container, service='web') + db_container = mock.create_autospec(Container, service='db') + + # `stop_grace_period` is not set to 'web' service, + # then it is stopped with the default timeout. + stop_op(web_container) + web_container.stop.assert_called_once_with(timeout=DEFAULT_TIMEOUT) + + # `stop_grace_period` is set to 'db' service, + # then it is stopped with the specified timeout and + # the value is not overridden by the previous function call. + stop_op(db_container) + db_container.stop.assert_called_once_with(timeout=1) + @mock.patch('compose.parallel.ParallelStreamWriter._write_noansi') def test_error_parallel_pull(self, mock_write): project = Project.from_config( @@ -774,7 +805,7 @@ class ProjectTest(unittest.TestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, }], networks=None, volumes=None, diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index 8b3352fcb..a6a633db8 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -333,7 +333,7 @@ class ServiceTest(unittest.TestCase): assert service.options['environment'] == environment assert opts['labels'][LABEL_CONFIG_HASH] == \ - '2524a06fcb3d781aa2c981fc40bcfa08013bb318e4273bfa388df22023e6f2aa' + '689149e6041a85f6fb4945a2146a497ed43c8a5cbd8991753d875b165f1b4de4' assert opts['environment'] == ['also=real'] def test_get_container_create_options_sets_affinity_with_binds(self): @@ -516,8 +516,8 @@ class ServiceTest(unittest.TestCase): with mock.patch('compose.service.log', autospec=True) as mock_log: service.create_container() - assert mock_log.warn.called - _, args, _ = mock_log.warn.mock_calls[0] + assert mock_log.warning.called + _, args, _ = mock_log.warning.mock_calls[0] assert 'was built because it did not already exist' in args[0] assert self.mock_client.build.call_count == 1 @@ -546,7 +546,7 @@ class ServiceTest(unittest.TestCase): with mock.patch('compose.service.log', autospec=True) as mock_log: service.ensure_image_exists(do_build=BuildAction.force) - assert not mock_log.warn.called + assert not mock_log.warning.called assert self.mock_client.build.call_count == 1 self.mock_client.build.call_args[1]['tag'] == 'default_foo' @@ -676,6 +676,7 @@ class ServiceTest(unittest.TestCase): 'options': {'image': 'example.com/foo'}, 'links': [('one', 'one')], 'net': 'other', + 'secrets': [], 'networks': {'default': None}, 'volumes_from': [('two', 'rw')], } @@ -698,6 +699,7 @@ class ServiceTest(unittest.TestCase): 'options': {'image': 'example.com/foo'}, 'links': [], 'networks': {}, + 'secrets': [], 'net': 'aaabbb', 'volumes_from': [], } @@ -826,7 +828,7 @@ class ServiceTest(unittest.TestCase): assert service.specifies_host_port() def test_image_name_from_config(self): - image_name = 'example/web:latest' + image_name = 'example/web:mytag' service = Service('foo', image=image_name) assert service.image_name == image_name @@ -845,13 +847,13 @@ class ServiceTest(unittest.TestCase): ports=["8080:80"]) service.scale(0) - assert not mock_log.warn.called + assert not mock_log.warning.called service.scale(1) - assert not mock_log.warn.called + assert not mock_log.warning.called service.scale(2) - mock_log.warn.assert_called_once_with( + mock_log.warning.assert_called_once_with( 'The "{}" service specifies a port on the host. If multiple containers ' 'for this service are created on a single host, the port will clash.'.format(name)) @@ -1332,10 +1334,8 @@ class ServiceVolumesTest(unittest.TestCase): number=1, ) - assert set(self.mock_client.create_host_config.call_args[1]['binds']) == set([ - '/host/path:/data1:rw', - '/host/path:/data2:rw', - ]) + assert set(self.mock_client.create_host_config.call_args[1]['binds']) == {'/host/path:/data1:rw', + '/host/path:/data2:rw'} def test_get_container_create_options_with_different_host_path_in_container_json(self): service = Service( @@ -1389,7 +1389,7 @@ class ServiceVolumesTest(unittest.TestCase): with mock.patch('compose.service.log', autospec=True) as mock_log: warn_on_masked_volume(volumes_option, container_volumes, service) - assert not mock_log.warn.called + assert not mock_log.warning.called def test_warn_on_masked_volume_when_masked(self): volumes_option = [VolumeSpec('/home/user', '/path', 'rw')] @@ -1402,7 +1402,7 @@ class ServiceVolumesTest(unittest.TestCase): with mock.patch('compose.service.log', autospec=True) as mock_log: warn_on_masked_volume(volumes_option, container_volumes, service) - mock_log.warn.assert_called_once_with(mock.ANY) + mock_log.warning.assert_called_once_with(mock.ANY) def test_warn_on_masked_no_warning_with_same_path(self): volumes_option = [VolumeSpec('/home/user', '/path', 'rw')] @@ -1412,7 +1412,7 @@ class ServiceVolumesTest(unittest.TestCase): with mock.patch('compose.service.log', autospec=True) as mock_log: warn_on_masked_volume(volumes_option, container_volumes, service) - assert not mock_log.warn.called + assert not mock_log.warning.called def test_warn_on_masked_no_warning_with_container_only_option(self): volumes_option = [VolumeSpec(None, '/path', 'rw')] @@ -1424,7 +1424,7 @@ class ServiceVolumesTest(unittest.TestCase): with mock.patch('compose.service.log', autospec=True) as mock_log: warn_on_masked_volume(volumes_option, container_volumes, service) - assert not mock_log.warn.called + assert not mock_log.warning.called def test_create_with_special_volume_mode(self): self.mock_client.inspect_image.return_value = {'Id': 'imageid'} diff --git a/tox.ini b/tox.ini index 08efd4e68..57e57bc63 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py36,py37,pre-commit +envlist = py27,py37,pre-commit [testenv] usedevelop=True