diff --git a/.fossa.yml b/.fossa.yml deleted file mode 100644 index b50761ef14a..00000000000 --- a/.fossa.yml +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by FOSSA CLI (https://github.com/fossas/fossa-cli) -# Visit https://fossa.io to learn more - -version: 2 -cli: - server: https://app.fossa.io - fetcher: custom - project: git@github.com:docker/compose -analyze: - modules: - - name: . - type: pip - target: . - path: . diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 13fb9bac0ea..85ab9015f3b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -3,4 +3,4 @@ # # KEEP THIS FILE SORTED. Order is important. Last match takes precedence. -* @ndeloof @rumpl @ulyssessouza +* @aiordache @ndeloof @rumpl @ulyssessouza diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e447294eb7a..05cd5202658 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,9 +17,12 @@ sha: v1.3.4 hooks: - id: reorder-python-imports - language_version: 'python2.7' + language_version: 'python3.7' args: - - --add-import - - from __future__ import absolute_import - - --add-import - - from __future__ import unicode_literals + - --py3-plus +- repo: https://github.com/asottile/pyupgrade + rev: v2.1.0 + hooks: + - id: pyupgrade + args: + - --py3-plus diff --git a/CHANGELOG.md b/CHANGELOG.md index e8cd889f079..75bc7c7493b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,411 @@ Change log ========== +1.29.2 (2021-05-10) +------------------- + +[List of PRs / issues for this release](https://github.com/docker/compose/milestone/59?closed=1) + +### Miscellaneous + +- Remove prompt to use `docker compose` in the `up` command + +- Bump `py` to `1.10.0` in `requirements-indirect.txt` + +1.29.1 (2021-04-13) +------------------- + +[List of PRs / issues for this release](https://github.com/docker/compose/milestone/58?closed=1) + +### Bugs + +- Fix for invalid handler warning on Windows builds + +- Fix config hash to trigger container recreation on IPC mode updates + +- Fix conversion map for `placement.max_replicas_per_node` + +- Remove extra scan suggestion on build + +1.29.0 (2021-04-06) +------------------- + +[List of PRs / issues for this release](https://github.com/docker/compose/milestone/56?closed=1) + +### Features + +- Add profile filter to `docker-compose config` + +- Add a `depends_on` condition to wait for successful service completion + +### Miscellaneous + +- Add image scan message on build + +- Update warning message for `--no-ansi` to mention `--ansi never` as alternative + +- Bump docker-py to 5.0.0 + +- Bump PyYAML to 5.4.1 + +- Bump python-dotenv to 0.17.0 + +1.28.6 (2021-03-23) +------------------- + +[List of PRs / issues for this release](https://github.com/docker/compose/milestone/57?closed=1) + +### Bugs + +- Make `--env-file` relative to the current working directory and error out for invalid paths. Environment file paths set with `--env-file` are relative to the current working directory while the default `.env` file is located in the project directory which by default is the base directory of the Compose file. + +- Fix missing service property `storage_opt` by updating the compose schema + +- Fix build `extra_hosts` list format + +- Remove extra error message on `exec` + +### Miscellaneous + +- Add `compose.yml` and `compose.yaml` to default filename list + +1.28.5 (2021-02-25) +------------------- + +[List of PRs / issues for this release](https://github.com/docker/compose/milestone/55?closed=1) + +### Bugs + +- Fix OpenSSL version mismatch error when shelling out to the ssh client (via bump to docker-py 4.4.4 which contains the fix) + +- Add missing build flags to the native builder: `platform`, `isolation` and `extra_hosts` + +- Remove info message on native build + +- Avoid fetching logs when service logging driver is set to 'none' + +1.28.4 (2021-02-18) +------------------- + +[List of PRs / issues for this release](https://github.com/docker/compose/milestone/54?closed=1) + +### Bugs + +- Fix SSH port parsing by bumping docker-py to 4.4.3 + +### Miscellaneous + +- Bump Python to 3.7.10 + +1.28.3 (2021-02-17) +------------------- + +[List of PRs / issues for this release](https://github.com/docker/compose/milestone/53?closed=1) + +### Bugs + +- Fix SSH hostname parsing when it contains leading s/h, and remove the quiet option that was hiding the error (via docker-py bump to 4.4.2) + +- Fix key error for '--no-log-prefix' option + +- Fix incorrect CLI environment variable name for service profiles: `COMPOSE_PROFILES` instead of `COMPOSE_PROFILE` + +- Fix fish completion + +### Miscellaneous + +- Bump cryptography to 3.3.2 + +- Remove log driver filter + +1.28.2 (2021-01-26) +------------------- + +### Miscellaneous + +- CI setup update + +1.28.1 (2021-01-25) +------------------- + +### Bugs + +- Revert to Python 3.7 bump for Linux static builds + +- Add bash completion for `docker-compose logs|up --no-log-prefix` + +1.28.0 (2021-01-20) +------------------- + +### Features + +- Support for Nvidia GPUs via device requests + +- Support for service profiles + +- Change the SSH connection approach to the Docker CLI's via shellout to the local SSH client (old behaviour enabled by setting `COMPOSE_PARAMIKO_SSH` environment variable) + +- Add flag to disable log prefix + +- Add flag for ansi output control + +### Bugs + +- Make `parallel_pull=True` by default + +- Bring back warning for configs in non-swarm mode + +- Take `--file` in account when defining `project_dir` + +- On `compose up`, attach only to services we read logs from + +### Miscellaneous + +- Make COMPOSE_DOCKER_CLI_BUILD=1 the default + +- Add usage metrics + +- Sync schema with COMPOSE specification + +- Improve failure report for missing mandatory environment variables + +- Bump attrs to 20.3.0 + +- Bump more_itertools to 8.6.0 + +- Bump cryptograhy to 3.2.1 + +- Bump cffi to 1.14.4 + +- Bump virtualenv to 20.2.2 + +- Bump bcrypt to 3.2.0 + +- Bump gitpython to 3.1.11 + +- Bump docker-py to 4.4.1 + +- Bump Python to 3.9 + +- Linux: bump Debian base image from stretch to buster (required for Python 3.9) + +- macOS: OpenSSL 1.1.1g to 1.1.1h, Python 3.7.7 to 3.9.0 + +- Bump pyinstaller 4.1 + +- Loosen restriction on base images to latest minor + +- Updates of READMEs + + +1.27.4 (2020-09-24) +------------------- + +### Bugs + +- Remove path checks for bind mounts + +- Fix port rendering to output long form syntax for non-v1 + +- Add protocol to the docker socket address + +1.27.3 (2020-09-16) +------------------- + +### Bugs + +- Merge `max_replicas_per_node` on `docker-compose config` + +- Fix `depends_on` serialization on `docker-compose config` + +- Fix scaling when some containers are not running on `docker-compose up` + +- Enable relative paths for `driver_opts.device` for `local` driver + +- Allow strings for `cpus` fields + +1.27.2 (2020-09-10) +------------------- + +### Bugs + +- Fix bug on `docker-compose run` container attach + +1.27.1 (2020-09-10) +------------------- + +### Bugs + +- Fix `docker-compose run` when `service.scale` is specified + +- Allow `driver` property for external networks as temporary workaround for swarm network propagation issue + +- Pin new internal schema version to `3.9` as the default + +- Preserve the version when configured in the compose file + +1.27.0 (2020-09-07) +------------------- + +### Features + +- Merge 2.x and 3.x compose formats and align with COMPOSE_SPEC schema + +- Implement service mode for ipc + +- Pass `COMPOSE_PROJECT_NAME` environment variable in container mode + +- Make run behave in the same way as up + +- Use `docker build` on `docker-compose run` when `COMPOSE_DOCKER_CLI_BUILD` environment variable is set + +- Use docker-py default API version for engine queries (`auto`) + +- Parse `network_mode` on build + +### Bugs + +- Ignore build context path validation when building is not required + +- Fix float to bytes conversion via docker-py bump to 4.3.1 + +- Fix scale bug when deploy section is set + +- Fix `docker-py` bump in `setup.py` + +- Fix experimental build failure detection + +- Fix context propagation to docker cli + +### Miscellaneous + +- Drop support for Python 2.7 + +- Bump `docker-py` to 4.3.1 + +- Bump `tox` to 3.19.0 + +- Bump `virtualenv` to 20.0.30 + +- Add script for docs syncronization + +1.26.2 (2020-07-02) +------------------- + +### Bugs + +- Enforce `docker-py` 4.2.2 as minimum version when installing with pip + +1.26.1 (2020-06-30) +------------------- + +### Features + +- Bump `docker-py` from 4.2.1 to 4.2.2 + +### Bugs + +- Enforce `docker-py` 4.2.1 as minimum version when installing with pip + +- Fix context load for non-docker endpoints + +1.26.0 (2020-06-03) +------------------- + +### Features + +- Add `docker context` support + +- Add missing test dependency `ddt` to `setup.py` + +- Add `--attach-dependencies` to command `up` for attaching to dependencies + +- Allow compatibility option with `COMPOSE_COMPATIBILITY` environment variable + +- Bump `Pytest` to 5.3.4 and add refactor compatibility with new version + +- Bump `OpenSSL` from 1.1.1f to 1.1.1g + +- Bump `docker-py` from 4.2.0 to 4.2.1 + +### Bugs + +- Properly escape values coming from env_files + +- Sync compose-schemas with upstream (docker/cli) + +- Remove `None` entries on exec command + +- Add `python-dotenv` to delegate `.env` file processing + +- Don't adjust output on terminal width when piped into another command + +- Show an error message when `version` attribute is malformed + +- Fix HTTPS connection when DOCKER_HOST is remote + +1.25.5 (2020-02-04) +------------------- + +### Features + +- Bump OpenSSL from 1.1.1d to 1.1.1f + +- Add 3.8 compose version + +1.25.4 (2020-01-23) +------------------- + +### Bugfixes + +- Fix CI script to enforce the minimal MacOS version to 10.11 + +- Fix docker-compose exec for keys with no value + +1.25.3 (2020-01-23) +------------------- + +### Bugfixes + +- Fix CI script to enforce the compilation with Python3 + +- Fix binary's sha256 in the release page + +1.25.2 (2020-01-20) +------------------- + +### Features + +- Allow compatibility option with `COMPOSE_COMPATIBILITY` environment variable + +- Bump PyInstaller from 3.5 to 3.6 + +- Bump pysocks from 1.6.7 to 1.7.1 + +- Bump websocket-client from 0.32.0 to 0.57.0 + +- Bump urllib3 from 1.24.2 to 1.25.7 + +- Bump jsonschema from 3.0.1 to 3.2.0 + +- Bump PyYAML from 4.2b1 to 5.3 + +- Bump certifi from 2017.4.17 to 2019.11.28 + +- Bump coverage from 4.5.4 to 5.0.3 + +- Bump paramiko from 2.6.0 to 2.7.1 + +- Bump cached-property from 1.3.0 to 1.5.1 + +- Bump minor Linux and MacOSX dependencies + +### Bugfixes + +- Validate version format on formats 2+ + +- Assume infinite terminal width when not running in a terminal + 1.25.1 (2020-01-06) ------------------- diff --git a/Dockerfile b/Dockerfile index 918c6876b9d..d934a8878c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,15 @@ -ARG DOCKER_VERSION=19.03.5 -ARG PYTHON_VERSION=3.7.6 -ARG BUILD_ALPINE_VERSION=3.11 +ARG DOCKER_VERSION=19.03 +ARG PYTHON_VERSION=3.7.10 + +ARG BUILD_ALPINE_VERSION=3.12 +ARG BUILD_CENTOS_VERSION=7 ARG BUILD_DEBIAN_VERSION=slim-stretch -ARG RUNTIME_ALPINE_VERSION=3.11.3 -ARG RUNTIME_DEBIAN_VERSION=stretch-20191224-slim -ARG BUILD_PLATFORM=alpine +ARG RUNTIME_ALPINE_VERSION=3.12 +ARG RUNTIME_CENTOS_VERSION=7 +ARG RUNTIME_DEBIAN_VERSION=stretch-slim + +ARG DISTRO=alpine FROM docker:${DOCKER_VERSION} AS docker-cli @@ -24,8 +28,6 @@ RUN apk add --no-cache \ musl-dev \ openssl \ openssl-dev \ - python2 \ - python2-dev \ zlib-dev ENV BUILD_BOOTLOADER=1 @@ -40,34 +42,58 @@ RUN apt-get update && apt-get install --no-install-recommends -y \ libssl-dev \ make \ openssl \ - python2.7-dev \ zlib1g-dev -FROM build-${BUILD_PLATFORM} AS build -COPY docker-compose-entrypoint.sh /usr/local/bin/ +FROM centos:${BUILD_CENTOS_VERSION} AS build-centos +RUN yum install -y \ + gcc \ + git \ + libffi-devel \ + make \ + openssl \ + openssl-devel +WORKDIR /tmp/python3/ +ARG PYTHON_VERSION +RUN curl -L https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz | tar xzf - \ + && cd Python-${PYTHON_VERSION} \ + && ./configure --enable-optimizations --enable-shared --prefix=/usr LDFLAGS="-Wl,-rpath /usr/lib" \ + && make altinstall +RUN alternatives --install /usr/bin/python python /usr/bin/python2.7 50 +RUN alternatives --install /usr/bin/python python /usr/bin/python$(echo "${PYTHON_VERSION%.*}") 60 +RUN curl https://bootstrap.pypa.io/get-pip.py | python - + +FROM build-${DISTRO} AS build 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.9.1 - -COPY requirements.txt . +COPY docker-compose-entrypoint.sh /usr/local/bin/ +COPY --from=docker-cli /usr/local/bin/docker /usr/local/bin/docker +RUN pip install \ + virtualenv==20.4.0 \ + tox==3.21.2 COPY requirements-dev.txt . +COPY requirements-indirect.txt . +COPY requirements.txt . +RUN pip install -r requirements.txt -r requirements-indirect.txt -r requirements-dev.txt COPY .pre-commit-config.yaml . COPY tox.ini . COPY setup.py . COPY README.md . COPY compose compose/ -RUN tox --notest +RUN tox -e py37 --notest COPY . . ARG GIT_COMMIT=unknown ENV DOCKER_COMPOSE_GITSHA=$GIT_COMMIT RUN script/build/linux-entrypoint +FROM scratch AS bin +ARG TARGETARCH +ARG TARGETOS +COPY --from=build /usr/local/bin/docker-compose /docker-compose-${TARGETOS}-${TARGETARCH} + FROM alpine:${RUNTIME_ALPINE_VERSION} AS runtime-alpine FROM debian:${RUNTIME_DEBIAN_VERSION} AS runtime-debian -FROM runtime-${BUILD_PLATFORM} AS runtime +FROM centos:${RUNTIME_CENTOS_VERSION} AS runtime-centos +FROM runtime-${DISTRO} 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 diff --git a/Dockerfile.s390x b/Dockerfile.s390x deleted file mode 100644 index 9bae72d6734..00000000000 --- a/Dockerfile.s390x +++ /dev/null @@ -1,15 +0,0 @@ -FROM s390x/alpine:3.10.1 - -ARG COMPOSE_VERSION=1.16.1 - -RUN apk add --update --no-cache \ - python \ - py-pip \ - && pip install --no-cache-dir docker-compose==$COMPOSE_VERSION \ - && rm -rf /var/cache/apk/* - -WORKDIR /data -VOLUME /data - - -ENTRYPOINT ["docker-compose"] diff --git a/Jenkinsfile b/Jenkinsfile index 5dd1101c2c7..454f9bb852d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,6 +1,6 @@ #!groovy -def dockerVersions = ['19.03.5'] +def dockerVersions = ['19.03.13'] def baseImages = ['alpine', 'debian'] def pythonVersions = ['py37'] @@ -13,6 +13,9 @@ pipeline { timeout(time: 2, unit: 'HOURS') timestamps() } + environment { + DOCKER_BUILDKIT="1" + } stages { stage('Build test images') { @@ -20,7 +23,7 @@ pipeline { parallel { stage('alpine') { agent { - label 'ubuntu && amd64 && !zfs' + label 'ubuntu-2004 && amd64 && !zfs && cgroup1' } steps { buildImage('alpine') @@ -28,7 +31,7 @@ pipeline { } stage('debian') { agent { - label 'ubuntu && amd64 && !zfs' + label 'ubuntu-2004 && amd64 && !zfs && cgroup1' } steps { buildImage('debian') @@ -59,7 +62,7 @@ pipeline { def buildImage(baseImage) { def scmvar = checkout(scm) - def imageName = "dockerbuildbot/compose:${baseImage}-${scmvar.GIT_COMMIT}" + def imageName = "dockerpinata/compose:${baseImage}-${scmvar.GIT_COMMIT}" image = docker.image(imageName) withDockerRegistry(credentialsId:'dockerbuildbot-index.docker.io') { @@ -69,7 +72,7 @@ def buildImage(baseImage) { ansiColor('xterm') { sh """docker build -t ${imageName} \\ --target build \\ - --build-arg BUILD_PLATFORM="${baseImage}" \\ + --build-arg DISTRO="${baseImage}" \\ --build-arg GIT_COMMIT="${scmvar.GIT_COMMIT}" \\ .\\ """ @@ -84,9 +87,9 @@ def buildImage(baseImage) { def runTests(dockerVersion, pythonVersion, baseImage) { return { stage("python=${pythonVersion} docker=${dockerVersion} ${baseImage}") { - node("ubuntu && amd64 && !zfs") { + node("ubuntu-2004 && amd64 && !zfs && cgroup1") { def scmvar = checkout(scm) - def imageName = "dockerbuildbot/compose:${baseImage}-${scmvar.GIT_COMMIT}" + def imageName = "dockerpinata/compose:${baseImage}-${scmvar.GIT_COMMIT}" def storageDriver = sh(script: "docker info -f \'{{.Driver}}\'", returnStdout: true).trim() echo "Using local system's storage driver: ${storageDriver}" withDockerRegistry(credentialsId:'dockerbuildbot-index.docker.io') { @@ -96,6 +99,8 @@ def runTests(dockerVersion, pythonVersion, baseImage) { --privileged \\ --volume="\$(pwd)/.git:/code/.git" \\ --volume="/var/run/docker.sock:/var/run/docker.sock" \\ + --volume="\${DOCKER_CONFIG}/config.json:/root/.docker/config.json" \\ + -e "DOCKER_TLS_CERTDIR=" \\ -e "TAG=${imageName}" \\ -e "STORAGE_DRIVER=${storageDriver}" \\ -e "DOCKER_VERSIONS=${dockerVersion}" \\ diff --git a/MAINTAINERS b/MAINTAINERS index 27bde5bb38e..7e178147e84 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -11,6 +11,7 @@ [Org] [Org."Core maintainers"] people = [ + "aiordache", "ndeloof", "rumpl", "ulyssessouza", @@ -29,7 +30,7 @@ # in 2015 with solid bugfixes and improved error handling # among them "mnowster", - # Daniel Nephin is one of the longest-running maitainers on + # Daniel Nephin is one of the longest-running maintainers on # the Compose project, and has contributed several major features # including muti-file support, variable interpolation, secrets # emulation and many more @@ -53,6 +54,11 @@ Email = "aanand.prasad@gmail.com" GitHub = "aanand" + [people.aiordache] + Name = "Anca Iordache" + Email = "anca.iordache@docker.com" + GitHub = "aiordache" + [people.bfirsh] Name = "Ben Firshman" Email = "ben@firshman.co.uk" diff --git a/MANIFEST.in b/MANIFEST.in index fca685eaae0..313b4e00814 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,6 @@ include Dockerfile include LICENSE +include requirements-indirect.txt include requirements.txt include requirements-dev.txt include tox.ini diff --git a/Makefile b/Makefile new file mode 100644 index 00000000000..0a7a5c366b4 --- /dev/null +++ b/Makefile @@ -0,0 +1,57 @@ +TAG = "docker-compose:alpine-$(shell git rev-parse --short HEAD)" +GIT_VOLUME = "--volume=$(shell pwd)/.git:/code/.git" + +DOCKERFILE ?="Dockerfile" +DOCKER_BUILD_TARGET ?="build" + +UNAME_S := $(shell uname -s) +ifeq ($(UNAME_S),Linux) + BUILD_SCRIPT = linux +endif +ifeq ($(UNAME_S),Darwin) + BUILD_SCRIPT = osx +endif + +COMPOSE_SPEC_SCHEMA_PATH = "compose/config/compose_spec.json" +COMPOSE_SPEC_RAW_URL = "https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json" + +all: cli + +cli: download-compose-spec ## Compile the cli + ./script/build/$(BUILD_SCRIPT) + +download-compose-spec: ## Download the compose-spec schema from it's repo + curl -so $(COMPOSE_SPEC_SCHEMA_PATH) $(COMPOSE_SPEC_RAW_URL) + +cache-clear: ## Clear the builder cache + @docker builder prune --force --filter type=exec.cachemount --filter=unused-for=24h + +base-image: ## Builds base image + docker build -f $(DOCKERFILE) -t $(TAG) --target $(DOCKER_BUILD_TARGET) . + +lint: base-image ## Run linter + docker run --rm \ + --tty \ + $(GIT_VOLUME) \ + $(TAG) \ + tox -e pre-commit + +test-unit: base-image ## Run tests + docker run --rm \ + --tty \ + $(GIT_VOLUME) \ + $(TAG) \ + pytest -v tests/unit/ + +test: ## Run all tests + ./script/test/default + +pre-commit: lint test-unit cli + +help: ## Show help + @echo Please specify a build target. The choices are: + @grep -E '^[0-9a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +FORCE: + +.PHONY: all cli download-compose-spec cache-clear base-image lint test-unit test pre-commit help diff --git a/README.md b/README.md index f1ae7e1ed2f..d0d23d8af60 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,86 @@ Docker Compose ============== +[![Build Status](https://ci-next.docker.com/public/buildStatus/icon?job=compose/master)](https://ci-next.docker.com/public/job/compose/job/master/) + ![Docker Compose](logo.png?raw=true "Docker Compose Logo") -## :exclamation: The docker-compose project announces that as Python 2 reaches it's EOL, versions 1.25.x will be the last to support it. For more information, please refer to this [issue](https://github.com/docker/compose/issues/6890). +Docker Compose is a tool for running multi-container applications on Docker +defined using the [Compose file format](https://compose-spec.io). +A Compose file is used to define how the one or more containers that make up +your application are configured. +Once you have a Compose file, you can create and start your application with a +single command: `docker-compose up`. -Compose is a tool for defining and running multi-container Docker applications. -With Compose, you use a Compose file to configure your application's services. -Then, using a single command, you create and start all the services -from your configuration. To learn more about all the features of Compose -see [the list of features](https://github.com/docker/docker.github.io/blob/master/compose/index.md#features). +Compose files can be used to deploy applications locally, or to the cloud on +[Amazon ECS](https://aws.amazon.com/ecs) or +[Microsoft ACI](https://azure.microsoft.com/services/container-instances/) using +the Docker CLI. You can read more about how to do this: +- [Compose for Amazon ECS](https://docs.docker.com/engine/context/ecs-integration/) +- [Compose for Microsoft ACI](https://docs.docker.com/engine/context/aci-integration/) -Compose is great for development, testing, and staging environments, as well as -CI workflows. You can learn more about each case in -[Common Use Cases](https://github.com/docker/docker.github.io/blob/master/compose/index.md#common-use-cases). +Where to get Docker Compose +---------------------------- -Using Compose is basically a three-step process. +### Windows and macOS -1. Define your app's environment with a `Dockerfile` so it can be -reproduced anywhere. -2. Define the services that make up your app in `docker-compose.yml` so -they can be run together in an isolated environment. -3. Lastly, run `docker-compose up` and Compose will start and run your entire app. +Docker Compose is included in +[Docker Desktop](https://www.docker.com/products/docker-desktop) +for Windows and macOS. -A `docker-compose.yml` looks like this: +### Linux - version: '2' +You can download Docker Compose binaries from the +[release page](https://github.com/docker/compose/releases) on this repository. - services: - web: - build: . - ports: - - "5000:5000" - volumes: - - .:/code - redis: - image: redis +### Using pip -For more information about the Compose file, see the -[Compose file reference](https://github.com/docker/docker.github.io/blob/master/compose/compose-file/compose-versioning.md). +If your platform is not supported, you can download Docker Compose using `pip`: -Compose has commands for managing the whole lifecycle of your application: +```console +pip install docker-compose +``` - * Start, stop and rebuild services - * View the status of running services - * Stream the log output of running services - * Run a one-off command on a service +> **Note:** Docker Compose requires Python 3.6 or later. -Installation and documentation ------------------------------- +Quick Start +----------- -- Full documentation is available on [Docker's website](https://docs.docker.com/compose/). -- Code repository for Compose is on [GitHub](https://github.com/docker/compose). -- If you find any problems please fill out an [issue](https://github.com/docker/compose/issues/new/choose). Thank you! +Using Docker Compose is basically a three-step process: +1. Define your app's environment with a `Dockerfile` so it can be + reproduced anywhere. +2. Define the services that make up your app in `docker-compose.yml` so + they can be run together in an isolated environment. +3. Lastly, run `docker-compose up` and Compose will start and run your entire + app. + +A Compose file looks like this: + +```yaml +services: + web: + build: . + ports: + - "5000:5000" + volumes: + - .:/code + redis: + image: redis +``` + +You can find examples of Compose applications in our +[Awesome Compose repository](https://github.com/docker/awesome-compose). + +For more information about the Compose format, see the +[Compose file reference](https://docs.docker.com/compose/compose-file/). Contributing ------------ -[![Build Status](https://jenkins.dockerproject.org/buildStatus/icon?job=docker/compose/master)](https://jenkins.dockerproject.org/job/docker/job/compose/job/master/) +Want to help develop Docker Compose? Check out our +[contributing documentation](https://github.com/docker/compose/blob/master/CONTRIBUTING.md). -Want to help build Compose? Check out our [contributing documentation](https://github.com/docker/compose/blob/master/CONTRIBUTING.md). +If you find an issue, please report it on the +[issue tracker](https://github.com/docker/compose/issues/new/choose). Releasing --------- diff --git a/Release.Jenkinsfile b/Release.Jenkinsfile index 953e19e4d91..4779c25a3e3 100644 --- a/Release.Jenkinsfile +++ b/Release.Jenkinsfile @@ -1,6 +1,6 @@ #!groovy -def dockerVersions = ['19.03.5', '18.09.9'] +def dockerVersions = ['19.03.13', '18.09.9'] def baseImages = ['alpine', 'debian'] def pythonVersions = ['py37'] @@ -13,6 +13,9 @@ pipeline { timeout(time: 2, unit: 'HOURS') timestamps() } + environment { + DOCKER_BUILDKIT="1" + } stages { stage('Build test images') { @@ -20,7 +23,7 @@ pipeline { parallel { stage('alpine') { agent { - label 'linux' + label 'linux && docker && ubuntu-2004 && amd64 && cgroup1' } steps { buildImage('alpine') @@ -28,7 +31,7 @@ pipeline { } stage('debian') { agent { - label 'linux' + label 'linux && docker && ubuntu-2004 && amd64 && cgroup1' } steps { buildImage('debian') @@ -37,6 +40,9 @@ pipeline { } } stage('Test') { + agent { + label 'linux && docker && ubuntu-2004 && amd64 && cgroup1' + } steps { // TODO use declarative 1.5.0 `matrix` once available on CI script { @@ -55,7 +61,7 @@ pipeline { } stage('Generate Changelog') { agent { - label 'linux' + label 'linux && docker && ubuntu-2004 && amd64 && cgroup1' } steps { checkout scm @@ -78,7 +84,7 @@ pipeline { steps { checkout scm sh './script/setup/osx' - sh 'tox -e py37 -- tests/unit' + sh 'tox -e py39 -- tests/unit' sh './script/build/osx' dir ('dist') { checksum('docker-compose-Darwin-x86_64') @@ -92,7 +98,7 @@ pipeline { } stage('linux binary') { agent { - label 'linux' + label 'linux && docker && ubuntu-2004 && amd64 && cgroup1' } steps { checkout scm @@ -111,11 +117,11 @@ pipeline { label 'windows-python' } environment { - PATH = "$PATH;C:\\Python37;C:\\Python37\\Scripts" + PATH = "C:\\Python39;C:\\Python39\\Scripts;$PATH" } steps { checkout scm - bat 'tox.exe -e py37 -- tests/unit' + bat 'tox.exe -e py39 -- tests/unit' powershell '.\\script\\build\\windows.ps1' dir ('dist') { checksum('docker-compose-Windows-x86_64.exe') @@ -128,7 +134,7 @@ pipeline { } stage('alpine image') { agent { - label 'linux' + label 'linux && docker && ubuntu-2004 && amd64 && cgroup1' } steps { buildRuntimeImage('alpine') @@ -136,7 +142,7 @@ pipeline { } stage('debian image') { agent { - label 'linux' + label 'linux && docker && ubuntu-2004 && amd64 && cgroup1' } steps { buildRuntimeImage('debian') @@ -151,7 +157,7 @@ pipeline { parallel { stage('Pushing images') { agent { - label 'linux' + label 'linux && docker && ubuntu-2004 && amd64 && cgroup1' } steps { pushRuntimeImage('alpine') @@ -160,7 +166,7 @@ pipeline { } stage('Creating Github Release') { agent { - label 'linux' + label 'linux && docker && ubuntu-2004 && amd64 && cgroup1' } environment { GITHUB_TOKEN = credentials('github-release-token') @@ -192,7 +198,7 @@ pipeline { } stage('Publishing Python packages') { agent { - label 'linux' + label 'linux && docker && ubuntu-2004 && amd64 && cgroup1' } environment { PYPIRC = credentials('pypirc-docker-dsg-cibot') @@ -201,9 +207,9 @@ pipeline { checkout scm sh """ rm -rf build/ dist/ - pip install wheel - python setup.py sdist bdist_wheel - pip install twine + pip3 install wheel + python3 setup.py sdist bdist_wheel + pip3 install twine ~/.local/bin/twine upload --config-file ${PYPIRC} ./dist/docker-compose-*.tar.gz ./dist/docker_compose-*-py2.py3-none-any.whl """ } @@ -216,7 +222,7 @@ pipeline { def buildImage(baseImage) { def scmvar = checkout(scm) - def imageName = "dockerbuildbot/compose:${baseImage}-${scmvar.GIT_COMMIT}" + def imageName = "dockerpinata/compose:${baseImage}-${scmvar.GIT_COMMIT}" image = docker.image(imageName) withDockerRegistry(credentialsId:'dockerbuildbot-index.docker.io') { @@ -226,7 +232,7 @@ def buildImage(baseImage) { ansiColor('xterm') { sh """docker build -t ${imageName} \\ --target build \\ - --build-arg BUILD_PLATFORM="${baseImage}" \\ + --build-arg DISTRO="${baseImage}" \\ --build-arg GIT_COMMIT="${scmvar.GIT_COMMIT}" \\ .\\ """ @@ -241,9 +247,9 @@ def buildImage(baseImage) { def runTests(dockerVersion, pythonVersion, baseImage) { return { stage("python=${pythonVersion} docker=${dockerVersion} ${baseImage}") { - node("linux") { + node("linux && docker && ubuntu-2004 && amd64 && cgroup1") { def scmvar = checkout(scm) - def imageName = "dockerbuildbot/compose:${baseImage}-${scmvar.GIT_COMMIT}" + def imageName = "dockerpinata/compose:${baseImage}-${scmvar.GIT_COMMIT}" def storageDriver = sh(script: "docker info -f \'{{.Driver}}\'", returnStdout: true).trim() echo "Using local system's storage driver: ${storageDriver}" withDockerRegistry(credentialsId:'dockerbuildbot-index.docker.io') { @@ -253,6 +259,8 @@ def runTests(dockerVersion, pythonVersion, baseImage) { --privileged \\ --volume="\$(pwd)/.git:/code/.git" \\ --volume="/var/run/docker.sock:/var/run/docker.sock" \\ + --volume="\${DOCKER_CONFIG}/config.json:/root/.docker/config.json" \\ + -e "DOCKER_TLS_CERTDIR=" \\ -e "TAG=${imageName}" \\ -e "STORAGE_DRIVER=${storageDriver}" \\ -e "DOCKER_VERSIONS=${dockerVersion}" \\ @@ -273,7 +281,7 @@ def buildRuntimeImage(baseImage) { def imageName = "docker/compose:${baseImage}-${env.BRANCH_NAME}" ansiColor('xterm') { sh """docker build -t ${imageName} \\ - --build-arg BUILD_PLATFORM="${baseImage}" \\ + --build-arg DISTRO="${baseImage}" \\ --build-arg GIT_COMMIT="${scmvar.GIT_COMMIT.take(7)}" \\ . """ diff --git a/bin/docker-compose b/bin/docker-compose index aeb53870303..5976e1d4aa5 100755 --- a/bin/docker-compose +++ b/bin/docker-compose @@ -1,6 +1,3 @@ #!/usr/bin/env python -from __future__ import absolute_import -from __future__ import unicode_literals - from compose.cli.main import main main() diff --git a/compose/__init__.py b/compose/__init__.py index 69c4e0e49d8..002761a5e9b 100644 --- a/compose/__init__.py +++ b/compose/__init__.py @@ -1,4 +1 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - -__version__ = '1.26.0dev' +__version__ = '1.30.0dev' diff --git a/compose/__main__.py b/compose/__main__.py index 27a7acbb8d1..199ba2ae9b4 100644 --- a/compose/__main__.py +++ b/compose/__main__.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - from compose.cli.main import main main() diff --git a/compose/cli/colors.py b/compose/cli/colors.py index ea45198e07d..042403a9f7d 100644 --- a/compose/cli/colors.py +++ b/compose/cli/colors.py @@ -1,5 +1,5 @@ -from __future__ import absolute_import -from __future__ import unicode_literals +import enum +import os from ..const import IS_WINDOWS_PLATFORM @@ -15,18 +15,33 @@ ] +@enum.unique +class AnsiMode(enum.Enum): + """Enumeration for when to output ANSI colors.""" + NEVER = "never" + ALWAYS = "always" + AUTO = "auto" + + def use_ansi_codes(self, stream): + if self is AnsiMode.ALWAYS: + return True + if self is AnsiMode.NEVER or os.environ.get('CLICOLOR') == '0': + return False + return stream.isatty() + + def get_pairs(): for i, name in enumerate(NAMES): - yield(name, str(30 + i)) - yield('intense_' + name, str(30 + i) + ';1') + yield (name, str(30 + i)) + yield ('intense_' + name, str(30 + i) + ';1') def ansi(code): - return '\033[{0}m'.format(code) + return '\033[{}m'.format(code) def ansi_color(code, s): - return '{0}{1}{2}'.format(ansi(code), s, ansi(0)) + return '{}{}{}'.format(ansi(code), s, ansi(0)) def make_color_fn(code): diff --git a/compose/cli/command.py b/compose/cli/command.py index 2fabbe18a62..e8d399aa1d0 100644 --- a/compose/cli/command.py +++ b/compose/cli/command.py @@ -1,26 +1,19 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import logging import os import re -import six - from . import errors -from . import verbose_proxy from .. import config from .. import parallel from ..config.environment import Environment -from ..const import API_VERSIONS from ..const import LABEL_CONFIG_FILES from ..const import LABEL_ENVIRONMENT_FILE from ..const import LABEL_WORKING_DIR from ..project import Project -from .docker_client import docker_client -from .docker_client import get_tls_version -from .docker_client import tls_config_from_options -from .utils import get_version_info +from .docker_client import get_client +from .docker_client import load_context +from .docker_client import make_context +from .errors import UserError log = logging.getLogger(__name__) @@ -42,27 +35,39 @@ def project_from_options(project_dir, options, additional_options=None): additional_options = additional_options or {} - override_dir = options.get('--project-directory') + override_dir = get_project_dir(options) 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') + # get the context for the run + context = None + context_name = options.get('--context', None) + if context_name: + context = load_context(context_name) + if not context: + raise UserError("Context '{}' not found".format(context_name)) + + host = options.get('--host', None) if host is not None: + if context: + raise UserError( + "-H, --host and -c, --context are mutually exclusive. Only one should be set.") host = host.lstrip('=') + context = make_context(host, options, environment) + return get_project( project_dir, - get_config_path_from_options(project_dir, options, environment), + get_config_path_from_options(options, environment), project_name=options.get('--project-name'), verbose=options.get('--verbose'), - host=host, - tls_config=tls_config_from_options(options, environment), + context=context, environment=environment, override_dir=override_dir, - compatibility=compatibility_from_options(project_dir, options, environment), interpolate=(not additional_options.get('--no-interpolate')), - environment_file=environment_file + environment_file=environment_file, + enabled_profiles=get_profiles_from_options(options, environment) ) @@ -82,24 +87,31 @@ def set_parallel_limit(environment): parallel.GlobalLimit.set_global_limit(parallel_limit) +def get_project_dir(options): + override_dir = None + files = get_config_path_from_options(options, os.environ) + if files: + if files[0] == '-': + return '.' + override_dir = os.path.dirname(files[0]) + return options.get('--project-directory') or override_dir + + def get_config_from_options(base_dir, options, additional_options=None): additional_options = additional_options or {} - override_dir = options.get('--project-directory') + override_dir = get_project_dir(options) 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 - ) + config_path = get_config_path_from_options(options, environment) return config.load( config.find(base_dir, config_path, environment, override_dir), - compatibility_from_options(config_path, options, environment), not additional_options.get('--no-interpolate') ) -def get_config_path_from_options(base_dir, options, environment): +def get_config_path_from_options(options, environment): def unicode_paths(paths): - return [p.decode('utf-8') if isinstance(p, six.binary_type) else p for p in paths] + return [p.decode('utf-8') if isinstance(p, bytes) else p for p in paths] file_option = options.get('--file') if file_option: @@ -112,41 +124,33 @@ def unicode_paths(paths): return None -def get_client(environment, verbose=False, version=None, tls_config=None, host=None, - tls_version=None): +def get_profiles_from_options(options, environment): + profile_option = options.get('--profile') + if profile_option: + return profile_option - client = docker_client( - version=version, tls_config=tls_config, host=host, - environment=environment, tls_version=get_tls_version(environment) - ) - if verbose: - version_info = six.iteritems(client.version()) - log.info(get_version_info('full')) - log.info("Docker base_url: %s", client.base_url) - log.info("Docker version: %s", - ", ".join("%s=%s" % item for item in version_info)) - return verbose_proxy.VerboseProxy('docker', client) - return client + profiles = environment.get('COMPOSE_PROFILES') + if profiles: + return profiles.split(',') + + return [] 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, interpolate=True, environment_file=None): + context=None, environment=None, override_dir=None, + interpolate=True, environment_file=None, enabled_profiles=None): 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, interpolate) + config_data = config.load(config_details, interpolate) - api_version = environment.get( - 'COMPOSE_API_VERSION', - API_VERSIONS[config_data.version]) + api_version = environment.get('COMPOSE_API_VERSION') client = get_client( - verbose=verbose, version=api_version, tls_config=tls_config, - host=host, environment=environment + verbose=verbose, version=api_version, context=context, environment=environment ) with errors.handle_connection_errors(client): @@ -156,20 +160,23 @@ def get_project(project_dir, config_path=None, project_name=None, verbose=False, client, environment.get('DOCKER_DEFAULT_PLATFORM'), execution_context_labels(config_details, environment_file), + enabled_profiles, ) def execution_context_labels(config_details, environment_file): extra_labels = [ - '{0}={1}'.format(LABEL_WORKING_DIR, os.path.abspath(config_details.working_dir)) + '{}={}'.format(LABEL_WORKING_DIR, os.path.abspath(config_details.working_dir)) ] if not use_config_from_stdin(config_details): - extra_labels.append('{0}={1}'.format(LABEL_CONFIG_FILES, config_files_label(config_details))) + extra_labels.append('{}={}'.format(LABEL_CONFIG_FILES, config_files_label(config_details))) if environment_file is not None: - extra_labels.append('{0}={1}'.format(LABEL_ENVIRONMENT_FILE, - os.path.normpath(environment_file))) + extra_labels.append('{}={}'.format( + LABEL_ENVIRONMENT_FILE, + os.path.normpath(environment_file)) + ) return extra_labels @@ -182,7 +189,8 @@ def use_config_from_stdin(config_details): def config_files_label(config_details): return ",".join( - map(str, (os.path.normpath(c.filename) for c in config_details.config_files))) + os.path.normpath(c.filename) for c in config_details.config_files + ) def get_project_name(working_dir, project_name=None, environment=None): @@ -200,13 +208,3 @@ def normalize_name(name): return normalize_name(project) return 'default' - - -def compatibility_from_options(working_dir, options=None, environment=None): - """Get compose v3 compatibility from --compatibility option - or from COMPOSE_COMPATIBILITY environment variable.""" - - compatibility_option = options.get('--compatibility') - compatibility_environment = environment.get_boolean('COMPOSE_COMPATIBILITY') - - return compatibility_option or compatibility_environment diff --git a/compose/cli/docker_client.py b/compose/cli/docker_client.py index a57a69b501b..e4a0fea61b4 100644 --- a/compose/cli/docker_client.py +++ b/compose/cli/docker_client.py @@ -1,21 +1,22 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import logging import os.path import ssl from docker import APIClient +from docker import Context +from docker import ContextAPI +from docker import TLSConfig from docker.errors import TLSParameterError -from docker.tls import TLSConfig from docker.utils import kwargs_from_env from docker.utils.config import home_dir +from . import verbose_proxy from ..config.environment import Environment from ..const import HTTP_TIMEOUT from ..utils import unquote_path from .errors import UserError from .utils import generate_user_agent +from .utils import get_version_info log = logging.getLogger(__name__) @@ -24,6 +25,33 @@ def default_cert_path(): return os.path.join(home_dir(), '.docker') +def make_context(host, options, environment): + tls = tls_config_from_options(options, environment) + ctx = Context("compose", host=host, tls=tls.verify if tls else False) + if tls: + ctx.set_endpoint("docker", host, tls, skip_tls_verify=not tls.verify) + return ctx + + +def load_context(name=None): + return ContextAPI.get_context(name) + + +def get_client(environment, verbose=False, version=None, context=None): + client = docker_client( + version=version, context=context, + environment=environment, tls_version=get_tls_version(environment) + ) + if verbose: + version_info = client.version().items() + log.info(get_version_info('full')) + log.info("Docker base_url: %s", client.base_url) + log.info("Docker version: %s", + ", ".join("%s=%s" % item for item in version_info)) + return verbose_proxy.VerboseProxy('docker', client) + return client + + def get_tls_version(environment): compose_tls_version = environment.get('COMPOSE_TLS_VERSION', None) if not compose_tls_version: @@ -87,8 +115,7 @@ def tls_config_from_options(options, environment=None): return None -def docker_client(environment, version=None, tls_config=None, host=None, - tls_version=None): +def docker_client(environment, version=None, context=None, tls_version=None): """ Returns a docker-py client configured using environment variables according to the same logic as the official Docker client. @@ -101,10 +128,26 @@ def docker_client(environment, version=None, tls_config=None, host=None, "and DOCKER_CERT_PATH are set correctly.\n" "You might need to run `eval \"$(docker-machine env default)\"`") - if host: - kwargs['base_url'] = host - if tls_config: - kwargs['tls'] = tls_config + if not context: + # check env for DOCKER_HOST and certs path + host = kwargs.get("base_url", None) + tls = kwargs.get("tls", None) + verify = False if not tls else tls.verify + if host: + context = Context("compose", host=host, tls=verify) + else: + context = ContextAPI.get_current_context() + if tls: + context.set_endpoint("docker", host=host, tls_cfg=tls, skip_tls_verify=not verify) + + if not context.is_docker_host(): + raise UserError( + "The platform targeted with the current context is not supported.\n" + "Make sure the context in use targets a Docker Engine.\n") + + kwargs['base_url'] = context.Host + if context.TLSConfig: + kwargs['tls'] = context.TLSConfig if version: kwargs['version'] = version @@ -123,8 +166,8 @@ def docker_client(environment, version=None, tls_config=None, host=None, kwargs['credstore_env'] = { 'LD_LIBRARY_PATH': environment.get('LD_LIBRARY_PATH_ORIG'), } - - client = APIClient(**kwargs) + use_paramiko_ssh = int(environment.get('COMPOSE_PARAMIKO_SSH', 0)) + client = APIClient(use_ssh_client=not use_paramiko_ssh, **kwargs) client._original_base_url = kwargs.get('base_url') return client diff --git a/compose/cli/docopt_command.py b/compose/cli/docopt_command.py index 809a4b7455e..e56b37835bc 100644 --- a/compose/cli/docopt_command.py +++ b/compose/cli/docopt_command.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - from inspect import getdoc from docopt import docopt @@ -14,16 +11,22 @@ def docopt_full_help(docstring, *args, **kwargs): raise SystemExit(docstring) -class DocoptDispatcher(object): +class DocoptDispatcher: def __init__(self, command_class, options): self.command_class = command_class self.options = options + @classmethod + def get_command_and_options(cls, doc_entity, argv, options): + command_help = getdoc(doc_entity) + opt = docopt_full_help(command_help, argv, **options) + command = opt['COMMAND'] + return command_help, opt, command + def parse(self, argv): - command_help = getdoc(self.command_class) - options = docopt_full_help(command_help, argv, **self.options) - command = options['COMMAND'] + command_help, options, command = DocoptDispatcher.get_command_and_options( + self.command_class, argv, self.options) if command is None: raise SystemExit(command_help) @@ -53,7 +56,7 @@ def get_handler(command_class, command): class NoSuchCommand(Exception): def __init__(self, command, supercommand): - super(NoSuchCommand, self).__init__("No such command: %s" % command) + super().__init__("No such command: %s" % command) self.command = command self.supercommand = supercommand diff --git a/compose/cli/errors.py b/compose/cli/errors.py index 189b67faf67..a807c7d1c78 100644 --- a/compose/cli/errors.py +++ b/compose/cli/errors.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import contextlib import logging import socket @@ -29,11 +26,9 @@ class UserError(Exception): def __init__(self, msg): self.msg = dedent(msg).strip() - def __unicode__(self): + def __str__(self): return self.msg - __str__ = __unicode__ - class ConnectionError(Exception): pass diff --git a/compose/cli/formatter.py b/compose/cli/formatter.py index 9651fb4da44..ff81ee65163 100644 --- a/compose/cli/formatter.py +++ b/compose/cli/formatter.py @@ -1,19 +1,10 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import logging -import shutil +from shutil import get_terminal_size -import six import texttable from compose.cli import colors -if hasattr(shutil, "get_terminal_size"): - from shutil import get_terminal_size -else: - from backports.shutil_get_terminal_size import get_terminal_size - def get_tty_width(): try: @@ -49,15 +40,15 @@ class ConsoleWarningFormatter(logging.Formatter): def get_level_message(self, record): separator = ': ' - if record.levelno == logging.WARNING: - return colors.yellow(record.levelname) + separator - if record.levelno == logging.ERROR: + if record.levelno >= logging.ERROR: return colors.red(record.levelname) + separator + if record.levelno >= logging.WARNING: + return colors.yellow(record.levelname) + separator return '' def format(self, record): - if isinstance(record.msg, six.binary_type): + if isinstance(record.msg, bytes): record.msg = record.msg.decode('utf-8') - message = super(ConsoleWarningFormatter, self).format(record) - return '{0}{1}'.format(self.get_level_message(record), message) + message = super().format(record) + return '{}{}'.format(self.get_level_message(record), message) diff --git a/compose/cli/log_printer.py b/compose/cli/log_printer.py index a4b70a67205..e56dc2fa37c 100644 --- a/compose/cli/log_printer.py +++ b/compose/cli/log_printer.py @@ -1,36 +1,37 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - +import _thread as thread import sys from collections import namedtuple from itertools import cycle +from operator import attrgetter +from queue import Empty +from queue import Queue from threading import Thread from docker.errors import APIError -from six.moves import _thread as thread -from six.moves.queue import Empty -from six.moves.queue import Queue from . import colors -from compose import utils from compose.cli.signals import ShutdownException from compose.utils import split_buffer -class LogPresenter(object): +class LogPresenter: - def __init__(self, prefix_width, color_func): + def __init__(self, prefix_width, color_func, keep_prefix=True): self.prefix_width = prefix_width self.color_func = color_func + self.keep_prefix = keep_prefix def present(self, container, line): - prefix = container.name_without_project.ljust(self.prefix_width) - return '{prefix} {line}'.format( - prefix=self.color_func(prefix + ' |'), - line=line) + to_log = '{line}'.format(line=line) + + if self.keep_prefix: + prefix = container.name_without_project.ljust(self.prefix_width) + to_log = '{prefix} '.format(prefix=self.color_func(prefix + ' |')) + to_log + + return to_log -def build_log_presenters(service_names, monochrome): +def build_log_presenters(service_names, monochrome, keep_prefix=True): """Return an iterable of functions. Each function can be used to format the logs output of a container. @@ -41,7 +42,7 @@ def no_color(text): return text for color_func in cycle([no_color] if monochrome else colors.rainbow()): - yield LogPresenter(prefix_width, color_func) + yield LogPresenter(prefix_width, color_func, keep_prefix) def max_name_width(service_names, max_index_width=3): @@ -54,7 +55,7 @@ def max_name_width(service_names, max_index_width=3): return max(len(name) for name in service_names) + max_index_width -class LogPrinter(object): +class LogPrinter: """Print logs from many containers to a single output stream.""" def __init__(self, @@ -67,7 +68,7 @@ def __init__(self, self.containers = containers self.presenters = presenters self.event_stream = event_stream - self.output = utils.get_output_stream(output) + self.output = output self.cascade_stop = cascade_stop self.log_args = log_args or {} @@ -137,7 +138,7 @@ def build_thread_map(initial_containers, presenters, thread_args): # Container order is unspecified, so they are sorted by name in order to make # container:presenter (log color) assignment deterministic when given a list of containers # with the same names. - for container in sorted(initial_containers, key=lambda c: c.name) + for container in sorted(initial_containers, key=attrgetter('name')) } @@ -157,10 +158,8 @@ def stop(cls, item=None): def tail_container_logs(container, presenter, queue, log_args): - generator = get_log_generator(container) - try: - for item in generator(container, log_args): + for item in build_log_generator(container, log_args): queue.put(QueueItem.new(presenter.present(container, item))) except Exception as e: queue.put(QueueItem.exception(e)) @@ -170,20 +169,6 @@ def tail_container_logs(container, presenter, queue, log_args): queue.put(QueueItem.stop(container.name)) -def get_log_generator(container): - if container.has_api_logs: - return build_log_generator - return build_no_log_generator - - -def build_no_log_generator(container, log_args): - """Return a generator that prints a warning about logs and waits for - container to exit. - """ - yield "WARNING: no logs are available with the '{}' log driver\n".format( - container.log_driver) - - def build_log_generator(container, log_args): # if the container doesn't have a log_stream we need to attach to container # before log printer starts running @@ -198,9 +183,9 @@ def build_log_generator(container, log_args): def wait_on_exit(container): try: exit_code = container.wait() - return "%s exited with code %s\n" % (container.name, exit_code) + return "{} exited with code {}\n".format(container.name, exit_code) except APIError as e: - return "Unexpected API error for %s (HTTP code %s)\nResponse body:\n%s\n" % ( + return "Unexpected API error for {} (HTTP code {})\nResponse body:\n{}\n".format( container.name, e.response.status_code, e.response.text or '[empty]' ) diff --git a/compose/cli/main.py b/compose/cli/main.py index 3f0f88384f3..1551b1311ba 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -1,12 +1,7 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import contextlib import functools import json import logging -import os import pipes import re import subprocess @@ -28,9 +23,11 @@ from ..config.environment import Environment from ..config.serialize import serialize_config from ..config.types import VolumeSpec -from ..const import COMPOSEFILE_V2_2 as V2_2 +from ..const import IS_LINUX_PLATFORM from ..const import IS_WINDOWS_PLATFORM from ..errors import StreamParseError +from ..metrics.decorator import metrics +from ..parallel import ParallelStreamWriter from ..progress_stream import StreamOutputError from ..project import get_image_digests from ..project import MissingDigests @@ -43,7 +40,10 @@ from ..service import ImageType from ..service import NeedsBuildError from ..service import OperationFailedError +from ..utils import filter_attached_for_up +from .colors import AnsiMode from .command import get_config_from_options +from .command import get_project_dir from .command import project_from_options from .docopt_command import DocoptDispatcher from .docopt_command import get_handler @@ -56,57 +56,132 @@ from .utils import get_version_info from .utils import human_readable_file_size from .utils import yesno +from compose.metrics.client import MetricsCommand +from compose.metrics.client import Status if not IS_WINDOWS_PLATFORM: from dockerpty.pty import PseudoTerminal, RunOperation, ExecOperation log = logging.getLogger(__name__) -console_handler = logging.StreamHandler(sys.stderr) -def main(): +def main(): # noqa: C901 signals.ignore_sigpipe() + command = None + try: + _, opts, command = DocoptDispatcher.get_command_and_options( + TopLevelCommand, + get_filtered_args(sys.argv[1:]), + {'options_first': True, 'version': get_version_info('compose')}) + except Exception: + pass try: - command = dispatch() - command() + command_func = dispatch() + command_func() + if not IS_LINUX_PLATFORM and command == 'help': + print("\nDocker Compose is now in the Docker CLI, try `docker compose` help") except (KeyboardInterrupt, signals.ShutdownException): - log.error("Aborting.") - sys.exit(1) + exit_with_metrics(command, "Aborting.", status=Status.CANCELED) except (UserError, NoSuchService, ConfigurationError, ProjectError, OperationFailedError) as e: - log.error(e.msg) - sys.exit(1) + exit_with_metrics(command, e.msg, status=Status.FAILURE) except BuildError as e: - log.error("Service '%s' failed to build: %s" % (e.service.name, e.reason)) - sys.exit(1) + reason = "" + if e.reason: + reason = " : " + e.reason + exit_with_metrics(command, + "Service '{}' failed to build{}".format(e.service.name, reason), + status=Status.FAILURE) except StreamOutputError as e: - log.error(e) - sys.exit(1) + exit_with_metrics(command, e, status=Status.FAILURE) except NeedsBuildError as e: - log.error("Service '%s' needs to be built, but --no-build was passed." % e.service.name) - sys.exit(1) + exit_with_metrics(command, + "Service '{}' needs to be built, but --no-build was passed.".format( + e.service.name), status=Status.FAILURE) except NoSuchCommand as e: commands = "\n".join(parse_doc_section("commands:", getdoc(e.supercommand))) - log.error("No such command: %s\n\n%s", e.command, commands) - sys.exit(1) + if not IS_LINUX_PLATFORM: + commands += "\n\nDocker Compose is now in the Docker CLI, try `docker compose`" + exit_with_metrics("", log_msg="No such command: {}\n\n{}".format( + e.command, commands), status=Status.FAILURE) except (errors.ConnectionError, StreamParseError): - sys.exit(1) + exit_with_metrics(command, status=Status.FAILURE) + except SystemExit as e: + status = Status.SUCCESS + if len(sys.argv) > 1 and '--help' not in sys.argv: + status = Status.FAILURE + + if command and len(sys.argv) >= 3 and sys.argv[2] == '--help': + command = '--help ' + command + + if not command and len(sys.argv) >= 2 and sys.argv[1] == '--help': + command = '--help' + + msg = e.args[0] if len(e.args) else "" + code = 0 + if isinstance(e.code, int): + code = e.code + + if not IS_LINUX_PLATFORM and not command: + msg += "\n\nDocker Compose is now in the Docker CLI, try `docker compose`" + + exit_with_metrics(command, log_msg=msg, status=status, + exit_code=code) + + +def get_filtered_args(args): + if args[0] in ('-h', '--help'): + return [] + if args[0] == '--version': + return ['version'] + + +def exit_with_metrics(command, log_msg=None, status=Status.SUCCESS, exit_code=1): + if log_msg and command != 'exec': + if not exit_code: + log.info(log_msg) + else: + log.error(log_msg) + + MetricsCommand(command, status=status).send_metrics() + sys.exit(exit_code) def dispatch(): - setup_logging() + console_stream = sys.stderr + console_handler = logging.StreamHandler(console_stream) + setup_logging(console_handler) dispatcher = DocoptDispatcher( TopLevelCommand, {'options_first': True, 'version': get_version_info('compose')}) options, handler, command_options = dispatcher.parse(sys.argv[1:]) + + ansi_mode = AnsiMode.AUTO + try: + if options.get("--ansi"): + ansi_mode = AnsiMode(options.get("--ansi")) + except ValueError: + raise UserError( + 'Invalid value for --ansi: {}. Expected one of {}.'.format( + options.get("--ansi"), + ', '.join(m.value for m in AnsiMode) + ) + ) + if options.get("--no-ansi"): + if options.get("--ansi"): + raise UserError("--no-ansi and --ansi cannot be combined.") + log.warning('--no-ansi option is deprecated and will be removed in future versions. ' + 'Use `--ansi never` instead.') + ansi_mode = AnsiMode.NEVER + setup_console_handler(console_handler, options.get('--verbose'), - set_no_color_if_clicolor(options.get('--no-ansi')), + ansi_mode.use_ansi_codes(console_handler.stream), options.get("--log-level")) - setup_parallel_logger(set_no_color_if_clicolor(options.get('--no-ansi'))) - if options.get('--no-ansi'): + setup_parallel_logger(ansi_mode) + if ansi_mode is AnsiMode.NEVER: command_options['--no-color'] = True return functools.partial(perform_command, options, handler, command_options) @@ -128,23 +203,23 @@ def perform_command(options, handler, command_options): handler(command, command_options) -def setup_logging(): +def setup_logging(console_handler): root_logger = logging.getLogger() root_logger.addHandler(console_handler) root_logger.setLevel(logging.DEBUG) - # Disable requests logging + # Disable requests and docker-py logging + logging.getLogger("urllib3").propagate = False logging.getLogger("requests").propagate = False + logging.getLogger("docker").propagate = False -def setup_parallel_logger(noansi): - if noansi: - import compose.parallel - compose.parallel.ParallelStreamWriter.set_noansi() +def setup_parallel_logger(ansi_mode): + ParallelStreamWriter.set_default_ansi_mode(ansi_mode) -def setup_console_handler(handler, verbose, noansi=False, level=None): - if handler.stream.isatty() and noansi is False: +def setup_console_handler(handler, verbose, use_console_formatter=True, level=None): + if use_console_formatter: format_class = ConsoleWarningFormatter else: format_class = logging.Formatter @@ -180,11 +255,11 @@ def parse_doc_section(name, source): return [s.strip() for s in pattern.findall(source)] -class TopLevelCommand(object): +class TopLevelCommand: """Define and run multi-container applications with Docker. Usage: - docker-compose [-f ...] [options] [COMMAND] [ARGS...] + docker-compose [-f ...] [--profile ...] [options] [--] [COMMAND] [ARGS...] docker-compose -h|--help Options: @@ -192,9 +267,12 @@ class TopLevelCommand(object): (default: docker-compose.yml) -p, --project-name NAME Specify an alternate project name (default: directory name) + --profile NAME Specify a profile to enable + -c, --context NAME Specify a context name --verbose Show more output --log-level LEVEL Set log level (DEBUG, INFO, WARNING, ERROR, CRITICAL) - --no-ansi Do not print ANSI control characters + --ansi (never|always|auto) Control when to print ANSI control characters + --no-ansi Do not print ANSI control characters (DEPRECATED) -v, --version Print version and exit -H, --host HOST Daemon socket to connect to @@ -208,14 +286,14 @@ class TopLevelCommand(object): --project-directory PATH Specify an alternate working directory (default: the path of the Compose file) --compatibility If set, Compose will attempt to convert keys - in v3 files to their non-Swarm equivalent + in v3 files to their non-Swarm equivalent (DEPRECATED) --env-file PATH Specify an alternate environment file Commands: build Build or rebuild services config Validate and view the Compose file create Create services - down Stop and remove containers, networks, images, and volumes + down Stop and remove resources events Receive real time events from containers exec Execute a command in a running container help Get help on a command @@ -236,7 +314,7 @@ class TopLevelCommand(object): top Display the running processes unpause Unpause services up Create and start containers - version Show the Docker-Compose version information + version Show version information and quit """ def __init__(self, project, options=None): @@ -245,13 +323,14 @@ def __init__(self, project, options=None): @property def project_dir(self): - return self.toplevel_options.get('--project-directory') or '.' + return get_project_dir(self.toplevel_options) @property def toplevel_environment(self): environment_file = self.toplevel_options.get('--env-file') return Environment.from_env_file(self.project_dir, environment_file) + @metrics() def build(self, options): """ Build or rebuild services. @@ -260,7 +339,7 @@ def build(self, options): e.g. `composetest_db`. If you change a service's `Dockerfile` or the contents of its build directory, you can run `docker-compose build` to rebuild it. - Usage: build [options] [--build-arg key=val...] [SERVICE...] + Usage: build [options] [--build-arg key=val...] [--] [SERVICE...] Options: --build-arg key=val Set build-time variables for services. @@ -271,8 +350,6 @@ def build(self, options): --no-rm Do not remove intermediate containers after a successful build. --parallel Build images in parallel. --progress string Set type of progress output (auto, plain, tty). - EXPERIMENTAL flag for native builder. - To enable, run with COMPOSE_DOCKER_CLI_BUILD=1) --pull Always attempt to pull a newer version of the image. -q, --quiet Don't print anything to STDOUT """ @@ -286,7 +363,7 @@ def build(self, options): ) build_args = resolve_build_args(build_args, self.toplevel_environment) - native_builder = self.toplevel_environment.get_boolean('COMPOSE_DOCKER_CLI_BUILD') + native_builder = self.toplevel_environment.get_boolean('COMPOSE_DOCKER_CLI_BUILD', True) self.project.build( service_names=options['SERVICE'], @@ -303,6 +380,7 @@ def build(self, options): progress=options.get('--progress'), ) + @metrics() def config(self, options): """ Validate and view the Compose file. @@ -311,14 +389,15 @@ def config(self, options): Options: --resolve-image-digests Pin image tags to digests. - --no-interpolate Don't interpolate environment variables + --no-interpolate Don't interpolate environment variables. -q, --quiet Only validate the configuration, don't print anything. + --profiles Print the profile names, one per line. --services Print the service names, one per line. --volumes Print the volume names, one per line. --hash="*" Print the service config hash, one per line. Set "service1,service2" for a list of specified services - or use the wildcard symbol to display all services + or use the wildcard symbol to display all services. """ additional_options = {'--no-interpolate': options.get('--no-interpolate')} @@ -333,6 +412,15 @@ def config(self, options): if options['--quiet']: return + if options['--profiles']: + profiles = set() + for service in compose_config.services: + if 'profiles' in service: + for profile in service['profiles']: + profiles.add(profile) + print('\n'.join(sorted(profiles))) + return + if options['--services']: print('\n'.join(service['name'] for service in compose_config.services)) return @@ -352,6 +440,7 @@ def config(self, options): print(serialize_config(compose_config, image_digests, not options['--no-interpolate'])) + @metrics() def create(self, options): """ Creates containers for a service. @@ -380,6 +469,7 @@ def create(self, options): do_build=build_action_from_opts(options), ) + @metrics() def down(self, options): """ Stops containers and removes containers, networks, volumes, and images @@ -426,11 +516,12 @@ def events(self, options): """ Receive real time events from containers. - Usage: events [options] [SERVICE...] + Usage: events [options] [--] [SERVICE...] Options: --json Output events as a stream of json objects """ + def format_event(event): attributes = ["%s=%s" % item for item in event['attributes'].items()] return ("{time} {type} {action} {id} ({attrs})").format( @@ -447,11 +538,12 @@ def json_format_event(event): print(formatter(event)) sys.stdout.flush() + @metrics("exec") def exec_command(self, options): """ Execute a command in a running container - Usage: exec [options] [-e KEY=VAL...] SERVICE COMMAND [ARGS...] + Usage: exec [options] [-e KEY=VAL...] [--] SERVICE COMMAND [ARGS...] Options: -d, --detach Detached mode: Run command in the background. @@ -523,6 +615,7 @@ def exec_command(self, options): sys.exit(exit_code) @classmethod + @metrics() def help(cls, options): """ Get help on a command. @@ -536,10 +629,11 @@ def help(cls, options): print(getdoc(subject)) + @metrics() def images(self, options): """ List images used by the created containers. - Usage: images [options] [SERVICE...] + Usage: images [options] [--] [SERVICE...] Options: -q, --quiet Only display IDs @@ -550,7 +644,7 @@ def images(self, options): key=attrgetter('name')) if options['--quiet']: - for image in set(c.image for c in containers): + for image in {c.image for c in containers}: print(image.split(':')[1]) return @@ -590,11 +684,12 @@ def add_default_tag(img_name): ]) print(Formatter.table(headers, rows)) + @metrics() def kill(self, options): """ Force stop service containers. - Usage: kill [options] [SERVICE...] + Usage: kill [options] [--] [SERVICE...] Options: -s SIGNAL SIGNAL to send to the container. @@ -604,18 +699,20 @@ def kill(self, options): self.project.kill(service_names=options['SERVICE'], signal=signal) + @metrics() def logs(self, options): """ View output from containers. - Usage: logs [options] [SERVICE...] + Usage: logs [options] [--] [SERVICE...] Options: - --no-color Produce monochrome output. - -f, --follow Follow log output. - -t, --timestamps Show timestamps. - --tail="all" Number of lines to show from the end of the logs - for each container. + --no-color Produce monochrome output. + -f, --follow Follow log output. + -t, --timestamps Show timestamps. + --tail="all" Number of lines to show from the end of the logs + for each container. + --no-log-prefix Don't print prefix in logs. """ containers = self.project.containers(service_names=options['SERVICE'], stopped=True) @@ -634,10 +731,12 @@ def logs(self, options): log_printer_from_project( self.project, containers, - set_no_color_if_clicolor(options['--no-color']), + options['--no-color'], log_args, - event_stream=self.project.events(service_names=options['SERVICE'])).run() + event_stream=self.project.events(service_names=options['SERVICE']), + keep_prefix=not options['--no-log-prefix']).run() + @metrics() def pause(self, options): """ Pause services. @@ -647,11 +746,12 @@ def pause(self, options): containers = self.project.pause(service_names=options['SERVICE']) exit_if(not containers, 'No containers to pause', 1) + @metrics() def port(self, options): """ Print the public port for a port binding. - Usage: port [options] SERVICE PRIVATE_PORT + Usage: port [options] [--] SERVICE PRIVATE_PORT Options: --protocol=proto tcp or udp [default: tcp] @@ -668,11 +768,12 @@ def port(self, options): options['PRIVATE_PORT'], protocol=options.get('--protocol') or 'tcp') or '') + @metrics() def ps(self, options): """ List containers. - Usage: ps [options] [SERVICE...] + Usage: ps [options] [--] [SERVICE...] Options: -q, --quiet Only display IDs @@ -724,11 +825,12 @@ def ps(self, options): ]) print(Formatter.table(headers, rows)) + @metrics() def pull(self, options): """ Pulls images for services defined in a Compose file, but does not start the containers. - Usage: pull [options] [SERVICE...] + Usage: pull [options] [--] [SERVICE...] Options: --ignore-pull-failures Pull what it can and ignores images with pull failures. @@ -747,11 +849,12 @@ def pull(self, options): include_deps=options.get('--include-deps'), ) + @metrics() def push(self, options): """ Pushes images for services. - Usage: push [options] [SERVICE...] + Usage: push [options] [--] [SERVICE...] Options: --ignore-push-failures Push what it can and ignores images with push failures. @@ -761,6 +864,7 @@ def push(self, options): ignore_push_failures=options.get('--ignore-push-failures') ) + @metrics() def rm(self, options): """ Removes stopped service containers. @@ -770,7 +874,7 @@ def rm(self, options): Any data which is not in a volume will be lost. - Usage: rm [options] [SERVICE...] + Usage: rm [options] [--] [SERVICE...] Options: -f, --force Don't ask to confirm removal @@ -805,6 +909,7 @@ def rm(self, options): else: print("No stopped containers") + @metrics() def run(self, options): """ Run a one-off command on a service. @@ -818,7 +923,7 @@ def run(self, options): `docker-compose run --no-deps SERVICE COMMAND [ARGS...]`. Usage: - run [options] [-v VOLUME...] [-p PORT...] [-e KEY=VAL...] [-l KEY=VALUE...] + run [options] [-v VOLUME...] [-p PORT...] [-e KEY=VAL...] [-l KEY=VALUE...] [--] SERVICE [COMMAND] [ARGS...] Options: @@ -865,6 +970,7 @@ def run(self, options): self.toplevel_options, self.toplevel_environment ) + @metrics() def scale(self, options): """ Set number of containers to run for a service. @@ -885,20 +991,15 @@ def scale(self, options): """ timeout = timeout_from_opts(options) - if self.project.config_version == V2_2: - raise UserError( - 'The scale command is incompatible with the v2.2 format. ' - 'Use the up command with the --scale flag instead.' - ) - else: - log.warning( - 'The scale command is deprecated. ' - 'Use the up command with the --scale flag instead.' - ) + log.warning( + 'The scale command is deprecated. ' + 'Use the up command with the --scale flag instead.' + ) for service_name, num in parse_scale_args(options['SERVICE=NUM']).items(): self.project.get_service(service_name).scale(num, timeout=timeout) + @metrics() def start(self, options): """ Start existing containers. @@ -908,13 +1009,14 @@ def start(self, options): containers = self.project.start(service_names=options['SERVICE']) exit_if(not containers, 'No containers to start', 1) + @metrics() def stop(self, options): """ Stop running containers without removing them. They can be started again with `docker-compose start`. - Usage: stop [options] [SERVICE...] + Usage: stop [options] [--] [SERVICE...] Options: -t, --timeout TIMEOUT Specify a shutdown timeout in seconds. @@ -923,11 +1025,12 @@ def stop(self, options): timeout = timeout_from_opts(options) self.project.stop(service_names=options['SERVICE'], timeout=timeout) + @metrics() def restart(self, options): """ Restart running containers. - Usage: restart [options] [SERVICE...] + Usage: restart [options] [--] [SERVICE...] Options: -t, --timeout TIMEOUT Specify a shutdown timeout in seconds. @@ -937,6 +1040,7 @@ def restart(self, options): containers = self.project.restart(service_names=options['SERVICE'], timeout=timeout) exit_if(not containers, 'No containers to restart', 1) + @metrics() def top(self, options): """ Display the running processes @@ -964,6 +1068,7 @@ def top(self, options): print(container.name) print(Formatter.table(headers, rows)) + @metrics() def unpause(self, options): """ Unpause services. @@ -973,6 +1078,7 @@ def unpause(self, options): containers = self.project.unpause(service_names=options['SERVICE']) exit_if(not containers, 'No containers to unpause', 1) + @metrics() def up(self, options): """ Builds, (re)creates, starts, and attaches to containers for a service. @@ -992,7 +1098,7 @@ def up(self, options): If you want to force Compose to stop and recreate all containers, use the `--force-recreate` flag. - Usage: up [options] [--scale SERVICE=NUM...] [SERVICE...] + Usage: up [options] [--scale SERVICE=NUM...] [--] [SERVICE...] Options: -d, --detach Detached mode: Run containers in the background, @@ -1005,6 +1111,7 @@ def up(self, options): and image haven't changed. --always-recreate-deps Recreate dependent containers. Incompatible with --no-recreate. + --pull Always attempt to pull a newer version of the image. --no-recreate If containers already exist, don't recreate them. Incompatible with --force-recreate and -V. --no-build Don't build an image, even if it's missing. @@ -1012,7 +1119,7 @@ def up(self, options): --build Build images before starting containers. --abort-on-container-exit Stops all containers if any container was stopped. Incompatible with -d. - --attach-dependencies Attach to dependent containers + --attach-dependencies Attach to dependent containers. -t, --timeout TIMEOUT Use this timeout in seconds for container shutdown when attached or when containers are already running. (default: 10) @@ -1024,9 +1131,11 @@ def up(self, options): container. Implies --abort-on-container-exit. --scale SERVICE=NUM Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present. + --no-log-prefix Don't print prefix in logs. """ start_deps = not options['--no-deps'] always_recreate_deps = options['--always-recreate-deps'] + pull = options['--pull'] exit_value_from = exitval_from_opts(options, self.project) cascade_stop = options['--abort-on-container-exit'] service_names = options['SERVICE'] @@ -1035,6 +1144,7 @@ def up(self, options): detached = options.get('--detach') no_start = options.get('--no-start') attach_dependencies = options.get('--attach-dependencies') + keep_prefix = not options.get('--no-log-prefix') if detached and (cascade_stop or exit_value_from or attach_dependencies): raise UserError( @@ -1049,7 +1159,7 @@ def up(self, options): for excluded in [x for x in opts if options.get(x) and no_start]: raise UserError('--no-start and {} cannot be combined.'.format(excluded)) - native_builder = self.toplevel_environment.get_boolean('COMPOSE_DOCKER_CLI_BUILD') + native_builder = self.toplevel_environment.get_boolean('COMPOSE_DOCKER_CLI_BUILD', True) with up_shutdown_context(self.project, service_names, timeout, detached): warn_for_swarm_mode(self.project.client) @@ -1067,10 +1177,12 @@ def up(rebuild): scale_override=parse_scale_args(options['--scale']), start=not no_start, always_recreate_deps=always_recreate_deps, + always_pull=pull, reset_container_image=rebuild, renew_anonymous_volumes=options.get('--renew-anon-volumes'), silent=options.get('--quiet-pull'), cli=native_builder, + attach_dependencies=attach_dependencies, ) try: @@ -1079,7 +1191,7 @@ def up(rebuild): log.error( "The image for the service you're trying to recreate has been removed. " "If you continue, volume data could be lost. Consider backing up your data " - "before continuing.\n".format(e.explanation) + "before continuing.\n" ) res = yesno("Continue with the new image? [yN]", False) if res is None or not res: @@ -1098,10 +1210,11 @@ def up(rebuild): log_printer = log_printer_from_project( self.project, attached_containers, - set_no_color_if_clicolor(options['--no-color']), + options['--no-color'], {'follow': True}, cascade_stop, - event_stream=self.project.events(service_names=service_names)) + event_stream=self.project.events(service_names=service_names), + keep_prefix=keep_prefix) print("Attaching to", list_containers(log_printer.containers)) cascade_starter = log_printer.run() @@ -1119,9 +1232,10 @@ def up(rebuild): sys.exit(exit_code) @classmethod + @metrics() def version(cls, options): """ - Show version information + Show version information and quit. Usage: version [--short] @@ -1140,7 +1254,7 @@ def compute_service_exit_code(exit_value_from, attached_containers): attached_containers)) if not candidates: log.error( - 'No containers matching the spec "{0}" ' + 'No containers matching the spec "{}" ' 'were run.'.format(exit_value_from) ) return 2 @@ -1308,31 +1422,31 @@ def build_one_off_container_options(options, detach, command): def run_one_off_container(container_options, project, service, options, toplevel_options, toplevel_environment): - if not options['--no-deps']: - deps = service.get_dependency_names() - if deps: - project.up( - service_names=deps, - start_deps=True, - strategy=ConvergenceStrategy.never, - rescale=False - ) - - project.initialize() - - container = service.create_container( - quiet=True, + native_builder = toplevel_environment.get_boolean('COMPOSE_DOCKER_CLI_BUILD') + detach = options.get('--detach') + use_network_aliases = options.get('--use-aliases') + service.scale_num = 1 + containers = project.up( + service_names=[service.name], + start_deps=not options['--no-deps'], + strategy=ConvergenceStrategy.never, + detached=True, + rescale=False, + cli=native_builder, one_off=True, - **container_options) - - use_network_aliases = options['--use-aliases'] + override_options=container_options, + ) + try: + container = next(c for c in containers if c.service == service.name) + except StopIteration: + raise OperationFailedError('Could not bring up the requested service') - if options.get('--detach'): + if detach: service.start_container(container, use_network_aliases) print(container.name) return - def remove_container(force=False): + def remove_container(): if options['--rm']: project.client.remove_container(container.id, force=True, v=True) @@ -1365,7 +1479,7 @@ def remove_container(force=False): exit_code = 1 except (signals.ShutdownException, signals.HangUpException): project.client.kill(container.id) - remove_container(force=True) + remove_container() sys.exit(2) remove_container() @@ -1383,29 +1497,28 @@ def get_docker_start_call(container_options, container_id): def log_printer_from_project( - project, - containers, - monochrome, - log_args, - cascade_stop=False, - event_stream=None, + project, + containers, + monochrome, + log_args, + cascade_stop=False, + event_stream=None, + keep_prefix=True, ): return LogPrinter( - containers, - build_log_presenters(project.service_names, monochrome), + [c for c in containers if c.log_driver not in (None, 'none')], + build_log_presenters(project.service_names, monochrome, keep_prefix), event_stream or project.events(), cascade_stop=cascade_stop, log_args=log_args) def filter_attached_containers(containers, service_names, attach_dependencies=False): - if attach_dependencies or not service_names: - return containers - - return [ - container - for container in containers if container.service in service_names - ] + return filter_attached_for_up( + containers, + service_names, + attach_dependencies, + lambda container: container.service) @contextlib.contextmanager @@ -1447,6 +1560,7 @@ def call_docker(args, dockeropts, environment): key = dockeropts.get('--tlskey') verify = dockeropts.get('--tlsverify') host = dockeropts.get('--host') + context = dockeropts.get('--context') tls_options = [] if tls: tls_options.append('--tls') @@ -1462,14 +1576,15 @@ def call_docker(args, dockeropts, environment): tls_options.extend( ['--host', re.sub(r'^https?://', 'tcp://', host.lstrip('='))] ) + if context: + tls_options.extend( + ['--context', context] + ) args = [executable_path] + tls_options + args log.debug(" ".join(map(pipes.quote, args))) - filtered_env = {} - for k, v in environment.items(): - if v is not None: - filtered_env[k] = environment[k] + filtered_env = {k: v for k, v in environment.items() if v is not None} return subprocess.call(args, env=filtered_env) @@ -1579,7 +1694,3 @@ def warn_for_swarm_mode(client): "To deploy your application across the swarm, " "use `docker stack deploy`.\n" ) - - -def set_no_color_if_clicolor(no_color_flag): - return no_color_flag or os.environ.get('CLICOLOR') == "0" diff --git a/compose/cli/signals.py b/compose/cli/signals.py index 44def2ece65..0244e70189a 100644 --- a/compose/cli/signals.py +++ b/compose/cli/signals.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import signal from ..const import IS_WINDOWS_PLATFORM diff --git a/compose/cli/utils.py b/compose/cli/utils.py index 931487a6cdc..6a4615a9660 100644 --- a/compose/cli/utils.py +++ b/compose/cli/utils.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import division -from __future__ import unicode_literals - import math import os import platform @@ -9,19 +5,12 @@ import subprocess import sys +import distro import docker -import six import compose from ..const import IS_WINDOWS_PLATFORM -# WindowsError is not defined on non-win32 platforms. Avoid runtime errors by -# defining it as OSError (its parent class) if missing. -try: - WindowsError -except NameError: - WindowsError = OSError - def yesno(prompt, default=None): """ @@ -62,7 +51,7 @@ def call_silently(*args, **kwargs): with open(os.devnull, 'w') as shutup: try: return subprocess.call(*args, stdout=shutup, stderr=shutup, **kwargs) - except WindowsError: + except OSError: # On Windows, subprocess.call() can still raise exceptions. Normalize # to POSIXy behaviour by returning a nonzero exit code. return 1 @@ -73,7 +62,7 @@ def is_mac(): def is_ubuntu(): - return platform.system() == 'Linux' and platform.linux_distribution()[0] == 'Ubuntu' + return platform.system() == 'Linux' and distro.linux_distribution()[0] == 'Ubuntu' def is_windows(): @@ -124,7 +113,7 @@ def generate_user_agent(): try: p_system = platform.system() p_release = platform.release() - except IOError: + except OSError: pass else: parts.append("{}/{}".format(p_system, p_release)) @@ -137,14 +126,14 @@ def human_readable_file_size(size): if order >= len(suffixes): order = len(suffixes) - 1 - return '{0:.4g} {1}'.format( + return '{:.4g} {}'.format( size / pow(10, order * 3), suffixes[order] ) def binarystr_to_unicode(s): - if not isinstance(s, six.binary_type): + if not isinstance(s, bytes): return s if IS_WINDOWS_PLATFORM: diff --git a/compose/cli/verbose_proxy.py b/compose/cli/verbose_proxy.py index b1592eabe73..c9340c4e0d2 100644 --- a/compose/cli/verbose_proxy.py +++ b/compose/cli/verbose_proxy.py @@ -1,23 +1,18 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import functools import logging import pprint from itertools import chain -import six - def format_call(args, kwargs): args = (repr(a) for a in args) - kwargs = ("{0!s}={1!r}".format(*item) for item in six.iteritems(kwargs)) - return "({0})".format(", ".join(chain(args, kwargs))) + kwargs = ("{!s}={!r}".format(*item) for item in kwargs.items()) + return "({})".format(", ".join(chain(args, kwargs))) def format_return(result, max_lines): if isinstance(result, (list, tuple, set)): - return "({0} with {1} items)".format(type(result).__name__, len(result)) + return "({} with {} items)".format(type(result).__name__, len(result)) if result: lines = pprint.pformat(result).split('\n') @@ -27,7 +22,7 @@ def format_return(result, max_lines): return result -class VerboseProxy(object): +class VerboseProxy: """Proxy all function calls to another class and log method name, arguments and return values for each call. """ @@ -41,7 +36,7 @@ def __init__(self, obj_name, obj, log_name=None, max_lines=10): def __getattr__(self, name): attr = getattr(self.obj, name) - if not six.callable(attr): + if not callable(attr): return attr return functools.partial(self.proxy_callable, name) diff --git a/compose/config/__init__.py b/compose/config/__init__.py index 2b40666f149..855b2401d94 100644 --- a/compose/config/__init__.py +++ b/compose/config/__init__.py @@ -1,7 +1,4 @@ # flake8: noqa -from __future__ import absolute_import -from __future__ import unicode_literals - from . import environment from .config import ConfigurationError from .config import DOCKER_CONFIG_KEYS diff --git a/compose/config/config_schema_v3.7.json b/compose/config/compose_spec.json similarity index 59% rename from compose/config/config_schema_v3.7.json rename to compose/config/compose_spec.json index cd7882f5b24..86e3de1eed5 100644 --- a/compose/config/config_schema_v3.7.json +++ b/compose/config/compose_spec.json @@ -1,12 +1,14 @@ { - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "config_schema_v3.7.json", + "$schema": "http://json-schema.org/draft/2019-09/schema#", + "id": "compose_spec.json", "type": "object", - "required": ["version"], + "title": "Compose Specification", + "description": "The Compose file is a YAML file defining a multi-containers based application.", "properties": { "version": { - "type": "string" + "type": "string", + "description": "Version of the Compose specification used. Tools not implementing required version MUST reject the configuration file." }, "services": { @@ -85,15 +87,45 @@ "dockerfile": {"type": "string"}, "args": {"$ref": "#/definitions/list_or_dict"}, "labels": {"$ref": "#/definitions/list_or_dict"}, - "cache_from": {"$ref": "#/definitions/list_of_strings"}, + "cache_from": {"type": "array", "items": {"type": "string"}}, "network": {"type": "string"}, "target": {"type": "string"}, - "shm_size": {"type": ["integer", "string"]} + "shm_size": {"type": ["integer", "string"]}, + "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, + "isolation": {"type": "string"} }, - "additionalProperties": false + "additionalProperties": false, + "patternProperties": {"^x-": {}} } ] }, + "blkio_config": { + "type": "object", + "properties": { + "device_read_bps": { + "type": "array", + "items": {"$ref": "#/definitions/blkio_limit"} + }, + "device_read_iops": { + "type": "array", + "items": {"$ref": "#/definitions/blkio_limit"} + }, + "device_write_bps": { + "type": "array", + "items": {"$ref": "#/definitions/blkio_limit"} + }, + "device_write_iops": { + "type": "array", + "items": {"$ref": "#/definitions/blkio_limit"} + }, + "weight": {"type": "integer"}, + "weight_device": { + "type": "array", + "items": {"$ref": "#/definitions/blkio_weight"} + } + }, + "additionalProperties": false + }, "cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, "cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, "cgroup_parent": {"type": "string"}, @@ -116,19 +148,59 @@ "uid": {"type": "string"}, "gid": {"type": "string"}, "mode": {"type": "number"} - } + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} } ] } }, "container_name": {"type": "string"}, - "credential_spec": {"type": "object", "properties": { - "file": {"type": "string"}, - "registry": {"type": "string"} - }}, - "depends_on": {"$ref": "#/definitions/list_of_strings"}, + "cpu_count": {"type": "integer", "minimum": 0}, + "cpu_percent": {"type": "integer", "minimum": 0, "maximum": 100}, + "cpu_shares": {"type": ["number", "string"]}, + "cpu_quota": {"type": ["number", "string"]}, + "cpu_period": {"type": ["number", "string"]}, + "cpu_rt_period": {"type": ["number", "string"]}, + "cpu_rt_runtime": {"type": ["number", "string"]}, + "cpus": {"type": ["number", "string"]}, + "cpuset": {"type": "string"}, + "credential_spec": { + "type": "object", + "properties": { + "config": {"type": "string"}, + "file": {"type": "string"}, + "registry": {"type": "string"} + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + }, + "depends_on": { + "oneOf": [ + {"$ref": "#/definitions/list_of_strings"}, + { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "type": "object", + "additionalProperties": false, + "properties": { + "condition": { + "type": "string", + "enum": ["service_started", "service_healthy", "service_completed_successfully"] + } + }, + "required": ["condition"] + } + } + } + ] + }, + "device_cgroup_rules": {"$ref": "#/definitions/list_of_strings"}, "devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, "dns": {"$ref": "#/definitions/string_or_list"}, + "dns_opt": {"type": "array","items": {"type": "string"}, "uniqueItems": true}, "dns_search": {"$ref": "#/definitions/string_or_list"}, "domainname": {"type": "string"}, "entrypoint": { @@ -148,36 +220,59 @@ }, "uniqueItems": true }, + "extends": { + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "properties": { + "service": {"type": "string"}, + "file": {"type": "string"} + }, + "required": ["service"], + "additionalProperties": false + } + ] + }, "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, + "group_add": { + "type": "array", + "items": { + "type": ["string", "number"] + }, + "uniqueItems": true + }, "healthcheck": {"$ref": "#/definitions/healthcheck"}, "hostname": {"type": "string"}, "image": {"type": "string"}, - "init": {"type": "boolean"}, + "init": {"type": "boolean"}, "ipc": {"type": "string"}, "isolation": {"type": "string"}, "labels": {"$ref": "#/definitions/list_or_dict"}, "links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "logging": { - "type": "object", + "type": "object", - "properties": { - "driver": {"type": "string"}, - "options": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number", "null"]} - } - } - }, - "additionalProperties": false + "properties": { + "driver": {"type": "string"}, + "options": { + "type": "object", + "patternProperties": { + "^.+$": {"type": ["string", "number", "null"]} + } + } + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} }, - "mac_address": {"type": "string"}, + "mem_limit": {"type": ["number", "string"]}, + "mem_reservation": {"type": ["string", "integer"]}, + "mem_swappiness": {"type": "integer"}, + "memswap_limit": {"type": ["number", "string"]}, "network_mode": {"type": "string"}, - "networks": { "oneOf": [ {"$ref": "#/definitions/list_of_strings"}, @@ -191,9 +286,12 @@ "properties": { "aliases": {"$ref": "#/definitions/list_of_strings"}, "ipv4_address": {"type": "string"}, - "ipv6_address": {"type": "string"} + "ipv6_address": {"type": "string"}, + "link_local_ips": {"$ref": "#/definitions/list_of_strings"}, + "priority": {"type": "number"} }, - "additionalProperties": false + "additionalProperties": false, + "patternProperties": {"^x-": {}} }, {"type": "null"} ] @@ -203,8 +301,11 @@ } ] }, + "oom_kill_disable": {"type": "boolean"}, + "oom_score_adj": {"type": "integer", "minimum": -1000, "maximum": 1000}, "pid": {"type": ["string", "null"]}, - + "pids_limit": {"type": ["number", "string"]}, + "platform": {"type": "string"}, "ports": { "type": "array", "items": { @@ -219,16 +320,26 @@ "published": {"type": "integer"}, "protocol": {"type": "string"} }, - "additionalProperties": false + "additionalProperties": false, + "patternProperties": {"^x-": {}} } ] }, "uniqueItems": true }, - "privileged": {"type": "boolean"}, + "profiles": {"$ref": "#/definitions/list_of_strings"}, + "pull_policy": {"type": "string", "enum": [ + "always", "never", "if_not_present", "build" + ]}, "read_only": {"type": "boolean"}, "restart": {"type": "string"}, + "runtime": { + "type": "string" + }, + "scale": { + "type": "integer" + }, "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, "shm_size": {"type": ["number", "string"]}, "secrets": { @@ -244,7 +355,9 @@ "uid": {"type": "string"}, "gid": {"type": "string"}, "mode": {"type": "number"} - } + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} } ] } @@ -253,6 +366,7 @@ "stdin_open": {"type": "boolean"}, "stop_grace_period": {"type": "string", "format": "duration"}, "stop_signal": {"type": "string"}, + "storage_opt": {"type": "object"}, "tmpfs": {"$ref": "#/definitions/string_or_list"}, "tty": {"type": "boolean"}, "ulimits": { @@ -262,13 +376,14 @@ "oneOf": [ {"type": "integer"}, { - "type":"object", + "type": "object", "properties": { "hard": {"type": "integer"}, "soft": {"type": "integer"} }, "required": ["soft", "hard"], - "additionalProperties": false + "additionalProperties": false, + "patternProperties": {"^x-": {}} } ] } @@ -294,13 +409,17 @@ "type": "object", "properties": { "propagation": {"type": "string"} - } + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} }, "volume": { "type": "object", "properties": { "nocopy": {"type": "boolean"} - } + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} }, "tmpfs": { "type": "object", @@ -309,14 +428,22 @@ "type": "integer", "minimum": 0 } - } + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} } }, - "additionalProperties": false + "additionalProperties": false, + "patternProperties": {"^x-": {}} } - ], - "uniqueItems": true - } + ] + }, + "uniqueItems": true + }, + "volumes_from": { + "type": "array", + "items": {"type": "string"}, + "uniqueItems": true }, "working_dir": {"type": "string"} }, @@ -327,7 +454,6 @@ "healthcheck": { "id": "#/definitions/healthcheck", "type": "object", - "additionalProperties": false, "properties": { "disable": {"type": "boolean"}, "interval": {"type": "string", "format": "duration"}, @@ -340,7 +466,9 @@ }, "timeout": {"type": "string", "format": "duration"}, "start_period": {"type": "string", "format": "duration"} - } + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} }, "deployment": { "id": "#/definitions/deployment", @@ -362,7 +490,8 @@ "start-first", "stop-first" ]} }, - "additionalProperties": false + "additionalProperties": false, + "patternProperties": {"^x-": {}} }, "update_config": { "type": "object", @@ -376,7 +505,8 @@ "start-first", "stop-first" ]} }, - "additionalProperties": false + "additionalProperties": false, + "patternProperties": {"^x-": {}} }, "resources": { "type": "object", @@ -384,22 +514,26 @@ "limits": { "type": "object", "properties": { - "cpus": {"type": "string"}, + "cpus": {"type": ["number", "string"]}, "memory": {"type": "string"} }, - "additionalProperties": false + "additionalProperties": false, + "patternProperties": {"^x-": {}} }, "reservations": { "type": "object", "properties": { - "cpus": {"type": "string"}, + "cpus": {"type": ["number", "string"]}, "memory": {"type": "string"}, - "generic_resources": {"$ref": "#/definitions/generic_resources"} + "generic_resources": {"$ref": "#/definitions/generic_resources"}, + "devices": {"$ref": "#/definitions/devices"} }, - "additionalProperties": false + "additionalProperties": false, + "patternProperties": {"^x-": {}} } }, - "additionalProperties": false + "additionalProperties": false, + "patternProperties": {"^x-": {}} }, "restart_policy": { "type": "object", @@ -409,7 +543,8 @@ "max_attempts": {"type": "integer"}, "window": {"type": "string", "format": "duration"} }, - "additionalProperties": false + "additionalProperties": false, + "patternProperties": {"^x-": {}} }, "placement": { "type": "object", @@ -422,14 +557,18 @@ "properties": { "spread": {"type": "string"} }, - "additionalProperties": false + "additionalProperties": false, + "patternProperties": {"^x-": {}} } - } + }, + "max_replicas_per_node": {"type": "integer"} }, - "additionalProperties": false + "additionalProperties": false, + "patternProperties": {"^x-": {}} } }, - "additionalProperties": false + "additionalProperties": false, + "patternProperties": {"^x-": {}} }, "generic_resources": { @@ -444,10 +583,29 @@ "kind": {"type": "string"}, "value": {"type": "number"} }, - "additionalProperties": false + "additionalProperties": false, + "patternProperties": {"^x-": {}} } }, - "additionalProperties": false + "additionalProperties": false, + "patternProperties": {"^x-": {}} + } + }, + + "devices": { + "id": "#/definitions/devices", + "type": "array", + "items": { + "type": "object", + "properties": { + "capabilities": {"$ref": "#/definitions/list_of_strings"}, + "count": {"type": ["string", "integer"]}, + "device_ids": {"$ref": "#/definitions/list_of_strings"}, + "driver":{"type": "string"}, + "options":{"$ref": "#/definitions/list_or_dict"} + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} } }, @@ -472,27 +630,46 @@ "items": { "type": "object", "properties": { - "subnet": {"type": "string"} + "subnet": {"type": "string", "format": "subnet_ip_address"}, + "ip_range": {"type": "string"}, + "gateway": {"type": "string"}, + "aux_addresses": { + "type": "object", + "additionalProperties": false, + "patternProperties": {"^.+$": {"type": "string"}} + } }, - "additionalProperties": false + "additionalProperties": false, + "patternProperties": {"^x-": {}} } + }, + "options": { + "type": "object", + "additionalProperties": false, + "patternProperties": {"^.+$": {"type": "string"}} } }, - "additionalProperties": false + "additionalProperties": false, + "patternProperties": {"^x-": {}} }, "external": { "type": ["boolean", "object"], "properties": { - "name": {"type": "string"} + "name": { + "deprecated": true, + "type": "string" + } }, - "additionalProperties": false + "additionalProperties": false, + "patternProperties": {"^x-": {}} }, "internal": {"type": "boolean"}, + "enable_ipv6": {"type": "boolean"}, "attachable": {"type": "boolean"}, "labels": {"$ref": "#/definitions/list_or_dict"} }, - "patternProperties": {"^x-": {}}, - "additionalProperties": false + "additionalProperties": false, + "patternProperties": {"^x-": {}} }, "volume": { @@ -510,14 +687,18 @@ "external": { "type": ["boolean", "object"], "properties": { - "name": {"type": "string"} + "name": { + "deprecated": true, + "type": "string" + } }, - "additionalProperties": false + "additionalProperties": false, + "patternProperties": {"^x-": {}} }, "labels": {"$ref": "#/definitions/list_or_dict"} }, - "patternProperties": {"^x-": {}}, - "additionalProperties": false + "additionalProperties": false, + "patternProperties": {"^x-": {}} }, "secret": { @@ -532,10 +713,18 @@ "name": {"type": "string"} } }, - "labels": {"$ref": "#/definitions/list_or_dict"} + "labels": {"$ref": "#/definitions/list_or_dict"}, + "driver": {"type": "string"}, + "driver_opts": { + "type": "object", + "patternProperties": { + "^.+$": {"type": ["string", "number"]} + } + }, + "template_driver": {"type": "string"} }, - "patternProperties": {"^x-": {}}, - "additionalProperties": false + "additionalProperties": false, + "patternProperties": {"^x-": {}} }, "config": { @@ -547,13 +736,17 @@ "external": { "type": ["boolean", "object"], "properties": { - "name": {"type": "string"} + "name": { + "deprecated": true, + "type": "string" + } } }, - "labels": {"$ref": "#/definitions/list_or_dict"} + "labels": {"$ref": "#/definitions/list_or_dict"}, + "template_driver": {"type": "string"} }, - "patternProperties": {"^x-": {}}, - "additionalProperties": false + "additionalProperties": false, + "patternProperties": {"^x-": {}} }, "string_or_list": { @@ -584,6 +777,23 @@ ] }, + "blkio_limit": { + "type": "object", + "properties": { + "path": {"type": "string"}, + "rate": {"type": ["integer", "string"]} + }, + "additionalProperties": false + }, + "blkio_weight": { + "type": "object", + "properties": { + "path": {"type": "string"}, + "weight": {"type": "integer"} + }, + "additionalProperties": false + }, + "constraints": { "service": { "id": "#/definitions/constraints/service", diff --git a/compose/config/config.py b/compose/config/config.py index 84933e9c9de..c2f5e95a1fb 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -1,27 +1,24 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import functools -import io import logging import os import re import string import sys from collections import namedtuple +from itertools import chain from operator import attrgetter +from operator import itemgetter -import six import yaml -from cached_property import cached_property + +try: + from functools import cached_property +except ImportError: + from cached_property import cached_property from . import types -from .. import const +from ..const import COMPOSE_SPEC as VERSION from ..const import COMPOSEFILE_V1 as V1 -from ..const import COMPOSEFILE_V2_1 as V2_1 -from ..const import COMPOSEFILE_V2_3 as V2_3 -from ..const import COMPOSEFILE_V3_0 as V3_0 -from ..const import COMPOSEFILE_V3_4 as V3_4 from ..utils import build_string_dict from ..utils import json_hash from ..utils import parse_bytes @@ -56,6 +53,7 @@ from .validation import validate_depends_on from .validation import validate_extends_file_path from .validation import validate_healthcheck +from .validation import validate_ipc_mode from .validation import validate_links from .validation import validate_network_mode from .validation import validate_pid_mode @@ -139,6 +137,7 @@ 'logging', 'network_mode', 'platform', + 'profiles', 'scale', 'stop_grace_period', ] @@ -154,9 +153,14 @@ SUPPORTED_FILENAMES = [ 'docker-compose.yml', 'docker-compose.yaml', + 'compose.yml', + 'compose.yaml', ] -DEFAULT_OVERRIDE_FILENAMES = ('docker-compose.override.yml', 'docker-compose.override.yaml') +DEFAULT_OVERRIDE_FILENAMES = ('docker-compose.override.yml', + 'docker-compose.override.yaml', + 'compose.override.yml', + 'compose.override.yaml') log = logging.getLogger(__name__) @@ -174,7 +178,7 @@ class ConfigDetails(namedtuple('_ConfigDetails', 'working_dir config_files envir def __new__(cls, working_dir, config_files, environment=None): if environment is None: environment = Environment.from_env_file(working_dir) - return super(ConfigDetails, cls).__new__( + return super().__new__( cls, working_dir, config_files, environment ) @@ -192,48 +196,65 @@ def from_filename(cls, filename): return cls(filename, load_yaml(filename)) @cached_property - def version(self): - if 'version' not in self.config: + def config_version(self): + version = self.config.get('version', None) + if isinstance(version, dict): return V1 + return ComposeVersion(version) if version else self.version - version = self.config['version'] + @cached_property + def version(self): + version = self.config.get('version', None) + if not version: + # no version is specified in the config file + services = self.config.get('services', None) + networks = self.config.get('networks', None) + volumes = self.config.get('volumes', None) + if services or networks or volumes: + # validate V2/V3 structure + for section in ['services', 'networks', 'volumes']: + validate_config_section( + self.filename, self.config.get(section, {}), section) + return VERSION + + # validate V1 structure + validate_config_section( + self.filename, self.config, 'services') + return V1 if isinstance(version, dict): 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)) + 'Compose file version {}.'.format(self.filename, V1)) return V1 - if not isinstance(version, six.string_types): + if not isinstance(version, str): raise ConfigurationError( 'Version in "{}" is invalid - it should be a string.' .format(self.filename)) - if version == '1': + if isinstance(version, str): + version_pattern = re.compile(r"^[1-3]+(\.\d+)?$") + if not version_pattern.match(version): + raise ConfigurationError( + 'Version "{}" in "{}" is invalid.' + .format(version, self.filename)) + + if version.startswith("1"): raise ConfigurationError( 'Version in "{}" is invalid. {}' .format(self.filename, VERSION_EXPLANATION) ) - version_pattern = re.compile(r"^[2-9]+(\.\d+)?$") - if not version_pattern.match(version): - raise ConfigurationError( - 'Version "{}" in "{}" is invalid.' - .format(version, self.filename)) - - if version == '2': - return const.COMPOSEFILE_V2_0 - - if version == '3': - return const.COMPOSEFILE_V3_0 - - return ComposeVersion(version) + return VERSION def get_service(self, name): return self.get_service_dicts()[name] def get_service_dicts(self): - return self.config if self.version == V1 else self.config.get('services', {}) + if self.version == V1: + return self.config + return self.config.get('services', {}) def get_volumes(self): return {} if self.version == V1 else self.config.get('volumes', {}) @@ -242,14 +263,16 @@ def get_networks(self): return {} if self.version == V1 else self.config.get('networks', {}) def get_secrets(self): - return {} if self.version < const.COMPOSEFILE_V3_1 else self.config.get('secrets', {}) + return {} if self.version == V1 else self.config.get('secrets', {}) def get_configs(self): - return {} if self.version < const.COMPOSEFILE_V3_3 else self.config.get('configs', {}) + return {} if self.version == V1 else self.config.get('configs', {}) -class Config(namedtuple('_Config', 'version services volumes networks secrets configs')): +class Config(namedtuple('_Config', 'config_version version services volumes networks secrets configs')): """ + :param config_version: configuration file version + :type config_version: int :param version: configuration version :type version: int :param services: List of service description dictionaries @@ -290,7 +313,16 @@ def find(base_dir, filenames, environment, override_dir=None): if filenames: filenames = [os.path.join(base_dir, f) for f in filenames] else: + # search for compose files in the base dir and its parents filenames = get_default_config_files(base_dir) + if not filenames and not override_dir: + # none found in base_dir and no override_dir defined + raise ComposeFileNotFound(SUPPORTED_FILENAMES) + if not filenames: + # search for compose files in the project directory and its parents + filenames = get_default_config_files(override_dir) + if not filenames: + raise ComposeFileNotFound(SUPPORTED_FILENAMES) log.debug("Using configuration files: {}".format(",".join(filenames))) return ConfigDetails( @@ -303,13 +335,14 @@ def find(base_dir, filenames, environment, override_dir=None): def validate_config_version(config_files): main_file = config_files[0] validate_top_level_object(main_file) + for next_file in config_files[1:]: validate_top_level_object(next_file) if main_file.version != next_file.version: raise ConfigurationError( - "Version mismatch: file {0} specifies version {1} but " - "extension file {2} uses version {3}".format( + "Version mismatch: file {} specifies version {} but " + "extension file {} uses version {}".format( main_file.filename, main_file.version, next_file.filename, @@ -320,7 +353,7 @@ def get_default_config_files(base_dir): (candidates, path) = find_candidates_in_parent_dirs(SUPPORTED_FILENAMES, base_dir) if not candidates: - raise ComposeFileNotFound(SUPPORTED_FILENAMES) + return None winner = candidates[0] @@ -359,34 +392,32 @@ def find_candidates_in_parent_dirs(filenames, path): return (candidates, path) -def check_swarm_only_config(service_dicts, compatibility=False): +def check_swarm_only_config(service_dicts): warning_template = ( "Some services ({services}) use the '{key}' key, which will be ignored. " "Compose does not support '{key}' configuration - use " "`docker stack deploy` to deploy to a swarm." ) - - def check_swarm_only_key(service_dicts, key): - services = [s for s in service_dicts if s.get(key)] - if services: - log.warning( - warning_template.format( - services=", ".join(sorted(s['name'] for s in services)), - key=key - ) + key = 'configs' + services = [s for s in service_dicts if s.get(key)] + if services: + log.warning( + warning_template.format( + services=", ".join(sorted(s['name'] for s in services)), + key=key ) - if not compatibility: - check_swarm_only_key(service_dicts, 'deploy') - check_swarm_only_key(service_dicts, 'configs') + ) -def load(config_details, compatibility=False, interpolate=True): +def load(config_details, 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. Return a fully interpolated, extended and validated configuration. """ + + # validate against latest version and if fails do it against v1 schema validate_config_version(config_details.config_files) processed_files = [ @@ -408,17 +439,16 @@ def load(config_details, compatibility=False, interpolate=True): configs = load_mapping( config_details.config_files, 'get_configs', 'Config', config_details.working_dir ) - service_dicts = load_services(config_details, main_file, compatibility) + service_dicts = load_services(config_details, main_file, interpolate=interpolate) if main_file.version != V1: for service_dict in service_dicts: match_named_volumes(service_dict, volumes) - check_swarm_only_config(service_dicts, compatibility) - - version = V2_3 if compatibility and main_file.version >= V3_0 else main_file.version + check_swarm_only_config(service_dicts) - return Config(version, service_dicts, volumes, networks, secrets, configs) + return Config(main_file.config_version, main_file.version, + service_dicts, volumes, networks, secrets, configs) def load_mapping(config_files, get_func, entity_type, working_dir=None): @@ -438,29 +468,48 @@ def load_mapping(config_files, get_func, entity_type, working_dir=None): elif not config.get('name'): config['name'] = name - if 'driver_opts' in config: - config['driver_opts'] = build_string_dict( - config['driver_opts'] - ) - if 'labels' in config: config['labels'] = parse_labels(config['labels']) if 'file' in config: config['file'] = expand_path(working_dir, config['file']) + if 'driver_opts' in config: + config['driver_opts'] = build_string_dict( + config['driver_opts'] + ) + device = format_device_option(entity_type, config) + if device: + config['driver_opts']['device'] = device return mapping +def format_device_option(entity_type, config): + if entity_type != 'Volume': + return + # default driver is 'local' + driver = config.get('driver', 'local') + if driver != 'local': + return + o = config['driver_opts'].get('o') + device = config['driver_opts'].get('device') + if o and o == 'bind' and device: + fullpath = os.path.abspath(os.path.expanduser(device)) + return fullpath + + def validate_external(entity_type, name, config, version): - if (version < V2_1 or (version >= V3_0 and version < V3_4)) and len(config.keys()) > 1: - raise ConfigurationError( - "{} {} declared as external but specifies additional attributes " - "({}).".format( - entity_type, name, ', '.join(k for k in config if k != 'external'))) + for k in config.keys(): + if entity_type == 'Network' and k == 'driver': + continue + if k not in ['external', 'name']: + raise ConfigurationError( + "{} {} declared as external but specifies additional attributes " + "({}).".format( + entity_type, name, ', '.join(k for k in config if k != 'external'))) -def load_services(config_details, config_file, compatibility=False): +def load_services(config_details, config_file, interpolate=True): def build_service(service_name, service_dict, service_names): service_config = ServiceConfig.with_abs_paths( config_details.working_dir, @@ -479,7 +528,7 @@ def build_service(service_name, service_dict, service_names): service_names, config_file.version, config_details.environment, - compatibility + interpolate ) return service_dict @@ -504,9 +553,7 @@ def merge_services(base, override): file.get_service_dicts() for file in config_details.config_files ] - service_config = service_configs[0] - for next_config in service_configs[1:]: - service_config = merge_services(service_config, next_config) + service_config = functools.reduce(merge_services, service_configs) return build_services(service_config) @@ -527,8 +574,7 @@ def process_config_section(config_file, config, section, environment, interpolat config_file.version, config, section, - environment - ) + environment) else: return config @@ -559,27 +605,25 @@ def process_config_file(config_file, environment, service_name=None, interpolate environment, interpolate, ) - if config_file.version >= const.COMPOSEFILE_V3_1: - processed_config['secrets'] = process_config_section( - config_file, - config_file.get_secrets(), - 'secret', - environment, - interpolate, - ) - if config_file.version >= const.COMPOSEFILE_V3_3: - processed_config['configs'] = process_config_section( - config_file, - config_file.get_configs(), - 'config', - environment, - interpolate, - ) + processed_config['secrets'] = process_config_section( + config_file, + config_file.get_secrets(), + 'secret', + environment, + interpolate, + ) + processed_config['configs'] = process_config_section( + config_file, + config_file.get_configs(), + 'config', + environment, + interpolate, + ) else: processed_config = services config_file = config_file._replace(config=processed_config) - validate_against_config_schema(config_file) + validate_against_config_schema(config_file, config_file.version) if service_name and service_name not in services: raise ConfigurationError( @@ -589,7 +633,7 @@ def process_config_file(config_file, environment, service_name=None, interpolate return config_file -class ServiceExtendsResolver(object): +class ServiceExtendsResolver: def __init__(self, service_config, config_file, environment, already_seen=None): self.service_config = service_config self.working_dir = service_config.working_dir @@ -679,25 +723,25 @@ def get_extended_config_path(self, extends_options): return filename -def resolve_environment(service_dict, environment=None): +def resolve_environment(service_dict, environment=None, interpolate=True): """Unpack any environment variables from an env_file, if set. Interpolate environment values if set. """ env = {} for env_file in service_dict.get('env_file', []): - env.update(env_vars_from_file(env_file)) + env.update(env_vars_from_file(env_file, interpolate)) env.update(parse_environment(service_dict.get('environment'))) - return dict(resolve_env_var(k, v, environment) for k, v in six.iteritems(env)) + return dict(resolve_env_var(k, v, environment) for k, v in env.items()) def resolve_build_args(buildargs, environment): args = parse_build_arguments(buildargs) - return dict(resolve_env_var(k, v, environment) for k, v in six.iteritems(args)) + return dict(resolve_env_var(k, v, environment) for k, v in args.items()) def validate_extended_service_dict(service_dict, filename, service): - error_prefix = "Cannot extend service '%s' in %s:" % (service, filename) + error_prefix = "Cannot extend service '{}' in {}:".format(service, filename) if 'links' in service_dict: raise ConfigurationError( @@ -723,12 +767,26 @@ def validate_extended_service_dict(service_dict, filename, service): def validate_service(service_config, service_names, config_file): + def build_image(): + args = sys.argv[1:] + if 'pull' in args: + return False + + if '--no-build' in args: + return False + + return True + service_dict, service_name = service_config.config, service_config.name validate_service_constraints(service_dict, service_name, config_file) - validate_paths(service_dict) + + if build_image(): + # We only care about valid paths when actually building images + validate_paths(service_dict) validate_cpu(service_config) validate_ulimits(service_config) + validate_ipc_mode(service_config, service_names) validate_network_mode(service_config, service_names) validate_pid_mode(service_config, service_names) validate_depends_on(service_config, service_names) @@ -780,7 +838,7 @@ def process_service(service_config): def process_build_section(service_dict, working_dir): - if isinstance(service_dict['build'], six.string_types): + if isinstance(service_dict['build'], str): service_dict['build'] = resolve_build_path(working_dir, service_dict['build']) elif isinstance(service_dict['build'], dict): if 'context' in service_dict['build']: @@ -806,9 +864,9 @@ def process_ports(service_dict): def process_depends_on(service_dict): if 'depends_on' in service_dict and not isinstance(service_dict['depends_on'], dict): - service_dict['depends_on'] = dict([ - (svc, {'condition': 'service_started'}) for svc in service_dict['depends_on'] - ]) + service_dict['depends_on'] = { + svc: {'condition': 'service_started'} for svc in service_dict['depends_on'] + } return service_dict @@ -848,7 +906,7 @@ def process_healthcheck(service_dict): hc['test'] = ['NONE'] for field in ['interval', 'timeout', 'start_period']: - if field not in hc or isinstance(hc[field], six.integer_types): + if field not in hc or isinstance(hc[field], int): continue hc[field] = parse_nanoseconds_int(hc[field]) @@ -881,11 +939,12 @@ def finalize_service_volumes(service_dict, environment): return service_dict -def finalize_service(service_config, service_names, version, environment, compatibility): +def finalize_service(service_config, service_names, version, environment, + interpolate=True): service_dict = dict(service_config.config) if 'environment' in service_dict or 'env_file' in service_dict: - service_dict['environment'] = resolve_environment(service_dict, environment) + service_dict['environment'] = resolve_environment(service_dict, environment, interpolate) service_dict.pop('env_file', None) if 'volumes_from' in service_dict: @@ -922,101 +981,10 @@ def finalize_service(service_config, service_names, version, environment, compat normalize_build(service_dict, service_config.working_dir, environment) - if compatibility: - service_dict = translate_credential_spec_to_security_opt(service_dict) - service_dict, ignored_keys = translate_deploy_keys_to_container_config( - service_dict - ) - if ignored_keys: - log.warning( - 'The following deploy sub-keys are not supported in compatibility mode and have' - ' been ignored: {}'.format(', '.join(ignored_keys)) - ) - service_dict['name'] = service_config.name return normalize_v1_service_format(service_dict) -def translate_resource_keys_to_container_config(resources_dict, service_dict): - if 'limits' in resources_dict: - service_dict['mem_limit'] = resources_dict['limits'].get('memory') - if 'cpus' in resources_dict['limits']: - service_dict['cpus'] = float(resources_dict['limits']['cpus']) - if 'reservations' in resources_dict: - service_dict['mem_reservation'] = resources_dict['reservations'].get('memory') - if 'cpus' in resources_dict['reservations']: - return ['resources.reservations.cpus'] - return [] - - -def convert_restart_policy(name): - try: - return { - 'any': 'always', - 'none': 'no', - 'on-failure': 'on-failure' - }[name] - except KeyError: - raise ConfigurationError('Invalid restart policy "{}"'.format(name)) - - -def convert_credential_spec_to_security_opt(credential_spec): - if 'file' in credential_spec: - return 'file://{file}'.format(file=credential_spec['file']) - return 'registry://{registry}'.format(registry=credential_spec['registry']) - - -def translate_credential_spec_to_security_opt(service_dict): - result = [] - - if 'credential_spec' in service_dict: - spec = convert_credential_spec_to_security_opt(service_dict['credential_spec']) - result.append('credentialspec={spec}'.format(spec=spec)) - - if result: - service_dict['security_opt'] = result - - return service_dict - - -def translate_deploy_keys_to_container_config(service_dict): - if 'credential_spec' in service_dict: - del service_dict['credential_spec'] - if 'configs' in service_dict: - del service_dict['configs'] - - if 'deploy' not in service_dict: - return service_dict, [] - - deploy_dict = service_dict['deploy'] - ignored_keys = [ - k for k in ['endpoint_mode', 'labels', 'update_config', 'rollback_config', 'placement'] - if k in deploy_dict - ] - - if 'replicas' in deploy_dict and deploy_dict.get('mode', 'replicated') == 'replicated': - service_dict['scale'] = deploy_dict['replicas'] - - if 'restart_policy' in deploy_dict: - service_dict['restart'] = { - 'Name': convert_restart_policy(deploy_dict['restart_policy'].get('condition', 'any')), - 'MaximumRetryCount': deploy_dict['restart_policy'].get('max_attempts', 0) - } - for k in deploy_dict['restart_policy'].keys(): - if k != 'condition' and k != 'max_attempts': - ignored_keys.append('restart_policy.{}'.format(k)) - - ignored_keys.extend( - translate_resource_keys_to_container_config( - deploy_dict.get('resources', {}), service_dict - ) - ) - - del service_dict['deploy'] - - return service_dict, ignored_keys - - def normalize_v1_service_format(service_dict): if 'log_driver' in service_dict or 'log_opt' in service_dict: if 'logging' not in service_dict: @@ -1116,7 +1084,7 @@ def merge_service_dicts(base, override, version): for field in [ 'cap_add', 'cap_drop', 'expose', 'external_links', - 'volumes_from', 'device_cgroup_rules', + 'volumes_from', 'device_cgroup_rules', 'profiles', ]: md.merge_field(field, merge_unique_items_lists, default=[]) @@ -1141,9 +1109,9 @@ def merge_service_dicts(base, override, version): def merge_unique_items_lists(base, override): - override = [str(o) for o in override] - base = [str(b) for b in base] - return sorted(set().union(base, override)) + override = (str(o) for o in override) + base = (str(b) for b in base) + return sorted(set(chain(base, override))) def merge_healthchecks(base, override): @@ -1156,9 +1124,7 @@ def merge_healthchecks(base, override): def merge_ports(md, base, override): def parse_sequence_func(seq): - acc = [] - for item in seq: - acc.extend(ServicePort.parse(item)) + acc = [s for item in seq for s in ServicePort.parse(item)] return to_mapping(acc, 'merge_field') field = 'ports' @@ -1168,13 +1134,13 @@ def parse_sequence_func(seq): merged = parse_sequence_func(md.base.get(field, [])) merged.update(parse_sequence_func(md.override.get(field, []))) - md[field] = [item for item in sorted(merged.values(), key=lambda x: x.target)] + md[field] = [item for item in sorted(merged.values(), key=attrgetter("target"))] def merge_build(output, base, override): def to_dict(service): build_config = service.get('build', {}) - if isinstance(build_config, six.string_types): + if isinstance(build_config, str): return {'context': build_config} return build_config @@ -1208,6 +1174,7 @@ def merge_deploy(base, override): md['resources'] = dict(resources_md) if md.needs_merge('placement'): placement_md = MergeDict(md.base.get('placement') or {}, md.override.get('placement') or {}) + placement_md.merge_scalar('max_replicas_per_node') placement_md.merge_field('constraints', merge_unique_items_lists, default=[]) placement_md.merge_field('preferences', merge_unique_objects_lists, default=[]) md['placement'] = dict(placement_md) @@ -1236,12 +1203,13 @@ def merge_reservations(base, override): md.merge_scalar('cpus') md.merge_scalar('memory') md.merge_sequence('generic_resources', types.GenericResource.parse) + md.merge_field('devices', merge_unique_objects_lists, default=[]) return dict(md) def merge_unique_objects_lists(base, override): - result = dict((json_hash(i), i) for i in base + override) - return [i[1] for i in sorted([(k, v) for k, v in result.items()], key=lambda x: x[0])] + result = {json_hash(i): i for i in base + override} + return [i[1] for i in sorted(((k, v) for k, v in result.items()), key=itemgetter(0))] def merge_blkio_config(base, override): @@ -1249,11 +1217,11 @@ def merge_blkio_config(base, override): md.merge_scalar('weight') def merge_blkio_limits(base, override): - index = dict((b['path'], b) for b in base) - for o in override: - index[o['path']] = o + get_path = itemgetter('path') + index = {get_path(b): b for b in base} + index.update((get_path(o), o) for o in override) - return sorted(list(index.values()), key=lambda x: x['path']) + return sorted(index.values(), key=get_path) for field in [ "device_read_bps", "device_read_iops", "device_write_bps", @@ -1374,7 +1342,7 @@ def resolve_volume_path(working_dir, volume): if host_path.startswith('.'): host_path = expand_path(working_dir, host_path) host_path = os.path.expanduser(host_path) - return u"{}:{}{}".format(host_path, container_path, (':' + mode if mode else '')) + return "{}:{}{}".format(host_path, container_path, (':' + mode if mode else '')) return container_path @@ -1384,7 +1352,7 @@ def normalize_build(service_dict, working_dir, environment): if 'build' in service_dict: build = {} # Shortcut where specifying a string is treated as the build context - if isinstance(service_dict['build'], six.string_types): + if isinstance(service_dict['build'], str): build['context'] = service_dict.pop('build') else: build.update(service_dict['build']) @@ -1410,7 +1378,7 @@ def validate_paths(service_dict): if 'build' in service_dict: build = service_dict.get('build', {}) - if isinstance(build, six.string_types): + if isinstance(build, str): build_path = build elif isinstance(build, dict) and 'context' in build: build_path = build['context'] @@ -1501,7 +1469,7 @@ def merge_list_or_string(base, override): def to_list(value): if value is None: return [] - elif isinstance(value, six.string_types): + elif isinstance(value, str): return [value] else: return value @@ -1517,13 +1485,13 @@ def has_uppercase(name): def load_yaml(filename, encoding=None, binary=True): try: - with io.open(filename, 'rb' if binary else 'r', encoding=encoding) as fh: + with open(filename, 'rb' if binary else 'r', encoding=encoding) as fh: return yaml.safe_load(fh) - except (IOError, yaml.YAMLError, UnicodeDecodeError) as e: + except (OSError, yaml.YAMLError, UnicodeDecodeError) as e: if encoding is None: # Sometimes the user's locale sets an encoding that doesn't match # the YAML files. Im such cases, retry once with the "default" # UTF-8 encoding return load_yaml(filename, encoding='utf-8-sig', binary=False) error_name = getattr(e, '__module__', '') + '.' + e.__class__.__name__ - raise ConfigurationError(u"{}: {}".format(error_name, e)) + raise ConfigurationError("{}: {}".format(error_name, e)) diff --git a/compose/config/config_schema_v2.0.json b/compose/config/config_schema_v2.0.json deleted file mode 100644 index 419f2e28c9d..00000000000 --- a/compose/config/config_schema_v2.0.json +++ /dev/null @@ -1,424 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "config_schema_v2.0.json", - "type": "object", - - "properties": { - "version": { - "type": "string" - }, - - "services": { - "id": "#/properties/services", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/service" - } - }, - "additionalProperties": false - }, - - "networks": { - "id": "#/properties/networks", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/network" - } - } - }, - - "volumes": { - "id": "#/properties/volumes", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/volume" - } - }, - "additionalProperties": false - } - }, - - "patternProperties": {"^x-": {}}, - "additionalProperties": false, - - "definitions": { - - "service": { - "id": "#/definitions/service", - "type": "object", - - "properties": { - "blkio_config": { - "type": "object", - "properties": { - "device_read_bps": { - "type": "array", - "items": {"$ref": "#/definitions/blkio_limit"} - }, - "device_read_iops": { - "type": "array", - "items": {"$ref": "#/definitions/blkio_limit"} - }, - "device_write_bps": { - "type": "array", - "items": {"$ref": "#/definitions/blkio_limit"} - }, - "device_write_iops": { - "type": "array", - "items": {"$ref": "#/definitions/blkio_limit"} - }, - "weight": {"type": "integer"}, - "weight_device": { - "type": "array", - "items": {"$ref": "#/definitions/blkio_weight"} - } - }, - "additionalProperties": false - }, - "build": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "properties": { - "context": {"type": "string"}, - "dockerfile": {"type": "string"}, - "args": {"$ref": "#/definitions/list_or_dict"} - }, - "additionalProperties": false - } - ] - }, - "cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "cgroup_parent": {"type": "string"}, - "command": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "container_name": {"type": "string"}, - "cpu_shares": {"type": ["number", "string"]}, - "cpu_quota": {"type": ["number", "string"]}, - "cpuset": {"type": "string"}, - "depends_on": {"$ref": "#/definitions/list_of_strings"}, - "devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "dns": {"$ref": "#/definitions/string_or_list"}, - "dns_opt": { - "type": "array", - "items": { - "type": "string" - }, - "uniqueItems": true - }, - "dns_search": {"$ref": "#/definitions/string_or_list"}, - "domainname": {"type": "string"}, - "entrypoint": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "env_file": {"$ref": "#/definitions/string_or_list"}, - "environment": {"$ref": "#/definitions/list_or_dict"}, - - "expose": { - "type": "array", - "items": { - "type": ["string", "number"], - "format": "expose" - }, - "uniqueItems": true - }, - - "extends": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "object", - - "properties": { - "service": {"type": "string"}, - "file": {"type": "string"} - }, - "required": ["service"], - "additionalProperties": false - } - ] - }, - - "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, - "hostname": {"type": "string"}, - "image": {"type": "string"}, - "ipc": {"type": "string"}, - "labels": {"$ref": "#/definitions/labels"}, - "links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - - "logging": { - "type": "object", - - "properties": { - "driver": {"type": "string"}, - "options": {"type": "object"} - }, - "additionalProperties": false - }, - - "mac_address": {"type": "string"}, - "mem_limit": {"type": ["number", "string"]}, - "mem_reservation": {"type": ["string", "integer"]}, - "mem_swappiness": {"type": "integer"}, - "memswap_limit": {"type": ["number", "string"]}, - "network_mode": {"type": "string"}, - - "networks": { - "oneOf": [ - {"$ref": "#/definitions/list_of_strings"}, - { - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "oneOf": [ - { - "type": "object", - "properties": { - "aliases": {"$ref": "#/definitions/list_of_strings"}, - "ipv4_address": {"type": "string"}, - "ipv6_address": {"type": "string"}, - "priority": {"type": "number"} - }, - "additionalProperties": false - }, - {"type": "null"} - ] - } - }, - "additionalProperties": false - } - ] - }, - "oom_score_adj": {"type": "integer", "minimum": -1000, "maximum": 1000}, - "group_add": { - "type": "array", - "items": { - "type": ["string", "number"] - }, - "uniqueItems": true - }, - "pid": {"type": ["string", "null"]}, - - "ports": { - "type": "array", - "items": { - "type": ["string", "number"], - "format": "ports" - }, - "uniqueItems": true - }, - - "privileged": {"type": "boolean"}, - "read_only": {"type": "boolean"}, - "restart": {"type": "string"}, - "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "shm_size": {"type": ["number", "string"]}, - "stdin_open": {"type": "boolean"}, - "stop_grace_period": {"type": "string", "format": "duration"}, - "stop_signal": {"type": "string"}, - "tmpfs": {"$ref": "#/definitions/string_or_list"}, - "tty": {"type": "boolean"}, - "ulimits": { - "type": "object", - "patternProperties": { - "^[a-z]+$": { - "oneOf": [ - {"type": "integer"}, - { - "type":"object", - "properties": { - "hard": {"type": "integer"}, - "soft": {"type": "integer"} - }, - "required": ["soft", "hard"], - "additionalProperties": false - } - ] - } - } - }, - "user": {"type": "string"}, - "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "volume_driver": {"type": "string"}, - "volumes_from": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "working_dir": {"type": "string"} - }, - - "dependencies": { - "memswap_limit": ["mem_limit"] - }, - "additionalProperties": false - }, - - "network": { - "id": "#/definitions/network", - "type": "object", - "properties": { - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "ipam": { - "type": "object", - "properties": { - "driver": {"type": "string"}, - "config": { - "type": "array", - "items": {"$ref": "#/definitions/ipam_config"} - }, - "options": { - "type": "object", - "patternProperties": { - "^.+$": {"type": "string"} - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - }, - "internal": {"type": "boolean"} - }, - "additionalProperties": false - }, - - "ipam_config": { - "id": "#/definitions/ipam_config", - "type": "object", - "properties": { - "subnet": {"type": "string"}, - "ip_range": {"type": "string"}, - "gateway": {"type": "string"}, - "aux_addresses": { - "type": "object", - "patternProperties": { - "^.+$": {"type": "string"} - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - - "volume": { - "id": "#/definitions/volume", - "type": ["object", "null"], - "properties": { - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - - "string_or_list": { - "oneOf": [ - {"type": "string"}, - {"$ref": "#/definitions/list_of_strings"} - ] - }, - - "list_of_strings": { - "type": "array", - "items": {"type": "string"}, - "uniqueItems": true - }, - - "list_or_dict": { - "oneOf": [ - { - "type": "object", - "patternProperties": { - ".+": { - "type": ["string", "number", "null"] - } - }, - "additionalProperties": false - }, - {"type": "array", "items": {"type": "string"}, "uniqueItems": true} - ] - }, - - "labels": { - "oneOf": [ - { - "type": "object", - "patternProperties": { - ".+": { - "type": "string" - } - }, - "additionalProperties": false - }, - {"type": "array", "items": {"type": "string"}, "uniqueItems": true} - ] - }, - - "blkio_limit": { - "type": "object", - "properties": { - "path": {"type": "string"}, - "rate": {"type": ["integer", "string"]} - }, - "additionalProperties": false - }, - "blkio_weight": { - "type": "object", - "properties": { - "path": {"type": "string"}, - "weight": {"type": "integer"} - }, - "additionalProperties": false - }, - - "constraints": { - "service": { - "id": "#/definitions/constraints/service", - "anyOf": [ - {"required": ["build"]}, - {"required": ["image"]} - ], - "properties": { - "build": { - "required": ["context"] - } - } - } - } - } -} diff --git a/compose/config/config_schema_v2.1.json b/compose/config/config_schema_v2.1.json deleted file mode 100644 index 3cb1ee21316..00000000000 --- a/compose/config/config_schema_v2.1.json +++ /dev/null @@ -1,480 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "config_schema_v2.1.json", - "type": "object", - - "properties": { - "version": { - "type": "string" - }, - - "services": { - "id": "#/properties/services", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/service" - } - }, - "additionalProperties": false - }, - - "networks": { - "id": "#/properties/networks", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/network" - } - } - }, - - "volumes": { - "id": "#/properties/volumes", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/volume" - } - }, - "additionalProperties": false - } - }, - - "patternProperties": {"^x-": {}}, - "additionalProperties": false, - - "definitions": { - - "service": { - "id": "#/definitions/service", - "type": "object", - - "properties": { - "blkio_config": { - "type": "object", - "properties": { - "device_read_bps": { - "type": "array", - "items": {"$ref": "#/definitions/blkio_limit"} - }, - "device_read_iops": { - "type": "array", - "items": {"$ref": "#/definitions/blkio_limit"} - }, - "device_write_bps": { - "type": "array", - "items": {"$ref": "#/definitions/blkio_limit"} - }, - "device_write_iops": { - "type": "array", - "items": {"$ref": "#/definitions/blkio_limit"} - }, - "weight": {"type": "integer"}, - "weight_device": { - "type": "array", - "items": {"$ref": "#/definitions/blkio_weight"} - } - }, - "additionalProperties": false - }, - - "build": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "properties": { - "context": {"type": "string"}, - "dockerfile": {"type": "string"}, - "args": {"$ref": "#/definitions/list_or_dict"}, - "labels": {"$ref": "#/definitions/labels"}, - "isolation": {"type": "string"} - }, - "additionalProperties": false - } - ] - }, - "cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "cgroup_parent": {"type": "string"}, - "command": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "container_name": {"type": "string"}, - "cpu_shares": {"type": ["number", "string"]}, - "cpu_quota": {"type": ["number", "string"]}, - "cpu_period": {"type": ["number", "string"]}, - "cpuset": {"type": "string"}, - "depends_on": { - "oneOf": [ - {"$ref": "#/definitions/list_of_strings"}, - { - "type": "object", - "additionalProperties": false, - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "type": "object", - "additionalProperties": false, - "properties": { - "condition": { - "type": "string", - "enum": ["service_started", "service_healthy"] - } - }, - "required": ["condition"] - } - } - } - ] - }, - "devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "dns_opt": { - "type": "array", - "items": { - "type": "string" - }, - "uniqueItems": true - }, - "dns": {"$ref": "#/definitions/string_or_list"}, - "dns_search": {"$ref": "#/definitions/string_or_list"}, - "domainname": {"type": "string"}, - "entrypoint": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "env_file": {"$ref": "#/definitions/string_or_list"}, - "environment": {"$ref": "#/definitions/list_or_dict"}, - - "expose": { - "type": "array", - "items": { - "type": ["string", "number"], - "format": "expose" - }, - "uniqueItems": true - }, - - "extends": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "object", - - "properties": { - "service": {"type": "string"}, - "file": {"type": "string"} - }, - "required": ["service"], - "additionalProperties": false - } - ] - }, - - "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, - "healthcheck": {"$ref": "#/definitions/healthcheck"}, - "hostname": {"type": "string"}, - "image": {"type": "string"}, - "ipc": {"type": "string"}, - "isolation": {"type": "string"}, - "labels": {"$ref": "#/definitions/labels"}, - "links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - - "logging": { - "type": "object", - - "properties": { - "driver": {"type": "string"}, - "options": {"type": "object"} - }, - "additionalProperties": false - }, - - "mac_address": {"type": "string"}, - "mem_limit": {"type": ["number", "string"]}, - "mem_reservation": {"type": ["string", "integer"]}, - "mem_swappiness": {"type": "integer"}, - "memswap_limit": {"type": ["number", "string"]}, - "network_mode": {"type": "string"}, - - "networks": { - "oneOf": [ - {"$ref": "#/definitions/list_of_strings"}, - { - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "oneOf": [ - { - "type": "object", - "properties": { - "aliases": {"$ref": "#/definitions/list_of_strings"}, - "ipv4_address": {"type": "string"}, - "ipv6_address": {"type": "string"}, - "link_local_ips": {"$ref": "#/definitions/list_of_strings"}, - "priority": {"type": "number"} - }, - "additionalProperties": false - }, - {"type": "null"} - ] - } - }, - "additionalProperties": false - } - ] - }, - "oom_kill_disable": {"type": "boolean"}, - "oom_score_adj": {"type": "integer", "minimum": -1000, "maximum": 1000}, - "group_add": { - "type": "array", - "items": { - "type": ["string", "number"] - }, - "uniqueItems": true - }, - "pid": {"type": ["string", "null"]}, - - "ports": { - "type": "array", - "items": { - "type": ["string", "number"], - "format": "ports" - }, - "uniqueItems": true - }, - - "privileged": {"type": "boolean"}, - "read_only": {"type": "boolean"}, - "restart": {"type": "string"}, - "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "shm_size": {"type": ["number", "string"]}, - "sysctls": {"$ref": "#/definitions/list_or_dict"}, - "pids_limit": {"type": ["number", "string"]}, - "stdin_open": {"type": "boolean"}, - "stop_grace_period": {"type": "string", "format": "duration"}, - "stop_signal": {"type": "string"}, - "storage_opt": {"type": "object"}, - "tmpfs": {"$ref": "#/definitions/string_or_list"}, - "tty": {"type": "boolean"}, - "ulimits": { - "type": "object", - "patternProperties": { - "^[a-z]+$": { - "oneOf": [ - {"type": "integer"}, - { - "type":"object", - "properties": { - "hard": {"type": "integer"}, - "soft": {"type": "integer"} - }, - "required": ["soft", "hard"], - "additionalProperties": false - } - ] - } - } - }, - "user": {"type": "string"}, - "userns_mode": {"type": "string"}, - "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "volume_driver": {"type": "string"}, - "volumes_from": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "working_dir": {"type": "string"} - }, - - "dependencies": { - "memswap_limit": ["mem_limit"] - }, - "additionalProperties": false - }, - - "healthcheck": { - "id": "#/definitions/healthcheck", - "type": "object", - "additionalProperties": false, - "properties": { - "disable": {"type": "boolean"}, - "interval": {"type": "string"}, - "retries": {"type": "number"}, - "test": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "timeout": {"type": "string"} - } - }, - - "network": { - "id": "#/definitions/network", - "type": "object", - "properties": { - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "ipam": { - "type": "object", - "properties": { - "driver": {"type": "string"}, - "config": { - "type": "array", - "items": {"$ref": "#/definitions/ipam_config"} - }, - "options": { - "type": "object", - "patternProperties": { - "^.+$": {"type": "string"} - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - }, - "internal": {"type": "boolean"}, - "enable_ipv6": {"type": "boolean"}, - "labels": {"$ref": "#/definitions/labels"}, - "name": {"type": "string"} - }, - "additionalProperties": false - }, - - "ipam_config": { - "id": "#/definitions/ipam_config", - "type": "object", - "properties": { - "subnet": {"type": "string"}, - "ip_range": {"type": "string"}, - "gateway": {"type": "string"}, - "aux_addresses": { - "type": "object", - "patternProperties": { - "^.+$": {"type": "string"} - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - - "volume": { - "id": "#/definitions/volume", - "type": ["object", "null"], - "properties": { - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - }, - "labels": {"$ref": "#/definitions/labels"}, - "name": {"type": "string"} - }, - "additionalProperties": false - }, - - "string_or_list": { - "oneOf": [ - {"type": "string"}, - {"$ref": "#/definitions/list_of_strings"} - ] - }, - - "list_of_strings": { - "type": "array", - "items": {"type": "string"}, - "uniqueItems": true - }, - - "list_or_dict": { - "oneOf": [ - { - "type": "object", - "patternProperties": { - ".+": { - "type": ["string", "number", "null"] - } - }, - "additionalProperties": false - }, - {"type": "array", "items": {"type": "string"}, "uniqueItems": true} - ] - }, - - "labels": { - "oneOf": [ - { - "type": "object", - "patternProperties": { - ".+": { - "type": "string" - } - }, - "additionalProperties": false - }, - {"type": "array", "items": {"type": "string"}, "uniqueItems": true} - ] - }, - - "blkio_limit": { - "type": "object", - "properties": { - "path": {"type": "string"}, - "rate": {"type": ["integer", "string"]} - }, - "additionalProperties": false - }, - "blkio_weight": { - "type": "object", - "properties": { - "path": {"type": "string"}, - "weight": {"type": "integer"} - }, - "additionalProperties": false - }, - - "constraints": { - "service": { - "id": "#/definitions/constraints/service", - "anyOf": [ - {"required": ["build"]}, - {"required": ["image"]} - ], - "properties": { - "build": { - "required": ["context"] - } - } - } - } - } -} diff --git a/compose/config/config_schema_v2.2.json b/compose/config/config_schema_v2.2.json deleted file mode 100644 index 8e1f288badf..00000000000 --- a/compose/config/config_schema_v2.2.json +++ /dev/null @@ -1,489 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "config_schema_v2.2.json", - "type": "object", - - "properties": { - "version": { - "type": "string" - }, - - "services": { - "id": "#/properties/services", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/service" - } - }, - "additionalProperties": false - }, - - "networks": { - "id": "#/properties/networks", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/network" - } - } - }, - - "volumes": { - "id": "#/properties/volumes", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/volume" - } - }, - "additionalProperties": false - } - }, - - "patternProperties": {"^x-": {}}, - "additionalProperties": false, - - "definitions": { - - "service": { - "id": "#/definitions/service", - "type": "object", - - "properties": { - "blkio_config": { - "type": "object", - "properties": { - "device_read_bps": { - "type": "array", - "items": {"$ref": "#/definitions/blkio_limit"} - }, - "device_read_iops": { - "type": "array", - "items": {"$ref": "#/definitions/blkio_limit"} - }, - "device_write_bps": { - "type": "array", - "items": {"$ref": "#/definitions/blkio_limit"} - }, - "device_write_iops": { - "type": "array", - "items": {"$ref": "#/definitions/blkio_limit"} - }, - "weight": {"type": "integer"}, - "weight_device": { - "type": "array", - "items": {"$ref": "#/definitions/blkio_weight"} - } - }, - "additionalProperties": false - }, - - "build": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "properties": { - "context": {"type": "string"}, - "dockerfile": {"type": "string"}, - "args": {"$ref": "#/definitions/list_or_dict"}, - "labels": {"$ref": "#/definitions/labels"}, - "cache_from": {"$ref": "#/definitions/list_of_strings"}, - "network": {"type": "string"}, - "isolation": {"type": "string"} - }, - "additionalProperties": false - } - ] - }, - "cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "cgroup_parent": {"type": "string"}, - "command": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "container_name": {"type": "string"}, - "cpu_count": {"type": "integer", "minimum": 0}, - "cpu_percent": {"type": "integer", "minimum": 0, "maximum": 100}, - "cpu_shares": {"type": ["number", "string"]}, - "cpu_quota": {"type": ["number", "string"]}, - "cpu_period": {"type": ["number", "string"]}, - "cpu_rt_period": {"type": ["number", "string"]}, - "cpu_rt_runtime": {"type": ["number", "string"]}, - "cpus": {"type": "number", "minimum": 0}, - "cpuset": {"type": "string"}, - "depends_on": { - "oneOf": [ - {"$ref": "#/definitions/list_of_strings"}, - { - "type": "object", - "additionalProperties": false, - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "type": "object", - "additionalProperties": false, - "properties": { - "condition": { - "type": "string", - "enum": ["service_started", "service_healthy"] - } - }, - "required": ["condition"] - } - } - } - ] - }, - "devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "dns_opt": { - "type": "array", - "items": { - "type": "string" - }, - "uniqueItems": true - }, - "dns": {"$ref": "#/definitions/string_or_list"}, - "dns_search": {"$ref": "#/definitions/string_or_list"}, - "domainname": {"type": "string"}, - "entrypoint": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "env_file": {"$ref": "#/definitions/string_or_list"}, - "environment": {"$ref": "#/definitions/list_or_dict"}, - - "expose": { - "type": "array", - "items": { - "type": ["string", "number"], - "format": "expose" - }, - "uniqueItems": true - }, - - "extends": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "object", - - "properties": { - "service": {"type": "string"}, - "file": {"type": "string"} - }, - "required": ["service"], - "additionalProperties": false - } - ] - }, - - "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, - "healthcheck": {"$ref": "#/definitions/healthcheck"}, - "hostname": {"type": "string"}, - "image": {"type": "string"}, - "init": {"type": ["boolean", "string"]}, - "ipc": {"type": "string"}, - "isolation": {"type": "string"}, - "labels": {"$ref": "#/definitions/labels"}, - "links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - - "logging": { - "type": "object", - - "properties": { - "driver": {"type": "string"}, - "options": {"type": "object"} - }, - "additionalProperties": false - }, - - "mac_address": {"type": "string"}, - "mem_limit": {"type": ["number", "string"]}, - "mem_reservation": {"type": ["string", "integer"]}, - "mem_swappiness": {"type": "integer"}, - "memswap_limit": {"type": ["number", "string"]}, - "network_mode": {"type": "string"}, - - "networks": { - "oneOf": [ - {"$ref": "#/definitions/list_of_strings"}, - { - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "oneOf": [ - { - "type": "object", - "properties": { - "aliases": {"$ref": "#/definitions/list_of_strings"}, - "ipv4_address": {"type": "string"}, - "ipv6_address": {"type": "string"}, - "link_local_ips": {"$ref": "#/definitions/list_of_strings"}, - "priority": {"type": "number"} - }, - "additionalProperties": false - }, - {"type": "null"} - ] - } - }, - "additionalProperties": false - } - ] - }, - "oom_kill_disable": {"type": "boolean"}, - "oom_score_adj": {"type": "integer", "minimum": -1000, "maximum": 1000}, - "group_add": { - "type": "array", - "items": { - "type": ["string", "number"] - }, - "uniqueItems": true - }, - "pid": {"type": ["string", "null"]}, - - "ports": { - "type": "array", - "items": { - "type": ["string", "number"], - "format": "ports" - }, - "uniqueItems": true - }, - - "privileged": {"type": "boolean"}, - "read_only": {"type": "boolean"}, - "restart": {"type": "string"}, - "scale": {"type": "integer"}, - "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "shm_size": {"type": ["number", "string"]}, - "sysctls": {"$ref": "#/definitions/list_or_dict"}, - "pids_limit": {"type": ["number", "string"]}, - "stdin_open": {"type": "boolean"}, - "stop_grace_period": {"type": "string", "format": "duration"}, - "stop_signal": {"type": "string"}, - "storage_opt": {"type": "object"}, - "tmpfs": {"$ref": "#/definitions/string_or_list"}, - "tty": {"type": "boolean"}, - "ulimits": { - "type": "object", - "patternProperties": { - "^[a-z]+$": { - "oneOf": [ - {"type": "integer"}, - { - "type":"object", - "properties": { - "hard": {"type": "integer"}, - "soft": {"type": "integer"} - }, - "required": ["soft", "hard"], - "additionalProperties": false - } - ] - } - } - }, - "user": {"type": "string"}, - "userns_mode": {"type": "string"}, - "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "volume_driver": {"type": "string"}, - "volumes_from": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "working_dir": {"type": "string"} - }, - - "dependencies": { - "memswap_limit": ["mem_limit"] - }, - "additionalProperties": false - }, - - "healthcheck": { - "id": "#/definitions/healthcheck", - "type": "object", - "additionalProperties": false, - "properties": { - "disable": {"type": "boolean"}, - "interval": {"type": "string"}, - "retries": {"type": "number"}, - "test": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "timeout": {"type": "string"} - } - }, - - "network": { - "id": "#/definitions/network", - "type": "object", - "properties": { - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "ipam": { - "type": "object", - "properties": { - "driver": {"type": "string"}, - "config": { - "type": "array", - "items": {"$ref": "#/definitions/ipam_config"} - }, - "options": { - "type": "object", - "patternProperties": { - "^.+$": {"type": "string"} - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - }, - "internal": {"type": "boolean"}, - "enable_ipv6": {"type": "boolean"}, - "labels": {"$ref": "#/definitions/labels"}, - "name": {"type": "string"} - }, - "additionalProperties": false - }, - - "ipam_config": { - "id": "#/definitions/ipam_config", - "type": "object", - "properties": { - "subnet": {"type": "string"}, - "ip_range": {"type": "string"}, - "gateway": {"type": "string"}, - "aux_addresses": { - "type": "object", - "patternProperties": { - "^.+$": {"type": "string"} - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - - "volume": { - "id": "#/definitions/volume", - "type": ["object", "null"], - "properties": { - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - }, - "labels": {"$ref": "#/definitions/labels"}, - "name": {"type": "string"} - }, - "additionalProperties": false - }, - - "string_or_list": { - "oneOf": [ - {"type": "string"}, - {"$ref": "#/definitions/list_of_strings"} - ] - }, - - "list_of_strings": { - "type": "array", - "items": {"type": "string"}, - "uniqueItems": true - }, - - "list_or_dict": { - "oneOf": [ - { - "type": "object", - "patternProperties": { - ".+": { - "type": ["string", "number", "null"] - } - }, - "additionalProperties": false - }, - {"type": "array", "items": {"type": "string"}, "uniqueItems": true} - ] - }, - - "labels": { - "oneOf": [ - { - "type": "object", - "patternProperties": { - ".+": { - "type": "string" - } - }, - "additionalProperties": false - }, - {"type": "array", "items": {"type": "string"}, "uniqueItems": true} - ] - }, - - "blkio_limit": { - "type": "object", - "properties": { - "path": {"type": "string"}, - "rate": {"type": ["integer", "string"]} - }, - "additionalProperties": false - }, - "blkio_weight": { - "type": "object", - "properties": { - "path": {"type": "string"}, - "weight": {"type": "integer"} - }, - "additionalProperties": false - }, - - "constraints": { - "service": { - "id": "#/definitions/constraints/service", - "anyOf": [ - {"required": ["build"]}, - {"required": ["image"]} - ], - "properties": { - "build": { - "required": ["context"] - } - } - } - } - } -} diff --git a/compose/config/config_schema_v2.3.json b/compose/config/config_schema_v2.3.json deleted file mode 100644 index 659dbcd1a49..00000000000 --- a/compose/config/config_schema_v2.3.json +++ /dev/null @@ -1,533 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "config_schema_v2.3.json", - "type": "object", - - "properties": { - "version": { - "type": "string" - }, - - "services": { - "id": "#/properties/services", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/service" - } - }, - "additionalProperties": false - }, - - "networks": { - "id": "#/properties/networks", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/network" - } - } - }, - - "volumes": { - "id": "#/properties/volumes", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/volume" - } - }, - "additionalProperties": false - } - }, - - "patternProperties": {"^x-": {}}, - "additionalProperties": false, - - "definitions": { - - "service": { - "id": "#/definitions/service", - "type": "object", - - "properties": { - "blkio_config": { - "type": "object", - "properties": { - "device_read_bps": { - "type": "array", - "items": {"$ref": "#/definitions/blkio_limit"} - }, - "device_read_iops": { - "type": "array", - "items": {"$ref": "#/definitions/blkio_limit"} - }, - "device_write_bps": { - "type": "array", - "items": {"$ref": "#/definitions/blkio_limit"} - }, - "device_write_iops": { - "type": "array", - "items": {"$ref": "#/definitions/blkio_limit"} - }, - "weight": {"type": "integer"}, - "weight_device": { - "type": "array", - "items": {"$ref": "#/definitions/blkio_weight"} - } - }, - "additionalProperties": false - }, - - "build": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "properties": { - "context": {"type": "string"}, - "dockerfile": {"type": "string"}, - "args": {"$ref": "#/definitions/list_or_dict"}, - "labels": {"$ref": "#/definitions/labels"}, - "cache_from": {"$ref": "#/definitions/list_of_strings"}, - "network": {"type": "string"}, - "target": {"type": "string"}, - "shm_size": {"type": ["integer", "string"]}, - "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, - "isolation": {"type": "string"} - }, - "additionalProperties": false - } - ] - }, - "cap_add": {"$ref": "#/definitions/list_of_strings"}, - "cap_drop": {"$ref": "#/definitions/list_of_strings"}, - "cgroup_parent": {"type": "string"}, - "command": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "container_name": {"type": "string"}, - "cpu_count": {"type": "integer", "minimum": 0}, - "cpu_percent": {"type": "integer", "minimum": 0, "maximum": 100}, - "cpu_shares": {"type": ["number", "string"]}, - "cpu_quota": {"type": ["number", "string"]}, - "cpu_period": {"type": ["number", "string"]}, - "cpu_rt_period": {"type": ["number", "string"]}, - "cpu_rt_runtime": {"type": ["number", "string"]}, - "cpus": {"type": "number", "minimum": 0}, - "cpuset": {"type": "string"}, - "depends_on": { - "oneOf": [ - {"$ref": "#/definitions/list_of_strings"}, - { - "type": "object", - "additionalProperties": false, - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "type": "object", - "additionalProperties": false, - "properties": { - "condition": { - "type": "string", - "enum": ["service_started", "service_healthy"] - } - }, - "required": ["condition"] - } - } - } - ] - }, - "device_cgroup_rules": {"$ref": "#/definitions/list_of_strings"}, - "devices": {"$ref": "#/definitions/list_of_strings"}, - "dns_opt": { - "type": "array", - "items": { - "type": "string" - }, - "uniqueItems": true - }, - "dns": {"$ref": "#/definitions/string_or_list"}, - "dns_search": {"$ref": "#/definitions/string_or_list"}, - "domainname": {"type": "string"}, - "entrypoint": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "env_file": {"$ref": "#/definitions/string_or_list"}, - "environment": {"$ref": "#/definitions/list_or_dict"}, - - "expose": { - "type": "array", - "items": { - "type": ["string", "number"], - "format": "expose" - }, - "uniqueItems": true - }, - - "extends": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "object", - - "properties": { - "service": {"type": "string"}, - "file": {"type": "string"} - }, - "required": ["service"], - "additionalProperties": false - } - ] - }, - - "external_links": {"$ref": "#/definitions/list_of_strings"}, - "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, - "healthcheck": {"$ref": "#/definitions/healthcheck"}, - "hostname": {"type": "string"}, - "image": {"type": "string"}, - "init": {"type": ["boolean", "string"]}, - "ipc": {"type": "string"}, - "isolation": {"type": "string"}, - "labels": {"$ref": "#/definitions/labels"}, - "links": {"$ref": "#/definitions/list_of_strings"}, - - "logging": { - "type": "object", - - "properties": { - "driver": {"type": "string"}, - "options": {"type": "object"} - }, - "additionalProperties": false - }, - - "mac_address": {"type": "string"}, - "mem_limit": {"type": ["number", "string"]}, - "mem_reservation": {"type": ["string", "integer"]}, - "mem_swappiness": {"type": "integer"}, - "memswap_limit": {"type": ["number", "string"]}, - "network_mode": {"type": "string"}, - - "networks": { - "oneOf": [ - {"$ref": "#/definitions/list_of_strings"}, - { - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "oneOf": [ - { - "type": "object", - "properties": { - "aliases": {"$ref": "#/definitions/list_of_strings"}, - "ipv4_address": {"type": "string"}, - "ipv6_address": {"type": "string"}, - "link_local_ips": {"$ref": "#/definitions/list_of_strings"}, - "priority": {"type": "number"} - }, - "additionalProperties": false - }, - {"type": "null"} - ] - } - }, - "additionalProperties": false - } - ] - }, - "oom_kill_disable": {"type": "boolean"}, - "oom_score_adj": {"type": "integer", "minimum": -1000, "maximum": 1000}, - "group_add": { - "type": "array", - "items": { - "type": ["string", "number"] - }, - "uniqueItems": true - }, - "pid": {"type": ["string", "null"]}, - - "ports": { - "type": "array", - "items": { - "type": ["string", "number"], - "format": "ports" - }, - "uniqueItems": true - }, - - "privileged": {"type": "boolean"}, - "read_only": {"type": "boolean"}, - "restart": {"type": "string"}, - "runtime": {"type": "string"}, - "scale": {"type": "integer"}, - "security_opt": {"$ref": "#/definitions/list_of_strings"}, - "shm_size": {"type": ["number", "string"]}, - "sysctls": {"$ref": "#/definitions/list_or_dict"}, - "pids_limit": {"type": ["number", "string"]}, - "stdin_open": {"type": "boolean"}, - "stop_grace_period": {"type": "string", "format": "duration"}, - "stop_signal": {"type": "string"}, - "storage_opt": {"type": "object"}, - "tmpfs": {"$ref": "#/definitions/string_or_list"}, - "tty": {"type": "boolean"}, - "ulimits": { - "type": "object", - "patternProperties": { - "^[a-z]+$": { - "oneOf": [ - {"type": "integer"}, - { - "type":"object", - "properties": { - "hard": {"type": "integer"}, - "soft": {"type": "integer"} - }, - "required": ["soft", "hard"], - "additionalProperties": false - } - ] - } - } - }, - "user": {"type": "string"}, - "userns_mode": {"type": "string"}, - "volumes": { - "type": "array", - "items": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "required": ["type"], - "additionalProperties": false, - "properties": { - "type": {"type": "string"}, - "source": {"type": "string"}, - "target": {"type": "string"}, - "read_only": {"type": "boolean"}, - "consistency": {"type": "string"}, - "bind": { - "type": "object", - "properties": { - "propagation": {"type": "string"} - } - }, - "volume": { - "type": "object", - "properties": { - "nocopy": {"type": "boolean"} - } - }, - "tmpfs": { - "type": "object", - "properties": { - "size": {"type": ["integer", "string"]} - } - } - } - } - ], - "uniqueItems": true - } - }, - "volume_driver": {"type": "string"}, - "volumes_from": {"$ref": "#/definitions/list_of_strings"}, - "working_dir": {"type": "string"} - }, - - "dependencies": { - "memswap_limit": ["mem_limit"] - }, - "additionalProperties": false - }, - - "healthcheck": { - "id": "#/definitions/healthcheck", - "type": "object", - "additionalProperties": false, - "properties": { - "disable": {"type": "boolean"}, - "interval": {"type": "string"}, - "retries": {"type": "number"}, - "start_period": {"type": "string"}, - "test": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "timeout": {"type": "string"} - } - }, - - "network": { - "id": "#/definitions/network", - "type": "object", - "properties": { - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "ipam": { - "type": "object", - "properties": { - "driver": {"type": "string"}, - "config": { - "type": "array", - "items": {"$ref": "#/definitions/ipam_config"} - }, - "options": { - "type": "object", - "patternProperties": { - "^.+$": {"type": "string"} - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - }, - "internal": {"type": "boolean"}, - "enable_ipv6": {"type": "boolean"}, - "labels": {"$ref": "#/definitions/labels"}, - "name": {"type": "string"} - }, - "additionalProperties": false - }, - - "ipam_config": { - "id": "#/definitions/ipam_config", - "type": "object", - "properties": { - "subnet": {"type": "string"}, - "ip_range": {"type": "string"}, - "gateway": {"type": "string"}, - "aux_addresses": { - "type": "object", - "patternProperties": { - "^.+$": {"type": "string"} - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - - "volume": { - "id": "#/definitions/volume", - "type": ["object", "null"], - "properties": { - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - }, - "labels": {"$ref": "#/definitions/labels"}, - "name": {"type": "string"} - }, - "additionalProperties": false - }, - - "string_or_list": { - "oneOf": [ - {"type": "string"}, - {"$ref": "#/definitions/list_of_strings"} - ] - }, - - "list_of_strings": { - "type": "array", - "items": {"type": "string"}, - "uniqueItems": true - }, - - "list_or_dict": { - "oneOf": [ - { - "type": "object", - "patternProperties": { - ".+": { - "type": ["string", "number", "null"] - } - }, - "additionalProperties": false - }, - {"type": "array", "items": {"type": "string"}, "uniqueItems": true} - ] - }, - - "labels": { - "oneOf": [ - { - "type": "object", - "patternProperties": { - ".+": { - "type": "string" - } - }, - "additionalProperties": false - }, - {"type": "array", "items": {"type": "string"}, "uniqueItems": true} - ] - }, - - "blkio_limit": { - "type": "object", - "properties": { - "path": {"type": "string"}, - "rate": {"type": ["integer", "string"]} - }, - "additionalProperties": false - }, - "blkio_weight": { - "type": "object", - "properties": { - "path": {"type": "string"}, - "weight": {"type": "integer"} - }, - "additionalProperties": false - }, - - "constraints": { - "service": { - "id": "#/definitions/constraints/service", - "anyOf": [ - {"required": ["build"]}, - {"required": ["image"]} - ], - "properties": { - "build": { - "required": ["context"] - } - } - } - } - } -} diff --git a/compose/config/config_schema_v2.4.json b/compose/config/config_schema_v2.4.json deleted file mode 100644 index 4e641788718..00000000000 --- a/compose/config/config_schema_v2.4.json +++ /dev/null @@ -1,535 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "config_schema_v2.4.json", - "type": "object", - - "properties": { - "version": { - "type": "string" - }, - - "services": { - "id": "#/properties/services", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/service" - } - }, - "additionalProperties": false - }, - - "networks": { - "id": "#/properties/networks", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/network" - } - } - }, - - "volumes": { - "id": "#/properties/volumes", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/volume" - } - }, - "additionalProperties": false - } - }, - - "patternProperties": {"^x-": {}}, - "additionalProperties": false, - - "definitions": { - - "service": { - "id": "#/definitions/service", - "type": "object", - - "properties": { - "blkio_config": { - "type": "object", - "properties": { - "device_read_bps": { - "type": "array", - "items": {"$ref": "#/definitions/blkio_limit"} - }, - "device_read_iops": { - "type": "array", - "items": {"$ref": "#/definitions/blkio_limit"} - }, - "device_write_bps": { - "type": "array", - "items": {"$ref": "#/definitions/blkio_limit"} - }, - "device_write_iops": { - "type": "array", - "items": {"$ref": "#/definitions/blkio_limit"} - }, - "weight": {"type": "integer"}, - "weight_device": { - "type": "array", - "items": {"$ref": "#/definitions/blkio_weight"} - } - }, - "additionalProperties": false - }, - - "build": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "properties": { - "context": {"type": "string"}, - "dockerfile": {"type": "string"}, - "args": {"$ref": "#/definitions/list_or_dict"}, - "labels": {"$ref": "#/definitions/labels"}, - "cache_from": {"$ref": "#/definitions/list_of_strings"}, - "network": {"type": "string"}, - "target": {"type": "string"}, - "shm_size": {"type": ["integer", "string"]}, - "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, - "isolation": {"type": "string"} - }, - "additionalProperties": false - } - ] - }, - "cap_add": {"$ref": "#/definitions/list_of_strings"}, - "cap_drop": {"$ref": "#/definitions/list_of_strings"}, - "cgroup_parent": {"type": "string"}, - "command": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "container_name": {"type": "string"}, - "cpu_count": {"type": "integer", "minimum": 0}, - "cpu_percent": {"type": "integer", "minimum": 0, "maximum": 100}, - "cpu_shares": {"type": ["number", "string"]}, - "cpu_quota": {"type": ["number", "string"]}, - "cpu_period": {"type": ["number", "string"]}, - "cpu_rt_period": {"type": ["number", "string"]}, - "cpu_rt_runtime": {"type": ["number", "string"]}, - "cpus": {"type": "number", "minimum": 0}, - "cpuset": {"type": "string"}, - "depends_on": { - "oneOf": [ - {"$ref": "#/definitions/list_of_strings"}, - { - "type": "object", - "additionalProperties": false, - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "type": "object", - "additionalProperties": false, - "properties": { - "condition": { - "type": "string", - "enum": ["service_started", "service_healthy"] - } - }, - "required": ["condition"] - } - } - } - ] - }, - "device_cgroup_rules": {"$ref": "#/definitions/list_of_strings"}, - "devices": {"$ref": "#/definitions/list_of_strings"}, - "dns_opt": { - "type": "array", - "items": { - "type": "string" - }, - "uniqueItems": true - }, - "dns": {"$ref": "#/definitions/string_or_list"}, - "dns_search": {"$ref": "#/definitions/string_or_list"}, - "domainname": {"type": "string"}, - "entrypoint": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "env_file": {"$ref": "#/definitions/string_or_list"}, - "environment": {"$ref": "#/definitions/list_or_dict"}, - - "expose": { - "type": "array", - "items": { - "type": ["string", "number"], - "format": "expose" - }, - "uniqueItems": true - }, - - "extends": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "object", - - "properties": { - "service": {"type": "string"}, - "file": {"type": "string"} - }, - "required": ["service"], - "additionalProperties": false - } - ] - }, - - "external_links": {"$ref": "#/definitions/list_of_strings"}, - "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, - "group_add": { - "type": "array", - "items": { - "type": ["string", "number"] - }, - "uniqueItems": true - }, - "healthcheck": {"$ref": "#/definitions/healthcheck"}, - "hostname": {"type": "string"}, - "image": {"type": "string"}, - "init": {"type": ["boolean", "string"]}, - "ipc": {"type": "string"}, - "isolation": {"type": "string"}, - "labels": {"$ref": "#/definitions/labels"}, - "links": {"$ref": "#/definitions/list_of_strings"}, - - "logging": { - "type": "object", - - "properties": { - "driver": {"type": "string"}, - "options": {"type": "object"} - }, - "additionalProperties": false - }, - - "mac_address": {"type": "string"}, - "mem_limit": {"type": ["number", "string"]}, - "mem_reservation": {"type": ["string", "integer"]}, - "mem_swappiness": {"type": "integer"}, - "memswap_limit": {"type": ["number", "string"]}, - "network_mode": {"type": "string"}, - - "networks": { - "oneOf": [ - {"$ref": "#/definitions/list_of_strings"}, - { - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "oneOf": [ - { - "type": "object", - "properties": { - "aliases": {"$ref": "#/definitions/list_of_strings"}, - "ipv4_address": {"type": "string"}, - "ipv6_address": {"type": "string"}, - "link_local_ips": {"$ref": "#/definitions/list_of_strings"}, - "priority": {"type": "number"} - }, - "additionalProperties": false - }, - {"type": "null"} - ] - } - }, - "additionalProperties": false - } - ] - }, - "oom_kill_disable": {"type": "boolean"}, - "oom_score_adj": {"type": "integer", "minimum": -1000, "maximum": 1000}, - "pid": {"type": ["string", "null"]}, - "platform": {"type": "string"}, - "ports": { - "type": "array", - "items": { - "type": ["string", "number"], - "format": "ports" - }, - "uniqueItems": true - }, - "privileged": {"type": "boolean"}, - "read_only": {"type": "boolean"}, - "restart": {"type": "string"}, - "runtime": {"type": "string"}, - "scale": {"type": "integer"}, - "security_opt": {"$ref": "#/definitions/list_of_strings"}, - "shm_size": {"type": ["number", "string"]}, - "sysctls": {"$ref": "#/definitions/list_or_dict"}, - "pids_limit": {"type": ["number", "string"]}, - "stdin_open": {"type": "boolean"}, - "stop_grace_period": {"type": "string", "format": "duration"}, - "stop_signal": {"type": "string"}, - "storage_opt": {"type": "object"}, - "tmpfs": {"$ref": "#/definitions/string_or_list"}, - "tty": {"type": "boolean"}, - "ulimits": { - "type": "object", - "patternProperties": { - "^[a-z]+$": { - "oneOf": [ - {"type": "integer"}, - { - "type":"object", - "properties": { - "hard": {"type": "integer"}, - "soft": {"type": "integer"} - }, - "required": ["soft", "hard"], - "additionalProperties": false - } - ] - } - } - }, - "user": {"type": "string"}, - "userns_mode": {"type": "string"}, - "volumes": { - "type": "array", - "items": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "required": ["type"], - "additionalProperties": false, - "properties": { - "type": {"type": "string"}, - "source": {"type": "string"}, - "target": {"type": "string"}, - "read_only": {"type": "boolean"}, - "consistency": {"type": "string"}, - "bind": { - "type": "object", - "properties": { - "propagation": {"type": "string"} - } - }, - "volume": { - "type": "object", - "properties": { - "nocopy": {"type": "boolean"} - } - }, - "tmpfs": { - "type": "object", - "properties": { - "size": {"type": ["integer", "string"]} - } - } - } - } - ], - "uniqueItems": true - } - }, - "volume_driver": {"type": "string"}, - "volumes_from": {"$ref": "#/definitions/list_of_strings"}, - "working_dir": {"type": "string"} - }, - - "dependencies": { - "memswap_limit": ["mem_limit"] - }, - "patternProperties": {"^x-": {}}, - "additionalProperties": false - }, - - "healthcheck": { - "id": "#/definitions/healthcheck", - "type": "object", - "additionalProperties": false, - "properties": { - "disable": {"type": "boolean"}, - "interval": {"type": "string"}, - "retries": {"type": "number"}, - "start_period": {"type": "string"}, - "test": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "timeout": {"type": "string"} - } - }, - - "network": { - "id": "#/definitions/network", - "type": "object", - "properties": { - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "ipam": { - "type": "object", - "properties": { - "driver": {"type": "string"}, - "config": { - "type": "array", - "items": {"$ref": "#/definitions/ipam_config"} - }, - "options": { - "type": "object", - "patternProperties": { - "^.+$": {"type": "string"} - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - }, - "internal": {"type": "boolean"}, - "enable_ipv6": {"type": "boolean"}, - "labels": {"$ref": "#/definitions/labels"}, - "name": {"type": "string"} - }, - "patternProperties": {"^x-": {}}, - "additionalProperties": false - }, - - "ipam_config": { - "id": "#/definitions/ipam_config", - "type": "object", - "properties": { - "subnet": {"type": "string"}, - "ip_range": {"type": "string"}, - "gateway": {"type": "string"}, - "aux_addresses": { - "type": "object", - "patternProperties": { - "^.+$": {"type": "string"} - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - - "volume": { - "id": "#/definitions/volume", - "type": ["object", "null"], - "properties": { - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - }, - "labels": {"$ref": "#/definitions/labels"}, - "name": {"type": "string"} - }, - "patternProperties": {"^x-": {}}, - "additionalProperties": false - }, - - "string_or_list": { - "oneOf": [ - {"type": "string"}, - {"$ref": "#/definitions/list_of_strings"} - ] - }, - - "list_of_strings": { - "type": "array", - "items": {"type": "string"}, - "uniqueItems": true - }, - - "list_or_dict": { - "oneOf": [ - { - "type": "object", - "patternProperties": { - ".+": { - "type": ["string", "number", "null"] - } - }, - "additionalProperties": false - }, - {"type": "array", "items": {"type": "string"}, "uniqueItems": true} - ] - }, - - "labels": { - "oneOf": [ - { - "type": "object", - "patternProperties": { - ".+": { - "type": "string" - } - }, - "additionalProperties": false - }, - {"type": "array", "items": {"type": "string"}, "uniqueItems": true} - ] - }, - - "blkio_limit": { - "type": "object", - "properties": { - "path": {"type": "string"}, - "rate": {"type": ["integer", "string"]} - }, - "additionalProperties": false - }, - "blkio_weight": { - "type": "object", - "properties": { - "path": {"type": "string"}, - "weight": {"type": "integer"} - }, - "additionalProperties": false - }, - - "constraints": { - "service": { - "id": "#/definitions/constraints/service", - "anyOf": [ - {"required": ["build"]}, - {"required": ["image"]} - ], - "properties": { - "build": { - "required": ["context"] - } - } - } - } - } -} diff --git a/compose/config/config_schema_v3.0.json b/compose/config/config_schema_v3.0.json deleted file mode 100644 index 10c3635215b..00000000000 --- a/compose/config/config_schema_v3.0.json +++ /dev/null @@ -1,399 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "config_schema_v3.0.json", - "type": "object", - "required": ["version"], - - "properties": { - "version": { - "type": "string" - }, - - "services": { - "id": "#/properties/services", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/service" - } - }, - "additionalProperties": false - }, - - "networks": { - "id": "#/properties/networks", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/network" - } - } - }, - - "volumes": { - "id": "#/properties/volumes", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/volume" - } - }, - "additionalProperties": false - } - }, - - "additionalProperties": false, - - "definitions": { - - "service": { - "id": "#/definitions/service", - "type": "object", - - "properties": { - "deploy": {"$ref": "#/definitions/deployment"}, - "build": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "properties": { - "context": {"type": "string"}, - "dockerfile": {"type": "string"}, - "args": {"$ref": "#/definitions/list_or_dict"} - }, - "additionalProperties": false - } - ] - }, - "cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "cgroup_parent": {"type": "string"}, - "command": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "container_name": {"type": "string"}, - "depends_on": {"$ref": "#/definitions/list_of_strings"}, - "devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "dns": {"$ref": "#/definitions/string_or_list"}, - "dns_search": {"$ref": "#/definitions/string_or_list"}, - "domainname": {"type": "string"}, - "entrypoint": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "env_file": {"$ref": "#/definitions/string_or_list"}, - "environment": {"$ref": "#/definitions/list_or_dict"}, - - "expose": { - "type": "array", - "items": { - "type": ["string", "number"], - "format": "expose" - }, - "uniqueItems": true - }, - - "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, - "healthcheck": {"$ref": "#/definitions/healthcheck"}, - "hostname": {"type": "string"}, - "image": {"type": "string"}, - "ipc": {"type": "string"}, - "labels": {"$ref": "#/definitions/labels"}, - "links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - - "logging": { - "type": "object", - - "properties": { - "driver": {"type": "string"}, - "options": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number", "null"]} - } - } - }, - "additionalProperties": false - }, - - "mac_address": {"type": "string"}, - "network_mode": {"type": "string"}, - - "networks": { - "oneOf": [ - {"$ref": "#/definitions/list_of_strings"}, - { - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "oneOf": [ - { - "type": "object", - "properties": { - "aliases": {"$ref": "#/definitions/list_of_strings"}, - "ipv4_address": {"type": "string"}, - "ipv6_address": {"type": "string"} - }, - "additionalProperties": false - }, - {"type": "null"} - ] - } - }, - "additionalProperties": false - } - ] - }, - "pid": {"type": ["string", "null"]}, - - "ports": { - "type": "array", - "items": { - "type": ["string", "number"], - "format": "ports" - }, - "uniqueItems": true - }, - - "privileged": {"type": "boolean"}, - "read_only": {"type": "boolean"}, - "restart": {"type": "string"}, - "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "shm_size": {"type": ["number", "string"]}, - "sysctls": {"$ref": "#/definitions/list_or_dict"}, - "stdin_open": {"type": "boolean"}, - "stop_grace_period": {"type": "string", "format": "duration"}, - "stop_signal": {"type": "string"}, - "tmpfs": {"$ref": "#/definitions/string_or_list"}, - "tty": {"type": "boolean"}, - "ulimits": { - "type": "object", - "patternProperties": { - "^[a-z]+$": { - "oneOf": [ - {"type": "integer"}, - { - "type":"object", - "properties": { - "hard": {"type": "integer"}, - "soft": {"type": "integer"} - }, - "required": ["soft", "hard"], - "additionalProperties": false - } - ] - } - } - }, - "user": {"type": "string"}, - "userns_mode": {"type": "string"}, - "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "working_dir": {"type": "string"} - }, - "additionalProperties": false - }, - - "healthcheck": { - "id": "#/definitions/healthcheck", - "type": "object", - "additionalProperties": false, - "properties": { - "disable": {"type": "boolean"}, - "interval": {"type": "string"}, - "retries": {"type": "number"}, - "test": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "timeout": {"type": "string"} - } - }, - "deployment": { - "id": "#/definitions/deployment", - "type": ["object", "null"], - "properties": { - "mode": {"type": "string"}, - "replicas": {"type": "integer"}, - "labels": {"$ref": "#/definitions/labels"}, - "update_config": { - "type": "object", - "properties": { - "parallelism": {"type": "integer"}, - "delay": {"type": "string", "format": "duration"}, - "failure_action": {"type": "string"}, - "monitor": {"type": "string", "format": "duration"}, - "max_failure_ratio": {"type": "number"} - }, - "additionalProperties": false - }, - "resources": { - "type": "object", - "properties": { - "limits": {"$ref": "#/definitions/resource"}, - "reservations": {"$ref": "#/definitions/resource"} - }, - "additionalProperties": false - }, - "restart_policy": { - "type": "object", - "properties": { - "condition": {"type": "string"}, - "delay": {"type": "string", "format": "duration"}, - "max_attempts": {"type": "integer"}, - "window": {"type": "string", "format": "duration"} - }, - "additionalProperties": false - }, - "placement": { - "type": "object", - "properties": { - "constraints": {"type": "array", "items": {"type": "string"}} - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - - "resource": { - "id": "#/definitions/resource", - "type": "object", - "properties": { - "cpus": {"type": "string"}, - "memory": {"type": "string"} - }, - "additionalProperties": false - }, - - "network": { - "id": "#/definitions/network", - "type": ["object", "null"], - "properties": { - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "ipam": { - "type": "object", - "properties": { - "driver": {"type": "string"}, - "config": { - "type": "array", - "items": { - "type": "object", - "properties": { - "subnet": {"type": "string", "format": "subnet_ip_address"} - }, - "additionalProperties": false - } - } - }, - "additionalProperties": false - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - }, - "internal": {"type": "boolean"}, - "labels": {"$ref": "#/definitions/labels"} - }, - "additionalProperties": false - }, - - "volume": { - "id": "#/definitions/volume", - "type": ["object", "null"], - "properties": { - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - }, - "labels": {"$ref": "#/definitions/labels"} - }, - "additionalProperties": false - }, - - "string_or_list": { - "oneOf": [ - {"type": "string"}, - {"$ref": "#/definitions/list_of_strings"} - ] - }, - - "list_of_strings": { - "type": "array", - "items": {"type": "string"}, - "uniqueItems": true - }, - - "list_or_dict": { - "oneOf": [ - { - "type": "object", - "patternProperties": { - ".+": { - "type": ["string", "number", "null"] - } - }, - "additionalProperties": false - }, - {"type": "array", "items": {"type": "string"}, "uniqueItems": true} - ] - }, - - "labels": { - "oneOf": [ - { - "type": "object", - "patternProperties": { - ".+": { - "type": "string" - } - }, - "additionalProperties": false - }, - {"type": "array", "items": {"type": "string"}, "uniqueItems": true} - ] - }, - - "constraints": { - "service": { - "id": "#/definitions/constraints/service", - "anyOf": [ - {"required": ["build"]}, - {"required": ["image"]} - ], - "properties": { - "build": { - "required": ["context"] - } - } - } - } - } -} diff --git a/compose/config/config_schema_v3.1.json b/compose/config/config_schema_v3.1.json deleted file mode 100644 index 8630ec3174c..00000000000 --- a/compose/config/config_schema_v3.1.json +++ /dev/null @@ -1,444 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "config_schema_v3.1.json", - "type": "object", - "required": ["version"], - - "properties": { - "version": { - "type": "string" - }, - - "services": { - "id": "#/properties/services", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/service" - } - }, - "additionalProperties": false - }, - - "networks": { - "id": "#/properties/networks", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/network" - } - } - }, - - "volumes": { - "id": "#/properties/volumes", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/volume" - } - }, - "additionalProperties": false - }, - - "secrets": { - "id": "#/properties/secrets", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/secret" - } - }, - "additionalProperties": false - } - }, - - "additionalProperties": false, - - "definitions": { - - "service": { - "id": "#/definitions/service", - "type": "object", - - "properties": { - "deploy": {"$ref": "#/definitions/deployment"}, - "build": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "properties": { - "context": {"type": "string"}, - "dockerfile": {"type": "string"}, - "args": {"$ref": "#/definitions/list_or_dict"} - }, - "additionalProperties": false - } - ] - }, - "cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "cgroup_parent": {"type": "string"}, - "command": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "container_name": {"type": "string"}, - "depends_on": {"$ref": "#/definitions/list_of_strings"}, - "devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "dns": {"$ref": "#/definitions/string_or_list"}, - "dns_search": {"$ref": "#/definitions/string_or_list"}, - "domainname": {"type": "string"}, - "entrypoint": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "env_file": {"$ref": "#/definitions/string_or_list"}, - "environment": {"$ref": "#/definitions/list_or_dict"}, - - "expose": { - "type": "array", - "items": { - "type": ["string", "number"], - "format": "expose" - }, - "uniqueItems": true - }, - - "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, - "healthcheck": {"$ref": "#/definitions/healthcheck"}, - "hostname": {"type": "string"}, - "image": {"type": "string"}, - "ipc": {"type": "string"}, - "labels": {"$ref": "#/definitions/labels"}, - "links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - - "logging": { - "type": "object", - - "properties": { - "driver": {"type": "string"}, - "options": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number", "null"]} - } - } - }, - "additionalProperties": false - }, - - "mac_address": {"type": "string"}, - "network_mode": {"type": "string"}, - - "networks": { - "oneOf": [ - {"$ref": "#/definitions/list_of_strings"}, - { - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "oneOf": [ - { - "type": "object", - "properties": { - "aliases": {"$ref": "#/definitions/list_of_strings"}, - "ipv4_address": {"type": "string"}, - "ipv6_address": {"type": "string"} - }, - "additionalProperties": false - }, - {"type": "null"} - ] - } - }, - "additionalProperties": false - } - ] - }, - "pid": {"type": ["string", "null"]}, - - "ports": { - "type": "array", - "items": { - "type": ["string", "number"], - "format": "ports" - }, - "uniqueItems": true - }, - - "privileged": {"type": "boolean"}, - "read_only": {"type": "boolean"}, - "restart": {"type": "string"}, - "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "shm_size": {"type": ["number", "string"]}, - "secrets": { - "type": "array", - "items": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "properties": { - "source": {"type": "string"}, - "target": {"type": "string"}, - "uid": {"type": "string"}, - "gid": {"type": "string"}, - "mode": {"type": "number"} - } - } - ] - } - }, - "sysctls": {"$ref": "#/definitions/list_or_dict"}, - "stdin_open": {"type": "boolean"}, - "stop_grace_period": {"type": "string", "format": "duration"}, - "stop_signal": {"type": "string"}, - "tmpfs": {"$ref": "#/definitions/string_or_list"}, - "tty": {"type": "boolean"}, - "ulimits": { - "type": "object", - "patternProperties": { - "^[a-z]+$": { - "oneOf": [ - {"type": "integer"}, - { - "type":"object", - "properties": { - "hard": {"type": "integer"}, - "soft": {"type": "integer"} - }, - "required": ["soft", "hard"], - "additionalProperties": false - } - ] - } - } - }, - "user": {"type": "string"}, - "userns_mode": {"type": "string"}, - "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "working_dir": {"type": "string"} - }, - "additionalProperties": false - }, - - "healthcheck": { - "id": "#/definitions/healthcheck", - "type": "object", - "additionalProperties": false, - "properties": { - "disable": {"type": "boolean"}, - "interval": {"type": "string"}, - "retries": {"type": "number"}, - "test": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "timeout": {"type": "string"} - } - }, - "deployment": { - "id": "#/definitions/deployment", - "type": ["object", "null"], - "properties": { - "mode": {"type": "string"}, - "replicas": {"type": "integer"}, - "labels": {"$ref": "#/definitions/labels"}, - "update_config": { - "type": "object", - "properties": { - "parallelism": {"type": "integer"}, - "delay": {"type": "string", "format": "duration"}, - "failure_action": {"type": "string"}, - "monitor": {"type": "string", "format": "duration"}, - "max_failure_ratio": {"type": "number"} - }, - "additionalProperties": false - }, - "resources": { - "type": "object", - "properties": { - "limits": {"$ref": "#/definitions/resource"}, - "reservations": {"$ref": "#/definitions/resource"} - }, - "additionalProperties": false - }, - "restart_policy": { - "type": "object", - "properties": { - "condition": {"type": "string"}, - "delay": {"type": "string", "format": "duration"}, - "max_attempts": {"type": "integer"}, - "window": {"type": "string", "format": "duration"} - }, - "additionalProperties": false - }, - "placement": { - "type": "object", - "properties": { - "constraints": {"type": "array", "items": {"type": "string"}} - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - - "resource": { - "id": "#/definitions/resource", - "type": "object", - "properties": { - "cpus": {"type": "string"}, - "memory": {"type": "string"} - }, - "additionalProperties": false - }, - - "network": { - "id": "#/definitions/network", - "type": ["object", "null"], - "properties": { - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "ipam": { - "type": "object", - "properties": { - "driver": {"type": "string"}, - "config": { - "type": "array", - "items": { - "type": "object", - "properties": { - "subnet": {"type": "string", "format": "subnet_ip_address"} - }, - "additionalProperties": false - } - } - }, - "additionalProperties": false - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - }, - "internal": {"type": "boolean"}, - "labels": {"$ref": "#/definitions/labels"} - }, - "additionalProperties": false - }, - - "volume": { - "id": "#/definitions/volume", - "type": ["object", "null"], - "properties": { - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - }, - "labels": {"$ref": "#/definitions/labels"} - }, - "additionalProperties": false - }, - - "secret": { - "id": "#/definitions/secret", - "type": "object", - "properties": { - "file": {"type": "string"}, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - } - }, - "labels": {"$ref": "#/definitions/labels"} - }, - "additionalProperties": false - }, - - "string_or_list": { - "oneOf": [ - {"type": "string"}, - {"$ref": "#/definitions/list_of_strings"} - ] - }, - - "list_of_strings": { - "type": "array", - "items": {"type": "string"}, - "uniqueItems": true - }, - - "list_or_dict": { - "oneOf": [ - { - "type": "object", - "patternProperties": { - ".+": { - "type": ["string", "number", "null"] - } - }, - "additionalProperties": false - }, - {"type": "array", "items": {"type": "string"}, "uniqueItems": true} - ] - }, - - "labels": { - "oneOf": [ - { - "type": "object", - "patternProperties": { - ".+": { - "type": "string" - } - }, - "additionalProperties": false - }, - {"type": "array", "items": {"type": "string"}, "uniqueItems": true} - ] - }, - - "constraints": { - "service": { - "id": "#/definitions/constraints/service", - "anyOf": [ - {"required": ["build"]}, - {"required": ["image"]} - ], - "properties": { - "build": { - "required": ["context"] - } - } - } - } - } -} diff --git a/compose/config/config_schema_v3.2.json b/compose/config/config_schema_v3.2.json deleted file mode 100644 index 5eccdea72c0..00000000000 --- a/compose/config/config_schema_v3.2.json +++ /dev/null @@ -1,492 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "config_schema_v3.2.json", - "type": "object", - "required": ["version"], - - "properties": { - "version": { - "type": "string" - }, - - "services": { - "id": "#/properties/services", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/service" - } - }, - "additionalProperties": false - }, - - "networks": { - "id": "#/properties/networks", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/network" - } - } - }, - - "volumes": { - "id": "#/properties/volumes", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/volume" - } - }, - "additionalProperties": false - }, - - "secrets": { - "id": "#/properties/secrets", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/secret" - } - }, - "additionalProperties": false - } - }, - - "additionalProperties": false, - - "definitions": { - - "service": { - "id": "#/definitions/service", - "type": "object", - - "properties": { - "deploy": {"$ref": "#/definitions/deployment"}, - "build": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "properties": { - "context": {"type": "string"}, - "dockerfile": {"type": "string"}, - "args": {"$ref": "#/definitions/list_or_dict"}, - "labels": {"$ref": "#/definitions/list_or_dict"}, - "cache_from": {"$ref": "#/definitions/list_of_strings"} - }, - "additionalProperties": false - } - ] - }, - "cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "cgroup_parent": {"type": "string"}, - "command": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "container_name": {"type": "string"}, - "depends_on": {"$ref": "#/definitions/list_of_strings"}, - "devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "dns": {"$ref": "#/definitions/string_or_list"}, - "dns_search": {"$ref": "#/definitions/string_or_list"}, - "domainname": {"type": "string"}, - "entrypoint": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "env_file": {"$ref": "#/definitions/string_or_list"}, - "environment": {"$ref": "#/definitions/list_or_dict"}, - - "expose": { - "type": "array", - "items": { - "type": ["string", "number"], - "format": "expose" - }, - "uniqueItems": true - }, - - "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, - "healthcheck": {"$ref": "#/definitions/healthcheck"}, - "hostname": {"type": "string"}, - "image": {"type": "string"}, - "ipc": {"type": "string"}, - "labels": {"$ref": "#/definitions/labels"}, - "links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - - "logging": { - "type": "object", - - "properties": { - "driver": {"type": "string"}, - "options": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number", "null"]} - } - } - }, - "additionalProperties": false - }, - - "mac_address": {"type": "string"}, - "network_mode": {"type": "string"}, - - "networks": { - "oneOf": [ - {"$ref": "#/definitions/list_of_strings"}, - { - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "oneOf": [ - { - "type": "object", - "properties": { - "aliases": {"$ref": "#/definitions/list_of_strings"}, - "ipv4_address": {"type": "string"}, - "ipv6_address": {"type": "string"} - }, - "additionalProperties": false - }, - {"type": "null"} - ] - } - }, - "additionalProperties": false - } - ] - }, - "pid": {"type": ["string", "null"]}, - - "ports": { - "type": "array", - "items": { - "oneOf": [ - {"type": "number", "format": "ports"}, - {"type": "string", "format": "ports"}, - { - "type": "object", - "properties": { - "mode": {"type": "string"}, - "target": {"type": "integer"}, - "published": {"type": "integer"}, - "protocol": {"type": "string"} - }, - "additionalProperties": false - } - ] - }, - "uniqueItems": true - }, - - "privileged": {"type": "boolean"}, - "read_only": {"type": "boolean"}, - "restart": {"type": "string"}, - "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "shm_size": {"type": ["number", "string"]}, - "secrets": { - "type": "array", - "items": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "properties": { - "source": {"type": "string"}, - "target": {"type": "string"}, - "uid": {"type": "string"}, - "gid": {"type": "string"}, - "mode": {"type": "number"} - } - } - ] - } - }, - "sysctls": {"$ref": "#/definitions/list_or_dict"}, - "stdin_open": {"type": "boolean"}, - "stop_grace_period": {"type": "string", "format": "duration"}, - "stop_signal": {"type": "string"}, - "tmpfs": {"$ref": "#/definitions/string_or_list"}, - "tty": {"type": "boolean"}, - "ulimits": { - "type": "object", - "patternProperties": { - "^[a-z]+$": { - "oneOf": [ - {"type": "integer"}, - { - "type":"object", - "properties": { - "hard": {"type": "integer"}, - "soft": {"type": "integer"} - }, - "required": ["soft", "hard"], - "additionalProperties": false - } - ] - } - } - }, - "user": {"type": "string"}, - "userns_mode": {"type": "string"}, - "volumes": { - "type": "array", - "items": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "required": ["type"], - "additionalProperties": false, - "properties": { - "type": {"type": "string"}, - "source": {"type": "string"}, - "target": {"type": "string"}, - "read_only": {"type": "boolean"}, - "consistency": {"type": "string"}, - "bind": { - "type": "object", - "properties": { - "propagation": {"type": "string"} - } - }, - "volume": { - "type": "object", - "properties": { - "nocopy": {"type": "boolean"} - } - } - } - } - ], - "uniqueItems": true - } - }, - "working_dir": {"type": "string"} - }, - "additionalProperties": false - }, - - "healthcheck": { - "id": "#/definitions/healthcheck", - "type": "object", - "additionalProperties": false, - "properties": { - "disable": {"type": "boolean"}, - "interval": {"type": "string"}, - "retries": {"type": "number"}, - "test": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "timeout": {"type": "string"} - } - }, - "deployment": { - "id": "#/definitions/deployment", - "type": ["object", "null"], - "properties": { - "mode": {"type": "string"}, - "endpoint_mode": {"type": "string"}, - "replicas": {"type": "integer"}, - "labels": {"$ref": "#/definitions/labels"}, - "update_config": { - "type": "object", - "properties": { - "parallelism": {"type": "integer"}, - "delay": {"type": "string", "format": "duration"}, - "failure_action": {"type": "string"}, - "monitor": {"type": "string", "format": "duration"}, - "max_failure_ratio": {"type": "number"} - }, - "additionalProperties": false - }, - "resources": { - "type": "object", - "properties": { - "limits": {"$ref": "#/definitions/resource"}, - "reservations": {"$ref": "#/definitions/resource"} - }, - "additionalProperties": false - }, - "restart_policy": { - "type": "object", - "properties": { - "condition": {"type": "string"}, - "delay": {"type": "string", "format": "duration"}, - "max_attempts": {"type": "integer"}, - "window": {"type": "string", "format": "duration"} - }, - "additionalProperties": false - }, - "placement": { - "type": "object", - "properties": { - "constraints": {"type": "array", "items": {"type": "string"}} - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - - "resource": { - "id": "#/definitions/resource", - "type": "object", - "properties": { - "cpus": {"type": "string"}, - "memory": {"type": "string"} - }, - "additionalProperties": false - }, - - "network": { - "id": "#/definitions/network", - "type": ["object", "null"], - "properties": { - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "ipam": { - "type": "object", - "properties": { - "driver": {"type": "string"}, - "config": { - "type": "array", - "items": { - "type": "object", - "properties": { - "subnet": {"type": "string", "format": "subnet_ip_address"} - }, - "additionalProperties": false - } - } - }, - "additionalProperties": false - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - }, - "internal": {"type": "boolean"}, - "attachable": {"type": "boolean"}, - "labels": {"$ref": "#/definitions/labels"} - }, - "additionalProperties": false - }, - - "volume": { - "id": "#/definitions/volume", - "type": ["object", "null"], - "properties": { - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - }, - "labels": {"$ref": "#/definitions/labels"} - }, - "additionalProperties": false - }, - - "secret": { - "id": "#/definitions/secret", - "type": "object", - "properties": { - "file": {"type": "string"}, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - } - }, - "labels": {"$ref": "#/definitions/labels"} - }, - "additionalProperties": false - }, - - "string_or_list": { - "oneOf": [ - {"type": "string"}, - {"$ref": "#/definitions/list_of_strings"} - ] - }, - - "list_of_strings": { - "type": "array", - "items": {"type": "string"}, - "uniqueItems": true - }, - - "list_or_dict": { - "oneOf": [ - { - "type": "object", - "patternProperties": { - ".+": { - "type": ["string", "number", "null"] - } - }, - "additionalProperties": false - }, - {"type": "array", "items": {"type": "string"}, "uniqueItems": true} - ] - }, - - "labels": { - "oneOf": [ - { - "type": "object", - "patternProperties": { - ".+": { - "type": "string" - } - }, - "additionalProperties": false - }, - {"type": "array", "items": {"type": "string"}, "uniqueItems": true} - ] - }, - - "constraints": { - "service": { - "id": "#/definitions/constraints/service", - "anyOf": [ - {"required": ["build"]}, - {"required": ["image"]} - ], - "properties": { - "build": { - "required": ["context"] - } - } - } - } - } -} diff --git a/compose/config/config_schema_v3.3.json b/compose/config/config_schema_v3.3.json deleted file mode 100644 index f63842b9de2..00000000000 --- a/compose/config/config_schema_v3.3.json +++ /dev/null @@ -1,551 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "config_schema_v3.3.json", - "type": "object", - "required": ["version"], - - "properties": { - "version": { - "type": "string" - }, - - "services": { - "id": "#/properties/services", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/service" - } - }, - "additionalProperties": false - }, - - "networks": { - "id": "#/properties/networks", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/network" - } - } - }, - - "volumes": { - "id": "#/properties/volumes", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/volume" - } - }, - "additionalProperties": false - }, - - "secrets": { - "id": "#/properties/secrets", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/secret" - } - }, - "additionalProperties": false - }, - - "configs": { - "id": "#/properties/configs", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/config" - } - }, - "additionalProperties": false - } - }, - - "additionalProperties": false, - - "definitions": { - - "service": { - "id": "#/definitions/service", - "type": "object", - - "properties": { - "deploy": {"$ref": "#/definitions/deployment"}, - "build": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "properties": { - "context": {"type": "string"}, - "dockerfile": {"type": "string"}, - "args": {"$ref": "#/definitions/list_or_dict"}, - "labels": {"$ref": "#/definitions/labels"}, - "cache_from": {"$ref": "#/definitions/list_of_strings"} - }, - "additionalProperties": false - } - ] - }, - "cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "cgroup_parent": {"type": "string"}, - "command": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "configs": { - "type": "array", - "items": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "properties": { - "source": {"type": "string"}, - "target": {"type": "string"}, - "uid": {"type": "string"}, - "gid": {"type": "string"}, - "mode": {"type": "number"} - } - } - ] - } - }, - "container_name": {"type": "string"}, - "credential_spec": {"type": "object", "properties": { - "file": {"type": "string"}, - "registry": {"type": "string"} - }}, - "depends_on": {"$ref": "#/definitions/list_of_strings"}, - "devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "dns": {"$ref": "#/definitions/string_or_list"}, - "dns_search": {"$ref": "#/definitions/string_or_list"}, - "domainname": {"type": "string"}, - "entrypoint": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "env_file": {"$ref": "#/definitions/string_or_list"}, - "environment": {"$ref": "#/definitions/list_or_dict"}, - - "expose": { - "type": "array", - "items": { - "type": ["string", "number"], - "format": "expose" - }, - "uniqueItems": true - }, - - "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, - "healthcheck": {"$ref": "#/definitions/healthcheck"}, - "hostname": {"type": "string"}, - "image": {"type": "string"}, - "ipc": {"type": "string"}, - "labels": {"$ref": "#/definitions/labels"}, - "links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - - "logging": { - "type": "object", - - "properties": { - "driver": {"type": "string"}, - "options": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number", "null"]} - } - } - }, - "additionalProperties": false - }, - - "mac_address": {"type": "string"}, - "network_mode": {"type": "string"}, - - "networks": { - "oneOf": [ - {"$ref": "#/definitions/list_of_strings"}, - { - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "oneOf": [ - { - "type": "object", - "properties": { - "aliases": {"$ref": "#/definitions/list_of_strings"}, - "ipv4_address": {"type": "string"}, - "ipv6_address": {"type": "string"} - }, - "additionalProperties": false - }, - {"type": "null"} - ] - } - }, - "additionalProperties": false - } - ] - }, - "pid": {"type": ["string", "null"]}, - - "ports": { - "type": "array", - "items": { - "oneOf": [ - {"type": "number", "format": "ports"}, - {"type": "string", "format": "ports"}, - { - "type": "object", - "properties": { - "mode": {"type": "string"}, - "target": {"type": "integer"}, - "published": {"type": "integer"}, - "protocol": {"type": "string"} - }, - "additionalProperties": false - } - ] - }, - "uniqueItems": true - }, - - "privileged": {"type": "boolean"}, - "read_only": {"type": "boolean"}, - "restart": {"type": "string"}, - "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "shm_size": {"type": ["number", "string"]}, - "secrets": { - "type": "array", - "items": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "properties": { - "source": {"type": "string"}, - "target": {"type": "string"}, - "uid": {"type": "string"}, - "gid": {"type": "string"}, - "mode": {"type": "number"} - } - } - ] - } - }, - "sysctls": {"$ref": "#/definitions/list_or_dict"}, - "stdin_open": {"type": "boolean"}, - "stop_grace_period": {"type": "string", "format": "duration"}, - "stop_signal": {"type": "string"}, - "tmpfs": {"$ref": "#/definitions/string_or_list"}, - "tty": {"type": "boolean"}, - "ulimits": { - "type": "object", - "patternProperties": { - "^[a-z]+$": { - "oneOf": [ - {"type": "integer"}, - { - "type":"object", - "properties": { - "hard": {"type": "integer"}, - "soft": {"type": "integer"} - }, - "required": ["soft", "hard"], - "additionalProperties": false - } - ] - } - } - }, - "user": {"type": "string"}, - "userns_mode": {"type": "string"}, - "volumes": { - "type": "array", - "items": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "required": ["type"], - "additionalProperties": false, - "properties": { - "type": {"type": "string"}, - "source": {"type": "string"}, - "target": {"type": "string"}, - "read_only": {"type": "boolean"}, - "consistency": {"type": "string"}, - "bind": { - "type": "object", - "properties": { - "propagation": {"type": "string"} - } - }, - "volume": { - "type": "object", - "properties": { - "nocopy": {"type": "boolean"} - } - } - } - } - ], - "uniqueItems": true - } - }, - "working_dir": {"type": "string"} - }, - "additionalProperties": false - }, - - "healthcheck": { - "id": "#/definitions/healthcheck", - "type": "object", - "additionalProperties": false, - "properties": { - "disable": {"type": "boolean"}, - "interval": {"type": "string"}, - "retries": {"type": "number"}, - "test": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "timeout": {"type": "string"} - } - }, - "deployment": { - "id": "#/definitions/deployment", - "type": ["object", "null"], - "properties": { - "mode": {"type": "string"}, - "endpoint_mode": {"type": "string"}, - "replicas": {"type": "integer"}, - "labels": {"$ref": "#/definitions/labels"}, - "update_config": { - "type": "object", - "properties": { - "parallelism": {"type": "integer"}, - "delay": {"type": "string", "format": "duration"}, - "failure_action": {"type": "string"}, - "monitor": {"type": "string", "format": "duration"}, - "max_failure_ratio": {"type": "number"} - }, - "additionalProperties": false - }, - "resources": { - "type": "object", - "properties": { - "limits": {"$ref": "#/definitions/resource"}, - "reservations": {"$ref": "#/definitions/resource"} - }, - "additionalProperties": false - }, - "restart_policy": { - "type": "object", - "properties": { - "condition": {"type": "string"}, - "delay": {"type": "string", "format": "duration"}, - "max_attempts": {"type": "integer"}, - "window": {"type": "string", "format": "duration"} - }, - "additionalProperties": false - }, - "placement": { - "type": "object", - "properties": { - "constraints": {"type": "array", "items": {"type": "string"}}, - "preferences": { - "type": "array", - "items": { - "type": "object", - "properties": { - "spread": {"type": "string"} - }, - "additionalProperties": false - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - - "resource": { - "id": "#/definitions/resource", - "type": "object", - "properties": { - "cpus": {"type": "string"}, - "memory": {"type": "string"} - }, - "additionalProperties": false - }, - - "network": { - "id": "#/definitions/network", - "type": ["object", "null"], - "properties": { - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "ipam": { - "type": "object", - "properties": { - "driver": {"type": "string"}, - "config": { - "type": "array", - "items": { - "type": "object", - "properties": { - "subnet": {"type": "string", "format": "subnet_ip_address"} - }, - "additionalProperties": false - } - } - }, - "additionalProperties": false - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - }, - "internal": {"type": "boolean"}, - "attachable": {"type": "boolean"}, - "labels": {"$ref": "#/definitions/labels"} - }, - "additionalProperties": false - }, - - "volume": { - "id": "#/definitions/volume", - "type": ["object", "null"], - "properties": { - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - }, - "labels": {"$ref": "#/definitions/labels"} - }, - "additionalProperties": false - }, - - "secret": { - "id": "#/definitions/secret", - "type": "object", - "properties": { - "file": {"type": "string"}, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - } - }, - "labels": {"$ref": "#/definitions/labels"} - }, - "additionalProperties": false - }, - - "config": { - "id": "#/definitions/config", - "type": "object", - "properties": { - "file": {"type": "string"}, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - } - }, - "labels": {"$ref": "#/definitions/labels"} - }, - "additionalProperties": false - }, - - "string_or_list": { - "oneOf": [ - {"type": "string"}, - {"$ref": "#/definitions/list_of_strings"} - ] - }, - - "list_of_strings": { - "type": "array", - "items": {"type": "string"}, - "uniqueItems": true - }, - - "list_or_dict": { - "oneOf": [ - { - "type": "object", - "patternProperties": { - ".+": { - "type": ["string", "number", "null"] - } - }, - "additionalProperties": false - }, - {"type": "array", "items": {"type": "string"}, "uniqueItems": true} - ] - }, - - "labels": { - "oneOf": [ - { - "type": "object", - "patternProperties": { - ".+": { - "type": "string" - } - }, - "additionalProperties": false - }, - {"type": "array", "items": {"type": "string"}, "uniqueItems": true} - ] - }, - - "constraints": { - "service": { - "id": "#/definitions/constraints/service", - "anyOf": [ - {"required": ["build"]}, - {"required": ["image"]} - ], - "properties": { - "build": { - "required": ["context"] - } - } - } - } - } -} diff --git a/compose/config/config_schema_v3.4.json b/compose/config/config_schema_v3.4.json deleted file mode 100644 index 23e95544656..00000000000 --- a/compose/config/config_schema_v3.4.json +++ /dev/null @@ -1,560 +0,0 @@ - -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "config_schema_v3.4.json", - "type": "object", - "required": ["version"], - - "properties": { - "version": { - "type": "string" - }, - - "services": { - "id": "#/properties/services", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/service" - } - }, - "additionalProperties": false - }, - - "networks": { - "id": "#/properties/networks", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/network" - } - } - }, - - "volumes": { - "id": "#/properties/volumes", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/volume" - } - }, - "additionalProperties": false - }, - - "secrets": { - "id": "#/properties/secrets", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/secret" - } - }, - "additionalProperties": false - }, - - "configs": { - "id": "#/properties/configs", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/config" - } - }, - "additionalProperties": false - } - }, - - "patternProperties": {"^x-": {}}, - "additionalProperties": false, - - "definitions": { - - "service": { - "id": "#/definitions/service", - "type": "object", - - "properties": { - "deploy": {"$ref": "#/definitions/deployment"}, - "build": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "properties": { - "context": {"type": "string"}, - "dockerfile": {"type": "string"}, - "args": {"$ref": "#/definitions/list_or_dict"}, - "labels": {"$ref": "#/definitions/labels"}, - "cache_from": {"$ref": "#/definitions/list_of_strings"}, - "network": {"type": "string"}, - "target": {"type": "string"} - }, - "additionalProperties": false - } - ] - }, - "cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "cgroup_parent": {"type": "string"}, - "command": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "configs": { - "type": "array", - "items": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "properties": { - "source": {"type": "string"}, - "target": {"type": "string"}, - "uid": {"type": "string"}, - "gid": {"type": "string"}, - "mode": {"type": "number"} - } - } - ] - } - }, - "container_name": {"type": "string"}, - "credential_spec": {"type": "object", "properties": { - "file": {"type": "string"}, - "registry": {"type": "string"} - }}, - "depends_on": {"$ref": "#/definitions/list_of_strings"}, - "devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "dns": {"$ref": "#/definitions/string_or_list"}, - "dns_search": {"$ref": "#/definitions/string_or_list"}, - "domainname": {"type": "string"}, - "entrypoint": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "env_file": {"$ref": "#/definitions/string_or_list"}, - "environment": {"$ref": "#/definitions/list_or_dict"}, - - "expose": { - "type": "array", - "items": { - "type": ["string", "number"], - "format": "expose" - }, - "uniqueItems": true - }, - - "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, - "healthcheck": {"$ref": "#/definitions/healthcheck"}, - "hostname": {"type": "string"}, - "image": {"type": "string"}, - "ipc": {"type": "string"}, - "labels": {"$ref": "#/definitions/labels"}, - "links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - - "logging": { - "type": "object", - - "properties": { - "driver": {"type": "string"}, - "options": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number", "null"]} - } - } - }, - "additionalProperties": false - }, - - "mac_address": {"type": "string"}, - "network_mode": {"type": "string"}, - - "networks": { - "oneOf": [ - {"$ref": "#/definitions/list_of_strings"}, - { - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "oneOf": [ - { - "type": "object", - "properties": { - "aliases": {"$ref": "#/definitions/list_of_strings"}, - "ipv4_address": {"type": "string"}, - "ipv6_address": {"type": "string"} - }, - "additionalProperties": false - }, - {"type": "null"} - ] - } - }, - "additionalProperties": false - } - ] - }, - "pid": {"type": ["string", "null"]}, - - "ports": { - "type": "array", - "items": { - "oneOf": [ - {"type": "number", "format": "ports"}, - {"type": "string", "format": "ports"}, - { - "type": "object", - "properties": { - "mode": {"type": "string"}, - "target": {"type": "integer"}, - "published": {"type": "integer"}, - "protocol": {"type": "string"} - }, - "additionalProperties": false - } - ] - }, - "uniqueItems": true - }, - - "privileged": {"type": "boolean"}, - "read_only": {"type": "boolean"}, - "restart": {"type": "string"}, - "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "shm_size": {"type": ["number", "string"]}, - "secrets": { - "type": "array", - "items": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "properties": { - "source": {"type": "string"}, - "target": {"type": "string"}, - "uid": {"type": "string"}, - "gid": {"type": "string"}, - "mode": {"type": "number"} - } - } - ] - } - }, - "sysctls": {"$ref": "#/definitions/list_or_dict"}, - "stdin_open": {"type": "boolean"}, - "stop_grace_period": {"type": "string", "format": "duration"}, - "stop_signal": {"type": "string"}, - "tmpfs": {"$ref": "#/definitions/string_or_list"}, - "tty": {"type": "boolean"}, - "ulimits": { - "type": "object", - "patternProperties": { - "^[a-z]+$": { - "oneOf": [ - {"type": "integer"}, - { - "type":"object", - "properties": { - "hard": {"type": "integer"}, - "soft": {"type": "integer"} - }, - "required": ["soft", "hard"], - "additionalProperties": false - } - ] - } - } - }, - "user": {"type": "string"}, - "userns_mode": {"type": "string"}, - "volumes": { - "type": "array", - "items": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "required": ["type"], - "additionalProperties": false, - "properties": { - "type": {"type": "string"}, - "source": {"type": "string"}, - "target": {"type": "string"}, - "read_only": {"type": "boolean"}, - "consistency": {"type": "string"}, - "bind": { - "type": "object", - "properties": { - "propagation": {"type": "string"} - } - }, - "volume": { - "type": "object", - "properties": { - "nocopy": {"type": "boolean"} - } - } - } - } - ], - "uniqueItems": true - } - }, - "working_dir": {"type": "string"} - }, - "additionalProperties": false - }, - - "healthcheck": { - "id": "#/definitions/healthcheck", - "type": "object", - "additionalProperties": false, - "properties": { - "disable": {"type": "boolean"}, - "interval": {"type": "string", "format": "duration"}, - "retries": {"type": "number"}, - "test": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "timeout": {"type": "string", "format": "duration"}, - "start_period": {"type": "string", "format": "duration"} - } - }, - "deployment": { - "id": "#/definitions/deployment", - "type": ["object", "null"], - "properties": { - "mode": {"type": "string"}, - "endpoint_mode": {"type": "string"}, - "replicas": {"type": "integer"}, - "labels": {"$ref": "#/definitions/labels"}, - "update_config": { - "type": "object", - "properties": { - "parallelism": {"type": "integer"}, - "delay": {"type": "string", "format": "duration"}, - "failure_action": {"type": "string"}, - "monitor": {"type": "string", "format": "duration"}, - "max_failure_ratio": {"type": "number"}, - "order": {"type": "string", "enum": [ - "start-first", "stop-first" - ]} - }, - "additionalProperties": false - }, - "resources": { - "type": "object", - "properties": { - "limits": {"$ref": "#/definitions/resource"}, - "reservations": {"$ref": "#/definitions/resource"} - }, - "additionalProperties": false - }, - "restart_policy": { - "type": "object", - "properties": { - "condition": {"type": "string"}, - "delay": {"type": "string", "format": "duration"}, - "max_attempts": {"type": "integer"}, - "window": {"type": "string", "format": "duration"} - }, - "additionalProperties": false - }, - "placement": { - "type": "object", - "properties": { - "constraints": {"type": "array", "items": {"type": "string"}}, - "preferences": { - "type": "array", - "items": { - "type": "object", - "properties": { - "spread": {"type": "string"} - }, - "additionalProperties": false - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - - "resource": { - "id": "#/definitions/resource", - "type": "object", - "properties": { - "cpus": {"type": "string"}, - "memory": {"type": "string"} - }, - "additionalProperties": false - }, - - "network": { - "id": "#/definitions/network", - "type": ["object", "null"], - "properties": { - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "ipam": { - "type": "object", - "properties": { - "driver": {"type": "string"}, - "config": { - "type": "array", - "items": { - "type": "object", - "properties": { - "subnet": {"type": "string", "format": "subnet_ip_address"} - }, - "additionalProperties": false - } - } - }, - "additionalProperties": false - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - }, - "internal": {"type": "boolean"}, - "attachable": {"type": "boolean"}, - "labels": {"$ref": "#/definitions/labels"} - }, - "additionalProperties": false - }, - - "volume": { - "id": "#/definitions/volume", - "type": ["object", "null"], - "properties": { - "name": {"type": "string"}, - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - }, - "labels": {"$ref": "#/definitions/labels"} - }, - "additionalProperties": false - }, - - "secret": { - "id": "#/definitions/secret", - "type": "object", - "properties": { - "file": {"type": "string"}, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - } - }, - "labels": {"$ref": "#/definitions/labels"} - }, - "additionalProperties": false - }, - - "config": { - "id": "#/definitions/config", - "type": "object", - "properties": { - "file": {"type": "string"}, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - } - }, - "labels": {"$ref": "#/definitions/labels"} - }, - "additionalProperties": false - }, - - "string_or_list": { - "oneOf": [ - {"type": "string"}, - {"$ref": "#/definitions/list_of_strings"} - ] - }, - - "list_of_strings": { - "type": "array", - "items": {"type": "string"}, - "uniqueItems": true - }, - - "list_or_dict": { - "oneOf": [ - { - "type": "object", - "patternProperties": { - ".+": { - "type": ["string", "number", "null"] - } - }, - "additionalProperties": false - }, - {"type": "array", "items": {"type": "string"}, "uniqueItems": true} - ] - }, - - "labels": { - "oneOf": [ - { - "type": "object", - "patternProperties": { - ".+": { - "type": "string" - } - }, - "additionalProperties": false - }, - {"type": "array", "items": {"type": "string"}, "uniqueItems": true} - ] - }, - - "constraints": { - "service": { - "id": "#/definitions/constraints/service", - "anyOf": [ - {"required": ["build"]}, - {"required": ["image"]} - ], - "properties": { - "build": { - "required": ["context"] - } - } - } - } - } -} diff --git a/compose/config/config_schema_v3.5.json b/compose/config/config_schema_v3.5.json deleted file mode 100644 index e3bdecbc1c5..00000000000 --- a/compose/config/config_schema_v3.5.json +++ /dev/null @@ -1,588 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "config_schema_v3.5.json", - "type": "object", - "required": ["version"], - - "properties": { - "version": { - "type": "string" - }, - - "services": { - "id": "#/properties/services", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/service" - } - }, - "additionalProperties": false - }, - - "networks": { - "id": "#/properties/networks", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/network" - } - } - }, - - "volumes": { - "id": "#/properties/volumes", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/volume" - } - }, - "additionalProperties": false - }, - - "secrets": { - "id": "#/properties/secrets", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/secret" - } - }, - "additionalProperties": false - }, - - "configs": { - "id": "#/properties/configs", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/config" - } - }, - "additionalProperties": false - } - }, - - "patternProperties": {"^x-": {}}, - "additionalProperties": false, - - "definitions": { - - "service": { - "id": "#/definitions/service", - "type": "object", - - "properties": { - "deploy": {"$ref": "#/definitions/deployment"}, - "build": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "properties": { - "context": {"type": "string"}, - "dockerfile": {"type": "string"}, - "args": {"$ref": "#/definitions/list_or_dict"}, - "labels": {"$ref": "#/definitions/labels"}, - "cache_from": {"$ref": "#/definitions/list_of_strings"}, - "network": {"type": "string"}, - "target": {"type": "string"}, - "shm_size": {"type": ["integer", "string"]} - }, - "additionalProperties": false - } - ] - }, - "cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "cgroup_parent": {"type": "string"}, - "command": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "configs": { - "type": "array", - "items": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "properties": { - "source": {"type": "string"}, - "target": {"type": "string"}, - "uid": {"type": "string"}, - "gid": {"type": "string"}, - "mode": {"type": "number"} - } - } - ] - } - }, - "container_name": {"type": "string"}, - "credential_spec": {"type": "object", "properties": { - "file": {"type": "string"}, - "registry": {"type": "string"} - }}, - "depends_on": {"$ref": "#/definitions/list_of_strings"}, - "devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "dns": {"$ref": "#/definitions/string_or_list"}, - "dns_search": {"$ref": "#/definitions/string_or_list"}, - "domainname": {"type": "string"}, - "entrypoint": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "env_file": {"$ref": "#/definitions/string_or_list"}, - "environment": {"$ref": "#/definitions/list_or_dict"}, - - "expose": { - "type": "array", - "items": { - "type": ["string", "number"], - "format": "expose" - }, - "uniqueItems": true - }, - - "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, - "healthcheck": {"$ref": "#/definitions/healthcheck"}, - "hostname": {"type": "string"}, - "image": {"type": "string"}, - "ipc": {"type": "string"}, - "isolation": {"type": "string"}, - "labels": {"$ref": "#/definitions/labels"}, - "links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - - "logging": { - "type": "object", - - "properties": { - "driver": {"type": "string"}, - "options": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number", "null"]} - } - } - }, - "additionalProperties": false - }, - - "mac_address": {"type": "string"}, - "network_mode": {"type": "string"}, - - "networks": { - "oneOf": [ - {"$ref": "#/definitions/list_of_strings"}, - { - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "oneOf": [ - { - "type": "object", - "properties": { - "aliases": {"$ref": "#/definitions/list_of_strings"}, - "ipv4_address": {"type": "string"}, - "ipv6_address": {"type": "string"} - }, - "additionalProperties": false - }, - {"type": "null"} - ] - } - }, - "additionalProperties": false - } - ] - }, - "pid": {"type": ["string", "null"]}, - - "ports": { - "type": "array", - "items": { - "oneOf": [ - {"type": "number", "format": "ports"}, - {"type": "string", "format": "ports"}, - { - "type": "object", - "properties": { - "mode": {"type": "string"}, - "target": {"type": "integer"}, - "published": {"type": "integer"}, - "protocol": {"type": "string"} - }, - "additionalProperties": false - } - ] - }, - "uniqueItems": true - }, - - "privileged": {"type": "boolean"}, - "read_only": {"type": "boolean"}, - "restart": {"type": "string"}, - "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "shm_size": {"type": ["number", "string"]}, - "secrets": { - "type": "array", - "items": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "properties": { - "source": {"type": "string"}, - "target": {"type": "string"}, - "uid": {"type": "string"}, - "gid": {"type": "string"}, - "mode": {"type": "number"} - } - } - ] - } - }, - "sysctls": {"$ref": "#/definitions/list_or_dict"}, - "stdin_open": {"type": "boolean"}, - "stop_grace_period": {"type": "string", "format": "duration"}, - "stop_signal": {"type": "string"}, - "tmpfs": {"$ref": "#/definitions/string_or_list"}, - "tty": {"type": "boolean"}, - "ulimits": { - "type": "object", - "patternProperties": { - "^[a-z]+$": { - "oneOf": [ - {"type": "integer"}, - { - "type":"object", - "properties": { - "hard": {"type": "integer"}, - "soft": {"type": "integer"} - }, - "required": ["soft", "hard"], - "additionalProperties": false - } - ] - } - } - }, - "user": {"type": "string"}, - "userns_mode": {"type": "string"}, - "volumes": { - "type": "array", - "items": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "required": ["type"], - "properties": { - "type": {"type": "string"}, - "source": {"type": "string"}, - "target": {"type": "string"}, - "read_only": {"type": "boolean"}, - "consistency": {"type": "string"}, - "bind": { - "type": "object", - "properties": { - "propagation": {"type": "string"} - } - }, - "volume": { - "type": "object", - "properties": { - "nocopy": {"type": "boolean"} - } - } - }, - "additionalProperties": false - } - ], - "uniqueItems": true - } - }, - "working_dir": {"type": "string"} - }, - "additionalProperties": false - }, - - "healthcheck": { - "id": "#/definitions/healthcheck", - "type": "object", - "additionalProperties": false, - "properties": { - "disable": {"type": "boolean"}, - "interval": {"type": "string", "format": "duration"}, - "retries": {"type": "number"}, - "test": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "timeout": {"type": "string", "format": "duration"}, - "start_period": {"type": "string", "format": "duration"} - } - }, - "deployment": { - "id": "#/definitions/deployment", - "type": ["object", "null"], - "properties": { - "mode": {"type": "string"}, - "endpoint_mode": {"type": "string"}, - "replicas": {"type": "integer"}, - "labels": {"$ref": "#/definitions/labels"}, - "update_config": { - "type": "object", - "properties": { - "parallelism": {"type": "integer"}, - "delay": {"type": "string", "format": "duration"}, - "failure_action": {"type": "string"}, - "monitor": {"type": "string", "format": "duration"}, - "max_failure_ratio": {"type": "number"}, - "order": {"type": "string", "enum": [ - "start-first", "stop-first" - ]} - }, - "additionalProperties": false - }, - "resources": { - "type": "object", - "properties": { - "limits": { - "type": "object", - "properties": { - "cpus": {"type": "string"}, - "memory": {"type": "string"} - }, - "additionalProperties": false - }, - "reservations": { - "type": "object", - "properties": { - "cpus": {"type": "string"}, - "memory": {"type": "string"}, - "generic_resources": {"$ref": "#/definitions/generic_resources"} - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - "restart_policy": { - "type": "object", - "properties": { - "condition": {"type": "string"}, - "delay": {"type": "string", "format": "duration"}, - "max_attempts": {"type": "integer"}, - "window": {"type": "string", "format": "duration"} - }, - "additionalProperties": false - }, - "placement": { - "type": "object", - "properties": { - "constraints": {"type": "array", "items": {"type": "string"}}, - "preferences": { - "type": "array", - "items": { - "type": "object", - "properties": { - "spread": {"type": "string"} - }, - "additionalProperties": false - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - - "generic_resources": { - "id": "#/definitions/generic_resources", - "type": "array", - "items": { - "type": "object", - "properties": { - "discrete_resource_spec": { - "type": "object", - "properties": { - "kind": {"type": "string"}, - "value": {"type": "number"} - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - }, - - "network": { - "id": "#/definitions/network", - "type": ["object", "null"], - "properties": { - "name": {"type": "string"}, - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "ipam": { - "type": "object", - "properties": { - "driver": {"type": "string"}, - "config": { - "type": "array", - "items": { - "type": "object", - "properties": { - "subnet": {"type": "string", "format": "subnet_ip_address"} - }, - "additionalProperties": false - } - } - }, - "additionalProperties": false - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - }, - "internal": {"type": "boolean"}, - "attachable": {"type": "boolean"}, - "labels": {"$ref": "#/definitions/labels"} - }, - "additionalProperties": false - }, - - "volume": { - "id": "#/definitions/volume", - "type": ["object", "null"], - "properties": { - "name": {"type": "string"}, - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - }, - "labels": {"$ref": "#/definitions/labels"} - }, - "additionalProperties": false - }, - - "secret": { - "id": "#/definitions/secret", - "type": "object", - "properties": { - "name": {"type": "string"}, - "file": {"type": "string"}, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - } - }, - "labels": {"$ref": "#/definitions/labels"} - }, - "additionalProperties": false - }, - - "config": { - "id": "#/definitions/config", - "type": "object", - "properties": { - "name": {"type": "string"}, - "file": {"type": "string"}, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - } - }, - "labels": {"$ref": "#/definitions/labels"} - }, - "additionalProperties": false - }, - - "string_or_list": { - "oneOf": [ - {"type": "string"}, - {"$ref": "#/definitions/list_of_strings"} - ] - }, - - "list_of_strings": { - "type": "array", - "items": {"type": "string"}, - "uniqueItems": true - }, - - "list_or_dict": { - "oneOf": [ - { - "type": "object", - "patternProperties": { - ".+": { - "type": ["string", "number", "null"] - } - }, - "additionalProperties": false - }, - {"type": "array", "items": {"type": "string"}, "uniqueItems": true} - ] - }, - - "labels": { - "oneOf": [ - { - "type": "object", - "patternProperties": { - ".+": { - "type": "string" - } - }, - "additionalProperties": false - }, - {"type": "array", "items": {"type": "string"}, "uniqueItems": true} - ] - }, - - "constraints": { - "service": { - "id": "#/definitions/constraints/service", - "anyOf": [ - {"required": ["build"]}, - {"required": ["image"]} - ], - "properties": { - "build": { - "required": ["context"] - } - } - } - } - } -} diff --git a/compose/config/config_schema_v3.6.json b/compose/config/config_schema_v3.6.json deleted file mode 100644 index 95a552b346c..00000000000 --- a/compose/config/config_schema_v3.6.json +++ /dev/null @@ -1,582 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "config_schema_v3.6.json", - "type": "object", - "required": ["version"], - - "properties": { - "version": { - "type": "string" - }, - - "services": { - "id": "#/properties/services", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/service" - } - }, - "additionalProperties": false - }, - - "networks": { - "id": "#/properties/networks", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/network" - } - } - }, - - "volumes": { - "id": "#/properties/volumes", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/volume" - } - }, - "additionalProperties": false - }, - - "secrets": { - "id": "#/properties/secrets", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/secret" - } - }, - "additionalProperties": false - }, - - "configs": { - "id": "#/properties/configs", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/config" - } - }, - "additionalProperties": false - } - }, - - "patternProperties": {"^x-": {}}, - "additionalProperties": false, - - "definitions": { - - "service": { - "id": "#/definitions/service", - "type": "object", - - "properties": { - "deploy": {"$ref": "#/definitions/deployment"}, - "build": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "properties": { - "context": {"type": "string"}, - "dockerfile": {"type": "string"}, - "args": {"$ref": "#/definitions/list_or_dict"}, - "labels": {"$ref": "#/definitions/list_or_dict"}, - "cache_from": {"$ref": "#/definitions/list_of_strings"}, - "network": {"type": "string"}, - "target": {"type": "string"}, - "shm_size": {"type": ["integer", "string"]} - }, - "additionalProperties": false - } - ] - }, - "cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "cgroup_parent": {"type": "string"}, - "command": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "configs": { - "type": "array", - "items": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "properties": { - "source": {"type": "string"}, - "target": {"type": "string"}, - "uid": {"type": "string"}, - "gid": {"type": "string"}, - "mode": {"type": "number"} - } - } - ] - } - }, - "container_name": {"type": "string"}, - "credential_spec": {"type": "object", "properties": { - "file": {"type": "string"}, - "registry": {"type": "string"} - }}, - "depends_on": {"$ref": "#/definitions/list_of_strings"}, - "devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "dns": {"$ref": "#/definitions/string_or_list"}, - "dns_search": {"$ref": "#/definitions/string_or_list"}, - "domainname": {"type": "string"}, - "entrypoint": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "env_file": {"$ref": "#/definitions/string_or_list"}, - "environment": {"$ref": "#/definitions/list_or_dict"}, - - "expose": { - "type": "array", - "items": { - "type": ["string", "number"], - "format": "expose" - }, - "uniqueItems": true - }, - - "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, - "healthcheck": {"$ref": "#/definitions/healthcheck"}, - "hostname": {"type": "string"}, - "image": {"type": "string"}, - "ipc": {"type": "string"}, - "isolation": {"type": "string"}, - "labels": {"$ref": "#/definitions/list_or_dict"}, - "links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - - "logging": { - "type": "object", - - "properties": { - "driver": {"type": "string"}, - "options": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number", "null"]} - } - } - }, - "additionalProperties": false - }, - - "mac_address": {"type": "string"}, - "network_mode": {"type": "string"}, - - "networks": { - "oneOf": [ - {"$ref": "#/definitions/list_of_strings"}, - { - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "oneOf": [ - { - "type": "object", - "properties": { - "aliases": {"$ref": "#/definitions/list_of_strings"}, - "ipv4_address": {"type": "string"}, - "ipv6_address": {"type": "string"} - }, - "additionalProperties": false - }, - {"type": "null"} - ] - } - }, - "additionalProperties": false - } - ] - }, - "pid": {"type": ["string", "null"]}, - - "ports": { - "type": "array", - "items": { - "oneOf": [ - {"type": "number", "format": "ports"}, - {"type": "string", "format": "ports"}, - { - "type": "object", - "properties": { - "mode": {"type": "string"}, - "target": {"type": "integer"}, - "published": {"type": "integer"}, - "protocol": {"type": "string"} - }, - "additionalProperties": false - } - ] - }, - "uniqueItems": true - }, - - "privileged": {"type": "boolean"}, - "read_only": {"type": "boolean"}, - "restart": {"type": "string"}, - "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "shm_size": {"type": ["number", "string"]}, - "secrets": { - "type": "array", - "items": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "properties": { - "source": {"type": "string"}, - "target": {"type": "string"}, - "uid": {"type": "string"}, - "gid": {"type": "string"}, - "mode": {"type": "number"} - } - } - ] - } - }, - "sysctls": {"$ref": "#/definitions/list_or_dict"}, - "stdin_open": {"type": "boolean"}, - "stop_grace_period": {"type": "string", "format": "duration"}, - "stop_signal": {"type": "string"}, - "tmpfs": {"$ref": "#/definitions/string_or_list"}, - "tty": {"type": "boolean"}, - "ulimits": { - "type": "object", - "patternProperties": { - "^[a-z]+$": { - "oneOf": [ - {"type": "integer"}, - { - "type":"object", - "properties": { - "hard": {"type": "integer"}, - "soft": {"type": "integer"} - }, - "required": ["soft", "hard"], - "additionalProperties": false - } - ] - } - } - }, - "user": {"type": "string"}, - "userns_mode": {"type": "string"}, - "volumes": { - "type": "array", - "items": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "required": ["type"], - "properties": { - "type": {"type": "string"}, - "source": {"type": "string"}, - "target": {"type": "string"}, - "read_only": {"type": "boolean"}, - "consistency": {"type": "string"}, - "bind": { - "type": "object", - "properties": { - "propagation": {"type": "string"} - } - }, - "volume": { - "type": "object", - "properties": { - "nocopy": {"type": "boolean"} - } - }, - "tmpfs": { - "type": "object", - "properties": { - "size": { - "type": "integer", - "minimum": 0 - } - } - } - }, - "additionalProperties": false - } - ], - "uniqueItems": true - } - }, - "working_dir": {"type": "string"} - }, - "additionalProperties": false - }, - - "healthcheck": { - "id": "#/definitions/healthcheck", - "type": "object", - "additionalProperties": false, - "properties": { - "disable": {"type": "boolean"}, - "interval": {"type": "string", "format": "duration"}, - "retries": {"type": "number"}, - "test": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "timeout": {"type": "string", "format": "duration"}, - "start_period": {"type": "string", "format": "duration"} - } - }, - "deployment": { - "id": "#/definitions/deployment", - "type": ["object", "null"], - "properties": { - "mode": {"type": "string"}, - "endpoint_mode": {"type": "string"}, - "replicas": {"type": "integer"}, - "labels": {"$ref": "#/definitions/list_or_dict"}, - "update_config": { - "type": "object", - "properties": { - "parallelism": {"type": "integer"}, - "delay": {"type": "string", "format": "duration"}, - "failure_action": {"type": "string"}, - "monitor": {"type": "string", "format": "duration"}, - "max_failure_ratio": {"type": "number"}, - "order": {"type": "string", "enum": [ - "start-first", "stop-first" - ]} - }, - "additionalProperties": false - }, - "resources": { - "type": "object", - "properties": { - "limits": { - "type": "object", - "properties": { - "cpus": {"type": "string"}, - "memory": {"type": "string"} - }, - "additionalProperties": false - }, - "reservations": { - "type": "object", - "properties": { - "cpus": {"type": "string"}, - "memory": {"type": "string"}, - "generic_resources": {"$ref": "#/definitions/generic_resources"} - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - "restart_policy": { - "type": "object", - "properties": { - "condition": {"type": "string"}, - "delay": {"type": "string", "format": "duration"}, - "max_attempts": {"type": "integer"}, - "window": {"type": "string", "format": "duration"} - }, - "additionalProperties": false - }, - "placement": { - "type": "object", - "properties": { - "constraints": {"type": "array", "items": {"type": "string"}}, - "preferences": { - "type": "array", - "items": { - "type": "object", - "properties": { - "spread": {"type": "string"} - }, - "additionalProperties": false - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - - "generic_resources": { - "id": "#/definitions/generic_resources", - "type": "array", - "items": { - "type": "object", - "properties": { - "discrete_resource_spec": { - "type": "object", - "properties": { - "kind": {"type": "string"}, - "value": {"type": "number"} - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - }, - - "network": { - "id": "#/definitions/network", - "type": ["object", "null"], - "properties": { - "name": {"type": "string"}, - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "ipam": { - "type": "object", - "properties": { - "driver": {"type": "string"}, - "config": { - "type": "array", - "items": { - "type": "object", - "properties": { - "subnet": {"type": "string"} - }, - "additionalProperties": false - } - } - }, - "additionalProperties": false - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - }, - "internal": {"type": "boolean"}, - "attachable": {"type": "boolean"}, - "labels": {"$ref": "#/definitions/list_or_dict"} - }, - "additionalProperties": false - }, - - "volume": { - "id": "#/definitions/volume", - "type": ["object", "null"], - "properties": { - "name": {"type": "string"}, - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - }, - "labels": {"$ref": "#/definitions/list_or_dict"} - }, - "additionalProperties": false - }, - - "secret": { - "id": "#/definitions/secret", - "type": "object", - "properties": { - "name": {"type": "string"}, - "file": {"type": "string"}, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - } - }, - "labels": {"$ref": "#/definitions/list_or_dict"} - }, - "additionalProperties": false - }, - - "config": { - "id": "#/definitions/config", - "type": "object", - "properties": { - "name": {"type": "string"}, - "file": {"type": "string"}, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - } - }, - "labels": {"$ref": "#/definitions/list_or_dict"} - }, - "additionalProperties": false - }, - - "string_or_list": { - "oneOf": [ - {"type": "string"}, - {"$ref": "#/definitions/list_of_strings"} - ] - }, - - "list_of_strings": { - "type": "array", - "items": {"type": "string"}, - "uniqueItems": true - }, - - "list_or_dict": { - "oneOf": [ - { - "type": "object", - "patternProperties": { - ".+": { - "type": ["string", "number", "null"] - } - }, - "additionalProperties": false - }, - {"type": "array", "items": {"type": "string"}, "uniqueItems": true} - ] - }, - - "constraints": { - "service": { - "id": "#/definitions/constraints/service", - "anyOf": [ - {"required": ["build"]}, - {"required": ["image"]} - ], - "properties": { - "build": { - "required": ["context"] - } - } - } - } - } -} diff --git a/compose/config/environment.py b/compose/config/environment.py index 6afbfc97244..5045a730b77 100644 --- a/compose/config/environment.py +++ b/compose/config/environment.py @@ -1,12 +1,8 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import logging import os import re import dotenv -import six from ..const import IS_WINDOWS_PLATFORM from .errors import ConfigurationError @@ -16,7 +12,7 @@ def split_env(env): - if isinstance(env, six.binary_type): + if isinstance(env, bytes): env = env.decode('utf-8', 'replace') key = value = None if '=' in env: @@ -30,7 +26,7 @@ def split_env(env): return key, value -def env_vars_from_file(filename): +def env_vars_from_file(filename, interpolate=True): """ Read in a line delimited file of environment variables. """ @@ -39,12 +35,15 @@ def env_vars_from_file(filename): elif not os.path.isfile(filename): raise EnvFileNotFound("{} is not a file.".format(filename)) - return dotenv.dotenv_values(dotenv_path=filename, encoding='utf-8-sig') + env = dotenv.dotenv_values(dotenv_path=filename, encoding='utf-8-sig', interpolate=interpolate) + for k, v in env.items(): + env[k] = v if interpolate else v.replace('$', '$$') + return env class Environment(dict): def __init__(self, *args, **kwargs): - super(Environment, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.missing_keys = [] self.silent = False @@ -55,9 +54,10 @@ def _initialize(): if base_dir is None: return result if env_file: - env_file_path = os.path.join(base_dir, env_file) - else: - env_file_path = os.path.join(base_dir, '.env') + env_file_path = os.path.join(os.getcwd(), env_file) + return cls(env_vars_from_file(env_file_path)) + + env_file_path = os.path.join(base_dir, '.env') try: return cls(env_vars_from_file(env_file_path)) except EnvFileNotFound: @@ -82,11 +82,11 @@ def from_command_line(cls, parsed_env_opts): def __getitem__(self, key): try: - return super(Environment, self).__getitem__(key) + return super().__getitem__(key) except KeyError: if IS_WINDOWS_PLATFORM: try: - return super(Environment, self).__getitem__(key.upper()) + return super().__getitem__(key.upper()) except KeyError: pass if not self.silent and key not in self.missing_keys: @@ -99,28 +99,28 @@ def __getitem__(self, key): return "" def __contains__(self, key): - result = super(Environment, self).__contains__(key) + result = super().__contains__(key) if IS_WINDOWS_PLATFORM: return ( - result or super(Environment, self).__contains__(key.upper()) + result or super().__contains__(key.upper()) ) return result def get(self, key, *args, **kwargs): if IS_WINDOWS_PLATFORM: - return super(Environment, self).get( + return super().get( key, - super(Environment, self).get(key.upper(), *args, **kwargs) + super().get(key.upper(), *args, **kwargs) ) - return super(Environment, self).get(key, *args, **kwargs) + return super().get(key, *args, **kwargs) - def get_boolean(self, key): + def get_boolean(self, key, default=False): # Convert a value to a boolean using "common sense" rules. # Unset, empty, "0" and "false" (i-case) yield False. # All other values yield True. value = self.get(key) if not value: - return False + return default if value.lower() in ['0', 'false']: return False return True diff --git a/compose/config/errors.py b/compose/config/errors.py index 9b2078f2c6d..b66433a7998 100644 --- a/compose/config/errors.py +++ b/compose/config/errors.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - - VERSION_EXPLANATION = ( 'You might be seeing this error because you\'re using the wrong Compose file version. ' 'Either specify a supported version (e.g "2.2" or "3.3") and place ' @@ -42,7 +38,7 @@ def msg(self): class ComposeFileNotFound(ConfigurationError): def __init__(self, supported_filenames): - super(ComposeFileNotFound, self).__init__(""" + super().__init__(""" Can't find a suitable configuration file in this directory or any parent. Are you in the right directory? @@ -53,7 +49,7 @@ def __init__(self, supported_filenames): class DuplicateOverrideFileFound(ConfigurationError): def __init__(self, override_filenames): self.override_filenames = override_filenames - super(DuplicateOverrideFileFound, self).__init__( + super().__init__( "Multiple override files found: {}. You may only use a single " "override file.".format(", ".join(override_filenames)) ) diff --git a/compose/config/interpolation.py b/compose/config/interpolation.py index 18be8562cbb..ff0d90e8332 100644 --- a/compose/config/interpolation.py +++ b/compose/config/interpolation.py @@ -1,14 +1,9 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import logging import re from string import Template -import six - from .errors import ConfigurationError -from compose.const import COMPOSEFILE_V2_0 as V2_0 +from compose.const import COMPOSEFILE_V1 as V1 from compose.utils import parse_bytes from compose.utils import parse_nanoseconds_int @@ -16,7 +11,7 @@ log = logging.getLogger(__name__) -class Interpolator(object): +class Interpolator: def __init__(self, templater, mapping): self.templater = templater @@ -30,21 +25,21 @@ def interpolate(self, string): def interpolate_environment_variables(version, config, section, environment): - if version <= V2_0: + if version == V1: interpolator = Interpolator(Template, environment) else: interpolator = Interpolator(TemplateWithDefaults, environment) def process_item(name, config_dict): - return dict( - (key, interpolate_value(name, key, val, section, interpolator)) + return { + key: interpolate_value(name, key, val, section, interpolator) for key, val in (config_dict or {}).items() - ) + } - return dict( - (name, process_item(name, config_dict or {})) + return { + name: process_item(name, config_dict or {}) for name, config_dict in config.items() - ) + } def get_config_path(config_key, section, name): @@ -77,13 +72,13 @@ def recursive_interpolate(obj, interpolator, config_path): def append(config_path, key): return '{}/{}'.format(config_path, key) - if isinstance(obj, six.string_types): + if isinstance(obj, str): return converter.convert(config_path, interpolator.interpolate(obj)) if isinstance(obj, dict): - return dict( - (key, recursive_interpolate(val, interpolator, append(config_path, key))) - for (key, val) in obj.items() - ) + return { + key: recursive_interpolate(val, interpolator, append(config_path, key)) + for key, val in obj.items() + } if isinstance(obj, list): return [recursive_interpolate(val, interpolator, config_path) for val in obj] return converter.convert(config_path, obj) @@ -91,17 +86,17 @@ def append(config_path, key): class TemplateWithDefaults(Template): pattern = r""" - %(delim)s(?: - (?P%(delim)s) | - (?P%(id)s) | - {(?P%(bid)s)} | + {delim}(?: + (?P{delim}) | + (?P{id}) | + {{(?P{bid})}} | (?P) ) - """ % { - 'delim': re.escape('$'), - 'id': r'[_a-z][_a-z0-9]*', - 'bid': r'[_a-z][_a-z0-9]*(?:(?P:?[-?])[^}]*)?', - } + """.format( + delim=re.escape('$'), + id=r'[_a-z][_a-z0-9]*', + bid=r'[_a-z][_a-z0-9]*(?:(?P:?[-?])[^}]*)?', + ) @staticmethod def process_braced_group(braced, sep, mapping): @@ -116,12 +111,14 @@ def process_braced_group(braced, sep, mapping): var, _, err = braced.partition(':?') result = mapping.get(var) if not result: + err = err or var raise UnsetRequiredSubstitution(err) return result elif '?' == sep: var, _, err = braced.partition('?') if var in mapping: return mapping.get(var) + err = err or var raise UnsetRequiredSubstitution(err) # Modified from python2.7/string.py @@ -138,9 +135,9 @@ def convert(mo): if named is not None: val = mapping[named] - if isinstance(val, six.binary_type): + if isinstance(val, bytes): val = val.decode('utf-8') - return '%s' % (val,) + return '{}'.format(val) if mo.group('escaped') is not None: return self.delimiter if mo.group('invalid') is not None: @@ -177,7 +174,7 @@ def service_path(*args): def to_boolean(s): - if not isinstance(s, six.string_types): + if not isinstance(s, str): return s s = s.lower() if s in ['y', 'yes', 'true', 'on']: @@ -188,11 +185,11 @@ def to_boolean(s): def to_int(s): - if not isinstance(s, six.string_types): + if not isinstance(s, str): return s # We must be able to handle octal representation for `mode` values notably - if six.PY3 and re.match('^0[0-9]+$', s.strip()): + if re.match('^0[0-9]+$', s.strip()): s = '0o' + s[1:] try: return int(s, base=0) @@ -201,7 +198,7 @@ def to_int(s): def to_float(s): - if not isinstance(s, six.string_types): + if not isinstance(s, str): return s try: @@ -224,12 +221,12 @@ def bytes_to_int(s): def to_microseconds(v): - if not isinstance(v, six.string_types): + if not isinstance(v, str): return v return int(parse_nanoseconds_int(v) / 1000) -class ConversionMap(object): +class ConversionMap: map = { service_path('blkio_config', 'weight'): to_int, service_path('blkio_config', 'weight_device', 'weight'): to_int, @@ -246,6 +243,8 @@ class ConversionMap(object): service_path('healthcheck', 'disable'): to_boolean, service_path('deploy', 'labels', PATH_JOKER): to_str, service_path('deploy', 'replicas'): to_int, + service_path('deploy', 'placement', 'max_replicas_per_node'): to_int, + service_path('deploy', 'resources', 'limits', "cpus"): to_float, service_path('deploy', 'update_config', 'parallelism'): to_int, service_path('deploy', 'update_config', 'max_failure_ratio'): to_float, service_path('deploy', 'rollback_config', 'parallelism'): to_int, diff --git a/compose/config/serialize.py b/compose/config/serialize.py index 5776ce957cb..e3295df78ea 100644 --- a/compose/config/serialize.py +++ b/compose/config/serialize.py @@ -1,21 +1,12 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - -import six import yaml from compose.config import types +from compose.const import COMPOSE_SPEC as VERSION from compose.const import COMPOSEFILE_V1 as V1 -from compose.const import COMPOSEFILE_V2_1 as V2_1 -from compose.const import COMPOSEFILE_V2_3 as V2_3 -from compose.const import COMPOSEFILE_V3_0 as V3_0 -from compose.const import COMPOSEFILE_V3_2 as V3_2 -from compose.const import COMPOSEFILE_V3_4 as V3_4 -from compose.const import COMPOSEFILE_V3_5 as V3_5 def serialize_config_type(dumper, data): - representer = dumper.represent_str if six.PY3 else dumper.represent_unicode + representer = dumper.represent_str return representer(data.repr()) @@ -25,9 +16,9 @@ def serialize_dict_type(dumper, data): def serialize_string(dumper, data): """ Ensure boolean-like strings are quoted in the output """ - representer = dumper.represent_str if six.PY3 else dumper.represent_unicode + representer = dumper.represent_str - if isinstance(data, six.binary_type): + if isinstance(data, bytes): data = data.decode('utf-8') if data.lower() in ('y', 'n', 'yes', 'no', 'on', 'off', 'true', 'false'): @@ -53,7 +44,7 @@ def serialize_string_escape_dollar(dumper, data): def denormalize_config(config, image_digests=None): - result = {'version': str(V2_1) if config.version == V1 else str(config.version)} + result = {'version': str(config.config_version)} denormalized_services = [ denormalize_service_dict( service_dict, @@ -76,32 +67,18 @@ def denormalize_config(config, image_digests=None): del conf['external_name'] if 'name' in conf: - if config.version < V2_1 or ( - config.version >= V3_0 and config.version < v3_introduced_name_key(key)): - del conf['name'] - elif 'external' in conf: + if 'external' in conf: conf['external'] = bool(conf['external']) - - if 'attachable' in conf and config.version < V3_2: - # For compatibility mode, this option is invalid in v2 - del conf['attachable'] - return result -def v3_introduced_name_key(key): - if key == 'volumes': - return V3_4 - return V3_5 - - 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) + yaml.SafeDumper.add_representer(str, serialize_string_escape_dollar) else: yaml.SafeDumper.add_representer(str, serialize_string) - yaml.SafeDumper.add_representer(six.text_type, serialize_string) + yaml.SafeDumper.add_representer(str, serialize_string) return yaml.safe_dump( denormalize_config(config, image_digests), default_flow_style=False, @@ -127,7 +104,7 @@ def serialize_ns_time_value(value): result = (int(value), stage[1]) else: break - return '{0}{1}'.format(*result) + return '{}{}'.format(*result) def denormalize_service_dict(service_dict, version, image_digest=None): @@ -144,11 +121,6 @@ def denormalize_service_dict(service_dict, version, image_digest=None): if version == V1 and 'network_mode' not in service_dict: service_dict['network_mode'] = 'bridge' - if 'depends_on' in service_dict and (version < V2_1 or version >= V3_0): - service_dict['depends_on'] = sorted([ - svc for svc in service_dict['depends_on'].keys() - ]) - if 'healthcheck' in service_dict: if 'interval' in service_dict['healthcheck']: service_dict['healthcheck']['interval'] = serialize_ns_time_value( @@ -166,10 +138,10 @@ def denormalize_service_dict(service_dict, version, image_digest=None): if 'ports' in service_dict: service_dict['ports'] = [ - p.legacy_repr() if p.external_ip or version < V3_2 else p + p.legacy_repr() if p.external_ip or version < VERSION else p for p in service_dict['ports'] ] - if 'volumes' in service_dict and (version < V2_3 or (version > V3_0 and version < V3_2)): + if 'volumes' in service_dict and (version == V1): service_dict['volumes'] = [ v.legacy_repr() if isinstance(v, types.MountSpec) else v for v in service_dict['volumes'] ] diff --git a/compose/config/sort_services.py b/compose/config/sort_services.py index 42f548a6dd1..0a7eb2b4fda 100644 --- a/compose/config/sort_services.py +++ b/compose/config/sort_services.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - from compose.config.errors import DependencyError @@ -24,7 +21,7 @@ def get_source_name_from_network_mode(network_mode, source_type): def get_service_names(links): - return [link.split(':')[0] for link in links] + return [link.split(':', 1)[0] for link in links] def get_service_names_from_volumes_from(volumes_from): @@ -39,6 +36,7 @@ def get_service_dependents(service_dict, services): name in get_service_names_from_volumes_from(service.get('volumes_from', [])) or name == get_service_name_from_network_mode(service.get('network_mode')) or name == get_service_name_from_network_mode(service.get('pid')) or + name == get_service_name_from_network_mode(service.get('ipc')) or name in service.get('depends_on', [])) ] diff --git a/compose/config/types.py b/compose/config/types.py index ab8f34e3d7f..f52b5654139 100644 --- a/compose/config/types.py +++ b/compose/config/types.py @@ -1,16 +1,12 @@ """ Types for objects parsed from the configuration. """ -from __future__ import absolute_import -from __future__ import unicode_literals - import json import ntpath import os import re from collections import namedtuple -import six from docker.utils.ports import build_port_bindings from ..const import COMPOSEFILE_V1 as V1 @@ -104,7 +100,7 @@ def serialize_restart_spec(restart_spec): return '' parts = [restart_spec['Name']] if restart_spec['MaximumRetryCount']: - parts.append(six.text_type(restart_spec['MaximumRetryCount'])) + parts.append(str(restart_spec['MaximumRetryCount'])) return ':'.join(parts) @@ -150,7 +146,7 @@ def normpath(path, win_host=False): return path -class MountSpec(object): +class MountSpec: options_map = { 'volume': { 'nocopy': 'no_copy' @@ -326,7 +322,7 @@ def merge_field(self): class ServiceConfigBase(namedtuple('_ServiceConfigBase', 'source target uid gid mode name')): @classmethod def parse(cls, spec): - if isinstance(spec, six.string_types): + if isinstance(spec, str): return cls(spec, None, None, None, None, None) return cls( spec.get('source'), @@ -342,9 +338,9 @@ def merge_field(self): return self.source def repr(self): - return dict( - [(k, v) for k, v in zip(self._fields, self) if v is not None] - ) + return { + k: v for k, v in zip(self._fields, self) if v is not None + } class ServiceSecret(ServiceConfigBase): @@ -364,12 +360,9 @@ def __new__(cls, target, published, *args, **kwargs): raise ConfigurationError('Invalid target port: {}'.format(target)) if published: - if isinstance(published, six.string_types) and '-' in published: # "x-y:z" format + if isinstance(published, str) and '-' in published: # "x-y:z" format a, b = published.split('-', 1) - try: - int(a) - int(b) - except ValueError: + if not a.isdigit() or not b.isdigit(): raise ConfigurationError('Invalid published port: {}'.format(published)) else: try: @@ -377,7 +370,7 @@ def __new__(cls, target, published, *args, **kwargs): except ValueError: raise ConfigurationError('Invalid published port: {}'.format(published)) - return super(ServicePort, cls).__new__( + return super().__new__( cls, target, published, *args, **kwargs ) @@ -426,9 +419,9 @@ def merge_field(self): return (self.target, self.published, self.external_ip, self.protocol) def repr(self): - return dict( - [(k, v) for k, v in zip(self._fields, self) if v is not None] - ) + return { + k: v for k, v in zip(self._fields, self) if v is not None + } def legacy_repr(self): return normalize_port_dict(self.repr()) @@ -477,7 +470,7 @@ def normalize_port_dict(port): class SecurityOpt(namedtuple('_SecurityOpt', 'value src_file')): @classmethod def parse(cls, value): - if not isinstance(value, six.string_types): + if not isinstance(value, str): return value # based on https://github.com/docker/cli/blob/9de1b162f/cli/command/container/opts.go#L673-L697 con = value.split('=', 2) @@ -488,9 +481,9 @@ def parse(cls, value): if con[0] == 'seccomp' and con[1] != 'unconfined': try: - with open(unquote_path(con[1]), 'r') as f: + with open(unquote_path(con[1])) as f: seccomp_data = json.load(f) - except (IOError, ValueError) as e: + except (OSError, ValueError) as e: raise ConfigurationError('Error reading seccomp profile: {}'.format(e)) return cls( 'seccomp={}'.format(json.dumps(seccomp_data)), con[1] diff --git a/compose/config/validation.py b/compose/config/validation.py index 1cceb71f0a4..d9aaeda4bd2 100644 --- a/compose/config/validation.py +++ b/compose/config/validation.py @@ -1,13 +1,9 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import json import logging import os import re import sys -import six from docker.utils.ports import split_port from jsonschema import Draft4Validator from jsonschema import FormatChecker @@ -75,13 +71,13 @@ def format_ports(instance): try: split_port(instance) except ValueError as e: - raise ValidationError(six.text_type(e)) + raise ValidationError(str(e)) return True @FormatChecker.cls_checks(format="expose", raises=ValidationError) def format_expose(instance): - if isinstance(instance, six.string_types): + if isinstance(instance, str): if not re.match(VALID_EXPOSE_FORMAT, instance): raise ValidationError( "should be of the format 'PORT[/PROTOCOL]'") @@ -91,7 +87,7 @@ def format_expose(instance): @FormatChecker.cls_checks("subnet_ip_address", raises=ValidationError) def format_subnet_ip_address(instance): - if isinstance(instance, six.string_types): + if isinstance(instance, str): if not re.match(VALID_REGEX_IPV4_CIDR, instance) and \ not re.match(VALID_REGEX_IPV6_CIDR, instance): raise ValidationError("should use the CIDR format") @@ -104,7 +100,7 @@ def match_named_volumes(service_dict, project_volumes): for volume_spec in service_volumes: if volume_spec.is_named_volume and volume_spec.external not in project_volumes: raise ConfigurationError( - 'Named volume "{0}" is used in service "{1}" but no' + 'Named volume "{}" is used in service "{}" but no' ' declaration was found in the volumes section.'.format( volume_spec.repr(), service_dict.get('name') ) @@ -138,7 +134,7 @@ def validate_config_section(filename, config, section): type=anglicize_json_type(python_type_to_yaml_type(config)))) for key, value in config.items(): - if not isinstance(key, six.string_types): + if not isinstance(key, str): raise ConfigurationError( "In file '{filename}', the {section} name {name} must be a " "quoted string, i.e. '{name}'.".format( @@ -166,7 +162,7 @@ def validate_top_level_object(config_file): def validate_ulimits(service_config): ulimit_config = service_config.config.get('ulimits', {}) - for limit_name, soft_hard_values in six.iteritems(ulimit_config): + for limit_name, soft_hard_values in ulimit_config.items(): if isinstance(soft_hard_values, dict): if not soft_hard_values['soft'] <= soft_hard_values['hard']: raise ConfigurationError( @@ -222,6 +218,21 @@ def validate_pid_mode(service_config, service_names): ) +def validate_ipc_mode(service_config, service_names): + ipc_mode = service_config.config.get('ipc') + if not ipc_mode: + return + + dependency = get_service_name_from_network_mode(ipc_mode) + if not dependency: + return + if dependency not in service_names: + raise ConfigurationError( + "Service '{s.name}' uses the IPC namespace of service '{dep}' which " + "is undefined.".format(s=service_config, dep=dependency) + ) + + def validate_links(service_config, service_names): for link in service_config.config.get('links', []): if link.split(':')[0] not in service_names: @@ -286,7 +297,7 @@ def handle_error_for_schema_with_id(error, path): invalid_config_key = parse_key_from_error_msg(error) return get_unsupported_config_msg(path, invalid_config_key) - if schema_id.startswith('config_schema_v'): + if schema_id.startswith('config_schema_'): invalid_config_key = parse_key_from_error_msg(error) return ('Invalid top-level property "{key}". Valid top-level ' 'sections for this Compose file are: {properties}, and ' @@ -329,7 +340,7 @@ def handle_generic_error(error, path): required_keys) elif error.cause: - error_msg = six.text_type(error.cause) + error_msg = str(error.cause) msg_format = "{path} is invalid: {msg}" elif error.path: @@ -349,7 +360,7 @@ def parse_key_from_error_msg(error): def path_string(path): - return ".".join(c for c in path if isinstance(c, six.string_types)) + return ".".join(c for c in path if isinstance(c, str)) def _parse_valid_types_from_validator(validator): @@ -439,15 +450,29 @@ def process_config_schema_errors(error): return handle_generic_error(error, path) -def validate_against_config_schema(config_file): - schema = load_jsonschema(config_file) +def keys_to_str(config_file): + """ + Non-string keys may break validator with patterned fields. + """ + d = {} + for k, v in config_file.items(): + d[str(k)] = v + if isinstance(v, dict): + d[str(k)] = keys_to_str(v) + return d + + +def validate_against_config_schema(config_file, version): + schema = load_jsonschema(version) + config = keys_to_str(config_file.config) + format_checker = FormatChecker(["ports", "expose", "subnet_ip_address"]) validator = Draft4Validator( schema, resolver=RefResolver(get_resolver_path(), schema), format_checker=format_checker) handle_errors( - validator.iter_errors(config_file.config), + validator.iter_errors(config), process_config_schema_errors, config_file.filename) @@ -457,7 +482,7 @@ def handler(errors): return process_service_constraint_errors( errors, service_name, config_file.version) - schema = load_jsonschema(config_file) + schema = load_jsonschema(config_file.version) validator = Draft4Validator(schema['definitions']['constraints']['service']) handle_errors(validator.iter_errors(config), handler, None) @@ -476,17 +501,20 @@ def get_schema_path(): return os.path.dirname(os.path.abspath(__file__)) -def load_jsonschema(config_file): +def load_jsonschema(version): + name = "compose_spec" + if version == V1: + name = "config_schema_v1" + filename = os.path.join( get_schema_path(), - "config_schema_v{0}.json".format(config_file.version)) + "{}.json".format(name)) if not os.path.exists(filename): raise ConfigurationError( 'Version in "{}" is unsupported. {}' - .format(config_file.filename, VERSION_EXPLANATION)) - - with open(filename, "r") as fh: + .format(filename, VERSION_EXPLANATION)) + with open(filename) as fh: return json.load(fh) @@ -506,7 +534,7 @@ def handle_errors(errors, format_error_func, filename): gone wrong. Process each error and pull out relevant information and re-write helpful error messages that are relevant. """ - errors = list(sorted(errors, key=str)) + errors = sorted(errors, key=str) if not errors: return diff --git a/compose/const.py b/compose/const.py index ab0389ce01e..90cd38e82eb 100644 --- a/compose/const.py +++ b/compose/const.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import sys from .version import ComposeVersion @@ -8,6 +5,7 @@ DEFAULT_TIMEOUT = 10 HTTP_TIMEOUT = 60 IS_WINDOWS_PLATFORM = (sys.platform == "win32") +IS_LINUX_PLATFORM = (sys.platform == "linux") LABEL_CONTAINER_NUMBER = 'com.docker.compose.container-number' LABEL_ONE_OFF = 'com.docker.compose.oneoff' LABEL_PROJECT = 'com.docker.compose.project' @@ -27,51 +25,16 @@ WINDOWS_LONGPATH_PREFIX = '\\\\?\\' COMPOSEFILE_V1 = ComposeVersion('1') -COMPOSEFILE_V2_0 = ComposeVersion('2.0') -COMPOSEFILE_V2_1 = ComposeVersion('2.1') -COMPOSEFILE_V2_2 = ComposeVersion('2.2') -COMPOSEFILE_V2_3 = ComposeVersion('2.3') -COMPOSEFILE_V2_4 = ComposeVersion('2.4') - -COMPOSEFILE_V3_0 = ComposeVersion('3.0') -COMPOSEFILE_V3_1 = ComposeVersion('3.1') -COMPOSEFILE_V3_2 = ComposeVersion('3.2') -COMPOSEFILE_V3_3 = ComposeVersion('3.3') -COMPOSEFILE_V3_4 = ComposeVersion('3.4') -COMPOSEFILE_V3_5 = ComposeVersion('3.5') -COMPOSEFILE_V3_6 = ComposeVersion('3.6') -COMPOSEFILE_V3_7 = ComposeVersion('3.7') +COMPOSE_SPEC = ComposeVersion('3.9') +# minimum DOCKER ENGINE API version needed to support +# features for each compose schema version API_VERSIONS = { COMPOSEFILE_V1: '1.21', - COMPOSEFILE_V2_0: '1.22', - COMPOSEFILE_V2_1: '1.24', - COMPOSEFILE_V2_2: '1.25', - COMPOSEFILE_V2_3: '1.30', - COMPOSEFILE_V2_4: '1.35', - COMPOSEFILE_V3_0: '1.25', - COMPOSEFILE_V3_1: '1.25', - COMPOSEFILE_V3_2: '1.25', - COMPOSEFILE_V3_3: '1.30', - COMPOSEFILE_V3_4: '1.30', - COMPOSEFILE_V3_5: '1.30', - COMPOSEFILE_V3_6: '1.36', - COMPOSEFILE_V3_7: '1.38', + COMPOSE_SPEC: '1.38', } API_VERSION_TO_ENGINE_VERSION = { API_VERSIONS[COMPOSEFILE_V1]: '1.9.0', - API_VERSIONS[COMPOSEFILE_V2_0]: '1.10.0', - API_VERSIONS[COMPOSEFILE_V2_1]: '1.12.0', - API_VERSIONS[COMPOSEFILE_V2_2]: '1.13.0', - API_VERSIONS[COMPOSEFILE_V2_3]: '17.06.0', - API_VERSIONS[COMPOSEFILE_V2_4]: '17.12.0', - API_VERSIONS[COMPOSEFILE_V3_0]: '1.13.0', - API_VERSIONS[COMPOSEFILE_V3_1]: '1.13.0', - API_VERSIONS[COMPOSEFILE_V3_2]: '1.13.0', - API_VERSIONS[COMPOSEFILE_V3_3]: '17.06.0', - API_VERSIONS[COMPOSEFILE_V3_4]: '17.06.0', - API_VERSIONS[COMPOSEFILE_V3_5]: '17.06.0', - API_VERSIONS[COMPOSEFILE_V3_6]: '18.02.0', - API_VERSIONS[COMPOSEFILE_V3_7]: '18.06.0', + API_VERSIONS[COMPOSE_SPEC]: '18.06.0', } diff --git a/compose/container.py b/compose/container.py index 8a2fb240e0d..c1c85d14339 100644 --- a/compose/container.py +++ b/compose/container.py @@ -1,9 +1,5 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - from functools import reduce -import six from docker.errors import ImageNotFound from .const import LABEL_CONTAINER_NUMBER @@ -16,7 +12,7 @@ from .version import ComposeVersion -class Container(object): +class Container: """ Represents a Docker container, constructed from the output of GET /containers/:id:/json. @@ -82,8 +78,8 @@ def service(self): @property def name_without_project(self): - if self.name.startswith('{0}_{1}'.format(self.project, self.service)): - return '{0}_{1}'.format(self.service, self.number if self.number is not None else self.slug) + if self.name.startswith('{}_{}'.format(self.project, self.service)): + return '{}_{}'.format(self.service, self.number if self.number is not None else self.slug) else: return self.name @@ -95,7 +91,7 @@ def number(self): number = self.labels.get(LABEL_CONTAINER_NUMBER) if not number: - raise ValueError("Container {0} does not have a {1} label".format( + raise ValueError("Container {} does not have a {} label".format( self.short_id, LABEL_CONTAINER_NUMBER)) return int(number) @@ -130,7 +126,7 @@ def format_port(private, public): return ', '.join( ','.join(format_port(*item)) - for item in sorted(six.iteritems(self.ports)) + for item in sorted(self.ports.items()) ) @property @@ -190,11 +186,6 @@ def is_paused(self): def log_driver(self): return self.get('HostConfig.LogConfig.Type') - @property - def has_api_logs(self): - log_type = self.log_driver - return not log_type or log_type in ('json-file', 'journald') - @property def human_readable_health_status(self): """ Generate UP status string with up time and health @@ -208,11 +199,7 @@ def human_readable_health_status(self): return status_string def attach_log_stream(self): - """A log stream can only be attached if the container uses a json-file - log driver. - """ - if self.has_api_logs: - self.log_stream = self.attach(stdout=True, stderr=True, stream=True) + self.log_stream = self.attach(stdout=True, stderr=True, stream=True) def get(self, key): """Return a value from the container or None if the value is not set. @@ -228,7 +215,7 @@ def get_value(dictionary, key): return reduce(get_value, key.split('.'), self.dictionary) def get_local_port(self, port, protocol='tcp'): - port = self.ports.get("%s/%s" % (port, protocol)) + port = self.ports.get("{}/{}".format(port, protocol)) return "{HostIp}:{HostPort}".format(**port[0]) if port else None def get_mount(self, mount_dest): @@ -270,7 +257,7 @@ def rename_to_tmp_name(self): """ if not self.name.startswith(self.short_id): self.client.rename( - self.id, '{0}_{1}'.format(self.short_id, self.name) + self.id, '{}_{}'.format(self.short_id, self.name) ) def inspect_if_not_inspected(self): @@ -313,7 +300,7 @@ def has_legacy_proj_name(self, project_name): ) def __repr__(self): - return '' % (self.name, self.id[:6]) + return ''.format(self.name, self.id[:6]) def __eq__(self, other): if type(self) != type(other): diff --git a/compose/errors.py b/compose/errors.py index 415b41e7f04..502b64b897e 100644 --- a/compose/errors.py +++ b/compose/errors.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - - class OperationFailedError(Exception): def __init__(self, reason): self.msg = reason @@ -19,15 +15,20 @@ def __init__(self, reason): class HealthCheckFailed(HealthCheckException): def __init__(self, container_id): - super(HealthCheckFailed, self).__init__( + super().__init__( 'Container "{}" is unhealthy.'.format(container_id) ) class NoHealthCheckConfigured(HealthCheckException): def __init__(self, service_name): - super(NoHealthCheckConfigured, self).__init__( + super().__init__( 'Service "{}" is missing a healthcheck configuration'.format( service_name ) ) + + +class CompletedUnsuccessfully(Exception): + def __init__(self, container_id, exit_code): + self.msg = 'Container "{}" exited with code {}.'.format(container_id, exit_code) diff --git a/compose/state.py b/compose/metrics/__init__.py similarity index 100% rename from compose/state.py rename to compose/metrics/__init__.py diff --git a/compose/metrics/client.py b/compose/metrics/client.py new file mode 100644 index 00000000000..204d7e55c16 --- /dev/null +++ b/compose/metrics/client.py @@ -0,0 +1,64 @@ +import os +from enum import Enum + +import requests +from docker import ContextAPI +from docker.transport import UnixHTTPAdapter + +from compose.const import IS_WINDOWS_PLATFORM + +if IS_WINDOWS_PLATFORM: + from docker.transport import NpipeHTTPAdapter + + +class Status(Enum): + SUCCESS = "success" + FAILURE = "failure" + CANCELED = "canceled" + + +class MetricsSource: + CLI = "docker-compose" + + +if IS_WINDOWS_PLATFORM: + METRICS_SOCKET_FILE = 'npipe://\\\\.\\pipe\\docker_cli' +else: + METRICS_SOCKET_FILE = 'http+unix:///var/run/docker-cli.sock' + + +class MetricsCommand(requests.Session): + """ + Representation of a command in the metrics. + """ + + def __init__(self, command, + context_type=None, status=Status.SUCCESS, + source=MetricsSource.CLI, uri=None): + super().__init__() + self.command = ("compose " + command).strip() if command else "compose --help" + self.context = context_type or ContextAPI.get_current_context().context_type or 'moby' + self.source = source + self.status = status.value + self.uri = uri or os.environ.get("METRICS_SOCKET_FILE", METRICS_SOCKET_FILE) + if IS_WINDOWS_PLATFORM: + self.mount("http+unix://", NpipeHTTPAdapter(self.uri)) + else: + self.mount("http+unix://", UnixHTTPAdapter(self.uri)) + + def send_metrics(self): + try: + return self.post("http+unix://localhost/usage", + json=self.to_map(), + timeout=.05, + headers={'Content-Type': 'application/json'}) + except Exception as e: + return e + + def to_map(self): + return { + 'command': self.command, + 'context': self.context, + 'source': self.source, + 'status': self.status, + } diff --git a/compose/metrics/decorator.py b/compose/metrics/decorator.py new file mode 100644 index 00000000000..3126e6941fa --- /dev/null +++ b/compose/metrics/decorator.py @@ -0,0 +1,21 @@ +import functools + +from compose.metrics.client import MetricsCommand +from compose.metrics.client import Status + + +class metrics: + def __init__(self, command_name=None): + self.command_name = command_name + + def __call__(self, fn): + @functools.wraps(fn, + assigned=functools.WRAPPER_ASSIGNMENTS, + updated=functools.WRAPPER_UPDATES) + def wrapper(*args, **kwargs): + if not self.command_name: + self.command_name = fn.__name__ + result = fn(*args, **kwargs) + MetricsCommand(self.command_name, status=Status.SUCCESS).send_metrics() + return result + return wrapper diff --git a/compose/network.py b/compose/network.py index 84531ecc7b8..a67c703c01f 100644 --- a/compose/network.py +++ b/compose/network.py @@ -1,9 +1,7 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import logging import re from collections import OrderedDict +from operator import itemgetter from docker.errors import NotFound from docker.types import IPAMConfig @@ -27,7 +25,7 @@ ] -class Network(object): +class Network: def __init__(self, client, project, name, driver=None, driver_opts=None, ipam=None, external=False, internal=False, enable_ipv6=False, labels=None, custom_name=False): @@ -54,7 +52,7 @@ def ensure(self): try: self.inspect() log.debug( - 'Network {0} declared as external. No new ' + 'Network {} declared as external. No new ' 'network will be created.'.format(self.name) ) except NotFound: @@ -110,7 +108,7 @@ def inspect(self, legacy=False): def legacy_full_name(self): if self.custom_name: return self.name - return '{0}_{1}'.format( + return '{}_{}'.format( re.sub(r'[_-]', '', self.project), self.name ) @@ -118,7 +116,7 @@ def legacy_full_name(self): def full_name(self): if self.custom_name: return self.name - return '{0}_{1}'.format(self.project, self.name) + return '{}_{}'.format(self.project, self.name) @property def true_name(self): @@ -170,7 +168,7 @@ def create_ipam_config_from_dict(ipam_dict): class NetworkConfigChangedError(ConfigurationError): def __init__(self, net_name, property_name): - super(NetworkConfigChangedError, self).__init__( + super().__init__( 'Network "{}" needs to be recreated - {} has changed'.format( net_name, property_name ) @@ -261,7 +259,7 @@ def build_networks(name, config_data, client): return networks -class ProjectNetworks(object): +class ProjectNetworks: def __init__(self, networks, use_networking): self.networks = networks or {} @@ -302,10 +300,10 @@ def get_network_defs_for_service(service_dict): if 'network_mode' in service_dict: return {} networks = service_dict.get('networks', {'default': None}) - return dict( - (net, (config or {})) + return { + net: (config or {}) for net, config in networks.items() - ) + } def get_network_names_for_service(service_dict): @@ -331,4 +329,4 @@ def get_networks(service_dict, network_definitions): else: # Ensure Compose will pick a consistent primary network if no # priority is set - return OrderedDict(sorted(networks.items(), key=lambda t: t[0])) + return OrderedDict(sorted(networks.items(), key=itemgetter(0))) diff --git a/compose/parallel.py b/compose/parallel.py index e242a318ae7..316e2217ade 100644 --- a/compose/parallel.py +++ b/compose/parallel.py @@ -1,27 +1,25 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - +import _thread as thread import logging import operator import sys +from queue import Empty +from queue import Queue from threading import Lock from threading import Semaphore from threading import Thread from docker.errors import APIError from docker.errors import ImageNotFound -from six.moves import _thread as thread -from six.moves.queue import Empty -from six.moves.queue import Queue +from compose.cli.colors import AnsiMode from compose.cli.colors import green from compose.cli.colors import red from compose.cli.signals import ShutdownException from compose.const import PARALLEL_LIMIT +from compose.errors import CompletedUnsuccessfully from compose.errors import HealthCheckFailed from compose.errors import NoHealthCheckConfigured from compose.errors import OperationFailedError -from compose.utils import get_output_stream log = logging.getLogger(__name__) @@ -29,7 +27,7 @@ STOP = object() -class GlobalLimit(object): +class GlobalLimit: """Simple class to hold a global semaphore limiter for a project. This class should be treated as a singleton that is instantiated when the project is. """ @@ -64,7 +62,8 @@ def parallel_execute_watch(events, writer, errors, results, msg, get_name, fail_ elif isinstance(exception, APIError): errors[get_name(obj)] = exception.explanation writer.write(msg, get_name(obj), 'error', red) - elif isinstance(exception, (OperationFailedError, HealthCheckFailed, NoHealthCheckConfigured)): + elif isinstance(exception, (OperationFailedError, HealthCheckFailed, NoHealthCheckConfigured, + CompletedUnsuccessfully)): errors[get_name(obj)] = exception.msg writer.write(msg, get_name(obj), 'error', red) elif isinstance(exception, UpstreamError): @@ -85,12 +84,9 @@ def parallel_execute(objects, func, get_name, msg, get_deps=None, limit=None, fa in the CLI logs, but don't raise an exception (such as attempting to start 0 containers) """ objects = list(objects) - stream = get_output_stream(sys.stderr) + stream = sys.stderr - if ParallelStreamWriter.instance: - writer = ParallelStreamWriter.instance - else: - writer = ParallelStreamWriter(stream) + writer = ParallelStreamWriter.get_or_assign_instance(ParallelStreamWriter(stream)) for obj in objects: writer.add_object(msg, get_name(obj)) @@ -118,7 +114,7 @@ def _no_deps(x): return [] -class State(object): +class State: """ Holds the state of a partially-complete parallel operation. @@ -140,7 +136,7 @@ def pending(self): return set(self.objects) - self.started - self.finished - self.failed -class NoLimit(object): +class NoLimit: def __enter__(self): pass @@ -247,6 +243,12 @@ def feed_queue(objects, func, get_deps, results, state, limiter): 'not processing'.format(obj) ) results.put((obj, None, e)) + except CompletedUnsuccessfully as e: + log.debug( + 'Service(s) upstream of {} did not completed successfully - ' + 'not processing'.format(obj) + ) + results.put((obj, None, e)) if state.is_done(): results.put(STOP) @@ -256,26 +258,44 @@ class UpstreamError(Exception): pass -class ParallelStreamWriter(object): +class ParallelStreamWriter: """Write out messages for operations happening in parallel. Each operation has its own line, and ANSI code characters are used to jump to the correct line, and write over the line. """ - noansi = False - lock = Lock() + default_ansi_mode = AnsiMode.AUTO + write_lock = Lock() + instance = None + instance_lock = Lock() + + @classmethod + def get_instance(cls): + return cls.instance @classmethod - def set_noansi(cls, value=True): - cls.noansi = value + def get_or_assign_instance(cls, writer): + cls.instance_lock.acquire() + try: + if cls.instance is None: + cls.instance = writer + return cls.instance + finally: + cls.instance_lock.release() + + @classmethod + def set_default_ansi_mode(cls, ansi_mode): + cls.default_ansi_mode = ansi_mode - def __init__(self, stream): + def __init__(self, stream, ansi_mode=None): + if ansi_mode is None: + ansi_mode = self.default_ansi_mode self.stream = stream + self.use_ansi_codes = ansi_mode.use_ansi_codes(stream) self.lines = [] self.width = 0 - ParallelStreamWriter.instance = self def add_object(self, msg, obj_index): if msg is None: @@ -289,7 +309,7 @@ def write_initial(self, msg, obj_index): return self._write_noansi(msg, obj_index, '') def _write_ansi(self, msg, obj_index, status): - self.lock.acquire() + self.write_lock.acquire() position = self.lines.index(msg + obj_index) diff = len(self.lines) - position # move up @@ -301,7 +321,7 @@ def _write_ansi(self, msg, obj_index, status): # move back down self.stream.write("%c[%dB" % (27, diff)) self.stream.flush() - self.lock.release() + self.write_lock.release() def _write_noansi(self, msg, obj_index, status): self.stream.write( @@ -314,17 +334,10 @@ def _write_noansi(self, msg, obj_index, status): def write(self, msg, obj_index, status, color_func): if msg is None: return - if self.noansi: - self._write_noansi(msg, obj_index, status) - else: + if self.use_ansi_codes: self._write_ansi(msg, obj_index, color_func(status)) - - -def get_stream_writer(): - instance = ParallelStreamWriter.instance - if instance is None: - raise RuntimeError('ParallelStreamWriter has not yet been instantiated') - return instance + else: + self._write_noansi(msg, obj_index, status) def parallel_operation(containers, operation, options, message): diff --git a/compose/progress_stream.py b/compose/progress_stream.py index 522ddf75d19..3c03cc4b5b9 100644 --- a/compose/progress_stream.py +++ b/compose/progress_stream.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - from compose import utils @@ -18,7 +15,7 @@ def write_to_stream(s, stream): def stream_output(output, stream): is_terminal = hasattr(stream, 'isatty') and stream.isatty() - stream = utils.get_output_stream(stream) + stream = stream lines = {} diff = 0 @@ -82,19 +79,19 @@ def print_output_event(event, stream, is_terminal): status = event.get('status', '') if 'progress' in event: - write_to_stream("%s %s%s" % (status, event['progress'], terminator), stream) + write_to_stream("{} {}{}".format(status, event['progress'], terminator), stream) elif 'progressDetail' in event: detail = event['progressDetail'] total = detail.get('total') if 'current' in detail and total: percentage = float(detail['current']) / float(total) * 100 - write_to_stream('%s (%.1f%%)%s' % (status, percentage, terminator), stream) + write_to_stream('{} ({:.1f}%){}'.format(status, percentage, terminator), stream) else: - write_to_stream('%s%s' % (status, terminator), stream) + write_to_stream('{}{}'.format(status, terminator), stream) elif 'stream' in event: - write_to_stream("%s%s" % (event['stream'], terminator), stream) + write_to_stream("{}{}".format(event['stream'], terminator), stream) else: - write_to_stream("%s%s\n" % (status, terminator), stream) + write_to_stream("{}{}\n".format(status, terminator), stream) def get_digest_from_pull(events): diff --git a/compose/project.py b/compose/project.py index 696c8b04023..ee5243adf62 100644 --- a/compose/project.py +++ b/compose/project.py @@ -1,15 +1,11 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import datetime +import enum import logging import operator import re from functools import reduce from os import path -import enum -import six from docker.errors import APIError from docker.errors import ImageNotFound from docker.errors import NotFound @@ -30,16 +26,20 @@ from .network import ProjectNetworks from .progress_stream import read_status from .service import BuildAction +from .service import ContainerIpcMode from .service import ContainerNetworkMode from .service import ContainerPidMode from .service import ConvergenceStrategy +from .service import IpcMode from .service import NetworkMode from .service import NoSuchImageError from .service import parse_repository_tag from .service import PidMode from .service import Service +from .service import ServiceIpcMode from .service import ServiceNetworkMode from .service import ServicePidMode +from .utils import filter_attached_for_up from .utils import microseconds_from_time_nano from .utils import truncate_string from .volume import ProjectVolumes @@ -56,38 +56,41 @@ class OneOffFilter(enum.Enum): @classmethod def update_labels(cls, value, labels): if value == cls.only: - labels.append('{0}={1}'.format(LABEL_ONE_OFF, "True")) + labels.append('{}={}'.format(LABEL_ONE_OFF, "True")) elif value == cls.exclude: - labels.append('{0}={1}'.format(LABEL_ONE_OFF, "False")) + labels.append('{}={}'.format(LABEL_ONE_OFF, "False")) elif value == cls.include: pass else: raise ValueError("Invalid value for one_off: {}".format(repr(value))) -class Project(object): +class Project: """ A collection of services. """ - def __init__(self, name, services, client, networks=None, volumes=None, config_version=None): + def __init__(self, name, services, client, networks=None, volumes=None, config_version=None, + enabled_profiles=None): self.name = name self.services = services self.client = client self.volumes = volumes or ProjectVolumes({}) self.networks = networks or ProjectNetworks({}, False) self.config_version = config_version + self.enabled_profiles = enabled_profiles or [] def labels(self, one_off=OneOffFilter.exclude, legacy=False): name = self.name if legacy: name = re.sub(r'[_-]', '', name) - labels = ['{0}={1}'.format(LABEL_PROJECT, name)] + labels = ['{}={}'.format(LABEL_PROJECT, name)] OneOffFilter.update_labels(one_off, labels) return labels @classmethod - def from_config(cls, name, config_data, client, default_platform=None, extra_labels=None): + def from_config(cls, name, config_data, client, default_platform=None, extra_labels=None, + enabled_profiles=None): """ Construct a Project from a config.Config object. """ @@ -99,7 +102,7 @@ def from_config(cls, name, config_data, client, default_platform=None, extra_lab networks, use_networking) volumes = ProjectVolumes.from_config(name, config_data, client) - project = cls(name, [], client, project_networks, volumes, config_data.version) + project = cls(name, [], client, project_networks, volumes, config_data.version, enabled_profiles) for service_dict in config_data.services: service_dict = dict(service_dict) @@ -110,6 +113,7 @@ def from_config(cls, name, config_data, client, default_platform=None, extra_lab service_dict.pop('networks', None) links = project.get_links(service_dict) + ipc_mode = project.get_ipc_mode(service_dict) network_mode = project.get_network_mode( service_dict, list(service_networks.keys()) ) @@ -127,6 +131,18 @@ def from_config(cls, name, config_data, client, default_platform=None, extra_lab service_dict.pop('secrets', None) or [], config_data.secrets) + service_dict['scale'] = project.get_service_scale(service_dict) + service_dict['device_requests'] = project.get_device_requests(service_dict) + service_dict = translate_credential_spec_to_security_opt(service_dict) + service_dict, ignored_keys = translate_deploy_keys_to_container_config( + service_dict + ) + if ignored_keys: + log.warning( + 'The following deploy sub-keys are not supported and have' + ' been ignored: {}'.format(', '.join(ignored_keys)) + ) + project.services.append( Service( service_dict.pop('name'), @@ -139,6 +155,7 @@ def from_config(cls, name, config_data, client, default_platform=None, extra_lab volumes_from=volumes_from, secrets=secrets, pid_mode=pid_mode, + ipc_mode=ipc_mode, platform=service_dict.pop('platform', None), default_platform=default_platform, extra_labels=extra_labels, @@ -172,7 +189,7 @@ def validate_service_names(self, service_names): if name not in valid_names: raise NoSuchService(name) - def get_services(self, service_names=None, include_deps=False): + def get_services(self, service_names=None, include_deps=False, auto_enable_profiles=True): """ Returns a list of this project's services filtered by the provided list of names, or all services if service_names is None @@ -185,15 +202,36 @@ def get_services(self, service_names=None, include_deps=False): reordering as needed to resolve dependencies. Raises NoSuchService if any of the named services do not exist. + + Raises ConfigurationError if any service depended on is not enabled by active profiles """ + # create a copy so we can *locally* add auto-enabled profiles later + enabled_profiles = self.enabled_profiles.copy() + if service_names is None or len(service_names) == 0: - service_names = self.service_names + auto_enable_profiles = False + service_names = [ + service.name + for service in self.services + if service.enabled_for_profiles(enabled_profiles) + ] unsorted = [self.get_service(name) for name in service_names] services = [s for s in self.services if s in unsorted] + if auto_enable_profiles: + # enable profiles of explicitly targeted services + for service in services: + for profile in service.get_profiles(): + if profile not in enabled_profiles: + enabled_profiles.append(profile) + if include_deps: - services = reduce(self._inject_deps, services, []) + services = reduce( + lambda acc, s: self._inject_deps(acc, s, enabled_profiles), + services, + [] + ) uniques = [] [uniques.append(s) for s in services if s not in uniques] @@ -266,6 +304,83 @@ def get_pid_mode(self, service_dict): return PidMode(pid_mode) + def get_ipc_mode(self, service_dict): + ipc_mode = service_dict.pop('ipc', None) + if not ipc_mode: + return IpcMode(None) + + service_name = get_service_name_from_network_mode(ipc_mode) + if service_name: + return ServiceIpcMode(self.get_service(service_name)) + + container_name = get_container_name_from_network_mode(ipc_mode) + if container_name: + try: + return ContainerIpcMode(Container.from_id(self.client, container_name)) + except APIError: + raise ConfigurationError( + "Service '{name}' uses the IPC namespace of container '{dep}' which " + "does not exist.".format(name=service_dict['name'], dep=container_name) + ) + + return IpcMode(ipc_mode) + + def get_service_scale(self, service_dict): + # service.scale for v2 and deploy.replicas for v3 + scale = service_dict.get('scale', None) + deploy_dict = service_dict.get('deploy', None) + if not deploy_dict: + return 1 if scale is None else scale + + if deploy_dict.get('mode', 'replicated') != 'replicated': + return 1 if scale is None else scale + + replicas = deploy_dict.get('replicas', None) + if scale is not None and replicas is not None: + raise ConfigurationError( + "Both service.scale and service.deploy.replicas are set." + " Only one of them must be set." + ) + if replicas is not None: + scale = replicas + if scale is None: + return 1 + # deploy may contain placement constraints introduced in v3.8 + max_replicas = deploy_dict.get('placement', {}).get( + 'max_replicas_per_node', + scale) + + scale = min(scale, max_replicas) + if max_replicas < scale: + log.warning("Scale is limited to {} ('max_replicas_per_node' field).".format( + max_replicas)) + return scale + + def get_device_requests(self, service_dict): + deploy_dict = service_dict.get('deploy', None) + if not deploy_dict: + return + + resources = deploy_dict.get('resources', None) + if not resources or not resources.get('reservations', None): + return + devices = resources['reservations'].get('devices') + if not devices: + return + + for dev in devices: + count = dev.get("count", -1) + if not isinstance(count, int): + if count != "all": + raise ConfigurationError( + 'Invalid value "{}" for devices count'.format(dev["count"]), + '(expected integer or "all")') + dev["count"] = -1 + + if 'capabilities' in dev: + dev['capabilities'] = [dev['capabilities']] + return devices + def start(self, service_names=None, **options): containers = [] @@ -347,10 +462,12 @@ def down( self.remove_images(remove_image_type) def remove_images(self, remove_image_type): - for service in self.get_services(): + for service in self.services: service.remove_image(remove_image_type) def restart(self, service_names=None, **options): + # filter service_names by enabled profiles + service_names = [s.name for s in self.get_services(service_names)] containers = self.containers(service_names, stopped=True) parallel.parallel_execute( @@ -373,7 +490,6 @@ def build(self, service_names=None, no_cache=False, pull=False, force_rm=False, log.info('%s uses an image, skipping' % service.name) if cli: - log.warning("Native build is an experimental feature and could change at any time") if parallel_build: log.warning("Flag '--parallel' is ignored when building with " "COMPOSE_DOCKER_CLI_BUILD=1") @@ -394,7 +510,7 @@ def build_service(service): ) if len(errors): combined_errors = '\n'.join([ - e.decode('utf-8') if isinstance(e, six.binary_type) else e for e in errors.values() + e.decode('utf-8') if isinstance(e, bytes) else e for e in errors.values() ]) raise ProjectError(combined_errors) @@ -486,10 +602,10 @@ def build_container_event(event): 'action': event['status'], 'id': event['Actor']['ID'], 'service': container_attrs.get(LABEL_SERVICE), - 'attributes': dict([ - (k, v) for k, v in container_attrs.items() + 'attributes': { + k: v for k, v in container_attrs.items() if not k.startswith('com.docker.compose.') - ]), + }, 'container': container, } @@ -524,15 +640,16 @@ def up(self, rescale=True, start=True, always_recreate_deps=False, + always_pull=False, reset_container_image=False, renew_anonymous_volumes=False, silent=False, cli=False, + one_off=False, + attach_dependencies=False, + override_options=None, ): - if cli: - log.warning("Native build is an experimental feature and could change at any time") - self.initialize() if not ignore_orphans: self.find_orphan_containers(remove_orphans) @@ -545,21 +662,31 @@ def up(self, include_deps=start_deps) for svc in services: - svc.ensure_image_exists(do_build=do_build, silent=silent, cli=cli) + svc.ensure_image_exists(do_build=do_build, always_pull=always_pull, silent=silent, cli=cli) plans = self._get_convergence_plans( - services, strategy, always_recreate_deps=always_recreate_deps) + services, + strategy, + always_recreate_deps=always_recreate_deps, + one_off=service_names if one_off else [], + ) - def do(service): + services_to_attach = filter_attached_for_up( + services, + service_names, + attach_dependencies, + lambda service: service.name) + def do(service): return service.execute_convergence_plan( plans[service.name], timeout=timeout, - detached=detached, + detached=detached or (service not in services_to_attach), scale_override=scale_override.get(service.name), rescale=rescale, start=start, reset_container_image=reset_container_image, renew_anonymous_volumes=renew_anonymous_volumes, + override_options=override_options, ) def get_deps(service): @@ -591,7 +718,7 @@ def initialize(self): self.networks.initialize() self.volumes.initialize() - def _get_convergence_plans(self, services, strategy, always_recreate_deps=False): + def _get_convergence_plans(self, services, strategy, always_recreate_deps=False, one_off=None): plans = {} for service in services: @@ -601,6 +728,7 @@ def _get_convergence_plans(self, services, strategy, always_recreate_deps=False) if name in plans and plans[name].action in ('recreate', 'create') ] + is_one_off = one_off and service.name in one_off if updated_dependencies and strategy.allows_recreate: log.debug('%s has upstream changes (%s)', @@ -612,17 +740,17 @@ def _get_convergence_plans(self, services, strategy, always_recreate_deps=False) 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) + plan = service.convergence_plan(ConvergenceStrategy.always, is_one_off) else: - plan = service.convergence_plan(strategy) + plan = service.convergence_plan(strategy, is_one_off) else: - plan = service.convergence_plan(strategy) + plan = service.convergence_plan(strategy, is_one_off) plans[service.name] = plan return plans - def pull(self, service_names=None, ignore_pull_failures=False, parallel_pull=False, silent=False, + def pull(self, service_names=None, ignore_pull_failures=False, parallel_pull=True, silent=False, include_deps=False): services = self.get_services(service_names, include_deps) @@ -656,7 +784,9 @@ def pull_service(service): return try: - writer = parallel.get_stream_writer() + writer = parallel.ParallelStreamWriter.get_instance() + if writer is None: + raise RuntimeError('ParallelStreamWriter has not yet been instantiated') for event in strm: if 'status' not in event: continue @@ -684,7 +814,7 @@ def pull_service(service): .format(' '.join(must_build))) if len(errors): combined_errors = '\n'.join([ - e.decode('utf-8') if isinstance(e, six.binary_type) else e for e in errors.values() + e.decode('utf-8') if isinstance(e, bytes) else e for e in errors.values() ]) raise ProjectError(combined_errors) @@ -741,7 +871,7 @@ def _find(): return if remove_orphans: for ctnr in orphans: - log.info('Removing orphan container "{0}"'.format(ctnr.name)) + log.info('Removing orphan container "{}"'.format(ctnr.name)) try: ctnr.kill() except APIError: @@ -749,7 +879,7 @@ def _find(): ctnr.remove(force=True) else: log.warning( - 'Found orphan containers ({0}) for this project. If ' + 'Found orphan containers ({}) for this project. If ' 'you removed or renamed this service in your compose ' 'file, you can run this command with the ' '--remove-orphans flag to clean it up.'.format( @@ -757,14 +887,26 @@ def _find(): ) ) - def _inject_deps(self, acc, service): + def _inject_deps(self, acc, service, enabled_profiles): dep_names = service.get_dependency_names() if len(dep_names) > 0: dep_services = self.get_services( service_names=list(set(dep_names)), - include_deps=True + include_deps=True, + auto_enable_profiles=False ) + + for dep in dep_services: + if not dep.enabled_for_profiles(enabled_profiles): + raise ConfigurationError( + 'Service "{dep_name}" was pulled in as a dependency of ' + 'service "{service_name}" but is not enabled by the ' + 'active profiles. ' + 'You may fix this by adding a common profile to ' + '"{dep_name}" and "{service_name}".' + .format(dep_name=dep.name, service_name=service.name) + ) else: dep_services = [] @@ -781,6 +923,81 @@ def container_operation_with_timeout(container): return container_operation_with_timeout +def translate_credential_spec_to_security_opt(service_dict): + result = [] + + if 'credential_spec' in service_dict: + spec = convert_credential_spec_to_security_opt(service_dict['credential_spec']) + result.append('credentialspec={spec}'.format(spec=spec)) + + if result: + service_dict['security_opt'] = result + + return service_dict + + +def translate_resource_keys_to_container_config(resources_dict, service_dict): + if 'limits' in resources_dict: + service_dict['mem_limit'] = resources_dict['limits'].get('memory') + if 'cpus' in resources_dict['limits']: + service_dict['cpus'] = float(resources_dict['limits']['cpus']) + if 'reservations' in resources_dict: + service_dict['mem_reservation'] = resources_dict['reservations'].get('memory') + if 'cpus' in resources_dict['reservations']: + return ['resources.reservations.cpus'] + return [] + + +def convert_restart_policy(name): + try: + return { + 'any': 'always', + 'none': 'no', + 'on-failure': 'on-failure' + }[name] + except KeyError: + raise ConfigurationError('Invalid restart policy "{}"'.format(name)) + + +def convert_credential_spec_to_security_opt(credential_spec): + if 'file' in credential_spec: + return 'file://{file}'.format(file=credential_spec['file']) + return 'registry://{registry}'.format(registry=credential_spec['registry']) + + +def translate_deploy_keys_to_container_config(service_dict): + if 'credential_spec' in service_dict: + del service_dict['credential_spec'] + if 'configs' in service_dict: + del service_dict['configs'] + + if 'deploy' not in service_dict: + return service_dict, [] + + deploy_dict = service_dict['deploy'] + ignored_keys = [ + k for k in ['endpoint_mode', 'labels', 'update_config', 'rollback_config'] + if k in deploy_dict + ] + + if 'restart_policy' in deploy_dict: + service_dict['restart'] = { + 'Name': convert_restart_policy(deploy_dict['restart_policy'].get('condition', 'any')), + 'MaximumRetryCount': deploy_dict['restart_policy'].get('max_attempts', 0) + } + for k in deploy_dict['restart_policy'].keys(): + if k != 'condition' and k != 'max_attempts': + ignored_keys.append('restart_policy.{}'.format(k)) + + ignored_keys.extend( + translate_resource_keys_to_container_config( + deploy_dict.get('resources', {}), service_dict + ) + ) + del service_dict['deploy'] + return service_dict, ignored_keys + + def get_volumes_from(project, service_dict): volumes_from = service_dict.pop('volumes_from', None) if not volumes_from: @@ -820,16 +1037,16 @@ def get_secrets(service, service_secrets, secret_defs): .format(service=service, secret=secret.source)) if secret_def.get('external'): - 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)) + 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.warning( - "Service \"{service}\" uses secret \"{secret}\" with uid, " - "gid, or mode. These fields are not supported by this " - "implementation of the Compose file".format( + 'Service "{service}" uses secret "{secret}" with uid, ' + 'gid, or mode. These fields are not supported by this ' + 'implementation of the Compose file'.format( service=service, secret=secret.source ) ) @@ -837,8 +1054,8 @@ def get_secrets(service, service_secrets, secret_defs): secret_file = secret_def.get('file') if not path.isfile(str(secret_file)): log.warning( - "Service \"{service}\" uses an undefined secret file \"{secret_file}\", " - "the following file should be created \"{secret_file}\"".format( + 'Service "{service}" uses an undefined secret file "{secret_file}", ' + 'the following file should be created "{secret_file}"'.format( service=service, secret_file=secret_file ) ) @@ -934,7 +1151,7 @@ def __init__(self, image_name, service_name): class NoSuchService(Exception): def __init__(self, name): - if isinstance(name, six.binary_type): + if isinstance(name, bytes): name = name.decode('utf-8') self.name = name self.msg = "No such service: %s" % self.name diff --git a/compose/service.py b/compose/service.py index ebe237b8cfc..bd744a8d380 100644 --- a/compose/service.py +++ b/compose/service.py @@ -1,19 +1,15 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - +import enum import itertools -import json import logging import os import re +import subprocess import sys import tempfile from collections import namedtuple from collections import OrderedDict from operator import attrgetter -import enum -import six from docker.errors import APIError from docker.errors import ImageNotFound from docker.errors import NotFound @@ -48,6 +44,7 @@ from .const import NANOCPUS_SCALE from .const import WINDOWS_LONGPATH_PREFIX from .container import Container +from .errors import CompletedUnsuccessfully from .errors import HealthCheckFailed from .errors import NoHealthCheckConfigured from .errors import OperationFailedError @@ -62,10 +59,6 @@ from .utils import unique_everseen from compose.cli.utils import binarystr_to_unicode -if six.PY2: - import subprocess32 as subprocess -else: - import subprocess log = logging.getLogger(__name__) @@ -84,6 +77,7 @@ 'cpuset', 'device_cgroup_rules', 'devices', + 'device_requests', 'dns', 'dns_search', 'dns_opt', @@ -118,6 +112,7 @@ CONDITION_STARTED = 'service_started' CONDITION_HEALTHY = 'service_healthy' +CONDITION_COMPLETED_SUCCESSFULLY = 'service_completed_successfully' class BuildError(Exception): @@ -170,7 +165,7 @@ class BuildAction(enum.Enum): skip = 2 -class Service(object): +class Service: def __init__( self, name, @@ -183,6 +178,7 @@ def __init__( networks=None, secrets=None, scale=1, + ipc_mode=None, pid_mode=None, default_platform=None, extra_labels=None, @@ -194,6 +190,7 @@ def __init__( self.use_networking = use_networking self.links = links or [] self.volumes_from = volumes_from or [] + self.ipc_mode = ipc_mode or IpcMode(None) self.network_mode = network_mode or NetworkMode(None) self.pid_mode = pid_mode or PidMode(None) self.networks = networks or {} @@ -235,10 +232,10 @@ def get_container(self, number=1): """Return a :class:`compose.container.Container` for this service. The container must be active, and match `number`. """ - for container in self.containers(labels=['{0}={1}'.format(LABEL_CONTAINER_NUMBER, number)]): + for container in self.containers(labels=['{}={}'.format(LABEL_CONTAINER_NUMBER, number)]): return container - raise ValueError("No container found for %s_%s" % (self.name, number)) + raise ValueError("No container found for {}_{}".format(self.name, number)) def start(self, **options): containers = self.containers(stopped=True) @@ -346,11 +343,15 @@ def create_container(self, raise OperationFailedError("Cannot create container for service %s: %s" % (self.name, binarystr_to_unicode(ex.explanation))) - def ensure_image_exists(self, do_build=BuildAction.none, silent=False, cli=False): + def ensure_image_exists(self, do_build=BuildAction.none, always_pull=False, silent=False, cli=False): if self.can_be_built() and do_build == BuildAction.force: self.build(cli=cli) return + if not self.can_be_built() and always_pull: + self.pull(silent=silent) + return + try: self.image() return @@ -395,9 +396,12 @@ def platform(self): platform = self.default_platform return platform - def convergence_plan(self, strategy=ConvergenceStrategy.changed): + def convergence_plan(self, strategy=ConvergenceStrategy.changed, one_off=False): containers = self.containers(stopped=True) + if one_off: + return ConvergencePlan('one_off', []) + if not containers: return ConvergencePlan('create', []) @@ -413,7 +417,7 @@ def convergence_plan(self, strategy=ConvergenceStrategy.changed): stopped = [c for c in containers if not c.is_running] if stopped: - return ConvergencePlan('start', stopped) + return ConvergencePlan('start', containers) return ConvergencePlan('noop', containers) @@ -425,7 +429,7 @@ def _containers_have_diverged(self, containers): except NoSuchImageError as e: log.debug( 'Service %s has diverged: %s', - self.name, six.text_type(e), + self.name, str(e), ) return True @@ -446,25 +450,37 @@ def _containers_have_diverged(self, containers): return has_diverged - def _execute_convergence_create(self, scale, detached, start): + def _execute_convergence_create(self, scale, detached, start, one_off=False, override_options=None): i = self._next_container_number() def create_and_start(service, n): - container = service.create_container(number=n, quiet=True) + if one_off: + container = service.create_container(one_off=True, quiet=True, **override_options) + else: + container = service.create_container(number=n, quiet=True) if not detached: container.attach_log_stream() - if start: + if start and not one_off: self.start_container(container) return container + def get_name(service_name): + if one_off: + return "_".join([ + service_name.project, + service_name.service, + "run", + ]) + return self.get_container_name(service_name.service, service_name.number) + containers, errors = parallel_execute( [ ServiceName(self.project, self.name, index) for index in range(i, i + scale) ], lambda service_name: create_and_start(self, service_name.number), - lambda service_name: self.get_container_name(service_name.service, service_name.number), + get_name, "Creating" ) for error in errors.values(): @@ -504,8 +520,9 @@ def _execute_convergence_start(self, containers, scale, timeout, detached, start self._downscale(containers[scale:], timeout) containers = containers[:scale] if start: + stopped = [c for c in containers if not c.is_running] _, errors = parallel_execute( - containers, + stopped, lambda c: self.start_container_if_stopped(c, attach_logs=not detached, quiet=True), lambda c: c.name, "Starting", @@ -535,16 +552,20 @@ def stop_and_remove(container): def execute_convergence_plan(self, plan, timeout=None, detached=False, start=True, scale_override=None, rescale=True, reset_container_image=False, - renew_anonymous_volumes=False): + renew_anonymous_volumes=False, override_options=None): (action, containers) = plan scale = scale_override if scale_override is not None else self.scale_num containers = sorted(containers, key=attrgetter('number')) self.show_scale_warnings(scale) - if action == 'create': + if action in ['create', 'one_off']: return self._execute_convergence_create( - scale, detached, start + scale, + detached, + start, + one_off=(action == 'one_off'), + override_options=override_options ) # The create action needs always needs an initial scale, but otherwise, @@ -628,7 +649,7 @@ def start_container(self, container, use_network_aliases=True): expl = binarystr_to_unicode(ex.explanation) if "driver failed programming external connectivity" in expl: log.warn("Host is already in use by another container") - raise OperationFailedError("Cannot start service %s: %s" % (self.name, expl)) + raise OperationFailedError("Cannot start service {}: {}".format(self.name, expl)) return container @property @@ -696,43 +717,50 @@ def image_id(): 'image_id': image_id(), 'links': self.get_link_names(), 'net': self.network_mode.id, + 'ipc_mode': self.ipc_mode.mode, 'networks': self.networks, 'secrets': self.secrets, 'volumes_from': [ (v.source.name, v.mode) for v in self.volumes_from if isinstance(v.source, Service) - ], + ] } def get_dependency_names(self): net_name = self.network_mode.service_name pid_namespace = self.pid_mode.service_name + ipc_namespace = self.ipc_mode.service_name return ( self.get_linked_service_names() + self.get_volumes_from_names() + ([net_name] if net_name else []) + ([pid_namespace] if pid_namespace else []) + + ([ipc_namespace] if ipc_namespace else []) + list(self.options.get('depends_on', {}).keys()) ) def get_dependency_configs(self): net_name = self.network_mode.service_name pid_namespace = self.pid_mode.service_name + ipc_namespace = self.ipc_mode.service_name - configs = dict( - [(name, None) for name in self.get_linked_service_names()] + configs = { + name: None for name in self.get_linked_service_names() + } + configs.update( + (name, None) for name in self.get_volumes_from_names() ) - configs.update(dict( - [(name, None) for name in self.get_volumes_from_names()] - )) configs.update({net_name: None} if net_name else {}) configs.update({pid_namespace: None} if pid_namespace else {}) + configs.update({ipc_namespace: None} if ipc_namespace else {}) configs.update(self.options.get('depends_on', {})) for svc, config in self.options.get('depends_on', {}).items(): if config['condition'] == CONDITION_STARTED: configs[svc] = lambda s: True elif config['condition'] == CONDITION_HEALTHY: configs[svc] = lambda s: s.is_healthy() + elif config['condition'] == CONDITION_COMPLETED_SUCCESSFULLY: + configs[svc] = lambda s: s.is_completed_successfully() else: # The config schema already prevents this, but it might be # bypassed if Compose is called programmatically. @@ -845,9 +873,9 @@ def _get_container_create_options( add_config_hash = (not one_off and not override_options) slug = generate_random_id() if one_off else None - container_options = dict( - (k, self.options[k]) - for k in DOCKER_CONFIG_KEYS if k in self.options) + container_options = { + k: self.options[k] + for k in DOCKER_CONFIG_KEYS if k in self.options} override_volumes = override_options.pop('volumes', []) container_options.update(override_options) @@ -939,7 +967,7 @@ def _build_container_volume_options(self, previous_container, container_options, ) container_options['environment'].update(affinity) - container_options['volumes'] = dict((v.internal, {}) for v in container_volumes or {}) + container_options['volumes'] = {v.internal: {} for v in container_volumes or {}} if version_gte(self.client.api_version, '1.30'): override_options['mounts'] = [build_mount(v) for v in container_mounts] or None else: @@ -975,7 +1003,7 @@ def _get_container_host_config(self, override_options, one_off=False): blkio_config = convert_blkio_config(options.get('blkio_config', None)) log_config = get_log_config(logging_dict) init_path = None - if isinstance(options.get('init'), six.string_types): + if isinstance(options.get('init'), str): init_path = options.get('init') options['init'] = True @@ -997,6 +1025,7 @@ def _get_container_host_config(self, override_options, one_off=False): privileged=options.get('privileged', False), network_mode=self.network_mode.mode, devices=options.get('devices'), + device_requests=options.get('device_requests'), dns=options.get('dns'), dns_opt=options.get('dns_opt'), dns_search=options.get('dns_search'), @@ -1013,7 +1042,7 @@ def _get_container_host_config(self, override_options, one_off=False): read_only=options.get('read_only'), pid_mode=self.pid_mode.mode, security_opt=security_opt, - ipc_mode=options.get('ipc'), + ipc_mode=self.ipc_mode.mode, cgroup_parent=options.get('cgroup_parent'), cpu_quota=options.get('cpu_quota'), shm_size=options.get('shm_size'), @@ -1082,8 +1111,9 @@ def build(self, no_cache=False, pull=False, force_rm=False, memory=None, build_a 'Impossible to perform platform-targeted builds for API version < 1.35' ) - builder = self.client if not cli else _CLIBuilder(progress) - build_output = builder.build( + builder = _ClientBuilder(self.client) if not cli else _CLIBuilder(progress) + return builder.build( + service=self, path=path, tag=self.image_name, rm=rm, @@ -1104,30 +1134,7 @@ def build(self, no_cache=False, pull=False, force_rm=False, memory=None, build_a gzip=gzip, isolation=build_opts.get('isolation', self.options.get('isolation', None)), platform=self.platform, - ) - - try: - all_events = list(stream_output(build_output, output_stream)) - except StreamOutputError as e: - raise BuildError(self, six.text_type(e)) - - # Ensure the HTTP connection is not reused for another - # streaming command, as the Docker daemon can sometimes - # complain about it - self.client.close() - - image_id = None - - for event in all_events: - if 'stream' in event: - match = re.search(r'Successfully built ([0-9a-f]+)', event.get('stream', '')) - if match: - image_id = match.group(1) - - if image_id is None: - raise BuildError(self, event if all_events else 'Unknown') - - return image_id + output_stream=output_stream) def get_cache_from(self, build_opts): cache_from = build_opts.get('cache_from', None) @@ -1141,9 +1148,9 @@ def can_be_built(self): def labels(self, one_off=False, legacy=False): proj_name = self.project if not legacy else re.sub(r'[_-]', '', self.project) return [ - '{0}={1}'.format(LABEL_PROJECT, proj_name), - '{0}={1}'.format(LABEL_SERVICE, self.name), - '{0}={1}'.format(LABEL_ONE_OFF, "True" if one_off else "False"), + '{}={}'.format(LABEL_PROJECT, proj_name), + '{}={}'.format(LABEL_SERVICE, self.name), + '{}={}'.format(LABEL_ONE_OFF, "True" if one_off else "False"), ] @property @@ -1157,10 +1164,10 @@ def get_container_name(self, service_name, number, slug=None): container_name = build_container_name( self.project, service_name, number, slug, ) - ext_links_origins = [l.split(':')[0] for l in self.options.get('external_links', [])] + ext_links_origins = [link.split(':')[0] for link in self.options.get('external_links', [])] if container_name in ext_links_origins: raise DependencyError( - 'Service {0} has a self-referential external link: {1}'.format( + 'Service {} has a self-referential external link: {}'.format( self.name, container_name ) ) @@ -1215,16 +1222,14 @@ def _do_pull(self, repo, pull_kwargs, silent, ignore_pull_failures): output = self.client.pull(repo, **pull_kwargs) if silent: with open(os.devnull, 'w') as devnull: - for event in stream_output(output, devnull): - yield event + yield from stream_output(output, devnull) else: - for event in stream_output(output, sys.stdout): - yield event + yield from stream_output(output, sys.stdout) except (StreamOutputError, NotFound) as e: if not ignore_pull_failures: raise else: - log.error(six.text_type(e)) + log.error(str(e)) def pull(self, ignore_pull_failures=False, silent=False, stream=False): if 'image' not in self.options: @@ -1237,7 +1242,7 @@ def pull(self, ignore_pull_failures=False, silent=False, stream=False): 'platform': self.platform, } if not silent: - log.info('Pulling %s (%s%s%s)...' % (self.name, repo, separator, tag)) + log.info('Pulling {} ({}{}{})...'.format(self.name, repo, separator, tag)) if kwargs['platform'] and version_lt(self.client.api_version, '1.35'): raise OperationFailedError( @@ -1255,7 +1260,7 @@ def push(self, ignore_push_failures=False): repo, tag, separator = parse_repository_tag(self.options['image']) tag = tag or 'latest' - log.info('Pushing %s (%s%s%s)...' % (self.name, repo, separator, tag)) + log.info('Pushing {} ({}{}{})...'.format(self.name, repo, separator, tag)) output = self.client.push(repo, tag=tag, stream=True) try: @@ -1265,7 +1270,7 @@ def push(self, ignore_push_failures=False): if not ignore_push_failures: raise else: - log.error(six.text_type(e)) + log.error(str(e)) def is_healthy(self): """ Check that all containers for this service report healthy. @@ -1285,6 +1290,21 @@ def is_healthy(self): raise HealthCheckFailed(ctnr.short_id) return result + def is_completed_successfully(self): + """ Check that all containers for this service has completed successfully + Returns false if at least one container does not exited and + raises CompletedUnsuccessfully exception if at least one container + exited with non-zero exit code. + """ + result = True + for ctnr in self.containers(stopped=True): + ctnr.inspect() + if ctnr.get('State.Status') != 'exited': + result = False + elif ctnr.exit_code != 0: + raise CompletedUnsuccessfully(ctnr.short_id, ctnr.exit_code) + return result + def _parse_proxy_config(self): client = self.client if 'proxies' not in client._general_configs: @@ -1310,6 +1330,24 @@ def _parse_proxy_config(self): return result + def get_profiles(self): + if 'profiles' not in self.options: + return [] + + return self.options.get('profiles') + + def enabled_for_profiles(self, enabled_profiles): + # if service has no profiles specified it is always enabled + if 'profiles' not in self.options: + return True + + service_profiles = self.options.get('profiles') + for profile in enabled_profiles: + if profile in service_profiles: + return True + + return False + def short_id_alias_exists(container, network): aliases = container.get( @@ -1317,7 +1355,47 @@ def short_id_alias_exists(container, network): return container.short_id in aliases -class PidMode(object): +class IpcMode: + def __init__(self, mode): + self._mode = mode + + @property + def mode(self): + return self._mode + + @property + def service_name(self): + return None + + +class ServiceIpcMode(IpcMode): + def __init__(self, service): + self.service = service + + @property + def service_name(self): + return self.service.name + + @property + def mode(self): + containers = self.service.containers() + if containers: + return 'container:' + containers[0].id + + log.warning( + "Service %s is trying to use reuse the IPC namespace " + "of another service that is not running." % (self.service_name) + ) + return None + + +class ContainerIpcMode(IpcMode): + def __init__(self, container): + self.container = container + self._mode = 'container:{}'.format(container.id) + + +class PidMode: def __init__(self, mode): self._mode = mode @@ -1357,7 +1435,7 @@ def __init__(self, container): self._mode = 'container:{}'.format(container.id) -class NetworkMode(object): +class NetworkMode: """A `standard` network mode (ex: host, bridge)""" service_name = None @@ -1372,7 +1450,7 @@ def id(self): mode = id -class ContainerNetworkMode(object): +class ContainerNetworkMode: """A network mode that uses a container's network stack.""" service_name = None @@ -1389,7 +1467,7 @@ def mode(self): return 'container:' + self.container.id -class ServiceNetworkMode(object): +class ServiceNetworkMode: """A network mode that uses a service's network stack.""" def __init__(self, service): @@ -1494,10 +1572,10 @@ def get_container_data_volumes(container, volumes_option, tmpfs_option, mounts_o volumes = [] volumes_option = volumes_option or [] - container_mounts = dict( - (mount['Destination'], mount) + container_mounts = { + mount['Destination']: mount for mount in container.get('Mounts') or {} - ) + } image_volumes = [ VolumeSpec.parse(volume) @@ -1549,9 +1627,9 @@ def get_container_data_volumes(container, volumes_option, tmpfs_option, mounts_o def warn_on_masked_volume(volumes_option, container_volumes, service): - container_volumes = dict( - (volume.internal, volume.external) - for volume in container_volumes) + container_volumes = { + volume.internal: volume.external + for volume in container_volumes} for volume in volumes_option: if ( @@ -1631,8 +1709,8 @@ def build_ulimits(ulimit_config): if not ulimit_config: return None ulimits = [] - for limit_name, soft_hard_values in six.iteritems(ulimit_config): - if isinstance(soft_hard_values, six.integer_types): + for limit_name, soft_hard_values in ulimit_config.items(): + if isinstance(soft_hard_values, int): ulimits.append({'name': limit_name, 'soft': soft_hard_values, 'hard': soft_hard_values}) elif isinstance(soft_hard_values, dict): ulimit_dict = {'name': limit_name} @@ -1656,7 +1734,7 @@ def format_environment(environment): def format_env(key, value): if value is None: return key - if isinstance(value, six.binary_type): + if isinstance(value, bytes): value = value.decode('utf-8') return '{key}={value}'.format(key=key, value=value) @@ -1701,37 +1779,89 @@ def convert_blkio_config(blkio_config): continue arr = [] for item in blkio_config[field]: - arr.append(dict([(k.capitalize(), v) for k, v in item.items()])) + arr.append({k.capitalize(): v for k, v in item.items()}) result[field] = arr return result def rewrite_build_path(path): - # python2 os.stat() doesn't support unicode on some UNIX, so we - # encode it to a bytestring to be safe - if not six.PY3 and not IS_WINDOWS_PLATFORM: - path = path.encode('utf8') - if IS_WINDOWS_PLATFORM and not is_url(path) and not path.startswith(WINDOWS_LONGPATH_PREFIX): path = WINDOWS_LONGPATH_PREFIX + os.path.normpath(path) return path -class _CLIBuilder(object): +class _ClientBuilder: + def __init__(self, client): + self.client = client + + def build(self, service, path, tag=None, quiet=False, fileobj=None, + nocache=False, rm=False, timeout=None, + custom_context=False, encoding=None, pull=False, + forcerm=False, dockerfile=None, container_limits=None, + decode=False, buildargs=None, gzip=False, shmsize=None, + labels=None, cache_from=None, target=None, network_mode=None, + squash=None, extra_hosts=None, platform=None, isolation=None, + use_config_proxy=True, output_stream=sys.stdout): + build_output = self.client.build( + path=path, + tag=tag, + nocache=nocache, + rm=rm, + pull=pull, + forcerm=forcerm, + dockerfile=dockerfile, + labels=labels, + cache_from=cache_from, + buildargs=buildargs, + network_mode=network_mode, + target=target, + shmsize=shmsize, + extra_hosts=extra_hosts, + container_limits=container_limits, + gzip=gzip, + isolation=isolation, + platform=platform) + + try: + all_events = list(stream_output(build_output, output_stream)) + except StreamOutputError as e: + raise BuildError(service, str(e)) + + # Ensure the HTTP connection is not reused for another + # streaming command, as the Docker daemon can sometimes + # complain about it + self.client.close() + + image_id = None + + for event in all_events: + if 'stream' in event: + match = re.search(r'Successfully built ([0-9a-f]+)', event.get('stream', '')) + if match: + image_id = match.group(1) + + if image_id is None: + raise BuildError(service, event if all_events else 'Unknown') + + return image_id + + +class _CLIBuilder: def __init__(self, progress): self._progress = progress - def build(self, path, tag=None, quiet=False, fileobj=None, + def build(self, service, path, tag=None, quiet=False, fileobj=None, nocache=False, rm=False, timeout=None, custom_context=False, encoding=None, pull=False, forcerm=False, dockerfile=None, container_limits=None, decode=False, buildargs=None, gzip=False, shmsize=None, labels=None, cache_from=None, target=None, network_mode=None, squash=None, extra_hosts=None, platform=None, isolation=None, - use_config_proxy=True): + use_config_proxy=True, output_stream=sys.stdout): """ Args: + service (str): Service to be built path (str): Path to the directory containing the Dockerfile buildargs (dict): A dictionary of build arguments cache_from (:py:class:`list`): A list of images used for build @@ -1780,10 +1910,11 @@ def build(self, path, tag=None, quiet=False, fileobj=None, configuration file (``~/.docker/config.json`` by default) contains a proxy configuration, the corresponding environment variables will be set in the container being built. + output_stream (writer): stream to use for build logs Returns: A generator for the build output. """ - if dockerfile: + if dockerfile and os.path.isdir(path): dockerfile = os.path.join(path, dockerfile) iidfile = tempfile.mktemp() @@ -1792,43 +1923,41 @@ def build(self, path, tag=None, quiet=False, fileobj=None, command_builder.add_list("--cache-from", cache_from) command_builder.add_arg("--file", dockerfile) command_builder.add_flag("--force-rm", forcerm) + command_builder.add_params("--label", labels) command_builder.add_arg("--memory", container_limits.get("memory")) + command_builder.add_arg("--network", network_mode) command_builder.add_flag("--no-cache", nocache) command_builder.add_arg("--progress", self._progress) command_builder.add_flag("--pull", pull) command_builder.add_arg("--tag", tag) command_builder.add_arg("--target", target) command_builder.add_arg("--iidfile", iidfile) + command_builder.add_arg("--platform", platform) + command_builder.add_arg("--isolation", isolation) + + if extra_hosts: + if isinstance(extra_hosts, dict): + extra_hosts = ["{}:{}".format(host, ip) for host, ip in extra_hosts.items()] + for host in extra_hosts: + command_builder.add_arg("--add-host", "{}".format(host)) + args = command_builder.build([path]) - magic_word = "Successfully built " - appear = False - with subprocess.Popen(args, stdout=subprocess.PIPE, universal_newlines=True) as p: - while True: - line = p.stdout.readline() - if not line: - break - # Fix non ascii chars on Python2. To remove when #6890 is complete. - if six.PY2: - magic_word = str(magic_word) - if line.startswith(magic_word): - appear = True - yield json.dumps({"stream": line}) + with subprocess.Popen(args, stdout=output_stream, stderr=sys.stderr, + universal_newlines=True) as p: + p.communicate() + if p.returncode != 0: + raise BuildError(service, "Build failed") with open(iidfile) as f: line = f.readline() image_id = line.split(":")[1].strip() os.remove(iidfile) - # In case of `DOCKER_BUILDKIT=1` - # there is no success message already present in the output. - # Since that's the way `Service::build` gets the `image_id` - # it has to be added `manually` - if not appear: - yield json.dumps({"stream": "{}{}\n".format(magic_word, image_id)}) + return image_id -class _CommandBuilder(object): +class _CommandBuilder: def __init__(self): self._args = ["docker", "build"] diff --git a/compose/timeparse.py b/compose/timeparse.py index 16ef8a6dc91..47744562519 100644 --- a/compose/timeparse.py +++ b/compose/timeparse.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- ''' timeparse.py (c) Will Roberts 1 February, 2014 @@ -31,9 +30,6 @@ # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import absolute_import -from __future__ import unicode_literals - import re HOURS = r'(?P[\d.]+)h' @@ -57,14 +53,14 @@ def opt(x): NANO=opt(NANO), ) -MULTIPLIERS = dict([ - ('hours', 60 * 60), - ('mins', 60), - ('secs', 1), - ('milli', 1.0 / 1000), - ('micro', 1.0 / 1000.0 / 1000), - ('nano', 1.0 / 1000.0 / 1000.0 / 1000.0), -]) +MULTIPLIERS = { + 'hours': 60 * 60, + 'mins': 60, + 'secs': 1, + 'milli': 1.0 / 1000, + 'micro': 1.0 / 1000.0 / 1000, + 'nano': 1.0 / 1000.0 / 1000.0 / 1000.0, +} def timeparse(sval): @@ -93,4 +89,4 @@ def timeparse(sval): def cast(value): - return int(value, 10) if value.isdigit() else float(value) + return int(value) if value.isdigit() else float(value) diff --git a/compose/utils.py b/compose/utils.py index a1e5e6435d8..86af8f8852a 100644 --- a/compose/utils.py +++ b/compose/utils.py @@ -1,14 +1,9 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - -import codecs import hashlib import json.decoder import logging import ntpath import random -import six from docker.errors import DockerException from docker.utils import parse_bytes as sdk_parse_bytes @@ -21,12 +16,6 @@ log = logging.getLogger(__name__) -def get_output_stream(stream): - if six.PY3: - return stream - return codecs.getwriter('utf-8')(stream) - - def stream_as_text(stream): """Given a stream of bytes or text, if any of the items in the stream are bytes convert them to text. @@ -35,13 +24,13 @@ def stream_as_text(stream): of byte streams. """ for data in stream: - if not isinstance(data, six.text_type): + if not isinstance(data, str): data = data.decode('utf-8', 'replace') yield data -def line_splitter(buffer, separator=u'\n'): - index = buffer.find(six.text_type(separator)) +def line_splitter(buffer, separator='\n'): + index = buffer.find(str(separator)) if index == -1: return None return buffer[:index + 1], buffer[index + 1:] @@ -56,7 +45,7 @@ def split_buffer(stream, splitter=None, decoder=lambda a: a): of the input. """ splitter = splitter or line_splitter - buffered = six.text_type('') + buffered = '' for data in stream_as_text(stream): buffered += data @@ -127,7 +116,7 @@ def parse_nanoseconds_int(value): def build_string_dict(source_dict): - return dict((k, str(v if v is not None else '')) for k, v in source_dict.items()) + return {k: str(v if v is not None else '') for k, v in source_dict.items()} def splitdrive(path): @@ -185,3 +174,18 @@ def truncate_string(s, max_chars=35): if len(s) > max_chars: return s[:max_chars - 2] + '...' return s + + +def filter_attached_for_up(items, service_names, attach_dependencies=False, + item_to_service_name=lambda x: x): + """This function contains the logic of choosing which services to + attach when doing docker-compose up. It may be used both with containers + and services, and any other entities that map to service names - + this mapping is provided by item_to_service_name.""" + if attach_dependencies or not service_names: + return items + + return [ + item + for item in items if item_to_service_name(item) in service_names + ] diff --git a/compose/version.py b/compose/version.py index 0532e16c717..c039263acb9 100644 --- a/compose/version.py +++ b/compose/version.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - from distutils.version import LooseVersion diff --git a/compose/volume.py b/compose/volume.py index b02fc5d8030..5f36e432ba9 100644 --- a/compose/volume.py +++ b/compose/volume.py @@ -1,8 +1,6 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import logging import re +from itertools import chain from docker.errors import NotFound from docker.utils import version_lt @@ -18,7 +16,7 @@ log = logging.getLogger(__name__) -class Volume(object): +class Volume: def __init__(self, client, project, name, driver=None, driver_opts=None, external=False, labels=None, custom_name=False): self.client = client @@ -60,13 +58,13 @@ def exists(self): def full_name(self): if self.custom_name: return self.name - return '{0}_{1}'.format(self.project.lstrip('-_'), self.name) + return '{}_{}'.format(self.project.lstrip('-_'), self.name) @property def legacy_full_name(self): if self.custom_name: return self.name - return '{0}_{1}'.format( + return '{}_{}'.format( re.sub(r'[_-]', '', self.project), self.name ) @@ -99,7 +97,7 @@ def _set_legacy_flag(self): self.legacy = False -class ProjectVolumes(object): +class ProjectVolumes: def __init__(self, volumes): self.volumes = volumes @@ -135,7 +133,7 @@ def initialize(self): volume_exists = volume.exists() if volume.external: log.debug( - 'Volume {0} declared as external. No new ' + 'Volume {} declared as external. No new ' 'volume will be created.'.format(volume.name) ) if not volume_exists: @@ -151,7 +149,7 @@ def initialize(self): if not volume_exists: log.info( - 'Creating volume "{0}" with {1} driver'.format( + 'Creating volume "{}" with {} driver'.format( volume.full_name, volume.driver or 'default' ) ) @@ -160,7 +158,7 @@ def initialize(self): check_remote_volume_config(volume.inspect(legacy=volume.legacy), volume) except NotFound: raise ConfigurationError( - 'Volume %s specifies nonexistent driver %s' % (volume.name, volume.driver) + 'Volume {} specifies nonexistent driver {}'.format(volume.name, volume.driver) ) def namespace_spec(self, volume_spec): @@ -177,7 +175,7 @@ def namespace_spec(self, volume_spec): class VolumeConfigChangedError(ConfigurationError): def __init__(self, local, property_name, local_value, remote_value): - super(VolumeConfigChangedError, self).__init__( + super().__init__( 'Configuration for volume {vol_name} specifies {property_name} ' '{local_value}, but a volume with the same name uses a different ' '{property_name} ({remote_value}). If you wish to use the new ' @@ -195,7 +193,7 @@ def check_remote_volume_config(remote, local): raise VolumeConfigChangedError(local, 'driver', local.driver, remote.get('Driver')) local_opts = local.driver_opts or {} remote_opts = remote.get('Options') or {} - for k in set.union(set(remote_opts.keys()), set(local_opts.keys())): + for k in set(chain(remote_opts, local_opts)): if k.startswith('com.docker.'): # These options are set internally continue if remote_opts.get(k) != local_opts.get(k): @@ -205,7 +203,7 @@ def check_remote_volume_config(remote, local): local_labels = local.labels or {} remote_labels = remote.get('Labels') or {} - for k in set.union(set(remote_labels.keys()), set(local_labels.keys())): + for k in set(chain(remote_labels, local_labels)): if k.startswith('com.docker.'): # We are only interested in user-specified labels continue if remote_labels.get(k) != local_labels.get(k): diff --git a/contrib/completion/bash/docker-compose b/contrib/completion/bash/docker-compose index ad0ce44c177..c3b6157d391 100644 --- a/contrib/completion/bash/docker-compose +++ b/contrib/completion/bash/docker-compose @@ -138,7 +138,7 @@ _docker_compose_config() { ;; esac - COMPREPLY=( $( compgen -W "--hash --help --no-interpolate --quiet -q --resolve-image-digests --services --volumes" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--hash --help --no-interpolate --profiles --quiet -q --resolve-image-digests --services --volumes" -- "$cur" ) ) } @@ -164,10 +164,18 @@ _docker_compose_docker_compose() { _filedir "y?(a)ml" return ;; + --ansi) + COMPREPLY=( $( compgen -W "never always auto" -- "$cur" ) ) + return + ;; --log-level) COMPREPLY=( $( compgen -W "debug info warning error critical" -- "$cur" ) ) return ;; + --profile) + COMPREPLY=( $( compgen -W "$(__docker_compose_q config --profiles)" -- "$cur" ) ) + return + ;; --project-directory) _filedir -d return @@ -290,7 +298,7 @@ _docker_compose_logs() { case "$cur" in -*) - COMPREPLY=( $( compgen -W "--follow -f --help --no-color --tail --timestamps -t" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--follow -f --help --no-color --no-log-prefix --tail --timestamps -t" -- "$cur" ) ) ;; *) __docker_compose_complete_services @@ -545,7 +553,7 @@ _docker_compose_up() { case "$cur" in -*) - COMPREPLY=( $( compgen -W "--abort-on-container-exit --always-recreate-deps --attach-dependencies --build -d --detach --exit-code-from --force-recreate --help --no-build --no-color --no-deps --no-recreate --no-start --renew-anon-volumes -V --remove-orphans --scale --timeout -t" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--abort-on-container-exit --always-recreate-deps --attach-dependencies --build -d --detach --exit-code-from --force-recreate --help --no-build --no-color --no-deps --no-log-prefix --no-recreate --no-start --renew-anon-volumes -V --remove-orphans --scale --timeout -t" -- "$cur" ) ) ;; *) __docker_compose_complete_services @@ -603,6 +611,7 @@ _docker_compose() { --tlsverify " local daemon_options_with_args=" + --context -c --env-file --file -f --host -H @@ -613,9 +622,11 @@ _docker_compose() { --tlskey " - # These options are require special treatment when searching the command. + # These options require special treatment when searching the command. local top_level_options_with_args=" + --ansi --log-level + --profile " COMPREPLY=() diff --git a/contrib/completion/fish/docker-compose.fish b/contrib/completion/fish/docker-compose.fish index 0566e16ae88..9183a098e68 100644 --- a/contrib/completion/fish/docker-compose.fish +++ b/contrib/completion/fish/docker-compose.fish @@ -21,5 +21,7 @@ complete -c docker-compose -l tlscert -r -d 'Path to TLS certif complete -c docker-compose -l tlskey -r -d 'Path to TLS key file' complete -c docker-compose -l tlsverify -d 'Use TLS and verify the remote' complete -c docker-compose -l skip-hostname-check -d "Don't check the daemon's hostname against the name specified in the client certificate (for example if your docker host is an IP address)" +complete -c docker-compose -l no-ansi -d 'Do not print ANSI control characters' +complete -c docker-compose -l ansi -a 'never always auto' -d 'Control when to print ANSI control characters' complete -c docker-compose -s h -l help -d 'Print usage' complete -c docker-compose -s v -l version -d 'Print version and exit' diff --git a/contrib/completion/zsh/_docker-compose b/contrib/completion/zsh/_docker-compose index de14149844a..c6b733500b3 100755 --- a/contrib/completion/zsh/_docker-compose +++ b/contrib/completion/zsh/_docker-compose @@ -342,6 +342,7 @@ _docker-compose() { '--verbose[Show more output]' \ '--log-level=[Set log level]:level:(DEBUG INFO WARNING ERROR CRITICAL)' \ '--no-ansi[Do not print ANSI control characters]' \ + '--ansi=[Control when to print ANSI control characters]:when:(never always auto)' \ '(-H --host)'{-H,--host}'[Daemon socket to connect to]:host:' \ '--tls[Use TLS; implied by --tlsverify]' \ '--tlscacert=[Trust certs signed only by this CA]:ca path:' \ diff --git a/contrib/migration/migrate-compose-file-v1-to-v2.py b/contrib/migration/migrate-compose-file-v1-to-v2.py index 274b499b9d4..26511206c5f 100755 --- a/contrib/migration/migrate-compose-file-v1-to-v2.py +++ b/contrib/migration/migrate-compose-file-v1-to-v2.py @@ -3,9 +3,6 @@ Migrate a Compose file from the V1 format in Compose 1.5 to the V2 format supported by Compose 1.6+ """ -from __future__ import absolute_import -from __future__ import unicode_literals - import argparse import logging import sys @@ -159,7 +156,7 @@ def main(args): opts = parse_opts(args) - with open(opts.filename, 'r') as fh: + with open(opts.filename) as fh: new_format = migrate(fh.read()) if opts.in_place: diff --git a/docker-compose.spec b/docker-compose.spec index 5ca1e4c24de..0c2fa3dec9f 100644 --- a/docker-compose.spec +++ b/docker-compose.spec @@ -23,68 +23,8 @@ exe = EXE(pyz, 'DATA' ), ( - 'compose/config/config_schema_v2.0.json', - 'compose/config/config_schema_v2.0.json', - 'DATA' - ), - ( - 'compose/config/config_schema_v2.1.json', - 'compose/config/config_schema_v2.1.json', - 'DATA' - ), - ( - 'compose/config/config_schema_v2.2.json', - 'compose/config/config_schema_v2.2.json', - 'DATA' - ), - ( - 'compose/config/config_schema_v2.3.json', - 'compose/config/config_schema_v2.3.json', - 'DATA' - ), - ( - 'compose/config/config_schema_v2.4.json', - 'compose/config/config_schema_v2.4.json', - 'DATA' - ), - ( - 'compose/config/config_schema_v3.0.json', - 'compose/config/config_schema_v3.0.json', - 'DATA' - ), - ( - 'compose/config/config_schema_v3.1.json', - 'compose/config/config_schema_v3.1.json', - 'DATA' - ), - ( - 'compose/config/config_schema_v3.2.json', - 'compose/config/config_schema_v3.2.json', - 'DATA' - ), - ( - 'compose/config/config_schema_v3.3.json', - 'compose/config/config_schema_v3.3.json', - 'DATA' - ), - ( - 'compose/config/config_schema_v3.4.json', - 'compose/config/config_schema_v3.4.json', - 'DATA' - ), - ( - 'compose/config/config_schema_v3.5.json', - 'compose/config/config_schema_v3.5.json', - 'DATA' - ), - ( - 'compose/config/config_schema_v3.6.json', - 'compose/config/config_schema_v3.6.json', - 'DATA' - ), - ( - 'compose/config/config_schema_v3.7.json', - 'compose/config/config_schema_v3.7.json', + 'compose/config/compose_spec.json', + 'compose/config/compose_spec.json', 'DATA' ), ( diff --git a/docker-compose_darwin.spec b/docker-compose_darwin.spec index 344c070d501..24889475990 100644 --- a/docker-compose_darwin.spec +++ b/docker-compose_darwin.spec @@ -1,4 +1,4 @@ -# -*- mode: python ; coding: utf-8 -*- +# -*- mode: python -*- block_cipher = None @@ -32,68 +32,8 @@ coll = COLLECT(exe, 'DATA' ), ( - 'compose/config/config_schema_v2.0.json', - 'compose/config/config_schema_v2.0.json', - 'DATA' - ), - ( - 'compose/config/config_schema_v2.1.json', - 'compose/config/config_schema_v2.1.json', - 'DATA' - ), - ( - 'compose/config/config_schema_v2.2.json', - 'compose/config/config_schema_v2.2.json', - 'DATA' - ), - ( - 'compose/config/config_schema_v2.3.json', - 'compose/config/config_schema_v2.3.json', - 'DATA' - ), - ( - 'compose/config/config_schema_v2.4.json', - 'compose/config/config_schema_v2.4.json', - 'DATA' - ), - ( - 'compose/config/config_schema_v3.0.json', - 'compose/config/config_schema_v3.0.json', - 'DATA' - ), - ( - 'compose/config/config_schema_v3.1.json', - 'compose/config/config_schema_v3.1.json', - 'DATA' - ), - ( - 'compose/config/config_schema_v3.2.json', - 'compose/config/config_schema_v3.2.json', - 'DATA' - ), - ( - 'compose/config/config_schema_v3.3.json', - 'compose/config/config_schema_v3.3.json', - 'DATA' - ), - ( - 'compose/config/config_schema_v3.4.json', - 'compose/config/config_schema_v3.4.json', - 'DATA' - ), - ( - 'compose/config/config_schema_v3.5.json', - 'compose/config/config_schema_v3.5.json', - 'DATA' - ), - ( - 'compose/config/config_schema_v3.6.json', - 'compose/config/config_schema_v3.6.json', - 'DATA' - ), - ( - 'compose/config/config_schema_v3.7.json', - 'compose/config/config_schema_v3.7.json', + 'compose/config/compose_spec.json', + 'compose/config/compose_spec.json', 'DATA' ), ( diff --git a/requirements-build.txt b/requirements-build.txt index 9126f8af9d8..9ca8d66679a 100644 --- a/requirements-build.txt +++ b/requirements-build.txt @@ -1 +1 @@ -pyinstaller==3.6 +pyinstaller==4.1 diff --git a/requirements-dev.txt b/requirements-dev.txt index d723b3705df..34a53f86262 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,9 +1,9 @@ -Click==7.0 -coverage==5.0.3 -ddt==1.2.2 -flake8==3.7.9 -gitpython==2.1.14 +Click==7.1.2 +coverage==5.5 +ddt==1.4.1 +flake8==3.8.3 +gitpython==3.1.11 mock==3.0.5 -pytest==5.3.4; python_version >= '3.5' +pytest==6.0.1; python_version >= '3.5' pytest==4.6.5; python_version < '3.5' -pytest-cov==2.8.1 +pytest-cov==2.10.1 diff --git a/requirements-indirect.txt b/requirements-indirect.txt new file mode 100644 index 00000000000..5c6b789b7f5 --- /dev/null +++ b/requirements-indirect.txt @@ -0,0 +1,28 @@ +altgraph==0.17 +appdirs==1.4.4 +attrs==20.3.0 +bcrypt==3.2.0 +cffi==1.14.4 +cryptography==3.3.2 +distlib==0.3.1 +entrypoints==0.3 +filelock==3.0.12 +gitdb2==4.0.2 +mccabe==0.6.1 +more-itertools==8.6.0; python_version >= '3.5' +more-itertools==5.0.0; python_version < '3.5' +packaging==20.9 +pluggy==0.13.1 +py==1.10.0 +pycodestyle==2.6.0 +pycparser==2.20 +pyflakes==2.2.0 +PyNaCl==1.4.0 +pyparsing==2.4.7 +pyrsistent==0.16.0 +smmap==3.0.4 +smmap2==3.0.1 +toml==0.10.1 +tox==3.21.2 +virtualenv==20.4.0 +wcwidth==0.2.5 diff --git a/requirements.txt b/requirements.txt index ee57c26b5af..f0cc8be7b17 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,27 +1,22 @@ backports.shutil_get_terminal_size==1.0.0 -backports.ssl-match-hostname==3.5.0.1; python_version < '3' -cached-property==1.5.1 -certifi==2019.11.28 +cached-property==1.5.1; python_version < '3.8' +certifi==2020.6.20 chardet==3.0.4 colorama==0.4.3; sys_platform == 'win32' -docker==4.1.0 +distro==1.5.0 +docker==5.0.0 docker-pycreds==0.4.0 dockerpty==0.4.1 docopt==0.6.2 -enum34==1.1.6; python_version < '3.4' -functools32==3.2.3.post2; python_version < '3.2' -idna==2.8 +idna==2.10 ipaddress==1.0.23 jsonschema==3.2.0 paramiko==2.7.1 -pypiwin32==219; sys_platform == 'win32' and python_version < '3.6' -pypiwin32==223; sys_platform == 'win32' and python_version >= '3.6' PySocks==1.7.1 -python-dotenv==0.10.5 -PyYAML==5.3 -requests==2.22.0 -six==1.12.0 -subprocess32==3.5.4; python_version < '3.2' +python-dotenv==0.17.0 +pywin32==227; sys_platform == 'win32' +PyYAML==5.4.1 +requests==2.24.0 texttable==1.6.2 -urllib3==1.25.7; python_version == '3.3' +urllib3==1.25.10; python_version == '3.3' websocket-client==0.57.0 diff --git a/script/build/linux b/script/build/linux index ca5620b8527..2e56b625c0f 100755 --- a/script/build/linux +++ b/script/build/linux @@ -5,14 +5,12 @@ set -ex ./script/clean 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 build . \ + --target bin \ + --build-arg DISTRO=debian \ + --build-arg GIT_COMMIT="${DOCKER_COMPOSE_GITSHA}" \ + --output dist/ ARCH=$(uname -m) -docker cp "${TMP_CONTAINER}":/usr/local/bin/docker-compose "dist/docker-compose-Linux-${ARCH}" -docker container rm -f "${TMP_CONTAINER}" -docker image rm -f "${TAG}" +# Ensure that we output the binary with the same name as we did before +mv dist/docker-compose-linux-amd64 "dist/docker-compose-Linux-${ARCH}" diff --git a/script/build/linux-entrypoint b/script/build/linux-entrypoint index d75b9927d09..74f47620e40 100755 --- a/script/build/linux-entrypoint +++ b/script/build/linux-entrypoint @@ -24,7 +24,7 @@ if [ ! -z "${BUILD_BOOTLOADER}" ]; then git clone --single-branch --branch develop https://github.com/pyinstaller/pyinstaller.git /tmp/pyinstaller cd /tmp/pyinstaller/bootloader # Checkout commit corresponding to version in requirements-build - git checkout v3.6 + git checkout v4.1 "${VENV}"/bin/python3 ./waf configure --no-lsb all "${VENV}"/bin/pip3 install .. cd "${CODE_PATH}" diff --git a/script/build/osx b/script/build/osx index 66868756b69..e2d17527b3a 100755 --- a/script/build/osx +++ b/script/build/osx @@ -6,6 +6,7 @@ TOOLCHAIN_PATH="$(realpath $(dirname $0)/../../build/toolchain)" rm -rf venv virtualenv -p "${TOOLCHAIN_PATH}"/bin/python3 venv +venv/bin/pip install -r requirements-indirect.txt venv/bin/pip install -r requirements.txt venv/bin/pip install -r requirements-build.txt venv/bin/pip install --no-deps . diff --git a/script/build/test-image b/script/build/test-image index 4964a5f9da4..ddb8057d05c 100755 --- a/script/build/test-image +++ b/script/build/test-image @@ -13,6 +13,6 @@ IMAGE="docker/compose-tests" DOCKER_COMPOSE_GITSHA="$(script/build/write-git-sha)" docker build -t "${IMAGE}:${TAG}" . \ --target build \ - --build-arg BUILD_PLATFORM="debian" \ + --build-arg DISTRO="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 7ba5ebde29e..147d0f07d88 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.7.x: +# 2. Install Python 3.9.x: # # https://www.python.org/downloads/ # -# 3. Append ";C:\Python37;C:\Python37\Scripts" to the "Path" environment variable: +# 3. Append ";C:\Python39;C:\Python39\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==16.2.0' +# $ pip install 'virtualenv==20.2.2' # $ Set-ExecutionPolicy -Scope CurrentUser RemoteSigned # # 5. Clone the repository: @@ -39,12 +39,13 @@ if (Test-Path venv) { Get-ChildItem -Recurse -Include *.pyc | foreach ($_) { Remove-Item $_.FullName } # Create virtualenv -virtualenv -p C:\Python37\python.exe .\venv +virtualenv -p C:\Python39\python.exe .\venv # pip and pyinstaller generate lots of warnings, so we need to ignore them $ErrorActionPreference = "Continue" .\venv\Scripts\pip install pypiwin32==223 +.\venv\Scripts\pip install -r requirements-indirect.txt .\venv\Scripts\pip install -r requirements.txt .\venv\Scripts\pip install --no-deps . .\venv\Scripts\pip install -r requirements-build.txt diff --git a/script/docs/check_help.py b/script/docs/check_help.py new file mode 100755 index 00000000000..0904f00c4f6 --- /dev/null +++ b/script/docs/check_help.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +import glob +import os.path +import re +import subprocess + +USAGE_RE = re.compile(r"```.*?\nUsage:.*?```", re.MULTILINE | re.DOTALL) +USAGE_IN_CMD_RE = re.compile(r"^Usage:.*", re.MULTILINE | re.DOTALL) + +HELP_CMD = "docker run --rm docker/compose:latest %s --help" + +for file in glob.glob("compose/reference/*.md"): + with open(file) as f: + data = f.read() + if not USAGE_RE.search(data): + print("Not a command:", file) + continue + subcmd = os.path.basename(file).replace(".md", "") + if subcmd == "overview": + continue + print(f"Found {subcmd}: {file}") + help_cmd = HELP_CMD % subcmd + help = subprocess.check_output(help_cmd.split()) + help = help.decode("utf-8") + help = USAGE_IN_CMD_RE.findall(help)[0] + help = help.strip() + data = USAGE_RE.sub(f"```none\n{help}\n```", data) + with open(file, "w") as f: + f.write(data) diff --git a/script/release/README.md b/script/release/README.md index 8c961e3ad04..d44a4ec5c27 100644 --- a/script/release/README.md +++ b/script/release/README.md @@ -4,7 +4,7 @@ The release process is fully automated by `Release.Jenkinsfile`. ## Usage -1. In the appropriate branch, run `./scripts/release/release tag ` +1. In the appropriate branch, run `./script/release/release.py tag ` By appropriate, we mean for a version `1.26.0` or `1.26.0-rc1` you should run the script in the `1.26.x` branch. @@ -16,3 +16,7 @@ After the executions, you should have a commit with the proper bumps for `docker This should trigger a new CI build on the new tag. When the CI finishes with the tests and builds a new draft release would be available on github's releases page. 3. Check and confirm the release on github's release page. + +4. In case of a GA version, please update `docker-compose`s release notes and version on [github documentation repository](https://github.com/docker/docker.github.io): + - [Release Notes](https://github.com/docker/docker.github.io/blob/master/compose/release-notes.md) + - [Config version](https://github.com/docker/docker.github.io/blob/master/_config.yml) diff --git a/script/release/const.py b/script/release/const.py index 77a32332a7f..8c90eebca6e 100644 --- a/script/release/const.py +++ b/script/release/const.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import os diff --git a/script/release/release.py b/script/release/release.py old mode 100755 new mode 100644 index f53d1f3c181..c8e5e7f767d --- a/script/release/release.py +++ b/script/release/release.py @@ -1,7 +1,4 @@ #!/usr/bin/env python3 -from __future__ import absolute_import -from __future__ import unicode_literals - import re import click diff --git a/script/release/utils.py b/script/release/utils.py index 4f57704872b..5ed53ec85b3 100644 --- a/script/release/utils.py +++ b/script/release/utils.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import os import re @@ -9,7 +6,7 @@ def update_init_py_version(version): path = os.path.join(REPO_ROOT, 'compose', '__init__.py') - with open(path, 'r') as f: + with open(path) as f: contents = f.read() contents = re.sub(r"__version__ = '[0-9a-z.-]+'", "__version__ = '{}'".format(version), contents) with open(path, 'w') as f: @@ -18,7 +15,7 @@ def update_init_py_version(version): def update_run_sh_version(version): path = os.path.join(REPO_ROOT, 'script', 'run', 'run.sh') - with open(path, 'r') as f: + with open(path) as f: contents = f.read() contents = re.sub(r'VERSION="[0-9a-z.-]+"', 'VERSION="{}"'.format(version), contents) with open(path, 'w') as f: diff --git a/script/run/run.sh b/script/run/run.sh index 7df5fe979d9..658cf47ac5a 100755 --- a/script/run/run.sh +++ b/script/run/run.sh @@ -15,16 +15,16 @@ set -e -VERSION="1.25.1" +VERSION="1.26.1" IMAGE="docker/compose:$VERSION" # Setup options for connecting to docker host if [ -z "$DOCKER_HOST" ]; then - DOCKER_HOST="/var/run/docker.sock" + DOCKER_HOST='unix:///var/run/docker.sock' fi -if [ -S "$DOCKER_HOST" ]; then - DOCKER_ADDR="-v $DOCKER_HOST:$DOCKER_HOST -e DOCKER_HOST" +if [ -S "${DOCKER_HOST#unix://}" ]; then + DOCKER_ADDR="-v ${DOCKER_HOST#unix://}:${DOCKER_HOST#unix://} -e DOCKER_HOST" else DOCKER_ADDR="-e DOCKER_HOST -e DOCKER_TLS_VERIFY -e DOCKER_CERT_PATH" fi @@ -36,15 +36,42 @@ if [ "$(pwd)" != '/' ]; then fi if [ -n "$COMPOSE_FILE" ]; then COMPOSE_OPTIONS="$COMPOSE_OPTIONS -e COMPOSE_FILE=$COMPOSE_FILE" - compose_dir=$(realpath "$(dirname "$COMPOSE_FILE")") + compose_dir="$(dirname "$COMPOSE_FILE")" + # canonicalize dir, do not use realpath or readlink -f + # since they are not available in some systems (e.g. macOS). + compose_dir="$(cd "$compose_dir" && pwd)" +fi +if [ -n "$COMPOSE_PROJECT_NAME" ]; then + COMPOSE_OPTIONS="-e COMPOSE_PROJECT_NAME $COMPOSE_OPTIONS" fi -# TODO: also check --file argument if [ -n "$compose_dir" ]; then VOLUMES="$VOLUMES -v $compose_dir:$compose_dir" fi if [ -n "$HOME" ]; then VOLUMES="$VOLUMES -v $HOME:$HOME -e HOME" # Pass in HOME to share docker.config and allow ~/-relative paths to work. fi +i=$# +while [ $i -gt 0 ]; do + arg=$1 + i=$((i - 1)) + shift + + case "$arg" in + -f|--file) + value=$1 + i=$((i - 1)) + shift + set -- "$@" "$arg" "$value" + + file_dir=$(realpath "$(dirname "$value")") + VOLUMES="$VOLUMES -v $file_dir:$file_dir" + ;; + *) set -- "$@" "$arg" ;; + esac +done + +# Setup environment variables for compose config and context +ENV_OPTIONS=$(printenv | sed -E "/^PATH=.*/d; s/^/-e /g; s/=.*//g; s/\n/ /g") # Only allocate tty if we detect one if [ -t 0 ] && [ -t 1 ]; then @@ -61,4 +88,4 @@ if docker info --format '{{json .SecurityOptions}}' 2>/dev/null | grep -q 'name= fi # shellcheck disable=SC2086 -exec docker run --rm $DOCKER_RUN_OPTIONS $DOCKER_ADDR $COMPOSE_OPTIONS $VOLUMES -w "$(pwd)" $IMAGE "$@" +exec docker run --rm $DOCKER_RUN_OPTIONS $DOCKER_ADDR $COMPOSE_OPTIONS $ENV_OPTIONS $VOLUMES -w "$(pwd)" $IMAGE "$@" diff --git a/script/setup/osx b/script/setup/osx index 00cca06cf8b..289155bafcd 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.1d +OPENSSL_VERSION=1.1.1h OPENSSL_URL=https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz -OPENSSL_SHA1=056057782325134b76d1931c48f2c7e6595d7ef4 +OPENSSL_SHA1=8d0d099e8973ec851368c8c775e05e1eadca1794 -PYTHON_VERSION=3.7.6 +PYTHON_VERSION=3.9.0 PYTHON_URL=https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz -PYTHON_SHA1=4642680fbf9a9a5382597dc0e9faa058fdfd94e2 +PYTHON_SHA1=5744a10ba989d2badacbab3c00cdcb83c83106c7 # # Install prerequisites. @@ -36,7 +36,7 @@ if ! [ -x "$(command -v python3)" ]; then brew install python3 fi if ! [ -x "$(command -v virtualenv)" ]; then - pip3 install virtualenv==16.2.0 + pip3 install virtualenv==20.2.2 fi # diff --git a/script/test/acceptance b/script/test/acceptance new file mode 100755 index 00000000000..92710a76a8d --- /dev/null +++ b/script/test/acceptance @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +pytest --conformity --binary ${1:-docker-compose} tests/acceptance/ diff --git a/script/test/all b/script/test/all index f929a57eecc..63fc0255ef5 100755 --- a/script/test/all +++ b/script/test/all @@ -11,7 +11,7 @@ docker run --rm \ "$TAG" tox -e pre-commit get_versions="docker run --rm - --entrypoint=/code/.tox/py27/bin/python + --entrypoint=/code/.tox/py37/bin/python $TAG /code/script/test/versions.py docker/docker-ce,moby/moby" @@ -21,9 +21,8 @@ elif [ "$DOCKER_VERSIONS" == "all" ]; then DOCKER_VERSIONS=$($get_versions -n 2 recent) fi - BUILD_NUMBER=${BUILD_NUMBER-$USER} -PY_TEST_VERSIONS=${PY_TEST_VERSIONS:-py27,py37} +PY_TEST_VERSIONS=${PY_TEST_VERSIONS:-py37} for version in $DOCKER_VERSIONS; do >&2 echo "Running tests against Docker $version" @@ -39,17 +38,23 @@ for version in $DOCKER_VERSIONS; do trap "on_exit" EXIT - repo="dockerswarm/dind" - docker run \ -d \ --name "$daemon_container" \ --privileged \ --volume="/var/lib/docker" \ - "$repo:$version" \ + -e "DOCKER_TLS_CERTDIR=" \ + "docker:$version-dind" \ dockerd -H tcp://0.0.0.0:2375 $DOCKER_DAEMON_ARGS \ 2>&1 | tail -n 10 + docker exec "$daemon_container" sh -c "apk add --no-cache git" + + # copy docker config from host for authentication with Docker Hub + docker exec "$daemon_container" sh -c "mkdir /root/.docker" + docker cp /root/.docker/config.json $daemon_container:/root/.docker/config.json + docker exec "$daemon_container" sh -c "chmod 644 /root/.docker/config.json" + docker run \ --rm \ --tty \ diff --git a/script/test/versions.py b/script/test/versions.py index a06c49f20b0..1a28dc19ad3 100755 --- a/script/test/versions.py +++ b/script/test/versions.py @@ -21,10 +21,6 @@ `default` would return `1.7.1` and `recent -n 3` would return `1.8.0-rc2 1.7.1 1.6.2` """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import argparse import itertools import operator diff --git a/setup.py b/setup.py index 548e2bc9398..aaf33f74868 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import codecs import os import re @@ -30,16 +25,16 @@ def find_version(*file_paths): install_requires = [ - 'cached-property >= 1.2.0, < 2', 'docopt >= 0.6.1, < 1', 'PyYAML >= 3.10, < 6', 'requests >= 2.20.0, < 3', 'texttable >= 0.9.0, < 2', 'websocket-client >= 0.32.0, < 1', - 'docker[ssh] >= 3.7.0, < 5', + 'distro >= 1.5.0, < 2', + 'docker[ssh] >= 5', 'dockerpty >= 0.4.1, < 1', - 'six >= 1.3.0, < 2', 'jsonschema >= 2.5.1, < 4', + 'python-dotenv >= 0.13.0, < 1', ] @@ -53,11 +48,8 @@ def find_version(*file_paths): tests_require.append('mock >= 1.0.1, < 4') extras_require = { - ':python_version < "3.2"': ['subprocess32 >= 3.5.4, < 4'], - ':python_version < "3.4"': ['enum34 >= 1.0.4, < 2'], ':python_version < "3.5"': ['backports.ssl_match_hostname >= 3.5, < 4'], - ':python_version < "3.3"': ['backports.shutil_get_terminal_size == 1.0.0', - 'ipaddress >= 1.0.16, < 2'], + ':python_version < "3.8"': ['cached-property >= 1.2.0, < 2'], ':sys_platform == "win32"': ['colorama >= 0.4, < 1'], 'socks': ['PySocks >= 1.5.6, != 1.5.7, < 2'], 'tests': tests_require, @@ -97,7 +89,7 @@ def find_version(*file_paths): install_requires=install_requires, extras_require=extras_require, tests_require=tests_require, - python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', + python_requires='>=3.4', entry_points={ 'console_scripts': ['docker-compose=compose.cli.main:main'], }, @@ -106,11 +98,11 @@ def find_version(*file_paths): 'Environment :: Console', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', ], ) diff --git a/tests/__init__.py b/tests/__init__.py index 1ac1b21cf74..9d732490098 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,14 +1,2 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - -import sys - -if sys.version_info >= (2, 7): - import unittest # NOQA -else: - import unittest2 as unittest # NOQA - -try: - from unittest import mock -except ImportError: - import mock # NOQA +import unittest # NOQA +from unittest import mock # NOQA diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 43d5a3f5ccc..aa879c8a48d 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -1,7 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import unicode_literals - import datetime import json import os.path @@ -15,7 +11,6 @@ from operator import attrgetter import pytest -import six import yaml from docker import errors @@ -24,6 +19,8 @@ from ..helpers import create_host_file from compose.cli.command import get_project from compose.config.errors import DuplicateOverrideFileFound +from compose.const import COMPOSE_SPEC as VERSION +from compose.const import COMPOSEFILE_V1 as V1 from compose.container import Container from compose.project import OneOffFilter from compose.utils import nanoseconds_from_time_seconds @@ -33,10 +30,8 @@ from tests.integration.testcases import no_cluster from tests.integration.testcases import pull_busybox from tests.integration.testcases import SWARM_SKIP_RM_VOLUMES -from tests.integration.testcases import v2_1_only -from tests.integration.testcases import v2_2_only -from tests.integration.testcases import v2_only -from tests.integration.testcases import v3_only + +DOCKER_COMPOSE_EXECUTABLE = 'docker-compose' ProcessResult = namedtuple('ProcessResult', 'stdout stderr') @@ -44,7 +39,7 @@ BUILD_CACHE_TEXT = 'Using cache' BUILD_PULL_TEXT = 'Status: Image is up to date for busybox:1.27.2' COMPOSE_COMPATIBILITY_DICT = { - 'version': '2.3', + 'version': str(VERSION), 'volumes': {'foo': {'driver': 'default'}}, 'networks': {'bar': {}}, 'services': { @@ -63,13 +58,16 @@ } -def start_process(base_dir, options): +def start_process(base_dir, options, executable=None, env=None): + executable = executable or DOCKER_COMPOSE_EXECUTABLE proc = subprocess.Popen( - ['docker-compose'] + options, + [executable] + options, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - cwd=base_dir) + cwd=base_dir, + env=env, + ) print("Running process: %s" % proc.pid) return proc @@ -83,9 +81,10 @@ def wait_on_process(proc, returncode=0, stdin=None): return ProcessResult(stdout.decode('utf-8'), stderr.decode('utf-8')) -def dispatch(base_dir, options, project_options=None, returncode=0, stdin=None): +def dispatch(base_dir, options, + project_options=None, returncode=0, stdin=None, executable=None, env=None): project_options = project_options or [] - proc = start_process(base_dir, project_options + options) + proc = start_process(base_dir, project_options + options, executable=executable, env=env) return wait_on_process(proc, returncode=returncode, stdin=stdin) @@ -103,7 +102,7 @@ def kill_service(service): container.kill() -class ContainerCountCondition(object): +class ContainerCountCondition: def __init__(self, project, expected): self.project = project @@ -116,7 +115,7 @@ def __str__(self): return "waiting for counter count == %s" % self.expected -class ContainerStateCondition(object): +class ContainerStateCondition: def __init__(self, client, name, status): self.client = client @@ -144,7 +143,7 @@ def __str__(self): class CLITestCase(DockerClientTestCase): def setUp(self): - super(CLITestCase, self).setUp() + super().setUp() self.base_dir = 'tests/fixtures/simple-composefile' self.override_dir = None @@ -166,7 +165,7 @@ def tearDown(self): if hasattr(self, '_project'): del self._project - super(CLITestCase, self).tearDown() + super().tearDown() @property def project(self): @@ -191,7 +190,7 @@ def lookup(self, container, hostname): def test_help(self): self.base_dir = 'tests/fixtures/no-composefile' result = self.dispatch(['help', 'up'], returncode=0) - assert 'Usage: up [options] [--scale SERVICE=NUM...] [SERVICE...]' in result.stdout + assert 'Usage: up [options] [--scale SERVICE=NUM...] [--] [SERVICE...]' in result.stdout # Prevent tearDown from trying to create a project self.base_dir = None @@ -210,14 +209,14 @@ def test_help_nonexistent(self): def test_shorthand_host_opt(self): self.dispatch( - ['-H={0}'.format(os.environ.get('DOCKER_HOST', 'unix://')), + ['-H={}'.format(os.environ.get('DOCKER_HOST', 'unix://')), 'up', '-d'], returncode=0 ) def test_shorthand_host_opt_interactive(self): self.dispatch( - ['-H={0}'.format(os.environ.get('DOCKER_HOST', 'unix://')), + ['-H={}'.format(os.environ.get('DOCKER_HOST', 'unix://')), 'run', 'another', 'ls'], returncode=0 ) @@ -238,6 +237,11 @@ def test_host_not_reachable_volumes_from_container(self): result = self.dispatch(['-H=tcp://doesnotexist:8000', 'ps'], returncode=1) assert "Couldn't connect to Docker daemon" in result.stderr + def test_config_list_profiles(self): + self.base_dir = 'tests/fixtures/config-profiles' + result = self.dispatch(['config', '--profiles']) + assert set(result.stdout.rstrip().split('\n')) == {'debug', 'frontend', 'gui'} + def test_config_list_services(self): self.base_dir = 'tests/fixtures/v2-full' result = self.dispatch(['config', '--services']) @@ -289,7 +293,7 @@ def test_config_default(self): output = yaml.safe_load(result.stdout) expected = { - 'version': '2.0', + 'version': '2', 'volumes': {'data': {'driver': 'local'}}, 'networks': {'front': {}}, 'services': { @@ -313,7 +317,7 @@ def test_config_restart(self): self.base_dir = 'tests/fixtures/restart' result = self.dispatch(['config']) assert yaml.safe_load(result.stdout) == { - 'version': '2.0', + 'version': '2', 'services': { 'never': { 'image': 'busybox', @@ -345,10 +349,12 @@ def test_config_external_network(self): assert 'networks' in json_result assert json_result['networks'] == { 'networks_foo': { - 'external': True # {'name': 'networks_foo'} + 'external': True, + 'name': 'networks_foo' }, 'bar': { - 'external': {'name': 'networks_bar'} + 'external': True, + 'name': 'networks_bar' } } @@ -357,14 +363,14 @@ def test_config_with_dot_env(self): result = self.dispatch(['config']) json_result = yaml.safe_load(result.stdout) assert json_result == { + 'version': '2.4', 'services': { 'web': { 'command': 'true', 'image': 'alpine:latest', - 'ports': ['5643/tcp', '9999/tcp'] + 'ports': [{'target': 5643}, {'target': 9999}] } - }, - 'version': '2.4' + } } def test_config_with_env_file(self): @@ -372,14 +378,14 @@ def test_config_with_env_file(self): result = self.dispatch(['--env-file', '.env2', 'config']) json_result = yaml.safe_load(result.stdout) assert json_result == { + 'version': '2.4', 'services': { 'web': { 'command': 'false', 'image': 'alpine:latest', - 'ports': ['5644/tcp', '9998/tcp'] + 'ports': [{'target': 5644}, {'target': 9998}] } - }, - 'version': '2.4' + } } def test_config_with_dot_env_and_override_dir(self): @@ -387,14 +393,14 @@ def test_config_with_dot_env_and_override_dir(self): result = self.dispatch(['--project-directory', 'alt/', 'config']) json_result = yaml.safe_load(result.stdout) assert json_result == { + 'version': '2.4', 'services': { 'web': { 'command': 'echo uwu', 'image': 'alpine:3.10.1', - 'ports': ['3341/tcp', '4449/tcp'] + 'ports': [{'target': 3341}, {'target': 4449}] } - }, - 'version': '2.4' + } } def test_config_external_volume_v2(self): @@ -405,11 +411,11 @@ def test_config_external_volume_v2(self): assert json_result['volumes'] == { 'foo': { 'external': True, + 'name': 'foo', }, 'bar': { - 'external': { - 'name': 'some_bar', - }, + 'external': True, + 'name': 'some_bar', } } @@ -437,11 +443,11 @@ def test_config_external_volume_v3_x(self): assert json_result['volumes'] == { 'foo': { 'external': True, + 'name': 'foo', }, 'bar': { - 'external': { - 'name': 'some_bar', - }, + 'external': True, + 'name': 'some_bar', } } @@ -481,7 +487,7 @@ def test_config_v1(self): self.base_dir = 'tests/fixtures/v1-config' result = self.dispatch(['config']) assert yaml.safe_load(result.stdout) == { - 'version': '2.1', + 'version': str(V1), 'services': { 'net': { 'image': 'busybox', @@ -500,11 +506,9 @@ def test_config_v1(self): }, } - @v3_only() def test_config_v3(self): self.base_dir = 'tests/fixtures/v3-full' result = self.dispatch(['config']) - assert yaml.safe_load(result.stdout) == { 'version': '3.5', 'volumes': { @@ -530,11 +534,11 @@ def test_config_v3(self): }, 'resources': { 'limits': { - 'cpus': '0.05', + 'cpus': 0.05, 'memory': '50M', }, 'reservations': { - 'cpus': '0.01', + 'cpus': 0.01, 'memory': '20M', }, }, @@ -578,12 +582,14 @@ def test_config_v3(self): }, } + @pytest.mark.skip(reason='deprecated option') def test_config_compatibility_mode(self): self.base_dir = 'tests/fixtures/compatibility-mode' result = self.dispatch(['--compatibility', 'config']) assert yaml.load(result.stdout) == COMPOSE_COMPATIBILITY_DICT + @pytest.mark.skip(reason='deprecated option') @mock.patch.dict(os.environ) def test_config_compatibility_mode_from_env(self): self.base_dir = 'tests/fixtures/compatibility-mode' @@ -592,6 +598,7 @@ def test_config_compatibility_mode_from_env(self): assert yaml.load(result.stdout) == COMPOSE_COMPATIBILITY_DICT + @pytest.mark.skip(reason='deprecated option') @mock.patch.dict(os.environ) def test_config_compatibility_mode_from_env_and_option_precedence(self): self.base_dir = 'tests/fixtures/compatibility-mode' @@ -749,6 +756,20 @@ def test_build_no_cache(self): assert BUILD_CACHE_TEXT not in result.stdout assert BUILD_PULL_TEXT not in result.stdout + def test_up_ignore_missing_build_directory(self): + self.base_dir = 'tests/fixtures/no-build' + result = self.dispatch(['up', '--no-build']) + + assert 'alpine exited with code 0' in result.stdout + self.base_dir = None + + def test_pull_ignore_missing_build_directory(self): + self.base_dir = 'tests/fixtures/no-build' + result = self.dispatch(['pull']) + + assert 'Pulling my-alpine' in result.stderr + self.base_dir = None + def test_build_pull(self): # Make sure we have the latest busybox already pull_busybox(self.client) @@ -771,7 +792,11 @@ def test_build_no_cache_pull(self): assert BUILD_CACHE_TEXT not in result.stdout assert BUILD_PULL_TEXT in result.stdout + @mock.patch.dict(os.environ) def test_build_log_level(self): + os.environ['COMPOSE_DOCKER_CLI_BUILD'] = '0' + os.environ['DOCKER_BUILDKIT'] = '0' + self.test_env_file_relative_to_compose_file() self.base_dir = 'tests/fixtures/simple-dockerfile' result = self.dispatch(['--log-level', 'warning', 'build', 'simple']) assert result.stderr == '' @@ -833,13 +858,17 @@ def test_build_rm(self): for c in self.project.client.containers(all=True): self.addCleanup(self.project.client.remove_container, c, force=True) + @mock.patch.dict(os.environ) def test_build_shm_size_build_option(self): + os.environ['COMPOSE_DOCKER_CLI_BUILD'] = '0' pull_busybox(self.client) self.base_dir = 'tests/fixtures/build-shm-size' result = self.dispatch(['build', '--no-cache'], None) assert 'shm_size: 96' in result.stdout + @mock.patch.dict(os.environ) def test_build_memory_build_option(self): + os.environ['COMPOSE_DOCKER_CLI_BUILD'] = '0' pull_busybox(self.client) self.base_dir = 'tests/fixtures/build-memory' result = self.dispatch(['build', '--no-cache', '--memory', '96m', 'service'], None) @@ -1020,7 +1049,6 @@ def test_down_invalid_rmi_flag(self): result = self.dispatch(['down', '--rmi', 'bogus'], returncode=1) assert '--rmi flag must be' in result.stderr - @v2_only() def test_down(self): self.base_dir = 'tests/fixtures/v2-full' @@ -1105,7 +1133,6 @@ def test_up_attached(self): assert '{} exited with code 0'.format(simple_name) in result.stdout assert '{} exited with code 0'.format(another_name) in result.stdout - @v2_only() def test_up(self): self.base_dir = 'tests/fixtures/v2-simple' self.dispatch(['up', '-d'], None) @@ -1137,7 +1164,6 @@ def test_up(self): for service in services: assert self.lookup(container, service.name) - @v2_only() def test_up_no_start(self): self.base_dir = 'tests/fixtures/v2-full' self.dispatch(['up', '--no-start'], None) @@ -1168,7 +1194,6 @@ def test_up_no_start(self): ] 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) @@ -1184,7 +1209,6 @@ def test_up_no_start_remove_orphans(self): 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' result = self.dispatch(['--no-ansi', 'up', '-d'], None) @@ -1192,7 +1216,6 @@ def test_up_no_ansi(self): assert "%c[1A" % 27 not in result.stderr assert "%c[1B" % 27 not in result.stderr - @v2_only() def test_up_with_default_network_config(self): filename = 'default-network-config.yml' @@ -1206,7 +1229,6 @@ def test_up_with_default_network_config(self): assert networks[0]['Options']['com.docker.network.bridge.enable_icc'] == 'false' - @v2_only() def test_up_with_network_aliases(self): filename = 'network-aliases.yml' self.base_dir = 'tests/fixtures/networks' @@ -1234,7 +1256,6 @@ def test_up_with_network_aliases(self): assert 'forward_facing' in front_aliases assert 'ahead' in front_aliases - @v2_only() def test_up_with_network_internal(self): self.require_api_version('1.23') filename = 'network-internal.yml' @@ -1252,7 +1273,6 @@ def test_up_with_network_internal(self): assert networks[0]['Internal'] is True - @v2_only() def test_up_with_network_static_addresses(self): filename = 'network-static-addresses.yml' ipv4_address = '172.16.100.100' @@ -1276,7 +1296,6 @@ def test_up_with_network_static_addresses(self): assert ipv4_address in ipam_config.values() assert ipv6_address in ipam_config.values() - @v2_only() def test_up_with_networks(self): self.base_dir = 'tests/fixtures/networks' self.dispatch(['up', '-d'], None) @@ -1324,7 +1343,6 @@ def test_up_with_networks(self): # app has aliased db to "database" assert self.lookup(app_container, "database") - @v2_only() def test_up_missing_network(self): self.base_dir = 'tests/fixtures/networks' @@ -1334,7 +1352,6 @@ def test_up_missing_network(self): assert 'Service "web" uses an undefined network "foo"' in result.stderr - @v2_only() @no_cluster('container networks not supported in Swarm') def test_up_with_network_mode(self): c = self.client.create_container( @@ -1373,7 +1390,6 @@ def test_up_with_network_mode(self): assert not container_mode_container.get('NetworkSettings.Networks') assert container_mode_container.get('HostConfig.NetworkMode') == container_mode_source - @v2_only() def test_up_external_networks(self): filename = 'external-networks.yml' @@ -1397,7 +1413,6 @@ def test_up_external_networks(self): container = self.project.containers()[0] assert sorted(list(container.get('NetworkSettings.Networks'))) == sorted(network_names) - @v2_only() def test_up_with_external_default_network(self): filename = 'external-default.yml' @@ -1420,7 +1435,6 @@ def test_up_with_external_default_network(self): container = self.project.containers()[0] assert list(container.get('NetworkSettings.Networks')) == [network_name] - @v2_1_only() def test_up_with_network_labels(self): filename = 'network-label.yml' @@ -1440,7 +1454,6 @@ def test_up_with_network_labels(self): assert 'label_key' in networks[0]['Labels'] assert networks[0]['Labels']['label_key'] == 'label_val' - @v2_1_only() def test_up_with_volume_labels(self): filename = 'volume-label.yml' @@ -1456,11 +1469,10 @@ def test_up_with_volume_labels(self): if v['Name'].split('/')[-1].startswith('{}_'.format(self.project.name)) ] - assert set([v['Name'].split('/')[-1] for v in volumes]) == {volume_with_label} + assert {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' - @v2_only() def test_up_no_services(self): self.base_dir = 'tests/fixtures/no-services' self.dispatch(['up', '-d'], None) @@ -1517,7 +1529,6 @@ def test_up_with_net_v1(self): bar_container.id ) - @v3_only() def test_up_with_healthcheck(self): def wait_on_health_status(container, status): def condition(): @@ -1651,7 +1662,6 @@ def test_up_handles_sigterm(self): os.kill(proc.pid, signal.SIGTERM) wait_on_condition(ContainerCountCondition(self.project, 0)) - @v2_only() def test_up_handles_force_shutdown(self): self.base_dir = 'tests/fixtures/sleeps-composefile' proc = start_process(self.base_dir, ['up', '-t', '200']) @@ -1676,7 +1686,6 @@ def test_up_handles_abort_on_container_exit_code(self): proc.wait() assert proc.returncode == 1 - @v2_only() @no_cluster('Container PID mode does not work across clusters') def test_up_with_pid_mode(self): c = self.client.create_container( @@ -1702,6 +1711,123 @@ def test_up_with_pid_mode(self): host_mode_container = self.project.get_service('host').containers()[0] assert host_mode_container.get('HostConfig.PidMode') == 'host' + @no_cluster('Container IPC mode does not work across clusters') + def test_up_with_ipc_mode(self): + c = self.client.create_container( + 'busybox', 'top', name='composetest_ipc_mode_container', + host_config={} + ) + self.addCleanup(self.client.remove_container, c, force=True) + self.client.start(c) + container_mode_source = 'container:{}'.format(c['Id']) + + self.base_dir = 'tests/fixtures/ipc-mode' + + self.dispatch(['up', '-d'], None) + + service_mode_source = 'container:{}'.format( + self.project.get_service('shareable').containers()[0].id) + service_mode_container = self.project.get_service('service').containers()[0] + assert service_mode_container.get('HostConfig.IpcMode') == service_mode_source + + container_mode_container = self.project.get_service('container').containers()[0] + assert container_mode_container.get('HostConfig.IpcMode') == container_mode_source + + shareable_mode_container = self.project.get_service('shareable').containers()[0] + assert shareable_mode_container.get('HostConfig.IpcMode') == 'shareable' + + def test_profiles_up_with_no_profile(self): + self.base_dir = 'tests/fixtures/profiles' + self.dispatch(['up']) + + containers = self.project.containers(stopped=True) + service_names = [c.service for c in containers] + + assert 'foo' in service_names + assert len(containers) == 1 + + def test_profiles_up_with_profile(self): + self.base_dir = 'tests/fixtures/profiles' + self.dispatch(['--profile', 'test', 'up']) + + containers = self.project.containers(stopped=True) + service_names = [c.service for c in containers] + + assert 'foo' in service_names + assert 'bar' in service_names + assert 'baz' in service_names + assert len(containers) == 3 + + def test_profiles_up_invalid_dependency(self): + self.base_dir = 'tests/fixtures/profiles' + result = self.dispatch(['--profile', 'debug', 'up'], returncode=1) + + assert ('Service "bar" was pulled in as a dependency of service "zot" ' + 'but is not enabled by the active profiles.') in result.stderr + + def test_profiles_up_with_multiple_profiles(self): + self.base_dir = 'tests/fixtures/profiles' + self.dispatch(['--profile', 'debug', '--profile', 'test', 'up']) + + containers = self.project.containers(stopped=True) + service_names = [c.service for c in containers] + + assert 'foo' in service_names + assert 'bar' in service_names + assert 'baz' in service_names + assert 'zot' in service_names + assert len(containers) == 4 + + def test_profiles_up_with_profile_enabled_by_service(self): + self.base_dir = 'tests/fixtures/profiles' + self.dispatch(['up', 'bar']) + + containers = self.project.containers(stopped=True) + service_names = [c.service for c in containers] + + assert 'bar' in service_names + assert len(containers) == 1 + + def test_profiles_up_with_dependency_and_profile_enabled_by_service(self): + self.base_dir = 'tests/fixtures/profiles' + self.dispatch(['up', 'baz']) + + containers = self.project.containers(stopped=True) + service_names = [c.service for c in containers] + + assert 'bar' in service_names + assert 'baz' in service_names + assert len(containers) == 2 + + def test_profiles_up_with_invalid_dependency_for_target_service(self): + self.base_dir = 'tests/fixtures/profiles' + result = self.dispatch(['up', 'zot'], returncode=1) + + assert ('Service "bar" was pulled in as a dependency of service "zot" ' + 'but is not enabled by the active profiles.') in result.stderr + + def test_profiles_up_with_profile_for_dependency(self): + self.base_dir = 'tests/fixtures/profiles' + self.dispatch(['--profile', 'test', 'up', 'zot']) + + containers = self.project.containers(stopped=True) + service_names = [c.service for c in containers] + + assert 'bar' in service_names + assert 'zot' in service_names + assert len(containers) == 2 + + def test_profiles_up_with_merged_profiles(self): + self.base_dir = 'tests/fixtures/profiles' + self.dispatch(['-f', 'docker-compose.yml', '-f', 'merge-profiles.yml', 'up', 'zot']) + + containers = self.project.containers(stopped=True) + service_names = [c.service for c in containers] + + assert 'bar' in service_names + assert 'zot' in service_names + assert len(containers) == 2 + def test_exec_without_tty(self): self.base_dir = 'tests/fixtures/links-composefile' self.dispatch(['up', '-d', 'console']) @@ -1740,7 +1866,6 @@ def test_exec_custom_user(self): assert stdout == "operator\n" assert stderr == "" - @v3_only() def test_exec_workdir(self): self.base_dir = 'tests/fixtures/links-composefile' os.environ['COMPOSE_API_VERSION'] = '1.35' @@ -1750,7 +1875,6 @@ def test_exec_workdir(self): stdout, stderr = self.dispatch(['exec', '-T', '--workdir', '/etc', 'console', 'ls']) assert 'passwd' in stdout - @v2_2_only() def test_exec_service_with_environment_overridden(self): name = 'service' self.base_dir = 'tests/fixtures/environment-exec' @@ -1795,7 +1919,6 @@ def test_run_service_with_links(self): assert len(db.containers()) == 1 assert len(console.containers()) == 0 - @v2_only() def test_run_service_with_dependencies(self): self.base_dir = 'tests/fixtures/v2-dependencies' self.dispatch(['run', 'web', '/bin/true'], None) @@ -1804,6 +1927,14 @@ def test_run_service_with_dependencies(self): assert len(db.containers()) == 1 assert len(console.containers()) == 0 + def test_run_service_with_unhealthy_dependencies(self): + self.base_dir = 'tests/fixtures/v2-unhealthy-dependencies' + result = self.dispatch(['run', 'web', '/bin/true'], returncode=1) + assert re.search( + re.compile('for web .*is unhealthy.*', re.MULTILINE), + result.stderr + ) + def test_run_service_with_scaled_dependencies(self): self.base_dir = 'tests/fixtures/v2-dependencies' self.dispatch(['up', '-d', '--scale', 'db=2', '--scale', 'console=0']) @@ -1843,12 +1974,12 @@ def test_run_without_command(self): self.dispatch(['run', 'implicit']) service = self.project.get_service('implicit') containers = service.containers(stopped=True, one_off=OneOffFilter.only) - assert [c.human_readable_command for c in containers] == [u'/bin/sh -c echo "success"'] + assert [c.human_readable_command for c in containers] == ['/bin/sh -c echo "success"'] self.dispatch(['run', 'explicit']) service = self.project.get_service('explicit') containers = service.containers(stopped=True, one_off=OneOffFilter.only) - assert [c.human_readable_command for c in containers] == [u'/bin/true'] + assert [c.human_readable_command for c in containers] == ['/bin/true'] @pytest.mark.skipif(SWARM_SKIP_RM_VOLUMES, reason='Swarm DELETE /containers/ bug') def test_run_rm(self): @@ -2107,7 +2238,6 @@ def test_run_service_with_workdir_overridden_short_form(self): container = service.containers(stopped=True, one_off=True)[0] assert workdir == container.get('Config.WorkingDir') - @v2_only() def test_run_service_with_use_aliases(self): filename = 'network-aliases.yml' self.base_dir = 'tests/fixtures/networks' @@ -2129,7 +2259,6 @@ def test_run_service_with_use_aliases(self): assert 'forward_facing' in front_aliases assert 'ahead' in front_aliases - @v2_only() def test_run_interactive_connects_to_network(self): self.base_dir = 'tests/fixtures/networks' @@ -2155,7 +2284,6 @@ def test_run_interactive_connects_to_network(self): aliases = set(config['Aliases'] or []) - {container.short_id} assert not aliases - @v2_only() def test_run_detached_connects_to_network(self): self.base_dir = 'tests/fixtures/networks' self.dispatch(['up', '-d']) @@ -2220,15 +2348,9 @@ def test_run_handles_sighup(self): @mock.patch.dict(os.environ) def test_run_unicode_env_values_from_system(self): value = 'ą, ć, ę, ł, ń, ó, ś, ź, ż' - if six.PY2: # os.environ doesn't support unicode values in Py2 - os.environ['BAR'] = value.encode('utf-8') - else: # ... and doesn't support byte values in Py3 - os.environ['BAR'] = value + os.environ['BAR'] = value self.base_dir = 'tests/fixtures/unicode-environment' - result = self.dispatch(['run', 'simple']) - - if six.PY2: # Can't retrieve output on Py3. See issue #3670 - assert value in result.stdout.strip() + self.dispatch(['run', 'simple']) container = self.project.containers(one_off=OneOffFilter.only, stopped=True)[0] environment = container.get('Config.Env') @@ -2340,7 +2462,6 @@ def test_start_no_containers(self): assert 'failed' in result.stderr assert 'No containers to start' in result.stderr - @v2_only() def test_up_logging(self): self.base_dir = 'tests/fixtures/logging-composefile' self.dispatch(['up', '-d']) @@ -2571,11 +2692,6 @@ def test_scale(self): assert len(project.get_service('simple').containers()) == 0 assert len(project.get_service('another').containers()) == 0 - def test_scale_v2_2(self): - self.base_dir = 'tests/fixtures/scale' - result = self.dispatch(['scale', 'web=1'], returncode=1) - assert 'incompatible with the v2.2 format' in result.stderr - def test_up_scale_scale_up(self): self.base_dir = 'tests/fixtures/scale' project = self.project @@ -2693,7 +2809,7 @@ def has_timestamp(string): str_iso_date, str_iso_time, container_info = string.split(' ', 2) try: return isinstance(datetime.datetime.strptime( - '%s %s' % (str_iso_date, str_iso_time), + '{} {}'.format(str_iso_date, str_iso_time), '%Y-%m-%d %H:%M:%S.%f'), datetime.datetime) except ValueError: @@ -2782,7 +2898,7 @@ def test_up_with_extends(self): self.base_dir = 'tests/fixtures/extends' self.dispatch(['up', '-d'], None) - assert set([s.name for s in self.project.services]) == {'mydb', 'myweb'} + assert {s.name for s in self.project.services} == {'mydb', 'myweb'} # Sort by name so we get [db, web] containers = sorted( @@ -2901,3 +3017,170 @@ def test_images_use_service_tag(self): assert re.search(r'foo1.+test[ \t]+dev', result.stdout) is not None assert re.search(r'foo2.+test[ \t]+prod', result.stdout) is not None assert re.search(r'foo3.+test[ \t]+latest', result.stdout) is not None + + def test_build_with_stop_process_flag(self): + self.base_dir = 'tests/fixtures/flag-as-service-name' + result = self.dispatch(['build', '--pull', '--', '--test-service']) + + assert BUILD_PULL_TEXT in result.stdout + + def test_events_with_stop_process_flag(self): + self.base_dir = 'tests/fixtures/flag-as-service-name' + events_proc = start_process(self.base_dir, ['events', '--json', '--', '--test-service']) + self.dispatch(['up', '-d', '--', '--test-service']) + wait_on_condition(ContainerCountCondition(self.project, 1)) + + os.kill(events_proc.pid, signal.SIGINT) + result = wait_on_process(events_proc, returncode=1) + lines = [json.loads(line) for line in result.stdout.rstrip().split('\n')] + assert Counter(e['action'] for e in lines) == {'create': 1, 'start': 1} + + def test_exec_with_stop_process_flag(self): + self.base_dir = 'tests/fixtures/flag-as-service-name' + self.dispatch(['up', '-d', '--', '--test-service']) + assert len(self.project.containers()) == 1 + + stdout, stderr = self.dispatch(['exec', '-T', '--', '--test-service', 'ls', '-1d', '/']) + + assert stderr == "" + assert stdout == "/\n" + + def test_images_with_stop_process_flag(self): + self.base_dir = 'tests/fixtures/flag-as-service-name' + self.dispatch(['up', '-d', '--', '--test-service']) + result = self.dispatch(['images', '--', '--test-service']) + + assert "busybox" in result.stdout + + def test_kill_with_stop_process_flag(self): + self.base_dir = 'tests/fixtures/flag-as-service-name' + self.dispatch(['up', '-d', '--', '--test-service']) + service = self.project.get_service('--test-service') + + assert len(service.containers()) == 1 + assert service.containers()[0].is_running + + self.dispatch(['kill', '--', '--test-service']) + + assert len(service.containers(stopped=True)) == 1 + assert not service.containers(stopped=True)[0].is_running + + def test_logs_with_stop_process_flag(self): + self.base_dir = 'tests/fixtures/flag-as-service-name' + self.dispatch(['up', '-d', '--', '--log-service']) + result = self.dispatch(['logs', '--', '--log-service']) + + assert 'hello' in result.stdout + assert 'exited with' not in result.stdout + + def test_port_with_stop_process_flag(self): + self.base_dir = 'tests/fixtures/flag-as-service-name' + self.dispatch(['up', '-d', '--', '--test-service']) + result = self.dispatch(['port', '--', '--test-service', '80']) + + assert result.stdout.strip() == "0.0.0.0:8080" + + def test_ps_with_stop_process_flag(self): + self.base_dir = 'tests/fixtures/flag-as-service-name' + self.dispatch(['up', '-d', '--', '--test-service']) + + result = self.dispatch(['ps', '--', '--test-service']) + + assert 'flag-as-service-name_--test-service_1' in result.stdout + + def test_pull_with_stop_process_flag(self): + self.base_dir = 'tests/fixtures/flag-as-service-name' + result = self.dispatch(['pull', '--', '--test-service']) + + assert 'Pulling --test-service' in result.stderr + assert 'failed' not in result.stderr + + def test_rm_with_stop_process_flag(self): + self.base_dir = 'tests/fixtures/flag-as-service-name' + self.dispatch(['up', '--no-start', '--', '--test-service']) + service = self.project.get_service('--test-service') + assert len(service.containers(stopped=True)) == 1 + + self.dispatch(['rm', '--force', '--', '--test-service']) + assert len(service.containers(stopped=True)) == 0 + + def test_run_with_stop_process_flag(self): + self.base_dir = 'tests/fixtures/flag-as-service-name' + result = self.dispatch(['run', '--no-deps', '--', '--test-service', 'echo', '-hello']) + + assert 'hello' in result.stdout + assert len(self.project.containers()) == 0 + + def test_stop_with_stop_process_flag(self): + self.base_dir = 'tests/fixtures/flag-as-service-name' + self.dispatch(['up', '-d', '--', '--test-service']) + service = self.project.get_service('--test-service') + assert len(service.containers()) == 1 + assert service.containers()[0].is_running + + self.dispatch(['stop', '-t', '1', '--', '--test-service']) + + assert len(service.containers(stopped=True)) == 1 + assert not service.containers(stopped=True)[0].is_running + + def test_restart_with_stop_process_flag(self): + self.base_dir = 'tests/fixtures/flag-as-service-name' + self.dispatch(['up', '-d', '--', '--test-service']) + service = self.project.get_service('--test-service') + assert len(service.containers()) == 1 + assert service.containers()[0].is_running + + self.dispatch(['restart', '-t', '1', '--', '--test-service']) + + assert len(service.containers()) == 1 + assert service.containers()[0].is_running + + def test_up_with_stop_process_flag(self): + self.base_dir = 'tests/fixtures/flag-as-service-name' + self.dispatch(['up', '-d', '--', '--test-service', '--log-service']) + + service = self.project.get_service('--test-service') + another = self.project.get_service('--log-service') + assert len(service.containers()) == 1 + assert len(another.containers()) == 1 + + def test_up_no_log_prefix(self): + self.base_dir = 'tests/fixtures/echo-services' + result = self.dispatch(['up', '--no-log-prefix']) + + assert 'simple' in result.stdout + assert 'another' in result.stdout + assert 'exited with code 0' in result.stdout + assert 'exited with code 0' in result.stdout + + def test_up_not_pull(self): + self.dispatch(['up', '-d'], None) + service = self.project.get_service('simple') + another = self.project.get_service('another') + + assert len(service.containers()) == 1 + assert len(another.containers()) == 1 + + result = self.dispatch(['up', '-d'], None) + assert len(service.containers()) == 1 + assert len(another.containers()) == 1 + + assert 'Pulling simple' not in result.stderr + assert 'Pulling another' not in result.stderr + assert 'failed' not in result.stderr + + def test_up_pull(self): + self.dispatch(['up', '-d'], None) + service = self.project.get_service('simple') + another = self.project.get_service('another') + + assert len(service.containers()) == 1 + assert len(another.containers()) == 1 + + result = self.dispatch(['up', '-d', '--pull'], None) + assert len(service.containers()) == 1 + assert len(another.containers()) == 1 + + assert 'Pulling simple' in result.stderr + assert 'Pulling another' in result.stderr + assert 'failed' not in result.stderr \ No newline at end of file diff --git a/tests/acceptance/context_test.py b/tests/acceptance/context_test.py new file mode 100644 index 00000000000..a5d0c14730f --- /dev/null +++ b/tests/acceptance/context_test.py @@ -0,0 +1,44 @@ +import os +import shutil +import unittest + +from docker import ContextAPI + +from tests.acceptance.cli_test import dispatch + + +class ContextTestCase(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.docker_dir = os.path.join(os.environ.get("HOME", "/tmp"), '.docker') + if not os.path.exists(cls.docker_dir): + os.makedirs(cls.docker_dir) + f = open(os.path.join(cls.docker_dir, "config.json"), "w") + f.write("{}") + f.close() + cls.docker_config = os.path.join(cls.docker_dir, "config.json") + os.environ['DOCKER_CONFIG'] = cls.docker_config + ContextAPI.create_context("testcontext", host="tcp://doesnotexist:8000") + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.docker_dir, ignore_errors=True) + + def setUp(self): + self.base_dir = 'tests/fixtures/simple-composefile' + self.override_dir = None + + def dispatch(self, options, project_options=None, returncode=0, stdin=None): + return dispatch(self.base_dir, options, project_options, returncode, stdin) + + def test_help(self): + result = self.dispatch(['help'], returncode=0) + assert '-c, --context NAME' in result.stdout + + def test_fail_on_both_host_and_context_opt(self): + result = self.dispatch(['-H', 'unix://', '-c', 'default', 'up'], returncode=1) + assert '-H, --host and -c, --context are mutually exclusive' in result.stderr + + def test_fail_run_on_inexistent_context(self): + result = self.dispatch(['-c', 'testcontext', 'up', '-d'], returncode=1) + assert "Couldn't connect to Docker daemon" in result.stderr diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000000..fd31a974840 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,240 @@ +import pytest + +import tests.acceptance.cli_test + +# FIXME Skipping all the acceptance tests when in `--conformity` +non_conformity_tests = [ + "test_build_failed", + "test_build_failed_forcerm", + "test_build_log_level", + "test_build_memory_build_option", + "test_build_no_cache", + "test_build_no_cache_pull", + "test_build_override_dir", + "test_build_override_dir_invalid_path", + "test_build_parallel", + "test_build_plain", + "test_build_pull", + "test_build_rm", + "test_build_shm_size_build_option", + "test_build_with_buildarg_cli_override", + "test_build_with_buildarg_from_compose_file", + "test_build_with_buildarg_old_api_version", + "test_config_compatibility_mode", + "test_config_compatibility_mode_from_env", + "test_config_compatibility_mode_from_env_and_option_precedence", + "test_config_default", + "test_config_external_network", + "test_config_external_network_v3_5", + "test_config_external_volume_v2", + "test_config_external_volume_v2_x", + "test_config_external_volume_v3_4", + "test_config_external_volume_v3_x", + "test_config_list_services", + "test_config_list_volumes", + "test_config_quiet", + "test_config_quiet_with_error", + "test_config_restart", + "test_config_stdin", + "test_config_v1", + "test_config_v3", + "test_config_with_dot_env", + "test_config_with_dot_env_and_override_dir", + "test_config_with_env_file", + "test_config_with_hash_option", + "test_create", + "test_create_with_force_recreate", + "test_create_with_force_recreate_and_no_recreate", + "test_create_with_no_recreate", + "test_down", + "test_down_invalid_rmi_flag", + "test_down_signal", + "test_down_timeout", + "test_env_file_relative_to_compose_file", + "test_events_human_readable", + "test_events_json", + "test_exec_custom_user", + "test_exec_detach_long_form", + "test_exec_novalue_var_dotenv_file", + "test_exec_service_with_environment_overridden", + "test_exec_without_tty", + "test_exec_workdir", + "test_exit_code_from_signal_stop", + "test_expanded_port", + "test_forward_exitval", + "test_help", + "test_help_nonexistent", + "test_home_and_env_var_in_volume_path", + "test_host_not_reachable", + "test_host_not_reachable_volumes_from_container", + "test_host_not_reachable_volumes_from_container", + "test_images", + "test_images_default_composefile", + "test_images_tagless_image", + "test_images_use_service_tag", + "test_kill", + "test_kill_signal_sigstop", + "test_kill_stopped_service", + "test_logs_default", + "test_logs_follow", + "test_logs_follow_logs_from_new_containers", + "test_logs_follow_logs_from_restarted_containers", + "test_logs_invalid_service_name", + "test_logs_on_stopped_containers_exits", + "test_logs_tail", + "test_logs_timestamps", + "test_pause_no_containers", + "test_pause_unpause", + "test_port", + "test_port_with_scale", + "test_ps", + "test_ps_all", + "test_ps_alternate_composefile", + "test_ps_default_composefile", + "test_ps_services_filter_option", + "test_ps_services_filter_status", + "test_pull", + "test_pull_can_build", + "test_pull_with_digest", + "test_pull_with_ignore_pull_failures", + "test_pull_with_include_deps", + "test_pull_with_no_deps", + "test_pull_with_parallel_failure", + "test_pull_with_quiet", + "test_quiet_build", + "test_restart", + "test_restart_no_containers", + "test_restart_stopped_container", + "test_rm", + "test_rm_all", + "test_rm_stop", + "test_run_detached_connects_to_network", + "test_run_does_not_recreate_linked_containers", + "test_run_env_values_from_system", + "test_run_handles_sighup", + "test_run_handles_sigint", + "test_run_handles_sigterm", + "test_run_interactive_connects_to_network", + "test_run_label_flag", + "test_run_one_off_with_multiple_volumes", + "test_run_one_off_with_volume", + "test_run_one_off_with_volume_merge", + "test_run_rm", + "test_run_service_with_compose_file_entrypoint", + "test_run_service_with_compose_file_entrypoint_and_command_overridden", + "test_run_service_with_compose_file_entrypoint_and_empty_string_command", + "test_run_service_with_compose_file_entrypoint_overridden", + "test_run_service_with_dependencies", + "test_run_service_with_dockerfile_entrypoint", + "test_run_service_with_dockerfile_entrypoint_and_command_overridden", + "test_run_service_with_dockerfile_entrypoint_overridden", + "test_run_service_with_environment_overridden", + "test_run_service_with_explicitly_mapped_ip_ports", + "test_run_service_with_explicitly_mapped_ports", + "test_run_service_with_links", + "test_run_service_with_map_ports", + "test_run_service_with_scaled_dependencies", + "test_run_service_with_unset_entrypoint", + "test_run_service_with_use_aliases", + "test_run_service_with_user_overridden", + "test_run_service_with_user_overridden_short_form", + "test_run_service_with_workdir_overridden", + "test_run_service_with_workdir_overridden_short_form", + "test_run_service_without_links", + "test_run_service_without_map_ports", + "test_run_unicode_env_values_from_system", + "test_run_with_custom_name", + "test_run_with_expose_ports", + "test_run_with_no_deps", + "test_run_without_command", + "test_scale", + "test_scale_v2_2", + "test_shorthand_host_opt", + "test_shorthand_host_opt_interactive", + "test_start_no_containers", + "test_stop", + "test_stop_signal", + "test_top_processes_running", + "test_top_services_not_running", + "test_top_services_running", + "test_unpause_no_containers", + "test_up", + "test_up_attached", + "test_up_detached", + "test_up_detached_long_form", + "test_up_external_networks", + "test_up_handles_abort_on_container_exit", + "test_up_handles_abort_on_container_exit_code", + "test_up_handles_aborted_dependencies", + "test_up_handles_force_shutdown", + "test_up_handles_sigint", + "test_up_handles_sigterm", + "test_up_logging", + "test_up_logging_legacy", + "test_up_missing_network", + "test_up_no_ansi", + "test_up_no_services", + "test_up_no_start", + "test_up_no_start_remove_orphans", + "test_up_scale_reset", + "test_up_scale_scale_down", + "test_up_scale_scale_up", + "test_up_scale_to_zero", + "test_up_with_attach_dependencies", + "test_up_with_default_network_config", + "test_up_with_default_override_file", + "test_up_with_duplicate_override_yaml_files", + "test_up_with_extends", + "test_up_with_external_default_network", + "test_up_with_force_recreate", + "test_up_with_force_recreate_and_no_recreate", + "test_up_with_healthcheck", + "test_up_with_ignore_remove_orphans", + "test_up_with_links_v1", + "test_up_with_multiple_files", + "test_up_with_net_is_invalid", + "test_up_with_net_v1", + "test_up_with_network_aliases", + "test_up_with_network_internal", + "test_up_with_network_labels", + "test_up_with_network_mode", + "test_up_with_network_static_addresses", + "test_up_with_networks", + "test_up_with_no_deps", + "test_up_with_no_recreate", + "test_up_with_override_yaml", + "test_up_with_pid_mode", + "test_up_with_timeout", + "test_up_with_volume_labels", + "test_fail_on_both_host_and_context_opt", + "test_fail_run_on_inexistent_context", +] + + +def pytest_addoption(parser): + parser.addoption( + "--conformity", + action="store_true", + default=False, + help="Only runs tests that are not black listed as non conformity test. " + "The conformity tests check for compatibility with the Compose spec." + ) + parser.addoption( + "--binary", + default=tests.acceptance.cli_test.DOCKER_COMPOSE_EXECUTABLE, + help="Forces the execution of a binary in the PATH. Default is `docker-compose`." + ) + + +def pytest_collection_modifyitems(config, items): + if not config.getoption("--conformity"): + return + if config.getoption("--binary"): + tests.acceptance.cli_test.DOCKER_COMPOSE_EXECUTABLE = config.getoption("--binary") + + print("Binary -> {}".format(tests.acceptance.cli_test.DOCKER_COMPOSE_EXECUTABLE)) + skip_non_conformity = pytest.mark.skip(reason="skipping because that's not a conformity test") + for item in items: + if item.name in non_conformity_tests: + print("Skipping '{}' when running in compatibility mode".format(item.name)) + item.add_marker(skip_non_conformity) diff --git a/tests/fixtures/config-profiles/docker-compose.yml b/tests/fixtures/config-profiles/docker-compose.yml new file mode 100644 index 00000000000..a445e7e0414 --- /dev/null +++ b/tests/fixtures/config-profiles/docker-compose.yml @@ -0,0 +1,15 @@ +version: '3.8' +services: + frontend: + image: frontend + profiles: ["frontend", "gui"] + phpmyadmin: + image: phpmyadmin + depends_on: + - db + profiles: + - debug + backend: + image: backend + db: + image: mysql diff --git a/tests/fixtures/env-file-override/.env b/tests/fixtures/env-file-override/.env new file mode 100644 index 00000000000..467f2c1d24f --- /dev/null +++ b/tests/fixtures/env-file-override/.env @@ -0,0 +1 @@ +WHEREAMI=default diff --git a/tests/fixtures/env/three.env b/tests/fixtures/env/three.env new file mode 100644 index 00000000000..c2da74f19ee --- /dev/null +++ b/tests/fixtures/env/three.env @@ -0,0 +1,2 @@ +FOO=NO $ENV VAR +DOO=NO ${ENV} VAR diff --git a/tests/fixtures/flag-as-service-name/Dockerfile b/tests/fixtures/flag-as-service-name/Dockerfile new file mode 100644 index 00000000000..098ff3eb195 --- /dev/null +++ b/tests/fixtures/flag-as-service-name/Dockerfile @@ -0,0 +1,3 @@ +FROM busybox:1.27.2 +LABEL com.docker.compose.test_image=true +CMD echo "success" diff --git a/tests/fixtures/flag-as-service-name/docker-compose.yml b/tests/fixtures/flag-as-service-name/docker-compose.yml new file mode 100644 index 00000000000..5b519a63efe --- /dev/null +++ b/tests/fixtures/flag-as-service-name/docker-compose.yml @@ -0,0 +1,12 @@ +version: "2" +services: + --test-service: + image: busybox:1.27.0.2 + build: . + command: top + ports: + - "8080:80" + + --log-service: + image: busybox:1.31.0-uclibc + command: sh -c "echo hello && tail -f /dev/null" diff --git a/tests/fixtures/ipc-mode/docker-compose.yml b/tests/fixtures/ipc-mode/docker-compose.yml new file mode 100644 index 00000000000..c58ce24484d --- /dev/null +++ b/tests/fixtures/ipc-mode/docker-compose.yml @@ -0,0 +1,17 @@ +version: "2.4" + +services: + service: + image: busybox + command: top + ipc: "service:shareable" + + container: + image: busybox + command: top + ipc: "container:composetest_ipc_mode_container" + + shareable: + image: busybox + command: top + ipc: shareable diff --git a/tests/fixtures/no-build/docker-compose.yml b/tests/fixtures/no-build/docker-compose.yml new file mode 100644 index 00000000000..f320d17c394 --- /dev/null +++ b/tests/fixtures/no-build/docker-compose.yml @@ -0,0 +1,8 @@ +version: "3" +services: + my-alpine: + image: alpine:3.12 + container_name: alpine + entrypoint: 'echo It works!' + build: + context: /this/path/doesnt/exist # and we don't really care. We just want to run containers already pulled. diff --git a/tests/fixtures/profiles/docker-compose.yml b/tests/fixtures/profiles/docker-compose.yml new file mode 100644 index 00000000000..ba77f03b446 --- /dev/null +++ b/tests/fixtures/profiles/docker-compose.yml @@ -0,0 +1,20 @@ +version: "3" +services: + foo: + image: busybox:1.31.0-uclibc + bar: + image: busybox:1.31.0-uclibc + profiles: + - test + baz: + image: busybox:1.31.0-uclibc + depends_on: + - bar + profiles: + - test + zot: + image: busybox:1.31.0-uclibc + depends_on: + - bar + profiles: + - debug diff --git a/tests/fixtures/profiles/merge-profiles.yml b/tests/fixtures/profiles/merge-profiles.yml new file mode 100644 index 00000000000..42b0cfa4308 --- /dev/null +++ b/tests/fixtures/profiles/merge-profiles.yml @@ -0,0 +1,5 @@ +version: "3" +services: + bar: + profiles: + - debug diff --git a/tests/fixtures/simple-failing-dockerfile/Dockerfile b/tests/fixtures/simple-failing-dockerfile/Dockerfile index 205021a23f5..a3328b0d5ec 100644 --- a/tests/fixtures/simple-failing-dockerfile/Dockerfile +++ b/tests/fixtures/simple-failing-dockerfile/Dockerfile @@ -1,7 +1,7 @@ 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 +# With the following label the container will be cleaned up automatically # Must be kept in sync with LABEL_PROJECT from compose/const.py LABEL com.docker.compose.project=composetest RUN exit 1 diff --git a/tests/fixtures/v2-unhealthy-dependencies/docker-compose.yml b/tests/fixtures/v2-unhealthy-dependencies/docker-compose.yml new file mode 100644 index 00000000000..d96473e5a9f --- /dev/null +++ b/tests/fixtures/v2-unhealthy-dependencies/docker-compose.yml @@ -0,0 +1,19 @@ +version: "2.1" +services: + db: + image: busybox:1.31.0-uclibc + command: top + healthcheck: + test: exit 1 + interval: 1s + timeout: 1s + retries: 1 + web: + image: busybox:1.31.0-uclibc + command: top + depends_on: + db: + condition: service_healthy + console: + image: busybox:1.31.0-uclibc + command: top diff --git a/tests/fixtures/v3-full/docker-compose.yml b/tests/fixtures/v3-full/docker-compose.yml index 3a7ac25c905..0a51565826e 100644 --- a/tests/fixtures/v3-full/docker-compose.yml +++ b/tests/fixtures/v3-full/docker-compose.yml @@ -14,10 +14,10 @@ services: max_failure_ratio: 0.3 resources: limits: - cpus: '0.05' + cpus: 0.05 memory: 50M reservations: - cpus: '0.01' + cpus: 0.01 memory: 20M restart_policy: condition: on-failure diff --git a/tests/helpers.py b/tests/helpers.py index 1365c5bcfb3..3642e6ebc5f 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import contextlib import os @@ -52,7 +49,7 @@ def create_custom_host_file(client, filename, content): def create_host_file(client, filename): - with open(filename, 'r') as fh: + with open(filename) as fh: content = fh.read() return create_custom_host_file(client, filename, content) diff --git a/tests/integration/environment_test.py b/tests/integration/environment_test.py index 671e65318a6..b7822a594b9 100644 --- a/tests/integration/environment_test.py +++ b/tests/integration/environment_test.py @@ -1,8 +1,6 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import tempfile +import pytest from ddt import data from ddt import ddt @@ -11,6 +9,7 @@ from compose.cli.command import get_project from compose.cli.command import project_from_options from compose.config.environment import Environment +from compose.config.errors import EnvFileNotFound from tests.integration.testcases import DockerClientTestCase @@ -18,7 +17,7 @@ class EnvironmentTest(DockerClientTestCase): @classmethod def setUpClass(cls): - super(EnvironmentTest, cls).setUpClass() + super().setUpClass() cls.compose_file = tempfile.NamedTemporaryFile(mode='w+b') cls.compose_file.write(bytes("""version: '3.2' services: @@ -30,7 +29,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): - super(EnvironmentTest, cls).tearDownClass() + super().tearDownClass() cls.compose_file.close() @data('events', @@ -58,13 +57,36 @@ def _test_no_warning_on_missing_host_environment_var_on_silent_commands(self, cm class EnvironmentOverrideFileTest(DockerClientTestCase): def test_env_file_override(self): base_dir = 'tests/fixtures/env-file-override' + # '--env-file' are relative to the current working dir + env = Environment.from_env_file(base_dir, base_dir+'/.env.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'), + environment=env, 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) + + def test_env_file_not_found_error(self): + base_dir = 'tests/fixtures/env-file-override' + with pytest.raises(EnvFileNotFound) as excinfo: + Environment.from_env_file(base_dir, '.env.override') + + assert "Couldn't find env file" in excinfo.exconly() + + def test_dot_env_file(self): + base_dir = 'tests/fixtures/env-file-override' + # '.env' is relative to the project_dir (base_dir) + env = Environment.from_env_file(base_dir, None) + dispatch(base_dir, ['up']) + project = get_project(project_dir=base_dir, + config_path=['docker-compose.yml'], + environment=env, + override_dir=base_dir) + containers = project.containers(stopped=True) + assert len(containers) == 1 + assert "WHEREAMI=default" in containers[0].get('Config.Env') + dispatch(base_dir, ['down'], None) diff --git a/tests/integration/metrics_test.py b/tests/integration/metrics_test.py new file mode 100644 index 00000000000..3d6e3fe220e --- /dev/null +++ b/tests/integration/metrics_test.py @@ -0,0 +1,125 @@ +import logging +import os +import socket +from http.server import BaseHTTPRequestHandler +from http.server import HTTPServer +from threading import Thread + +import requests +from docker.transport import UnixHTTPAdapter + +from tests.acceptance.cli_test import dispatch +from tests.integration.testcases import DockerClientTestCase + + +TEST_SOCKET_FILE = '/tmp/test-metrics-docker-cli.sock' + + +class MetricsTest(DockerClientTestCase): + test_session = requests.sessions.Session() + test_env = None + base_dir = 'tests/fixtures/v3-full' + + @classmethod + def setUpClass(cls): + super().setUpClass() + MetricsTest.test_session.mount("http+unix://", UnixHTTPAdapter(TEST_SOCKET_FILE)) + MetricsTest.test_env = os.environ.copy() + MetricsTest.test_env['METRICS_SOCKET_FILE'] = TEST_SOCKET_FILE + MetricsServer().start() + + @classmethod + def test_metrics_help(cls): + # root `docker-compose` command is considered as a `--help` + dispatch(cls.base_dir, [], env=MetricsTest.test_env) + assert cls.get_content() == \ + b'{"command": "compose --help", "context": "moby", ' \ + b'"source": "docker-compose", "status": "success"}' + dispatch(cls.base_dir, ['help', 'run'], env=MetricsTest.test_env) + assert cls.get_content() == \ + b'{"command": "compose help", "context": "moby", ' \ + b'"source": "docker-compose", "status": "success"}' + dispatch(cls.base_dir, ['--help'], env=MetricsTest.test_env) + assert cls.get_content() == \ + b'{"command": "compose --help", "context": "moby", ' \ + b'"source": "docker-compose", "status": "success"}' + dispatch(cls.base_dir, ['run', '--help'], env=MetricsTest.test_env) + assert cls.get_content() == \ + b'{"command": "compose --help run", "context": "moby", ' \ + b'"source": "docker-compose", "status": "success"}' + dispatch(cls.base_dir, ['up', '--help', 'extra_args'], env=MetricsTest.test_env) + assert cls.get_content() == \ + b'{"command": "compose --help up", "context": "moby", ' \ + b'"source": "docker-compose", "status": "success"}' + + @classmethod + def test_metrics_simple_commands(cls): + dispatch(cls.base_dir, ['ps'], env=MetricsTest.test_env) + assert cls.get_content() == \ + b'{"command": "compose ps", "context": "moby", ' \ + b'"source": "docker-compose", "status": "success"}' + dispatch(cls.base_dir, ['version'], env=MetricsTest.test_env) + assert cls.get_content() == \ + b'{"command": "compose version", "context": "moby", ' \ + b'"source": "docker-compose", "status": "success"}' + dispatch(cls.base_dir, ['version', '--yyy'], env=MetricsTest.test_env) + assert cls.get_content() == \ + b'{"command": "compose version", "context": "moby", ' \ + b'"source": "docker-compose", "status": "failure"}' + + @staticmethod + def get_content(): + resp = MetricsTest.test_session.get("http+unix://localhost") + print(resp.content) + return resp.content + + +def start_server(uri=TEST_SOCKET_FILE): + try: + os.remove(uri) + except OSError: + pass + httpd = HTTPServer(uri, MetricsHTTPRequestHandler, False) + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.bind(TEST_SOCKET_FILE) + sock.listen(0) + httpd.socket = sock + print('Serving on ', uri) + httpd.serve_forever() + sock.shutdown(socket.SHUT_RDWR) + sock.close() + os.remove(uri) + + +class MetricsServer: + @classmethod + def start(cls): + t = Thread(target=start_server, daemon=True) + t.start() + + +class MetricsHTTPRequestHandler(BaseHTTPRequestHandler): + usages = [] + + def do_GET(self): + self.client_address = ('',) # avoid exception in BaseHTTPServer.py log_message() + self.send_response(200) + self.end_headers() + for u in MetricsHTTPRequestHandler.usages: + self.wfile.write(u) + MetricsHTTPRequestHandler.usages = [] + + def do_POST(self): + self.client_address = ('',) # avoid exception in BaseHTTPServer.py log_message() + content_length = int(self.headers['Content-Length']) + body = self.rfile.read(content_length) + print(body) + MetricsHTTPRequestHandler.usages.append(body) + self.send_response(200) + self.end_headers() + + +if __name__ == '__main__': + logging.getLogger("urllib3").propagate = False + logging.getLogger("requests").propagate = False + start_server() diff --git a/tests/integration/network_test.py b/tests/integration/network_test.py index a2493fda1a2..23c9e9a4bda 100644 --- a/tests/integration/network_test.py +++ b/tests/integration/network_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import pytest from .testcases import DockerClientTestCase diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index cb620a8c972..54fe1ad7b06 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import copy import json import os @@ -24,14 +21,11 @@ from compose.config import types from compose.config.types import VolumeFromSpec from compose.config.types import VolumeSpec -from compose.const import COMPOSEFILE_V2_0 as V2_0 -from compose.const import COMPOSEFILE_V2_1 as V2_1 -from compose.const import COMPOSEFILE_V2_2 as V2_2 -from compose.const import COMPOSEFILE_V2_3 as V2_3 -from compose.const import COMPOSEFILE_V3_1 as V3_1 +from compose.const import COMPOSE_SPEC as VERSION from compose.const import LABEL_PROJECT from compose.const import LABEL_SERVICE from compose.container import Container +from compose.errors import CompletedUnsuccessfully from compose.errors import HealthCheckFailed from compose.errors import NoHealthCheckConfigured from compose.project import Project @@ -40,16 +34,12 @@ from tests.integration.testcases import if_runtime_available from tests.integration.testcases import is_cluster from tests.integration.testcases import no_cluster -from tests.integration.testcases import v2_1_only -from tests.integration.testcases import v2_2_only -from tests.integration.testcases import v2_3_only -from tests.integration.testcases import v2_only -from tests.integration.testcases import v3_only def build_config(**kwargs): return config.Config( - version=kwargs.get('version'), + config_version=kwargs.get('version', VERSION), + version=kwargs.get('version', VERSION), services=kwargs.get('services'), volumes=kwargs.get('volumes'), networks=kwargs.get('networks'), @@ -109,7 +99,6 @@ def test_containers_with_extra_service(self): def test_parallel_pull_with_no_image(self): config_data = build_config( - version=V2_3, services=[{ 'name': 'web', 'build': {'context': '.'}, @@ -165,14 +154,12 @@ def test_volumes_from_container(self): db = project.get_service('db') assert db._get_volumes_from() == [data_container.id + ':rw'] - @v2_only() @no_cluster('container networks not supported in Swarm') def test_network_mode_from_service(self): project = Project.from_config( name='composetest', client=self.client, config_data=load_config({ - 'version': str(V2_0), 'services': { 'net': { 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -193,14 +180,12 @@ def test_network_mode_from_service(self): net = project.get_service('net') assert web.network_mode.mode == 'container:' + net.containers()[0].id - @v2_only() @no_cluster('container networks not supported in Swarm') def test_network_mode_from_container(self): def get_project(): return Project.from_config( name='composetest', config_data=load_config({ - 'version': str(V2_0), 'services': { 'web': { 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -306,19 +291,19 @@ def test_start_pause_unpause_stop_kill_remove(self): db_container = db.create_container() project.start(service_names=['web']) - assert set(c.name for c in project.containers() if c.is_running) == { + assert {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) == { + assert {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]) == { + assert {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]) == { + assert {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']) @@ -328,7 +313,7 @@ def test_start_pause_unpause_stop_kill_remove(self): 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) == {db_container.name} + assert {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 @@ -451,7 +436,6 @@ def test_recreate_preserves_volumes(self): assert db_container.id != old_db_id assert db_container.get('Volumes./etc') == db_volume_path - @v2_3_only() def test_recreate_preserves_mounts(self): web = self.create_service('web') db = self.create_service('db', volumes=[types.MountSpec(type='volume', target='/etc')]) @@ -659,10 +643,8 @@ def test_unscale_after_restart(self): service = project.get_service('web') assert len(service.containers()) == 1 - @v2_only() def test_project_up_networks(self): config_data = build_config( - version=V2_0, services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -704,10 +686,8 @@ def test_project_up_networks(self): foo_data = self.client.inspect_network('composetest_foo') assert foo_data['Driver'] == 'bridge' - @v2_only() def test_up_with_ipam_config(self): config_data = build_config( - version=V2_0, services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -764,10 +744,8 @@ def test_up_with_ipam_config(self): }], } - @v2_only() def test_up_with_ipam_options(self): config_data = build_config( - version=V2_0, services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -799,10 +777,8 @@ def test_up_with_ipam_options(self): "com.docker.compose.network.test": "9-29-045" } - @v2_1_only() def test_up_with_network_static_addresses(self): config_data = build_config( - version=V2_1, services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -848,13 +824,11 @@ def test_up_with_network_static_addresses(self): assert ipam_config.get('IPv4Address') == '172.16.100.100' assert ipam_config.get('IPv6Address') == 'fe80::1001:102' - @v2_3_only() def test_up_with_network_priorities(self): mac_address = '74:6f:75:68:6f:75' def get_config_data(p1, p2, p3): return build_config( - version=V2_3, services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -913,11 +887,9 @@ def get_config_data(p1, p2, p3): net_config = service_container.inspect()['NetworkSettings']['Networks']['composetest_n3'] assert net_config['MacAddress'] == mac_address - @v2_1_only() def test_up_with_enable_ipv6(self): self.require_api_version('1.23') config_data = build_config( - version=V2_1, services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -957,10 +929,8 @@ def test_up_with_enable_ipv6(self): get('IPAMConfig', {})) assert ipam_config.get('IPv6Address') == 'fe80::1001:102' - @v2_only() def test_up_with_network_static_addresses_missing_subnet(self): config_data = build_config( - version=V2_0, services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -993,10 +963,8 @@ def test_up_with_network_static_addresses_missing_subnet(self): with pytest.raises(ProjectError): project.up() - @v2_1_only() def test_up_with_network_link_local_ips(self): config_data = build_config( - version=V2_1, services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -1028,10 +996,8 @@ def test_up_with_network_link_local_ips(self): assert 'LinkLocalIPs' in ipam_config assert ipam_config['LinkLocalIPs'] == ['169.254.8.8'] - @v2_1_only() def test_up_with_custom_name_resources(self): config_data = build_config( - version=V2_2, services=[{ 'name': 'web', 'volumes': [VolumeSpec.parse('foo:/container-path')], @@ -1065,11 +1031,9 @@ def test_up_with_custom_name_resources(self): assert network['Labels']['com.docker.compose.test_value'] == 'sharpdressedman' assert volume['Labels']['com.docker.compose.test_value'] == 'thefuror' - @v2_1_only() def test_up_with_isolation(self): self.require_api_version('1.24') config_data = build_config( - version=V2_1, services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -1085,11 +1049,9 @@ def test_up_with_isolation(self): service_container = project.get_service('web').containers(stopped=True)[0] assert service_container.inspect()['HostConfig']['Isolation'] == 'default' - @v2_1_only() def test_up_with_invalid_isolation(self): self.require_api_version('1.24') config_data = build_config( - version=V2_1, services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -1104,12 +1066,10 @@ def test_up_with_invalid_isolation(self): with pytest.raises(ProjectError): project.up() - @v2_3_only() @if_runtime_available('runc') def test_up_with_runtime(self): self.require_api_version('1.30') config_data = build_config( - version=V2_3, services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -1125,11 +1085,9 @@ def test_up_with_runtime(self): service_container = project.get_service('web').containers(stopped=True)[0] assert service_container.inspect()['HostConfig']['Runtime'] == 'runc' - @v2_3_only() def test_up_with_invalid_runtime(self): self.require_api_version('1.30') config_data = build_config( - version=V2_3, services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -1144,12 +1102,10 @@ def test_up_with_invalid_runtime(self): with pytest.raises(ProjectError): project.up() - @v2_3_only() @if_runtime_available('nvidia') def test_up_with_nvidia_runtime(self): self.require_api_version('1.30') config_data = build_config( - version=V2_3, services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -1165,11 +1121,9 @@ def test_up_with_nvidia_runtime(self): service_container = project.get_service('web').containers(stopped=True)[0] assert service_container.inspect()['HostConfig']['Runtime'] == 'nvidia' - @v2_only() def test_project_up_with_network_internal(self): self.require_api_version('1.23') config_data = build_config( - version=V2_0, services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -1191,14 +1145,12 @@ def test_project_up_with_network_internal(self): assert network['Internal'] is True - @v2_1_only() def test_project_up_with_network_label(self): self.require_api_version('1.23') network_name = 'network_with_label' config_data = build_config( - version=V2_1, services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -1226,12 +1178,10 @@ def test_project_up_with_network_label(self): assert 'label_key' in networks[0]['Labels'] assert networks[0]['Labels']['label_key'] == 'label_val' - @v2_only() def test_project_up_volumes(self): - vol_name = '{0:x}'.format(random.getrandbits(32)) - full_vol_name = 'composetest_{0}'.format(vol_name) + vol_name = '{:x}'.format(random.getrandbits(32)) + full_vol_name = 'composetest_{}'.format(vol_name) config_data = build_config( - version=V2_0, services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -1251,14 +1201,12 @@ def test_project_up_volumes(self): assert volume_data['Name'].split('/')[-1] == full_vol_name assert volume_data['Driver'] == 'local' - @v2_1_only() def test_project_up_with_volume_labels(self): self.require_api_version('1.23') volume_name = 'volume_with_label' config_data = build_config( - version=V2_1, services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -1286,19 +1234,17 @@ def test_project_up_with_volume_labels(self): if v['Name'].split('/')[-1].startswith('composetest_') ] - assert set([v['Name'].split('/')[-1] for v in volumes]) == set( - ['composetest_{}'.format(volume_name)] - ) + assert {v['Name'].split('/')[-1] for v in volumes} == { + 'composetest_{}'.format(volume_name) + } assert 'label_key' in volumes[0]['Labels'] assert volumes[0]['Labels']['label_key'] == 'label_val' - @v2_only() def test_project_up_logging_with_multiple_files(self): base_file = config.ConfigFile( 'base.yml', { - 'version': str(V2_0), 'services': { 'simple': {'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top'}, 'another': { @@ -1317,7 +1263,6 @@ def test_project_up_logging_with_multiple_files(self): override_file = config.ConfigFile( 'override.yml', { - 'version': str(V2_0), 'services': { 'another': { 'logging': { @@ -1345,12 +1290,10 @@ def test_project_up_logging_with_multiple_files(self): assert log_config assert log_config.get('Type') == 'none' - @v2_only() def test_project_up_port_mappings_with_multiple_files(self): base_file = config.ConfigFile( 'base.yml', { - 'version': str(V2_0), 'services': { 'simple': { 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -1363,7 +1306,6 @@ def test_project_up_port_mappings_with_multiple_files(self): override_file = config.ConfigFile( 'override.yml', { - 'version': str(V2_0), 'services': { 'simple': { 'ports': ['1234:1234'] @@ -1381,10 +1323,8 @@ def test_project_up_port_mappings_with_multiple_files(self): containers = project.containers() assert len(containers) == 1 - @v2_2_only() def test_project_up_config_scale(self): config_data = build_config( - version=V2_2, services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -1409,12 +1349,40 @@ def test_project_up_config_scale(self): project.up() assert len(project.containers()) == 3 - @v2_only() + def test_project_up_scale_with_stopped_containers(self): + config_data = build_config( + services=[{ + 'name': 'web', + 'image': BUSYBOX_IMAGE_WITH_TAG, + 'command': 'top', + 'scale': 2 + }] + ) + project = Project.from_config( + name='composetest', config_data=config_data, client=self.client + ) + + project.up() + containers = project.containers() + assert len(containers) == 2 + + self.client.stop(containers[0].id) + project.up(scale_override={'web': 2}) + containers = project.containers() + assert len(containers) == 2 + + self.client.stop(containers[0].id) + project.up(scale_override={'web': 3}) + assert len(project.containers()) == 3 + + self.client.stop(containers[0].id) + project.up(scale_override={'web': 1}) + assert len(project.containers()) == 1 + def test_initialize_volumes(self): - vol_name = '{0:x}'.format(random.getrandbits(32)) - full_vol_name = 'composetest_{0}'.format(vol_name) + vol_name = '{:x}'.format(random.getrandbits(32)) + full_vol_name = 'composetest_{}'.format(vol_name) config_data = build_config( - version=V2_0, services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -1433,12 +1401,10 @@ def test_initialize_volumes(self): assert volume_data['Name'].split('/')[-1] == full_vol_name assert volume_data['Driver'] == 'local' - @v2_only() def test_project_up_implicit_volume_driver(self): - vol_name = '{0:x}'.format(random.getrandbits(32)) - full_vol_name = 'composetest_{0}'.format(vol_name) + vol_name = '{:x}'.format(random.getrandbits(32)) + full_vol_name = 'composetest_{}'.format(vol_name) config_data = build_config( - version=V2_0, services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -1457,12 +1423,10 @@ def test_project_up_implicit_volume_driver(self): assert volume_data['Name'].split('/')[-1] == full_vol_name assert volume_data['Driver'] == 'local' - @v3_only() def test_project_up_with_secrets(self): node = create_host_file(self.client, os.path.abspath('tests/fixtures/secrets/default')) config_data = build_config( - version=V3_1, services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -1494,12 +1458,10 @@ def test_project_up_with_secrets(self): 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', @@ -1548,12 +1510,11 @@ def test_project_up_with_added_secrets(self): 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)) + vol_name = '{:x}'.format(random.getrandbits(32)) config_data = build_config( - version=V2_0, + version=VERSION, services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -1569,14 +1530,12 @@ def test_initialize_volumes_invalid_volume_driver(self): with pytest.raises(APIError if is_cluster(self.client) else config.ConfigurationError): project.volumes.initialize() - @v2_only() @no_cluster('inspect volume by name defect on Swarm Classic') def test_initialize_volumes_updated_driver(self): - vol_name = '{0:x}'.format(random.getrandbits(32)) - full_vol_name = 'composetest_{0}'.format(vol_name) + vol_name = '{:x}'.format(random.getrandbits(32)) + full_vol_name = 'composetest_{}'.format(vol_name) config_data = build_config( - version=V2_0, services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -1604,21 +1563,19 @@ def test_initialize_volumes_updated_driver(self): ) with pytest.raises(config.ConfigurationError) as e: project.volumes.initialize() - assert 'Configuration for volume {0} specifies driver smb'.format( + assert 'Configuration for volume {} specifies driver smb'.format( vol_name ) in str(e.value) - @v2_only() @no_cluster('inspect volume by name defect on Swarm Classic') def test_initialize_volumes_updated_driver_opts(self): - vol_name = '{0:x}'.format(random.getrandbits(32)) - full_vol_name = 'composetest_{0}'.format(vol_name) + vol_name = '{:x}'.format(random.getrandbits(32)) + full_vol_name = 'composetest_{}'.format(vol_name) tmpdir = tempfile.mkdtemp(prefix='compose_test_') self.addCleanup(shutil.rmtree, tmpdir) driver_opts = {'o': 'bind', 'device': tmpdir, 'type': 'none'} config_data = build_config( - version=V2_0, services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -1650,17 +1607,15 @@ def test_initialize_volumes_updated_driver_opts(self): ) with pytest.raises(config.ConfigurationError) as e: project.volumes.initialize() - assert 'Configuration for volume {0} specifies "device" driver_opt {1}'.format( + assert 'Configuration for volume {} specifies "device" driver_opt {}'.format( vol_name, driver_opts['device'] ) in str(e.value) - @v2_only() def test_initialize_volumes_updated_blank_driver(self): - vol_name = '{0:x}'.format(random.getrandbits(32)) - full_vol_name = 'composetest_{0}'.format(vol_name) + vol_name = '{:x}'.format(random.getrandbits(32)) + full_vol_name = 'composetest_{}'.format(vol_name) config_data = build_config( - version=V2_0, services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -1691,15 +1646,13 @@ def test_initialize_volumes_updated_blank_driver(self): assert volume_data['Name'].split('/')[-1] == full_vol_name assert volume_data['Driver'] == 'local' - @v2_only() @no_cluster('inspect volume by name defect on Swarm Classic') def test_initialize_volumes_external_volumes(self): # Use composetest_ prefix so it gets garbage-collected in tearDown() - vol_name = 'composetest_{0:x}'.format(random.getrandbits(32)) - full_vol_name = 'composetest_{0}'.format(vol_name) + vol_name = 'composetest_{:x}'.format(random.getrandbits(32)) + full_vol_name = 'composetest_{}'.format(vol_name) self.client.create_volume(vol_name) config_data = build_config( - version=V2_0, services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -1718,12 +1671,10 @@ def test_initialize_volumes_external_volumes(self): with pytest.raises(NotFound): self.client.inspect_volume(full_vol_name) - @v2_only() def test_initialize_volumes_inexistent_external_volume(self): - vol_name = '{0:x}'.format(random.getrandbits(32)) + vol_name = '{:x}'.format(random.getrandbits(32)) config_data = build_config( - version=V2_0, services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -1739,24 +1690,22 @@ def test_initialize_volumes_inexistent_external_volume(self): ) with pytest.raises(config.ConfigurationError) as e: project.volumes.initialize() - assert 'Volume {0} declared as external'.format( + assert 'Volume {} declared as external'.format( vol_name ) in str(e.value) - @v2_only() def test_project_up_named_volumes_in_binds(self): - vol_name = '{0:x}'.format(random.getrandbits(32)) - full_vol_name = 'composetest_{0}'.format(vol_name) + vol_name = '{:x}'.format(random.getrandbits(32)) + full_vol_name = 'composetest_{}'.format(vol_name) base_file = config.ConfigFile( 'base.yml', { - 'version': str(V2_0), 'services': { 'simple': { 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', - 'volumes': ['{0}:/data'.format(vol_name)] + 'volumes': ['{}:/data'.format(vol_name)] }, }, 'volumes': { @@ -1842,7 +1791,6 @@ def test_project_up_ignore_orphans(self): mock_log.warning.assert_not_called() - @v2_1_only() def test_project_up_healthy_dependency(self): config_dict = { 'version': '2.1', @@ -1879,7 +1827,6 @@ def test_project_up_healthy_dependency(self): assert 'svc1' in svc2.get_dependency_names() assert svc1.is_healthy() - @v2_1_only() def test_project_up_unhealthy_dependency(self): config_dict = { 'version': '2.1', @@ -1918,7 +1865,6 @@ def test_project_up_unhealthy_dependency(self): with pytest.raises(HealthCheckFailed): svc1.is_healthy() - @v2_1_only() def test_project_up_no_healthcheck_dependency(self): config_dict = { 'version': '2.1', @@ -1954,6 +1900,106 @@ def test_project_up_no_healthcheck_dependency(self): with pytest.raises(NoHealthCheckConfigured): svc1.is_healthy() + def test_project_up_completed_successfully_dependency(self): + config_dict = { + 'version': '2.1', + 'services': { + 'svc1': { + 'image': BUSYBOX_IMAGE_WITH_TAG, + 'command': 'true' + }, + 'svc2': { + 'image': BUSYBOX_IMAGE_WITH_TAG, + 'command': 'top', + 'depends_on': { + 'svc1': {'condition': 'service_completed_successfully'}, + } + } + } + } + config_data = load_config(config_dict) + project = Project.from_config( + name='composetest', config_data=config_data, client=self.client + ) + project.up() + + svc1 = project.get_service('svc1') + svc2 = project.get_service('svc2') + + assert 'svc1' in svc2.get_dependency_names() + assert svc2.containers()[0].is_running + assert len(svc1.containers()) == 0 + assert svc1.is_completed_successfully() + + def test_project_up_completed_unsuccessfully_dependency(self): + config_dict = { + 'version': '2.1', + 'services': { + 'svc1': { + 'image': BUSYBOX_IMAGE_WITH_TAG, + 'command': 'false' + }, + 'svc2': { + 'image': BUSYBOX_IMAGE_WITH_TAG, + 'command': 'top', + 'depends_on': { + 'svc1': {'condition': 'service_completed_successfully'}, + } + } + } + } + config_data = load_config(config_dict) + project = Project.from_config( + name='composetest', config_data=config_data, client=self.client + ) + with pytest.raises(ProjectError): + project.up() + + svc1 = project.get_service('svc1') + svc2 = project.get_service('svc2') + assert 'svc1' in svc2.get_dependency_names() + assert len(svc2.containers()) == 0 + with pytest.raises(CompletedUnsuccessfully): + svc1.is_completed_successfully() + + def test_project_up_completed_differently_dependencies(self): + config_dict = { + 'version': '2.1', + 'services': { + 'svc1': { + 'image': BUSYBOX_IMAGE_WITH_TAG, + 'command': 'true' + }, + 'svc2': { + 'image': BUSYBOX_IMAGE_WITH_TAG, + 'command': 'false' + }, + 'svc3': { + 'image': BUSYBOX_IMAGE_WITH_TAG, + 'command': 'top', + 'depends_on': { + 'svc1': {'condition': 'service_completed_successfully'}, + 'svc2': {'condition': 'service_completed_successfully'}, + } + } + } + } + config_data = load_config(config_dict) + project = Project.from_config( + name='composetest', config_data=config_data, client=self.client + ) + with pytest.raises(ProjectError): + project.up() + + svc1 = project.get_service('svc1') + svc2 = project.get_service('svc2') + svc3 = project.get_service('svc3') + assert ['svc1', 'svc2'] == svc3.get_dependency_names() + assert svc1.is_completed_successfully() + assert len(svc3.containers()) == 0 + with pytest.raises(CompletedUnsuccessfully): + svc2.is_completed_successfully() + def test_project_up_seccomp_profile(self): seccomp_data = { 'defaultAction': 'SCMP_ACT_ALLOW', diff --git a/tests/integration/resilience_test.py b/tests/integration/resilience_test.py index 3de16e977b8..2fbaafb2894 100644 --- a/tests/integration/resilience_test.py +++ b/tests/integration/resilience_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import pytest from .. import mock @@ -25,7 +22,7 @@ def setUp(self): def tearDown(self): del self.project del self.db - super(ResilienceTest, self).tearDown() + super().tearDown() def test_successful_recreate(self): self.project.up(strategy=ConvergenceStrategy.always) diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index c50aab08bb2..06a97508d1f 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -1,18 +1,14 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import os import re import shutil import tempfile from distutils.spawn import find_executable +from io import StringIO from os import path import pytest from docker.errors import APIError from docker.errors import ImageNotFound -from six import StringIO -from six import text_type from .. import mock from ..helpers import BUSYBOX_IMAGE_WITH_TAG @@ -40,8 +36,10 @@ from compose.project import OneOffFilter from compose.project import Project from compose.service import BuildAction +from compose.service import BuildError from compose.service import ConvergencePlan from compose.service import ConvergenceStrategy +from compose.service import IpcMode from compose.service import NetworkMode from compose.service import PidMode from compose.service import Service @@ -49,11 +47,6 @@ from tests.helpers import create_custom_host_file from tests.integration.testcases import is_cluster from tests.integration.testcases import no_cluster -from tests.integration.testcases import v2_1_only -from tests.integration.testcases import v2_2_only -from tests.integration.testcases import v2_3_only -from tests.integration.testcases import v2_only -from tests.integration.testcases import v3_only def create_and_start_container(service, **override_options): @@ -139,7 +132,6 @@ def test_create_container_with_cpu_rt(self): assert container.get('HostConfig.CpuRealtimeRuntime') == 40000 assert container.get('HostConfig.CpuRealtimePeriod') == 150000 - @v2_2_only() def test_create_container_with_cpu_count(self): self.require_api_version('1.25') service = self.create_service('db', cpu_count=2) @@ -147,7 +139,6 @@ def test_create_container_with_cpu_count(self): service.start_container(container) assert container.get('HostConfig.CpuCount') == 2 - @v2_2_only() @pytest.mark.skipif(not IS_WINDOWS_PLATFORM, reason='cpu_percent is not supported for Linux') def test_create_container_with_cpu_percent(self): self.require_api_version('1.25') @@ -156,7 +147,6 @@ def test_create_container_with_cpu_percent(self): service.start_container(container) assert container.get('HostConfig.CpuPercent') == 12 - @v2_2_only() def test_create_container_with_cpus(self): self.require_api_version('1.25') service = self.create_service('db', cpus=1) @@ -223,6 +213,9 @@ def test_create_container_with_read_only_root_fs(self): service.start_container(container) assert container.get('HostConfig.ReadonlyRootfs') == read_only + @pytest.mark.xfail(True, reason='Getting "Your kernel does not support ' + 'cgroup blkio weight and weight_device" on daemon start ' + 'on Linux kernel 5.3.x') def test_create_container_with_blkio_config(self): blkio_config = { 'weight': 300, @@ -256,7 +249,7 @@ def test_create_container_with_security_opt(self): service = self.create_service('db', security_opt=security_opt) container = service.create_container() service.start_container(container) - assert set(container.get('HostConfig.SecurityOpt')) == set([o.repr() for o in security_opt]) + assert set(container.get('HostConfig.SecurityOpt')) == {o.repr() for o in security_opt} @pytest.mark.xfail(True, reason='Not supported on most drivers') def test_create_container_with_storage_opt(self): @@ -298,10 +291,9 @@ def test_create_container_with_specified_volume(self): actual_host_path = container.get_mount(container_path)['Source'] assert path.basename(actual_host_path) == path.basename(host_path), ( - "Last component differs: %s, %s" % (actual_host_path, host_path) + "Last component differs: {}, {}".format(actual_host_path, host_path) ) - @v2_3_only() def test_create_container_with_host_mount(self): host_path = '/tmp/host-path' container_path = '/container-path' @@ -321,7 +313,6 @@ def test_create_container_with_host_mount(self): assert path.basename(mount['Source']) == path.basename(host_path) assert mount['RW'] is False - @v2_3_only() def test_create_container_with_tmpfs_mount(self): container_path = '/container-tmpfs' service = self.create_service( @@ -334,7 +325,6 @@ def test_create_container_with_tmpfs_mount(self): assert mount assert mount['Type'] == 'tmpfs' - @v2_3_only() def test_create_container_with_tmpfs_mount_tmpfs_size(self): container_path = '/container-tmpfs' service = self.create_service( @@ -351,7 +341,6 @@ def test_create_container_with_tmpfs_mount_tmpfs_size(self): 'SizeBytes': 5368709 } - @v2_3_only() def test_create_container_with_volume_mount(self): container_path = '/container-volume' volume_name = 'composetest_abcde' @@ -366,7 +355,6 @@ def test_create_container_with_volume_mount(self): assert mount assert mount['Name'] == volume_name - @v3_only() def test_create_container_with_legacy_mount(self): # Ensure mounts are converted to volumes if API version < 1.30 # Needed to support long syntax in the 3.2 format @@ -383,7 +371,6 @@ def test_create_container_with_legacy_mount(self): assert mount assert mount['Name'] == volume_name - @v3_only() def test_create_container_with_legacy_tmpfs_mount(self): # Ensure tmpfs mounts are converted to tmpfs entries if API version < 1.30 # Needed to support long syntax in the 3.2 format @@ -590,7 +577,6 @@ def test_execute_convergence_plan_recreate_twice(self): orig_container = new_container - @v2_3_only() def test_execute_convergence_plan_recreate_twice_with_mount(self): service = self.create_service( 'db', @@ -859,11 +845,11 @@ def test_start_container_creates_links(self): db2 = create_and_start_container(db) create_and_start_container(web) - assert set(get_links(web.containers()[0])) == set([ + assert set(get_links(web.containers()[0])) == { db1.name, db1.name_without_project, db2.name, db2.name_without_project, 'db' - ]) + } @no_cluster('No legacy links support in Swarm') def test_start_container_creates_links_with_names(self): @@ -874,11 +860,11 @@ def test_start_container_creates_links_with_names(self): db2 = create_and_start_container(db) create_and_start_container(web) - assert set(get_links(web.containers()[0])) == set([ + assert set(get_links(web.containers()[0])) == { db1.name, db1.name_without_project, db2.name, db2.name_without_project, 'custom_link_name' - ]) + } @no_cluster('No legacy links support in Swarm') def test_start_container_with_external_links(self): @@ -894,11 +880,11 @@ def test_start_container_with_external_links(self): create_and_start_container(web) - assert set(get_links(web.containers()[0])) == set([ + assert set(get_links(web.containers()[0])) == { db_ctnrs[0].name, db_ctnrs[1].name, 'db_3' - ]) + } @no_cluster('No legacy links support in Swarm') def test_start_normal_container_does_not_create_links_to_its_own_service(self): @@ -908,7 +894,7 @@ def test_start_normal_container_does_not_create_links_to_its_own_service(self): create_and_start_container(db) c = create_and_start_container(db) - assert set(get_links(c)) == set([]) + assert set(get_links(c)) == set() @no_cluster('No legacy links support in Swarm') def test_start_one_off_container_creates_links_to_its_own_service(self): @@ -919,11 +905,11 @@ def test_start_one_off_container_creates_links_to_its_own_service(self): c = create_and_start_container(db, one_off=OneOffFilter.only) - assert set(get_links(c)) == set([ + assert set(get_links(c)) == { db1.name, db1.name_without_project, db2.name, db2.name_without_project, 'db' - ]) + } def test_start_container_builds_images(self): service = Service( @@ -962,7 +948,12 @@ def test_build(self): with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f: f.write("FROM busybox\n") - service = self.create_service('web', build={'context': base_dir}) + service = self.create_service('web', + build={'context': base_dir}, + environment={ + 'COMPOSE_DOCKER_CLI_BUILD': '0', + 'DOCKER_BUILDKIT': '0', + }) service.build() self.addCleanup(self.client.remove_image, service.image_name) @@ -978,13 +969,46 @@ def test_build_cli(self): service = self.create_service('web', build={'context': base_dir}, environment={ - 'COMPOSE_DOCKER_CLI_BUILD': '1', 'DOCKER_BUILDKIT': '1', }) service.build(cli=True) self.addCleanup(self.client.remove_image, service.image_name) assert self.client.inspect_image('composetest_web') + def test_build_cli_with_build_labels(self): + base_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, base_dir) + + with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f: + f.write("FROM busybox\n") + + service = self.create_service('web', + build={ + 'context': base_dir, + 'labels': {'com.docker.compose.test': 'true'}}, + ) + service.build(cli=True) + self.addCleanup(self.client.remove_image, service.image_name) + image = self.client.inspect_image('composetest_web') + assert image['Config']['Labels']['com.docker.compose.test'] + + def test_build_cli_with_build_error(self): + base_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, base_dir) + + with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f: + f.write('\n'.join([ + "FROM busybox", + "RUN exit 2", + ])) + service = self.create_service('web', + build={ + 'context': base_dir, + 'labels': {'com.docker.compose.test': 'true'}}, + ) + with pytest.raises(BuildError): + service.build(cli=True) + def test_up_build_cli(self): base_dir = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, base_dir) @@ -995,7 +1019,6 @@ def test_up_build_cli(self): web = self.create_service('web', build={'context': base_dir}, environment={ - 'COMPOSE_DOCKER_CLI_BUILD': '1', 'DOCKER_BUILDKIT': '1', }) project = Project('composetest', [web], self.client) @@ -1015,7 +1038,7 @@ def test_build_non_ascii_filename(self): with open(os.path.join(base_dir.encode('utf8'), b'foo\xE2bar'), 'w') as f: f.write("hello world\n") - service = self.create_service('web', build={'context': text_type(base_dir)}) + service = self.create_service('web', build={'context': str(base_dir)}) service.build() self.addCleanup(self.client.remove_image, service.image_name) assert self.client.inspect_image('composetest_web') @@ -1049,7 +1072,7 @@ def test_build_with_build_args(self): f.write("RUN echo ${build_version}\n") service = self.create_service('buildwithargs', - build={'context': text_type(base_dir), + build={'context': str(base_dir), 'args': {"build_version": "1"}}) service.build() self.addCleanup(self.client.remove_image, service.image_name) @@ -1066,7 +1089,7 @@ def test_build_with_build_args_override(self): f.write("RUN echo ${build_version}\n") service = self.create_service('buildwithargs', - build={'context': text_type(base_dir), + build={'context': str(base_dir), 'args': {"build_version": "1"}}) service.build(build_args_override={'build_version': '2'}) self.addCleanup(self.client.remove_image, service.image_name) @@ -1082,7 +1105,7 @@ def test_build_with_build_labels(self): f.write('FROM busybox\n') service = self.create_service('buildlabels', build={ - 'context': text_type(base_dir), + 'context': str(base_dir), 'labels': {'com.docker.compose.test': 'true'} }) service.build() @@ -1109,7 +1132,7 @@ def test_build_with_network(self): self.client.start(net_container) service = self.create_service('buildwithnet', build={ - 'context': text_type(base_dir), + 'context': str(base_dir), 'network': 'container:{}'.format(net_container['Id']) }) @@ -1118,7 +1141,6 @@ def test_build_with_network(self): assert service.image() - @v2_3_only() @no_cluster('Not supported on UCP 2.2.0-beta1') # FIXME: remove once support is added def test_build_with_target(self): self.require_api_version('1.30') @@ -1133,7 +1155,7 @@ def test_build_with_target(self): f.write('LABEL com.docker.compose.test.target=two\n') service = self.create_service('buildtarget', build={ - 'context': text_type(base_dir), + 'context': str(base_dir), 'target': 'one' }) @@ -1141,7 +1163,6 @@ def test_build_with_target(self): assert service.image() assert service.image()['Config']['Labels']['com.docker.compose.test.target'] == 'one' - @v2_3_only() def test_build_with_extra_hosts(self): self.require_api_version('1.27') base_dir = tempfile.mkdtemp() @@ -1155,7 +1176,7 @@ def test_build_with_extra_hosts(self): ])) service = self.create_service('build_extra_hosts', build={ - 'context': text_type(base_dir), + 'context': str(base_dir), 'extra_hosts': { 'foobar': '127.0.0.1', 'baz': '127.0.0.1' @@ -1177,12 +1198,11 @@ def test_build_with_gzip(self): f.write('hello world\n') service = self.create_service('build_gzip', build={ - 'context': text_type(base_dir), + 'context': str(base_dir), }) service.build(gzip=True) assert service.image() - @v2_1_only() def test_build_with_isolation(self): base_dir = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, base_dir) @@ -1190,7 +1210,7 @@ def test_build_with_isolation(self): f.write('FROM busybox\n') service = self.create_service('build_isolation', build={ - 'context': text_type(base_dir), + 'context': str(base_dir), 'isolation': 'default', }) service.build() @@ -1204,7 +1224,7 @@ def test_build_with_illegal_leading_chars(self): service = Service( 'build_leading_slug', client=self.client, project='___-composetest', build={ - 'context': text_type(base_dir) + 'context': str(base_dir) } ) assert service.image_name == 'composetest_build_leading_slug' @@ -1464,13 +1484,22 @@ def test_pid_mode_host(self): container = create_and_start_container(service) assert container.get('HostConfig.PidMode') == 'host' - @v2_1_only() + def test_ipc_mode_none_defined(self): + service = self.create_service('web', ipc_mode=None) + container = create_and_start_container(service) + print(container.get('HostConfig.IpcMode')) + assert container.get('HostConfig.IpcMode') == 'shareable' + + def test_ipc_mode_host(self): + service = self.create_service('web', ipc_mode=IpcMode('host')) + container = create_and_start_container(service) + assert container.get('HostConfig.IpcMode') == 'host' + def test_userns_mode_none_defined(self): service = self.create_service('web', userns_mode=None) container = create_and_start_container(service) assert container.get('HostConfig.UsernsMode') == '' - @v2_1_only() def test_userns_mode_host(self): service = self.create_service('web', userns_mode='host') container = create_and_start_container(service) @@ -1546,7 +1575,6 @@ def test_dns_search(self): container = create_and_start_container(service) assert container.get('HostConfig.DnsSearch') == ['dc1.example.com', 'dc2.example.com'] - @v2_only() def test_tmpfs(self): service = self.create_service('web', tmpfs=['/run']) container = create_and_start_container(service) @@ -1580,7 +1608,6 @@ def test_env_from_file_combined_with_env(self): }.items(): assert env[k] == v - @v3_only() def test_build_with_cachefrom(self): base_dir = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, base_dir) @@ -1713,14 +1740,14 @@ def test_duplicate_containers(self): options = service._get_container_create_options({}, service._next_container_number()) original = Container.create(service.client, **options) - assert set(service.containers(stopped=True)) == set([original]) + assert set(service.containers(stopped=True)) == {original} assert set(service.duplicate_containers()) == set() options['name'] = 'temporary_container_name' duplicate = Container.create(service.client, **options) - assert set(service.containers(stopped=True)) == set([original, duplicate]) - assert set(service.duplicate_containers()) == set([duplicate]) + assert set(service.containers(stopped=True)) == {original, duplicate} + assert set(service.duplicate_containers()) == {duplicate} def converge(service, strategy=ConvergenceStrategy.changed): diff --git a/tests/integration/state_test.py b/tests/integration/state_test.py index 492de7b8ab1..8168cddf010 100644 --- a/tests/integration/state_test.py +++ b/tests/integration/state_test.py @@ -2,9 +2,6 @@ Integration tests which cover state convergence (aka smart recreate) performed by `docker-compose up`. """ -from __future__ import absolute_import -from __future__ import unicode_literals - import copy import os import shutil @@ -42,7 +39,7 @@ def make_project(self, cfg): class BasicProjectTest(ProjectTestCase): def setUp(self): - super(BasicProjectTest, self).setUp() + super().setUp() self.cfg = { 'db': {'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top'}, @@ -98,7 +95,7 @@ def test_all_change(self): class ProjectWithDependenciesTest(ProjectTestCase): def setUp(self): - super(ProjectWithDependenciesTest, self).setUp() + super().setUp() self.cfg = { 'db': { @@ -119,7 +116,7 @@ def setUp(self): def test_up(self): containers = self.run_up(self.cfg) - assert set(c.service for c in containers) == set(['db', 'web', 'nginx']) + assert {c.service for c in containers} == {'db', 'web', 'nginx'} def test_change_leaf(self): old_containers = self.run_up(self.cfg) @@ -127,7 +124,7 @@ def test_change_leaf(self): self.cfg['nginx']['environment'] = {'NEW_VAR': '1'} new_containers = self.run_up(self.cfg) - assert set(c.service for c in new_containers - old_containers) == set(['nginx']) + assert {c.service for c in new_containers - old_containers} == {'nginx'} def test_change_middle(self): old_containers = self.run_up(self.cfg) @@ -135,7 +132,7 @@ def test_change_middle(self): self.cfg['web']['environment'] = {'NEW_VAR': '1'} new_containers = self.run_up(self.cfg) - assert set(c.service for c in new_containers - old_containers) == set(['web']) + assert {c.service for c in new_containers - old_containers} == {'web'} def test_change_middle_always_recreate_deps(self): old_containers = self.run_up(self.cfg, always_recreate_deps=True) @@ -143,7 +140,7 @@ def test_change_middle_always_recreate_deps(self): self.cfg['web']['environment'] = {'NEW_VAR': '1'} new_containers = self.run_up(self.cfg, always_recreate_deps=True) - assert set(c.service for c in new_containers - old_containers) == {'web', 'nginx'} + assert {c.service for c in new_containers - old_containers} == {'web', 'nginx'} def test_change_root(self): old_containers = self.run_up(self.cfg) @@ -151,7 +148,7 @@ def test_change_root(self): self.cfg['db']['environment'] = {'NEW_VAR': '1'} new_containers = self.run_up(self.cfg) - assert set(c.service for c in new_containers - old_containers) == set(['db']) + assert {c.service for c in new_containers - old_containers} == {'db'} def test_change_root_always_recreate_deps(self): old_containers = self.run_up(self.cfg, always_recreate_deps=True) @@ -159,7 +156,7 @@ def test_change_root_always_recreate_deps(self): self.cfg['db']['environment'] = {'NEW_VAR': '1'} new_containers = self.run_up(self.cfg, always_recreate_deps=True) - assert set(c.service for c in new_containers - old_containers) == { + assert {c.service for c in new_containers - old_containers} == { 'db', 'web', 'nginx' } @@ -216,7 +213,7 @@ def test_service_recreated_when_dependency_created(self): class ProjectWithDependsOnDependenciesTest(ProjectTestCase): def setUp(self): - super(ProjectWithDependsOnDependenciesTest, self).setUp() + super().setUp() self.cfg = { 'version': '2', @@ -241,7 +238,7 @@ def setUp(self): 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']) + assert {c.service for c in containers} == {'db', 'web', 'nginx'} def test_change_leaf(self): local_cfg = copy.deepcopy(self.cfg) @@ -250,7 +247,7 @@ def test_change_leaf(self): 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']) + assert {c.service for c in new_containers - old_containers} == {'nginx'} def test_change_middle(self): local_cfg = copy.deepcopy(self.cfg) @@ -259,7 +256,7 @@ def test_change_middle(self): 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']) + assert {c.service for c in new_containers - old_containers} == {'web'} def test_change_middle_always_recreate_deps(self): local_cfg = copy.deepcopy(self.cfg) @@ -268,7 +265,7 @@ def test_change_middle_always_recreate_deps(self): 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']) + assert {c.service for c in new_containers - old_containers} == {'web', 'nginx'} def test_change_root(self): local_cfg = copy.deepcopy(self.cfg) @@ -277,7 +274,7 @@ def test_change_root(self): 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']) + assert {c.service for c in new_containers - old_containers} == {'db'} def test_change_root_always_recreate_deps(self): local_cfg = copy.deepcopy(self.cfg) @@ -286,7 +283,7 @@ def test_change_root_always_recreate_deps(self): 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']) + assert {c.service for c in new_containers - old_containers} == {'db', 'web', 'nginx'} def test_change_root_no_recreate(self): local_cfg = copy.deepcopy(self.cfg) @@ -306,24 +303,24 @@ def test_service_removed_while_down(self): 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']) + assert {c.service for c in containers} == {'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']) + assert {c.service for c in next_containers} == {'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']) + assert {c.service for c in containers} == {'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']) + assert {c.service for c in containers} == {'web', 'nginx'} def test_dependency_removed(self): local_cfg = copy.deepcopy(self.cfg) @@ -331,24 +328,24 @@ def test_dependency_removed(self): 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']) + assert {c.service for c in containers} == {'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']) + assert {c.service for c in next_containers if c.is_running} == {'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']) + assert {c.service for c in containers} == {'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']) + assert {c.service for c in containers} == {'nginx', 'db'} class ServiceStateTest(DockerClientTestCase): @@ -378,7 +375,7 @@ def test_trigger_start(self): assert [c.is_running for c in containers] == [False, True] - assert ('start', containers[0:1]) == web.convergence_plan() + assert ('start', containers) == web.convergence_plan() def test_trigger_recreate_with_config_change(self): web = self.create_service('web', command=["top"]) diff --git a/tests/integration/testcases.py b/tests/integration/testcases.py index fe70d1f7246..d4fbc9f61a2 100644 --- a/tests/integration/testcases.py +++ b/tests/integration/testcases.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import functools import os @@ -14,14 +11,8 @@ from compose.config.config import resolve_environment from compose.config.environment import Environment from compose.const import API_VERSIONS +from compose.const import COMPOSE_SPEC as VERSION from compose.const import COMPOSEFILE_V1 as V1 -from compose.const import COMPOSEFILE_V2_0 as V2_0 -from compose.const import COMPOSEFILE_V2_0 as V2_1 -from compose.const import COMPOSEFILE_V2_2 as V2_2 -from compose.const import COMPOSEFILE_V2_3 as V2_3 -from compose.const import COMPOSEFILE_V3_0 as V3_0 -from compose.const import COMPOSEFILE_V3_2 as V3_2 -from compose.const import COMPOSEFILE_V3_5 as V3_5 from compose.const import LABEL_PROJECT from compose.progress_stream import stream_output from compose.service import Service @@ -48,17 +39,11 @@ def format_link(link): def engine_max_version(): if 'DOCKER_VERSION' not in os.environ: - return V3_5 + return VERSION version = os.environ['DOCKER_VERSION'].partition('-')[0] if version_lt(version, '1.10'): return V1 - if version_lt(version, '1.12'): - return V2_0 - if version_lt(version, '1.13'): - return V2_1 - if version_lt(version, '17.06'): - return V3_2 - return V3_5 + return VERSION def min_version_skip(version): @@ -68,26 +53,6 @@ def min_version_skip(version): ) -def v2_only(): - return min_version_skip(V2_0) - - -def v2_1_only(): - return min_version_skip(V2_1) - - -def v2_2_only(): - return min_version_skip(V2_2) - - -def v2_3_only(): - return min_version_skip(V2_3) - - -def v3_only(): - return min_version_skip(V3_0) - - class DockerClientTestCase(unittest.TestCase): @classmethod def setUpClass(cls): @@ -96,6 +61,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): + cls.client.close() del cls.client def tearDown(self): diff --git a/tests/integration/volume_test.py b/tests/integration/volume_test.py index 2a521d4c5b1..0e7c78bc25a 100644 --- a/tests/integration/volume_test.py +++ b/tests/integration/volume_test.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - -import six from docker.errors import DockerException from .testcases import DockerClientTestCase @@ -22,12 +18,12 @@ def tearDown(self): except DockerException: pass del self.tmp_volumes - super(VolumeTest, self).tearDown() + super().tearDown() def create_volume(self, name, driver=None, opts=None, external=None, custom_name=False): if external: custom_name = True - if isinstance(external, six.text_type): + if isinstance(external, str): name = external vol = Volume( diff --git a/tests/unit/cli/colors_test.py b/tests/unit/cli/colors_test.py new file mode 100644 index 00000000000..79b9cf10d90 --- /dev/null +++ b/tests/unit/cli/colors_test.py @@ -0,0 +1,56 @@ +import os + +import pytest + +from compose.cli.colors import AnsiMode +from tests import mock + + +@pytest.fixture +def tty_stream(): + stream = mock.Mock() + stream.isatty.return_value = True + return stream + + +@pytest.fixture +def non_tty_stream(): + stream = mock.Mock() + stream.isatty.return_value = False + return stream + + +class TestAnsiModeTestCase: + + @mock.patch.dict(os.environ) + def test_ansi_mode_never(self, tty_stream, non_tty_stream): + if "CLICOLOR" in os.environ: + del os.environ["CLICOLOR"] + assert not AnsiMode.NEVER.use_ansi_codes(tty_stream) + assert not AnsiMode.NEVER.use_ansi_codes(non_tty_stream) + + os.environ["CLICOLOR"] = "0" + assert not AnsiMode.NEVER.use_ansi_codes(tty_stream) + assert not AnsiMode.NEVER.use_ansi_codes(non_tty_stream) + + @mock.patch.dict(os.environ) + def test_ansi_mode_always(self, tty_stream, non_tty_stream): + if "CLICOLOR" in os.environ: + del os.environ["CLICOLOR"] + assert AnsiMode.ALWAYS.use_ansi_codes(tty_stream) + assert AnsiMode.ALWAYS.use_ansi_codes(non_tty_stream) + + os.environ["CLICOLOR"] = "0" + assert AnsiMode.ALWAYS.use_ansi_codes(tty_stream) + assert AnsiMode.ALWAYS.use_ansi_codes(non_tty_stream) + + @mock.patch.dict(os.environ) + def test_ansi_mode_auto(self, tty_stream, non_tty_stream): + if "CLICOLOR" in os.environ: + del os.environ["CLICOLOR"] + assert AnsiMode.AUTO.use_ansi_codes(tty_stream) + assert not AnsiMode.AUTO.use_ansi_codes(non_tty_stream) + + os.environ["CLICOLOR"] = "0" + assert not AnsiMode.AUTO.use_ansi_codes(tty_stream) + assert not AnsiMode.AUTO.use_ansi_codes(non_tty_stream) diff --git a/tests/unit/cli/command_test.py b/tests/unit/cli/command_test.py index 3a9844c4f7c..60638864c37 100644 --- a/tests/unit/cli/command_test.py +++ b/tests/unit/cli/command_test.py @@ -1,11 +1,6 @@ -# ~*~ encoding: utf-8 ~*~ -from __future__ import absolute_import -from __future__ import unicode_literals - import os import pytest -import six from compose.cli.command import get_config_path_from_options from compose.config.environment import Environment @@ -13,64 +8,47 @@ from tests import mock -class TestGetConfigPathFromOptions(object): +class TestGetConfigPathFromOptions: def test_path_from_options(self): paths = ['one.yml', 'two.yml'] opts = {'--file': paths} environment = Environment.from_env_file('.') - assert get_config_path_from_options('.', opts, environment) == paths + assert get_config_path_from_options(opts, environment) == paths def test_single_path_from_env(self): with mock.patch.dict(os.environ): os.environ['COMPOSE_FILE'] = 'one.yml' environment = Environment.from_env_file('.') - assert get_config_path_from_options('.', {}, environment) == ['one.yml'] + assert get_config_path_from_options({}, environment) == ['one.yml'] @pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='posix separator') def test_multiple_path_from_env(self): with mock.patch.dict(os.environ): os.environ['COMPOSE_FILE'] = 'one.yml:two.yml' environment = Environment.from_env_file('.') - assert get_config_path_from_options( - '.', {}, environment - ) == ['one.yml', 'two.yml'] + assert get_config_path_from_options({}, environment) == ['one.yml', 'two.yml'] @pytest.mark.skipif(not IS_WINDOWS_PLATFORM, reason='windows separator') def test_multiple_path_from_env_windows(self): with mock.patch.dict(os.environ): os.environ['COMPOSE_FILE'] = 'one.yml;two.yml' environment = Environment.from_env_file('.') - assert get_config_path_from_options( - '.', {}, environment - ) == ['one.yml', 'two.yml'] + assert get_config_path_from_options({}, environment) == ['one.yml', 'two.yml'] def test_multiple_path_from_env_custom_separator(self): with mock.patch.dict(os.environ): os.environ['COMPOSE_PATH_SEPARATOR'] = '^' os.environ['COMPOSE_FILE'] = 'c:\\one.yml^.\\semi;colon.yml' environment = Environment.from_env_file('.') - assert get_config_path_from_options( - '.', {}, environment - ) == ['c:\\one.yml', '.\\semi;colon.yml'] + assert get_config_path_from_options({}, environment) == ['c:\\one.yml', '.\\semi;colon.yml'] def test_no_path(self): environment = Environment.from_env_file('.') - assert not get_config_path_from_options('.', {}, environment) + assert not get_config_path_from_options({}, environment) def test_unicode_path_from_options(self): paths = [b'\xe5\xb0\xb1\xe5\x90\x83\xe9\xa5\xad/docker-compose.yml'] opts = {'--file': paths} environment = Environment.from_env_file('.') - assert get_config_path_from_options( - '.', opts, environment - ) == ['就吃饭/docker-compose.yml'] - - @pytest.mark.skipif(six.PY3, reason='Env values in Python 3 are already Unicode') - def test_unicode_path_from_env(self): - with mock.patch.dict(os.environ): - os.environ['COMPOSE_FILE'] = b'\xe5\xb0\xb1\xe5\x90\x83\xe9\xa5\xad/docker-compose.yml' - environment = Environment.from_env_file('.') - assert get_config_path_from_options( - '.', {}, environment - ) == ['就吃饭/docker-compose.yml'] + assert get_config_path_from_options(opts, environment) == ['就吃饭/docker-compose.yml'] diff --git a/tests/unit/cli/docker_client_test.py b/tests/unit/cli/docker_client_test.py index 772c136eefd..307e47f1bae 100644 --- a/tests/unit/cli/docker_client_test.py +++ b/tests/unit/cli/docker_client_test.py @@ -1,12 +1,10 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import os import platform import ssl import docker import pytest +from docker.constants import DEFAULT_DOCKER_API_VERSION import compose from compose.cli import errors @@ -26,18 +24,18 @@ def test_docker_client_no_home(self): del os.environ['HOME'] except KeyError: pass - docker_client(os.environ) + docker_client(os.environ, version=DEFAULT_DOCKER_API_VERSION) @mock.patch.dict(os.environ) def test_docker_client_with_custom_timeout(self): os.environ['COMPOSE_HTTP_TIMEOUT'] = '123' - client = docker_client(os.environ) + client = docker_client(os.environ, version=DEFAULT_DOCKER_API_VERSION) assert client.timeout == 123 @mock.patch.dict(os.environ) def test_custom_timeout_error(self): os.environ['COMPOSE_HTTP_TIMEOUT'] = '123' - client = docker_client(os.environ) + client = docker_client(os.environ, version=DEFAULT_DOCKER_API_VERSION) with mock.patch('compose.cli.errors.log') as fake_log: with pytest.raises(errors.ConnectionError): @@ -57,8 +55,8 @@ def test_custom_timeout_error(self): assert '123' in fake_log.error.call_args[0][0] def test_user_agent(self): - client = docker_client(os.environ) - expected = "docker-compose/{0} docker-py/{1} {2}/{3}".format( + client = docker_client(os.environ, version=DEFAULT_DOCKER_API_VERSION) + expected = "docker-compose/{} docker-py/{} {}/{}".format( compose.__version__, docker.__version__, platform.system(), @@ -154,9 +152,9 @@ def test_assert_hostname_explicit_skip(self): def test_tls_client_and_ca_quoted_paths(self): options = { - '--tlscacert': '"{0}"'.format(self.ca_cert), - '--tlscert': '"{0}"'.format(self.client_cert), - '--tlskey': '"{0}"'.format(self.key), + '--tlscacert': '"{}"'.format(self.ca_cert), + '--tlscert': '"{}"'.format(self.client_cert), + '--tlskey': '"{}"'.format(self.key), '--tlsverify': True } result = tls_config_from_options(options) @@ -188,9 +186,9 @@ def test_tls_flags_override_environment(self): 'DOCKER_TLS_VERIFY': 'false' }) options = { - '--tlscacert': '"{0}"'.format(self.ca_cert), - '--tlscert': '"{0}"'.format(self.client_cert), - '--tlskey': '"{0}"'.format(self.key), + '--tlscacert': '"{}"'.format(self.ca_cert), + '--tlscert': '"{}"'.format(self.client_cert), + '--tlskey': '"{}"'.format(self.key), '--tlsverify': True } @@ -233,7 +231,7 @@ def test_tls_verify_default_cert_path(self): assert result.cert == (self.client_cert, self.key) -class TestGetTlsVersion(object): +class TestGetTlsVersion: def test_get_tls_version_default(self): environment = {} assert get_tls_version(environment) is None diff --git a/tests/unit/cli/errors_test.py b/tests/unit/cli/errors_test.py index 7b53ed2b15f..3b70ffe7b37 100644 --- a/tests/unit/cli/errors_test.py +++ b/tests/unit/cli/errors_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import pytest from docker.errors import APIError from requests.exceptions import ConnectionError @@ -24,7 +21,7 @@ def patch_find_executable(side_effect): side_effect=side_effect) -class TestHandleConnectionErrors(object): +class TestHandleConnectionErrors: def test_generic_connection_error(self, mock_logging): with pytest.raises(errors.ConnectionError): @@ -37,19 +34,19 @@ def test_generic_connection_error(self, mock_logging): def test_api_error_version_mismatch(self, mock_logging): with pytest.raises(errors.ConnectionError): - with handle_connection_errors(mock.Mock(api_version='1.22')): + with handle_connection_errors(mock.Mock(api_version='1.38')): raise APIError(None, None, b"client is newer than server") _, args, _ = mock_logging.error.mock_calls[0] - assert "Docker Engine of version 1.10.0 or greater" in args[0] + assert "Docker Engine of version 18.06.0 or greater" in args[0] def test_api_error_version_mismatch_unicode_explanation(self, mock_logging): with pytest.raises(errors.ConnectionError): - with handle_connection_errors(mock.Mock(api_version='1.22')): - raise APIError(None, None, u"client is newer than server") + with handle_connection_errors(mock.Mock(api_version='1.38')): + raise APIError(None, None, "client is newer than server") _, args, _ = mock_logging.error.mock_calls[0] - assert "Docker Engine of version 1.10.0 or greater" in args[0] + assert "Docker Engine of version 18.06.0 or greater" in args[0] def test_api_error_version_other(self, mock_logging): msg = b"Something broke!" @@ -60,7 +57,7 @@ def test_api_error_version_other(self, mock_logging): mock_logging.error.assert_called_once_with(msg.decode('utf-8')) def test_api_error_version_other_unicode_explanation(self, mock_logging): - msg = u"Something broke!" + msg = "Something broke!" with pytest.raises(errors.ConnectionError): with handle_connection_errors(mock.Mock(api_version='1.22')): raise APIError(None, None, msg) diff --git a/tests/unit/cli/formatter_test.py b/tests/unit/cli/formatter_test.py index e685725112f..08752a6226b 100644 --- a/tests/unit/cli/formatter_test.py +++ b/tests/unit/cli/formatter_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import logging from compose.cli import colors @@ -43,10 +40,10 @@ def test_format_unicode_warn(self): message = b'\xec\xa0\x95\xec\x88\x98\xec\xa0\x95' output = self.formatter.format(make_log_record(logging.WARN, message)) expected = colors.yellow('WARNING') + ': ' - assert output == '{0}{1}'.format(expected, message.decode('utf-8')) + assert output == '{}{}'.format(expected, message.decode('utf-8')) def test_format_unicode_error(self): message = b'\xec\xa0\x95\xec\x88\x98\xec\xa0\x95' output = self.formatter.format(make_log_record(logging.ERROR, message)) expected = colors.red('ERROR') + ': ' - assert output == '{0}{1}'.format(expected, message.decode('utf-8')) + assert output == '{}{}'.format(expected, message.decode('utf-8')) diff --git a/tests/unit/cli/log_printer_test.py b/tests/unit/cli/log_printer_test.py index 5e387241d67..f76e2d626af 100644 --- a/tests/unit/cli/log_printer_test.py +++ b/tests/unit/cli/log_printer_test.py @@ -1,17 +1,13 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import itertools +from io import StringIO +from queue import Queue import pytest import requests -import six from docker.errors import APIError -from six.moves.queue import Queue from compose.cli.log_printer import build_log_generator from compose.cli.log_printer import build_log_presenters -from compose.cli.log_printer import build_no_log_generator from compose.cli.log_printer import consume_queue from compose.cli.log_printer import QueueItem from compose.cli.log_printer import wait_on_exit @@ -22,7 +18,7 @@ @pytest.fixture def output_stream(): - output = six.StringIO() + output = StringIO() output.flush = mock.Mock() return output @@ -32,7 +28,7 @@ def mock_container(): return mock.Mock(spec=Container, name_without_project='web_1') -class TestLogPresenter(object): +class TestLogPresenter: def test_monochrome(self, mock_container): presenters = build_log_presenters(['foo', 'bar'], True) @@ -78,15 +74,7 @@ def mock_wait(): assert expected in wait_on_exit(mock_container) -def test_build_no_log_generator(mock_container): - mock_container.has_api_logs = False - mock_container.log_driver = 'none' - output, = build_no_log_generator(mock_container, None) - assert "WARNING: no logs are available with the 'none' log driver\n" in output - assert "exited with code" not in output - - -class TestBuildLogGenerator(object): +class TestBuildLogGenerator: def test_no_log_stream(self, mock_container): mock_container.log_stream = None @@ -111,7 +99,7 @@ def test_with_log_stream(self, mock_container): assert next(generator) == "world" def test_unicode(self, output_stream): - glyph = u'\u2022\n' + glyph = '\u2022\n' mock_container.log_stream = iter([glyph.encode('utf-8')]) generator = build_log_generator(mock_container, {}) @@ -128,7 +116,7 @@ def mock_presenters(): return itertools.cycle([mock.Mock()]) -class TestWatchEvents(object): +class TestWatchEvents: def test_stop_event(self, thread_map, mock_presenters): event_stream = [{'action': 'stop', 'id': 'cid'}] @@ -170,7 +158,7 @@ def test_other_event(self, thread_map, mock_presenters): assert container_id not in thread_map -class TestConsumeQueue(object): +class TestConsumeQueue: def test_item_is_an_exception(self): diff --git a/tests/unit/cli/main_test.py b/tests/unit/cli/main_test.py index 067c74f0b0e..12b5c3dc132 100644 --- a/tests/unit/cli/main_test.py +++ b/tests/unit/cli/main_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import logging import docker @@ -25,7 +22,7 @@ def mock_container(service, number): container.Container, service=service, number=number, - name_without_project='{0}_{1}'.format(service, number)) + name_without_project='{}_{}'.format(service, number)) @pytest.fixture @@ -35,7 +32,7 @@ def logging_handler(): return logging.StreamHandler(stream=stream) -class TestCLIMainTestCase(object): +class TestCLIMainTestCase: def test_filter_attached_containers(self): containers = [ @@ -138,27 +135,26 @@ def test_get_docker_start_call(self): assert expected_docker_start_call == docker_start_call -class TestSetupConsoleHandlerTestCase(object): +class TestSetupConsoleHandlerTestCase: - def test_with_tty_verbose(self, logging_handler): + def test_with_console_formatter_verbose(self, logging_handler): setup_console_handler(logging_handler, True) assert type(logging_handler.formatter) == ConsoleWarningFormatter assert '%(name)s' in logging_handler.formatter._fmt assert '%(funcName)s' in logging_handler.formatter._fmt - def test_with_tty_not_verbose(self, logging_handler): + def test_with_console_formatter_not_verbose(self, logging_handler): setup_console_handler(logging_handler, False) assert type(logging_handler.formatter) == ConsoleWarningFormatter assert '%(name)s' not in logging_handler.formatter._fmt assert '%(funcName)s' not in logging_handler.formatter._fmt - def test_with_not_a_tty(self, logging_handler): - logging_handler.stream.isatty.return_value = False - setup_console_handler(logging_handler, False) + def test_without_console_formatter(self, logging_handler): + setup_console_handler(logging_handler, False, use_console_formatter=False) assert type(logging_handler.formatter) == logging.Formatter -class TestConvergeStrategyFromOptsTestCase(object): +class TestConvergeStrategyFromOptsTestCase: def test_invalid_opts(self): options = {'--force-recreate': True, '--no-recreate': True} @@ -192,7 +188,7 @@ def mock_find_executable(exe): @mock.patch('compose.cli.main.find_executable', mock_find_executable) -class TestCallDocker(object): +class TestCallDocker: def test_simple_no_options(self): with mock.patch('subprocess.call') as fake_call: call_docker(['ps'], {}, {}) diff --git a/tests/unit/cli/utils_test.py b/tests/unit/cli/utils_test.py index 7a762890370..d67c8ba8aff 100644 --- a/tests/unit/cli/utils_test.py +++ b/tests/unit/cli/utils_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import unittest from compose.cli.utils import human_readable_file_size diff --git a/tests/unit/cli/verbose_proxy_test.py b/tests/unit/cli/verbose_proxy_test.py index f111f8cdbf2..0da662fd0c0 100644 --- a/tests/unit/cli/verbose_proxy_test.py +++ b/tests/unit/cli/verbose_proxy_test.py @@ -1,8 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - -import six - from compose.cli import verbose_proxy from tests import unittest @@ -10,7 +5,7 @@ class VerboseProxyTestCase(unittest.TestCase): def test_format_call(self): - prefix = '' if six.PY3 else 'u' + prefix = '' expected = "(%(p)s'arg1', True, key=%(p)s'value')" % dict(p=prefix) actual = verbose_proxy.format_call( ("arg1", True), diff --git a/tests/unit/cli_test.py b/tests/unit/cli_test.py index a7522f939b9..fa6e76747f4 100644 --- a/tests/unit/cli_test.py +++ b/tests/unit/cli_test.py @@ -1,7 +1,3 @@ -# encoding: utf-8 -from __future__ import absolute_import -from __future__ import unicode_literals - import os import shutil import tempfile @@ -20,7 +16,10 @@ from compose.cli.docopt_command import NoSuchCommand from compose.cli.errors import UserError from compose.cli.main import TopLevelCommand +from compose.config.environment import Environment from compose.const import IS_WINDOWS_PLATFORM +from compose.const import LABEL_SERVICE +from compose.container import Container from compose.project import Project @@ -79,7 +78,9 @@ def test_project_name_with_environment_file(self): def test_get_project(self): base_dir = 'tests/fixtures/longer-filename-composefile' - project = get_project(base_dir) + env = Environment.from_env_file(base_dir) + env['COMPOSE_API_VERSION'] = DEFAULT_DOCKER_API_VERSION + project = get_project(base_dir, environment=env) assert project.name == 'longer-filename-composefile' assert project.client assert project.services @@ -97,12 +98,26 @@ def test_command_help_nonexistent(self): @pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason="requires dockerpty") @mock.patch('compose.cli.main.RunOperation', autospec=True) @mock.patch('compose.cli.main.PseudoTerminal', autospec=True) + @mock.patch('compose.service.Container.create') @mock.patch.dict(os.environ) - def test_run_interactive_passes_logs_false(self, mock_pseudo_terminal, mock_run_operation): + def test_run_interactive_passes_logs_false( + self, + mock_container_create, + mock_pseudo_terminal, + mock_run_operation, + ): os.environ['COMPOSE_INTERACTIVE_NO_CLI'] = 'true' mock_client = mock.create_autospec(docker.APIClient) mock_client.api_version = DEFAULT_DOCKER_API_VERSION mock_client._general_configs = {} + mock_container_create.return_value = Container(mock_client, { + 'Id': '37b35e0ba80d91009d37e16f249b32b84f72bda269985578ed6c75a0a13fcaa8', + 'Config': { + 'Labels': { + LABEL_SERVICE: 'service', + } + }, + }, has_been_inspected=True) project = Project.from_config( name='composetest', client=mock_client, @@ -135,10 +150,20 @@ def test_run_interactive_passes_logs_false(self, mock_pseudo_terminal, mock_run_ _, _, call_kwargs = mock_run_operation.mock_calls[0] assert call_kwargs['logs'] is False - def test_run_service_with_restart_always(self): + @mock.patch('compose.service.Container.create') + def test_run_service_with_restart_always(self, mock_container_create): mock_client = mock.create_autospec(docker.APIClient) mock_client.api_version = DEFAULT_DOCKER_API_VERSION mock_client._general_configs = {} + mock_container_create.return_value = Container(mock_client, { + 'Id': '37b35e0ba80d91009d37e16f249b32b84f72bda269985578ed6c75a0a13fcaa8', + 'Name': 'composetest_service_37b35', + 'Config': { + 'Labels': { + LABEL_SERVICE: 'service', + } + }, + }, has_been_inspected=True) project = Project.from_config( name='composetest', @@ -198,6 +223,55 @@ def test_run_service_with_restart_always(self): assert not mock_client.create_host_config.call_args[1].get('restart_policy') + @mock.patch('compose.project.Project.up') + @mock.patch.dict(os.environ) + def test_run_up_with_docker_cli_build(self, mock_project_up): + os.environ['COMPOSE_DOCKER_CLI_BUILD'] = '1' + mock_client = mock.create_autospec(docker.APIClient) + mock_client.api_version = DEFAULT_DOCKER_API_VERSION + mock_client._general_configs = {} + container = Container(mock_client, { + 'Id': '37b35e0ba80d91009d37e16f249b32b84f72bda269985578ed6c75a0a13fcaa8', + 'Name': 'composetest_service_37b35', + 'Config': { + 'Labels': { + LABEL_SERVICE: 'service', + } + }, + }, has_been_inspected=True) + mock_project_up.return_value = [container] + + project = Project.from_config( + name='composetest', + config_data=build_config({ + 'service': {'image': 'busybox'} + }), + client=mock_client, + ) + + command = TopLevelCommand(project) + command.run({ + 'SERVICE': 'service', + 'COMMAND': None, + '-e': [], + '--label': [], + '--user': None, + '--no-deps': None, + '--detach': True, + '-T': None, + '--entrypoint': None, + '--service-ports': None, + '--use-aliases': None, + '--publish': [], + '--volume': [], + '--rm': None, + '--name': None, + '--workdir': None, + }) + + _, _, call_kwargs = mock_project_up.mock_calls[0] + assert call_kwargs.get('cli') + def test_command_manual_and_service_ports_together(self): project = Project.from_config( name='composetest', diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index dc346df9556..72e39e4f2c9 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -1,8 +1,3 @@ -# encoding: utf-8 -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import codecs import os import shutil @@ -30,21 +25,13 @@ from compose.config.serialize import serialize_config from compose.config.serialize import serialize_ns_time_value from compose.config.types import VolumeSpec +from compose.const import COMPOSE_SPEC as VERSION from compose.const import COMPOSEFILE_V1 as V1 -from compose.const import COMPOSEFILE_V2_0 as V2_0 -from compose.const import COMPOSEFILE_V2_1 as V2_1 -from compose.const import COMPOSEFILE_V2_2 as V2_2 -from compose.const import COMPOSEFILE_V2_3 as V2_3 -from compose.const import COMPOSEFILE_V3_0 as V3_0 -from compose.const import COMPOSEFILE_V3_1 as V3_1 -from compose.const import COMPOSEFILE_V3_2 as V3_2 -from compose.const import COMPOSEFILE_V3_3 as V3_3 -from compose.const import COMPOSEFILE_V3_5 as V3_5 from compose.const import IS_WINDOWS_PLATFORM from tests import mock from tests import unittest -DEFAULT_VERSION = V2_0 +DEFAULT_VERSION = VERSION def make_service_dict(name, service_dict, working_dir='.', filename=None): @@ -77,8 +64,10 @@ def test_load(self): service_dicts = config.load( build_config_details( { - 'foo': {'image': 'busybox'}, - 'bar': {'image': 'busybox', 'environment': ['FOO=1']}, + 'services': { + 'foo': {'image': 'busybox'}, + 'bar': {'image': 'busybox', 'environment': ['FOO=1']}, + } }, 'tests/fixtures/extends', 'common.yml' @@ -171,25 +160,22 @@ def test_load_v2(self): } def test_valid_versions(self): - for version in ['2', '2.0']: - cfg = config.load(build_config_details({'version': version})) - assert cfg.version == V2_0 - - cfg = config.load(build_config_details({'version': '2.1'})) - assert cfg.version == V2_1 - - cfg = config.load(build_config_details({'version': '2.2'})) - assert cfg.version == V2_2 - - cfg = config.load(build_config_details({'version': '2.3'})) - assert cfg.version == V2_3 + cfg = config.load( + build_config_details({ + 'services': { + 'foo': {'image': 'busybox'}, + 'bar': {'image': 'busybox', 'environment': ['FOO=1']}, + } + }) + ) + assert cfg.config_version == VERSION + assert cfg.version == VERSION - for version in ['3', '3.0']: + for version in ['2', '2.0', '2.1', '2.2', '2.3', + '3', '3.0', '3.1', '3.2', '3.3', '3.4', '3.5', '3.6', '3.7', '3.8']: cfg = config.load(build_config_details({'version': version})) - assert cfg.version == V3_0 - - cfg = config.load(build_config_details({'version': '3.1'})) - assert cfg.version == V3_1 + assert cfg.config_version == version + assert cfg.version == VERSION def test_v1_file_version(self): cfg = config.load(build_config_details({'web': {'image': 'busybox'}})) @@ -201,7 +187,7 @@ def test_v1_file_version(self): assert list(s['name'] for s in cfg.services) == ['version'] def test_wrong_version_type(self): - for version in [None, 1, 2, 2.0]: + for version in [1, 2, 2.0]: with pytest.raises(ConfigurationError) as excinfo: config.load( build_config_details( @@ -217,12 +203,12 @@ def test_unsupported_version(self): with pytest.raises(ConfigurationError) as excinfo: config.load( build_config_details( - {'version': '2.18'}, + {'version': '1'}, filename='filename.yml', ) ) - assert 'Version in "filename.yml" is unsupported' in excinfo.exconly() + assert 'Version in "filename.yml" is invalid' in excinfo.exconly() assert VERSION_EXPLANATION in excinfo.exconly() def test_version_1_is_invalid(self): @@ -252,7 +238,9 @@ def test_v1_file_with_version_is_invalid(self): ) ) - assert 'Invalid top-level property "web"' in excinfo.exconly() + assert "compose.config.errors.ConfigurationError: " \ + "The Compose file 'filename.yml' is invalid because:\n" \ + "'web' does not match any of the regexes: '^x-'" in excinfo.exconly() assert VERSION_EXPLANATION in excinfo.exconly() def test_named_volume_config_empty(self): @@ -332,7 +320,6 @@ def test_load_service_with_name_version(self): } }, 'working_dir', 'filename.yml') ) - assert 'Unexpected type for "version" key in "filename.yml"' \ in mock_logging.warning.call_args[0][0] @@ -380,7 +367,7 @@ def test_load_config_link_local_ips_network(self): base_file = config.ConfigFile( 'base.yaml', { - 'version': str(V2_1), + 'version': '2', 'services': { 'web': { 'image': 'example/web', @@ -515,7 +502,15 @@ def test_load_config_invalid_service_names(self): for invalid_name in ['?not?allowed', ' ', '', '!', '/', '\xe2']: with pytest.raises(ConfigurationError) as exc: config.load(build_config_details( - {invalid_name: {'image': 'busybox'}})) + { + 'version': '2', + 'services': { + invalid_name: + { + 'image': 'busybox' + } + } + })) assert 'Invalid service name \'%s\'' % invalid_name in exc.exconly() def test_load_config_invalid_service_names_v2(self): @@ -547,17 +542,24 @@ def test_load_with_invalid_field_name_v1(self): with pytest.raises(ConfigurationError) as exc: config.load(build_config_details( { - 'web': {'image': 'busybox', 'name': 'bogus'}, + 'version': '2', + 'services': { + 'web': {'image': 'busybox', 'name': 'bogus'} + } }, 'working_dir', 'filename.yml', )) - - assert "Unsupported config option for web: 'name'" in exc.exconly() + assert "Unsupported config option for services.web: 'name'" in exc.exconly() def test_load_invalid_service_definition(self): config_details = build_config_details( - {'web': 'wrong'}, + { + 'version': '2', + 'services': { + 'web': 'wrong' + } + }, 'working_dir', 'filename.yml') with pytest.raises(ConfigurationError) as exc: @@ -589,7 +591,10 @@ def test_config_integer_service_name_raise_validation_error(self): with pytest.raises(ConfigurationError) as excinfo: config.load( build_config_details( - {1: {'image': 'busybox'}}, + { + 'version': '2', + 'services': {1: {'image': 'busybox'}} + }, 'working_dir', 'filename.yml' ) @@ -664,7 +669,7 @@ def test_config_invalid_service_name_raise_validation_error(self): assert 'Invalid service name \'mong\\o\'' in excinfo.exconly() - def test_config_duplicate_cache_from_values_validation_error(self): + def test_config_duplicate_cache_from_values_no_validation_error(self): with pytest.raises(ConfigurationError) as exc: config.load( build_config_details({ @@ -676,7 +681,7 @@ def test_config_duplicate_cache_from_values_validation_error(self): }) ) - assert 'build.cache_from contains non-unique items' in exc.exconly() + assert 'build.cache_from contains non-unique items' not in exc.exconly() def test_load_with_multiple_files_v1(self): base_file = config.ConfigFile( @@ -840,10 +845,10 @@ def test_load_mixed_extends_resolution(self): def test_load_with_multiple_files_and_invalid_override(self): base_file = config.ConfigFile( 'base.yaml', - {'web': {'image': 'example/web'}}) + {'version': '2', 'services': {'web': {'image': 'example/web'}}}) override_file = config.ConfigFile( 'override.yaml', - {'bogus': 'thing'}) + {'version': '2', 'services': {'bogus': 'thing'}}) details = config.ConfigDetails('.', [base_file, override_file]) with pytest.raises(ConfigurationError) as exc: @@ -981,7 +986,6 @@ def test_load_build_labels_dict(self): service = config.load( build_config_details( { - 'version': str(V3_3), 'services': { 'web': { 'build': { @@ -1428,7 +1432,7 @@ def test_config_invalid_ipam_config(self): config.load( build_config_details( { - 'version': str(V2_1), + 'version': str(VERSION), 'networks': { 'foo': { 'driver': 'default', @@ -1459,7 +1463,6 @@ def test_config_valid_ipam_config(self): networks = config.load( build_config_details( { - 'version': str(V2_1), 'networks': { 'foo': { 'driver': 'default', @@ -1491,7 +1494,10 @@ def test_config_hint(self): config.load( build_config_details( { - 'foo': {'image': 'busybox', 'privilige': 'something'}, + 'version': str(VERSION), + 'services': { + 'foo': {'image': 'busybox', 'privilige': 'something'}, + } }, 'tests/fixtures/extends', 'filename.yml' @@ -1512,7 +1518,10 @@ def test_invalid_config_v1(self): config.load( build_config_details( { - 'foo': {'image': 1}, + 'version': str(VERSION), + 'services': { + 'foo': {'image': 1}, + } }, 'tests/fixtures/extends', 'filename.yml' @@ -1559,7 +1568,10 @@ def test_invalid_config_type_should_be_an_array(self): config.load( build_config_details( { - 'foo': {'image': 'busybox', 'links': 'an_link'}, + 'version': str(VERSION), + 'services': { + 'foo': {'image': 'busybox', 'links': 'an_link'}, + } }, 'tests/fixtures/extends', 'filename.yml' @@ -1587,7 +1599,10 @@ def test_invalid_config_not_unique_items(self): config.load( build_config_details( { - 'web': {'build': '.', 'devices': ['/dev/foo:/dev/foo', '/dev/foo:/dev/foo']} + 'version': str(VERSION), + 'services': { + 'web': {'build': '.', 'devices': ['/dev/foo:/dev/foo', '/dev/foo:/dev/foo']} + } }, 'tests/fixtures/extends', 'filename.yml' @@ -1601,7 +1616,10 @@ def test_invalid_list_of_strings_format(self): config.load( build_config_details( { - 'web': {'build': '.', 'command': [1]} + 'version': str(VERSION), + 'services': { + 'web': {'build': '.', 'command': [1]} + } }, 'tests/fixtures/extends', 'filename.yml' @@ -1626,10 +1644,13 @@ def test_config_extra_hosts_string_raises_validation_error(self): with pytest.raises(ConfigurationError) as excinfo: config.load( build_config_details( - {'web': { - 'image': 'busybox', - 'extra_hosts': 'somehost:162.242.195.82' - }}, + { + 'version': str(VERSION), + 'services': { + 'web': { + 'image': 'busybox', + 'extra_hosts': 'somehost:162.242.195.82'}} + }, 'working_dir', 'filename.yml' ) @@ -1642,13 +1663,16 @@ def test_config_extra_hosts_list_of_dicts_validation_error(self): with pytest.raises(ConfigurationError) as excinfo: config.load( build_config_details( - {'web': { - 'image': 'busybox', - 'extra_hosts': [ - {'somehost': '162.242.195.82'}, - {'otherhost': '50.31.209.229'} - ] - }}, + { + 'version': str(VERSION), + 'services': { + 'web': { + 'image': 'busybox', + 'extra_hosts': [ + {'somehost': '162.242.195.82'}, + {'otherhost': '50.31.209.229'} + ]}} + }, 'working_dir', 'filename.yml' ) @@ -1662,13 +1686,16 @@ def test_config_ulimits_invalid_keys_validation_error(self): with pytest.raises(ConfigurationError) as exc: config.load(build_config_details( { - 'web': { - 'image': 'busybox', - 'ulimits': { - 'nofile': { - "not_soft_or_hard": 100, - "soft": 10000, - "hard": 20000, + 'version': str(VERSION), + 'services': { + 'web': { + 'image': 'busybox', + 'ulimits': { + 'nofile': { + "not_soft_or_hard": 100, + "soft": 10000, + "hard": 20000, + } } } } @@ -1683,9 +1710,12 @@ def test_config_ulimits_required_keys_validation_error(self): with pytest.raises(ConfigurationError) as exc: config.load(build_config_details( { - 'web': { - 'image': 'busybox', - 'ulimits': {'nofile': {"soft": 10000}} + 'version': str(VERSION), + 'services': { + 'web': { + 'image': 'busybox', + 'ulimits': {'nofile': {"soft": 10000}} + } } }, 'working_dir', @@ -1699,10 +1729,13 @@ def test_config_ulimits_soft_greater_than_hard_error(self): with pytest.raises(ConfigurationError) as exc: config.load(build_config_details( { - 'web': { - 'image': 'busybox', - 'ulimits': { - 'nofile': {"soft": 10000, "hard": 1000} + 'version': str(VERSION), + 'services': { + 'web': { + 'image': 'busybox', + 'ulimits': { + 'nofile': {"soft": 10000, "hard": 1000} + } } } }, @@ -1715,10 +1748,12 @@ def test_valid_config_which_allows_two_type_definitions(self): for expose in expose_values: service = config.load( build_config_details( - {'web': { - 'image': 'busybox', - 'expose': expose - }}, + { + 'version': str(VERSION), + 'services': { + 'web': { + 'image': 'busybox', + 'expose': expose}}}, 'working_dir', 'filename.yml' ) @@ -1730,10 +1765,12 @@ def test_valid_config_oneof_string_or_list(self): for entrypoint in entrypoint_values: service = config.load( build_config_details( - {'web': { - 'image': 'busybox', - 'entrypoint': entrypoint - }}, + { + 'version': str(VERSION), + 'services': { + 'web': { + 'image': 'busybox', + 'entrypoint': entrypoint}}}, 'working_dir', 'filename.yml' ) @@ -1742,9 +1779,12 @@ def test_valid_config_oneof_string_or_list(self): def test_logs_warning_for_boolean_in_environment(self): config_details = build_config_details({ - 'web': { - 'image': 'busybox', - 'environment': {'SHOW_STUFF': True} + 'version': str(VERSION), + 'services': { + 'web': { + 'image': 'busybox', + 'environment': {'SHOW_STUFF': True} + } } }) @@ -1756,10 +1796,12 @@ def test_logs_warning_for_boolean_in_environment(self): def test_config_valid_environment_dict_key_contains_dashes(self): services = config.load( build_config_details( - {'web': { - 'image': 'busybox', - 'environment': {'SPRING_JPA_HIBERNATE_DDL-AUTO': 'none'} - }}, + { + 'version': str(VERSION), + 'services': { + 'web': { + 'image': 'busybox', + 'environment': {'SPRING_JPA_HIBERNATE_DDL-AUTO': 'none'}}}}, 'working_dir', 'filename.yml' ) @@ -1798,9 +1840,12 @@ def test_load_yaml_with_bom(self): def test_validate_extra_hosts_invalid(self): with pytest.raises(ConfigurationError) as exc: config.load(build_config_details({ - 'web': { - 'image': 'alpine', - 'extra_hosts': "www.example.com: 192.168.0.17", + 'version': str(VERSION), + 'services': { + 'web': { + 'image': 'alpine', + 'extra_hosts': "www.example.com: 192.168.0.17", + } } })) assert "web.extra_hosts contains an invalid type" in exc.exconly() @@ -1808,22 +1853,28 @@ def test_validate_extra_hosts_invalid(self): def test_validate_extra_hosts_invalid_list(self): with pytest.raises(ConfigurationError) as exc: config.load(build_config_details({ - 'web': { - 'image': 'alpine', - 'extra_hosts': [ - {'www.example.com': '192.168.0.17'}, - {'api.example.com': '192.168.0.18'} - ], + 'version': str(VERSION), + 'services': { + 'web': { + 'image': 'alpine', + 'extra_hosts': [ + {'www.example.com': '192.168.0.17'}, + {'api.example.com': '192.168.0.18'} + ], + } } })) assert "which is an invalid type" in exc.exconly() def test_normalize_dns_options(self): actual = config.load(build_config_details({ - 'web': { - 'image': 'alpine', - 'dns': '8.8.8.8', - 'dns_search': 'domain.local', + 'version': str(VERSION), + 'services': { + 'web': { + 'image': 'alpine', + 'dns': '8.8.8.8', + 'dns_search': 'domain.local', + } } })) assert actual.services == [ @@ -1951,7 +2002,6 @@ def test_dns_opt_option(self): def test_isolation_option(self): actual = config.load(build_config_details({ - 'version': str(V2_1), 'services': { 'web': { 'image': 'win10', @@ -1970,7 +2020,6 @@ def test_isolation_option(self): def test_runtime_option(self): actual = config.load(build_config_details({ - 'version': str(V2_3), 'services': { 'web': { 'image': 'nvidia/cuda', @@ -2092,7 +2141,7 @@ def test_merge_service_dicts_heterogeneous_volumes(self): } actual = config.merge_service_dicts_from_files( - base, override, V3_2 + base, override, VERSION ) assert actual['volumes'] == [ @@ -2139,7 +2188,7 @@ def test_merge_logging_v2(self): } } - actual = config.merge_service_dicts(base, override, V2_0) + actual = config.merge_service_dicts(base, override, VERSION) assert actual == { 'image': 'alpine:edge', 'logging': { @@ -2173,7 +2222,7 @@ def test_merge_logging_v2_override_driver(self): } } - actual = config.merge_service_dicts(base, override, V2_0) + actual = config.merge_service_dicts(base, override, VERSION) assert actual == { 'image': 'alpine:edge', 'logging': { @@ -2205,7 +2254,7 @@ def test_merge_logging_v2_no_base_driver(self): } } - actual = config.merge_service_dicts(base, override, V2_0) + actual = config.merge_service_dicts(base, override, VERSION) assert actual == { 'image': 'alpine:edge', 'logging': { @@ -2237,7 +2286,7 @@ def test_merge_logging_v2_no_drivers(self): } } - actual = config.merge_service_dicts(base, override, V2_0) + actual = config.merge_service_dicts(base, override, VERSION) assert actual == { 'image': 'alpine:edge', 'logging': { @@ -2266,7 +2315,7 @@ def test_merge_logging_v2_no_override_options(self): } } - actual = config.merge_service_dicts(base, override, V2_0) + actual = config.merge_service_dicts(base, override, VERSION) assert actual == { 'image': 'alpine:edge', 'logging': { @@ -2286,7 +2335,7 @@ def test_merge_logging_v2_no_base(self): } } } - actual = config.merge_service_dicts(base, override, V2_0) + actual = config.merge_service_dicts(base, override, VERSION) assert actual == { 'image': 'alpine:edge', 'logging': { @@ -2308,7 +2357,7 @@ def test_merge_logging_v2_no_override(self): } } override = {} - actual = config.merge_service_dicts(base, override, V2_0) + actual = config.merge_service_dicts(base, override, VERSION) assert actual == { 'image': 'alpine:edge', 'logging': { @@ -2336,7 +2385,7 @@ def test_merge_mixed_ports(self): 'ports': ['1245:1245/udp'] } - actual = config.merge_service_dicts(base, override, V3_1) + actual = config.merge_service_dicts(base, override, VERSION) assert actual == { 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', @@ -2348,11 +2397,12 @@ def test_merge_depends_on_no_override(self): 'image': 'busybox', 'depends_on': { 'app1': {'condition': 'service_started'}, - 'app2': {'condition': 'service_healthy'} + 'app2': {'condition': 'service_healthy'}, + 'app3': {'condition': 'service_completed_successfully'} } } override = {} - actual = config.merge_service_dicts(base, override, V2_1) + actual = config.merge_service_dicts(base, override, VERSION) assert actual == base def test_merge_depends_on_mixed_syntax(self): @@ -2360,20 +2410,22 @@ def test_merge_depends_on_mixed_syntax(self): 'image': 'busybox', 'depends_on': { 'app1': {'condition': 'service_started'}, - 'app2': {'condition': 'service_healthy'} + 'app2': {'condition': 'service_healthy'}, + 'app3': {'condition': 'service_completed_successfully'} } } override = { - 'depends_on': ['app3'] + 'depends_on': ['app4'] } - actual = config.merge_service_dicts(base, override, V2_1) + actual = config.merge_service_dicts(base, override, VERSION) assert actual == { 'image': 'busybox', 'depends_on': { 'app1': {'condition': 'service_started'}, 'app2': {'condition': 'service_healthy'}, - 'app3': {'condition': 'service_started'} + 'app3': {'condition': 'service_completed_successfully'}, + 'app4': {'condition': 'service_started'}, } } @@ -2405,7 +2457,7 @@ def test_merge_pid(self): 'labels': {'com.docker.compose.test': 'yes'} } - actual = config.merge_service_dicts(base, override, V2_0) + actual = config.merge_service_dicts(base, override, VERSION) assert actual == { 'image': 'busybox', 'pid': 'host', @@ -2421,7 +2473,7 @@ def test_merge_different_secrets(self): } override = {'secrets': ['other-src.txt']} - actual = config.merge_service_dicts(base, override, V3_1) + actual = config.merge_service_dicts(base, override, VERSION) assert secret_sort(actual['secrets']) == secret_sort([ {'source': 'src.txt'}, {'source': 'other-src.txt'} @@ -2441,7 +2493,7 @@ def test_merge_secrets_override(self): } ] } - actual = config.merge_service_dicts(base, override, V3_1) + actual = config.merge_service_dicts(base, override, VERSION) assert actual['secrets'] == override['secrets'] def test_merge_different_configs(self): @@ -2453,7 +2505,7 @@ def test_merge_different_configs(self): } override = {'configs': ['other-src.txt']} - actual = config.merge_service_dicts(base, override, V3_3) + actual = config.merge_service_dicts(base, override, VERSION) assert secret_sort(actual['configs']) == secret_sort([ {'source': 'src.txt'}, {'source': 'other-src.txt'} @@ -2473,7 +2525,7 @@ def test_merge_configs_override(self): } ] } - actual = config.merge_service_dicts(base, override, V3_3) + actual = config.merge_service_dicts(base, override, VERSION) assert actual['configs'] == override['configs'] def test_merge_deploy(self): @@ -2488,7 +2540,7 @@ def test_merge_deploy(self): } } } - actual = config.merge_service_dicts(base, override, V3_0) + actual = config.merge_service_dicts(base, override, VERSION) assert actual['deploy'] == override['deploy'] def test_merge_deploy_override(self): @@ -2498,6 +2550,7 @@ def test_merge_deploy_override(self): 'labels': ['com.docker.compose.a=1', 'com.docker.compose.b=2'], 'mode': 'replicated', 'placement': { + 'max_replicas_per_node': 1, 'constraints': [ 'node.role == manager', 'engine.labels.aws == true' ], @@ -2544,7 +2597,7 @@ def test_merge_deploy_override(self): 'update_config': {'max_failure_ratio': 0.712, 'parallelism': 4} } } - actual = config.merge_service_dicts(base, override, V3_5) + actual = config.merge_service_dicts(base, override, VERSION) assert actual['deploy'] == { 'mode': 'replicated', 'endpoint_mode': 'vip', @@ -2554,6 +2607,7 @@ def test_merge_deploy_override(self): 'com.docker.compose.c': '3' }, 'placement': { + 'max_replicas_per_node': 1, 'constraints': [ 'engine.labels.aws == true', 'engine.labels.dev == true', 'node.role == manager', 'node.role == worker' @@ -2600,7 +2654,7 @@ def test_merge_credential_spec(self): } } - actual = config.merge_service_dicts(base, override, V3_3) + actual = config.merge_service_dicts(base, override, VERSION) assert actual['credential_spec'] == override['credential_spec'] def test_merge_scale(self): @@ -2613,7 +2667,7 @@ def test_merge_scale(self): 'scale': 4, } - actual = config.merge_service_dicts(base, override, V2_2) + actual = config.merge_service_dicts(base, override, VERSION) assert actual == {'image': 'bar', 'scale': 4} def test_merge_blkio_config(self): @@ -2648,7 +2702,7 @@ def test_merge_blkio_config(self): } } - actual = config.merge_service_dicts(base, override, V2_2) + actual = config.merge_service_dicts(base, override, VERSION) assert actual == { 'image': 'bar', 'blkio_config': { @@ -2675,7 +2729,7 @@ def test_merge_extra_hosts(self): 'extra_hosts': ['bar:5.6.7.8', 'foo:127.0.0.1'] } - actual = config.merge_service_dicts(base, override, V2_0) + actual = config.merge_service_dicts(base, override, VERSION) assert actual['extra_hosts'] == { 'foo': '127.0.0.1', 'bar': '5.6.7.8', @@ -2699,7 +2753,7 @@ def test_merge_healthcheck_config(self): } } - actual = config.merge_service_dicts(base, override, V2_3) + actual = config.merge_service_dicts(base, override, VERSION) assert actual['healthcheck'] == { 'start_period': base['healthcheck']['start_period'], 'test': override['healthcheck']['test'], @@ -2725,7 +2779,7 @@ def test_merge_healthcheck_override_disables(self): } } - actual = config.merge_service_dicts(base, override, V2_3) + actual = config.merge_service_dicts(base, override, VERSION) assert actual['healthcheck'] == {'disabled': True} def test_merge_healthcheck_override_enables(self): @@ -2747,7 +2801,7 @@ def test_merge_healthcheck_override_enables(self): } } - actual = config.merge_service_dicts(base, override, V2_3) + actual = config.merge_service_dicts(base, override, VERSION) assert actual['healthcheck'] == override['healthcheck'] def test_merge_device_cgroup_rules(self): @@ -2760,7 +2814,7 @@ def test_merge_device_cgroup_rules(self): 'device_cgroup_rules': ['c 7:128 rwm', 'f 0:128 n'] } - actual = config.merge_service_dicts(base, override, V2_3) + actual = config.merge_service_dicts(base, override, VERSION) assert sorted(actual['device_cgroup_rules']) == sorted( ['c 7:128 rwm', 'x 3:244 rw', 'f 0:128 n'] ) @@ -2775,7 +2829,7 @@ def test_merge_isolation(self): 'isolation': 'hyperv', } - actual = config.merge_service_dicts(base, override, V2_3) + actual = config.merge_service_dicts(base, override, VERSION) assert actual == { 'image': 'bar', 'isolation': 'hyperv', @@ -2797,7 +2851,7 @@ def test_merge_storage_opt(self): } } - actual = config.merge_service_dicts(base, override, V2_3) + actual = config.merge_service_dicts(base, override, VERSION) assert actual['storage_opt'] == { 'size': '2G', 'readonly': 'false', @@ -3180,12 +3234,12 @@ def test_config_duplicate_mount_points(self): with self.assertRaises(ConfigurationError) as e: config.load(config1) - self.assertEquals(str(e.exception), 'Duplicate mount points: [%s]' % ( + self.assertEqual(str(e.exception), 'Duplicate mount points: [%s]' % ( ', '.join(['/tmp/foo:/tmp/foo:rw']*2))) with self.assertRaises(ConfigurationError) as e: config.load(config2) - self.assertEquals(str(e.exception), 'Duplicate mount points: [%s]' % ( + self.assertEqual(str(e.exception), 'Duplicate mount points: [%s]' % ( ', '.join(['/x:/y:rw', '/z:/y:rw']))) @@ -3354,6 +3408,7 @@ def test_config_non_unique_ports_validation(self): assert "non-unique" in exc.value.msg + @pytest.mark.skip(reason="Validator is one_off (generic error)") def test_config_invalid_ports_format_validation(self): for invalid_ports in self.INVALID_PORT_MAPPINGS: with pytest.raises(ConfigurationError) as exc: @@ -3515,9 +3570,11 @@ def test_config_file_with_environment_file(self): @mock.patch.dict(os.environ) def test_config_file_with_options_environment_file(self): project_dir = 'tests/fixtures/default-env-file' + # env-file is relative to current working dir + env = Environment.from_env_file(project_dir, project_dir + '/.env2') service_dicts = config.load( config.find( - project_dir, None, Environment.from_env_file(project_dir, '.env2') + project_dir, None, env ) ).services @@ -3603,6 +3660,7 @@ def test_unset_variable_produces_warning(self): assert 'BAR' in warnings[0] assert 'FOO' in warnings[1] + @pytest.mark.skip(reason='compatibility mode was removed internally') def test_compatibility_mode_warnings(self): config_details = build_config_details({ 'version': '3.5', @@ -3637,11 +3695,11 @@ def test_compatibility_mode_warnings(self): assert 'labels' in warn_message assert 'endpoint_mode' in warn_message assert 'update_config' in warn_message - assert 'placement' in warn_message assert 'resources.reservations.cpus' in warn_message assert 'restart_policy.delay' in warn_message assert 'restart_policy.window' in warn_message + @pytest.mark.skip(reason='compatibility mode was removed internally') def test_compatibility_mode_load(self): config_details = build_config_details({ 'version': '3.5', @@ -3832,12 +3890,12 @@ def test_home_directory_with_driver_does_not_expand(self): assert d['volumes'] == ['~:/data'] def test_volume_path_with_non_ascii_directory(self): - volume = u'/Füü/data:/data' + volume = '/Füü/data:/data' container_path = config.resolve_volume_path(".", volume) assert container_path == volume -class MergePathMappingTest(object): +class MergePathMappingTest: config_name = "" def test_empty(self): @@ -3910,7 +3968,7 @@ def test_merge_build_or_image_override_with_other(self): assert config.merge_service_dicts({'image': 'redis'}, {'build': '.'}, V1) == {'build': '.'} -class MergeListsTest(object): +class MergeListsTest: config_name = "" base_config = [] override_config = [] @@ -4343,7 +4401,7 @@ def test_resolve_environment_from_env_file_with_empty_values(self): {'env_file': ['tests/fixtures/env/resolve.env']}, Environment.from_env_file(None) ) == { - 'FILE_DEF': u'bär', + 'FILE_DEF': 'bär', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', 'NO_DEF': None @@ -4374,7 +4432,8 @@ def test_resolve_path(self): service_dict = config.load( build_config_details( - {'foo': {'build': '.', 'volumes': ['$HOSTENV:$CONTAINERENV']}}, + {'services': { + 'foo': {'build': '.', 'volumes': ['$HOSTENV:$CONTAINERENV']}}}, "tests/fixtures/env", ) ).services[0] @@ -4382,7 +4441,8 @@ def test_resolve_path(self): service_dict = config.load( build_config_details( - {'foo': {'build': '.', 'volumes': ['/opt${HOSTENV}:/opt${CONTAINERENV}']}}, + {'services': { + 'foo': {'build': '.', 'volumes': ['/opt${HOSTENV}:/opt${CONTAINERENV}']}}}, "tests/fixtures/env", ) ).services[0] @@ -4503,7 +4563,11 @@ def test_extends_validation_empty_dictionary(self): config.load( build_config_details( { - 'web': {'image': 'busybox', 'extends': {}}, + 'version': '3', + 'services': + { + 'web': {'image': 'busybox', 'extends': {}}, + } }, 'tests/fixtures/extends', 'filename.yml' @@ -4517,7 +4581,14 @@ def test_extends_validation_missing_service_key(self): config.load( build_config_details( { - 'web': {'image': 'busybox', 'extends': {'file': 'common.yml'}}, + 'version': '3', + 'services': + { + 'web': { + 'image': 'busybox', + 'extends': {'file': 'common.yml'} + } + } }, 'tests/fixtures/extends', 'filename.yml' @@ -4531,14 +4602,18 @@ def test_extends_validation_invalid_key(self): config.load( build_config_details( { - 'web': { - 'image': 'busybox', - 'extends': { - 'file': 'common.yml', - 'service': 'web', - 'rogue_key': 'is not allowed' - } - }, + 'version': '3', + 'services': + { + 'web': { + 'image': 'busybox', + 'extends': { + 'file': 'common.yml', + 'service': 'web', + 'rogue_key': 'is not allowed' + } + }, + } }, 'tests/fixtures/extends', 'filename.yml' @@ -4553,11 +4628,14 @@ def test_extends_validation_sub_property_key(self): config.load( build_config_details( { - 'web': { - 'image': 'busybox', - 'extends': { - 'file': 1, - 'service': 'web', + 'version': '3', + 'services': { + 'web': { + 'image': 'busybox', + 'extends': { + 'file': 1, + 'service': 'web', + } } }, }, @@ -4969,14 +5047,14 @@ def test_split_path_mapping_with_windows_path_in_container(self): container_path = 'c:\\scarletdevil\\data' expected_mapping = (container_path, (host_path, None)) - mapping = config.split_path_mapping('{0}:{1}'.format(host_path, container_path)) + mapping = config.split_path_mapping('{}:{}'.format(host_path, container_path)) assert mapping == expected_mapping def test_split_path_mapping_with_root_mount(self): host_path = '/' container_path = '/var/hostroot' expected_mapping = (container_path, (host_path, None)) - mapping = config.split_path_mapping('{0}:{1}'.format(host_path, container_path)) + mapping = config.split_path_mapping('{}:{}'.format(host_path, container_path)) assert mapping == expected_mapping @@ -5160,6 +5238,8 @@ class GetDefaultConfigFilesTestCase(unittest.TestCase): files = [ 'docker-compose.yml', 'docker-compose.yaml', + 'compose.yml', + 'compose.yaml', ] def test_get_config_path_default_file_in_basedir(self): @@ -5193,14 +5273,16 @@ def make_files(dirname, filenames): base_dir = tempfile.mkdtemp(dir=project_dir) else: base_dir = project_dir - filename, = config.get_default_config_files(base_dir) - return os.path.basename(filename) + filenames = config.get_default_config_files(base_dir) + if not filenames: + raise config.ComposeFileNotFound(config.SUPPORTED_FILENAMES) + return os.path.basename(filenames[0]) finally: shutil.rmtree(project_dir) class SerializeTest(unittest.TestCase): - def test_denormalize_depends_on_v3(self): + def test_denormalize_depends(self): service_dict = { 'image': 'busybox', 'command': 'true', @@ -5210,23 +5292,7 @@ def test_denormalize_depends_on_v3(self): } } - assert denormalize_service_dict(service_dict, V3_0) == { - 'image': 'busybox', - 'command': 'true', - 'depends_on': ['service2', 'service3'] - } - - def test_denormalize_depends_on_v2_1(self): - service_dict = { - 'image': 'busybox', - 'command': 'true', - 'depends_on': { - 'service2': {'condition': 'service_started'}, - 'service3': {'condition': 'service_started'}, - } - } - - assert denormalize_service_dict(service_dict, V2_1) == service_dict + assert denormalize_service_dict(service_dict, VERSION) == service_dict def test_serialize_time(self): data = { @@ -5260,7 +5326,7 @@ def test_denormalize_healthcheck(self): processed_service = config.process_service(config.ServiceConfig( '.', 'test', 'test', service_dict )) - denormalized_service = denormalize_service_dict(processed_service, V2_3) + denormalized_service = denormalize_service_dict(processed_service, VERSION) assert denormalized_service['healthcheck']['interval'] == '100s' assert denormalized_service['healthcheck']['timeout'] == '30s' assert denormalized_service['healthcheck']['start_period'] == '2090ms' @@ -5271,7 +5337,7 @@ def test_denormalize_image_has_digest(self): } image_digest = 'busybox@sha256:abcde' - assert denormalize_service_dict(service_dict, V3_0, image_digest) == { + assert denormalize_service_dict(service_dict, VERSION, image_digest) == { 'image': 'busybox@sha256:abcde' } @@ -5280,7 +5346,7 @@ def test_denormalize_image_no_digest(self): 'image': 'busybox' } - assert denormalize_service_dict(service_dict, V3_0) == { + assert denormalize_service_dict(service_dict, VERSION) == { 'image': 'busybox' } @@ -5313,10 +5379,22 @@ def test_serialize_secrets(self): serialized_service = serialized_config['services']['web'] assert secret_sort(serialized_service['secrets']) == secret_sort(service_dict['secrets']) assert 'secrets' in serialized_config - assert serialized_config['secrets']['two'] == secrets_dict['two'] + assert serialized_config['secrets']['two'] == {'external': True, 'name': 'two'} def test_serialize_ports(self): - config_dict = config.Config(version=V2_0, services=[ + config_dict = config.Config(config_version=VERSION, version=VERSION, services=[ + { + 'ports': [types.ServicePort('80', '8080', None, None, None)], + 'image': 'alpine', + 'name': 'web' + } + ], volumes={}, networks={}, secrets={}, configs={}) + + serialized_config = yaml.safe_load(serialize_config(config_dict)) + assert [{'published': 8080, 'target': 80}] == serialized_config['services']['web']['ports'] + + def test_serialize_ports_v1(self): + config_dict = config.Config(config_version=V1, version=V1, services=[ { 'ports': [types.ServicePort('80', '8080', None, None, None)], 'image': 'alpine', @@ -5325,10 +5403,10 @@ def test_serialize_ports(self): ], volumes={}, networks={}, secrets={}, configs={}) serialized_config = yaml.safe_load(serialize_config(config_dict)) - assert '8080:80/tcp' in serialized_config['services']['web']['ports'] + assert ['8080:80/tcp'] == serialized_config['services']['web']['ports'] def test_serialize_ports_with_ext_ip(self): - config_dict = config.Config(version=V3_5, services=[ + config_dict = config.Config(config_version=VERSION, version=VERSION, services=[ { 'ports': [types.ServicePort('80', '8080', None, None, '127.0.0.1')], 'image': 'alpine', @@ -5368,7 +5446,7 @@ def test_serialize_configs(self): serialized_service = serialized_config['services']['web'] assert secret_sort(serialized_service['configs']) == secret_sort(service_dict['configs']) assert 'configs' in serialized_config - assert serialized_config['configs']['two'] == configs_dict['two'] + assert serialized_config['configs']['two'] == {'external': True, 'name': 'two'} def test_serialize_bool_string(self): cfg = { @@ -5420,15 +5498,19 @@ def test_serialize_escape_dont_interpolate(self): 'environment': { 'CURRENCY': '$' }, + 'env_file': ['tests/fixtures/env/three.env'], 'entrypoint': ['$SHELL', '-c'], } } } - config_dict = config.load(build_config_details(cfg), interpolate=False) + config_dict = config.load(build_config_details(cfg, working_dir='.'), interpolate=False) serialized_config = yaml.safe_load(serialize_config(config_dict, escape_dollar=False)) serialized_service = serialized_config['services']['web'] assert serialized_service['environment']['CURRENCY'] == '$' + # Values coming from env_files are not allowed to have variables + assert serialized_service['environment']['FOO'] == 'NO $$ENV VAR' + assert serialized_service['environment']['DOO'] == 'NO $${ENV} VAR' assert serialized_service['command'] == 'echo $FOO' assert serialized_service['entrypoint'][0] == '$SHELL' diff --git a/tests/unit/config/environment_test.py b/tests/unit/config/environment_test.py index 7e394d248f5..6a80ff12254 100644 --- a/tests/unit/config/environment_test.py +++ b/tests/unit/config/environment_test.py @@ -1,8 +1,3 @@ -# encoding: utf-8 -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import codecs import os import shutil diff --git a/tests/unit/config/interpolation_test.py b/tests/unit/config/interpolation_test.py index 91fc3e69db5..1fd50d60bdd 100644 --- a/tests/unit/config/interpolation_test.py +++ b/tests/unit/config/interpolation_test.py @@ -1,7 +1,3 @@ -# encoding: utf-8 -from __future__ import absolute_import -from __future__ import unicode_literals - import pytest from compose.config.environment import Environment @@ -11,9 +7,7 @@ from compose.config.interpolation import InvalidInterpolation from compose.config.interpolation import TemplateWithDefaults from compose.config.interpolation import UnsetRequiredSubstitution -from compose.const import COMPOSEFILE_V2_0 as V2_0 -from compose.const import COMPOSEFILE_V2_3 as V2_3 -from compose.const import COMPOSEFILE_V3_4 as V3_4 +from compose.const import COMPOSE_SPEC as VERSION @pytest.fixture @@ -66,7 +60,7 @@ def test_interpolate_environment_variables_in_services(mock_env): } } } - value = interpolate_environment_variables(V2_0, services, 'service', mock_env) + value = interpolate_environment_variables(VERSION, services, 'service', mock_env) assert value == expected @@ -91,7 +85,7 @@ def test_interpolate_environment_variables_in_volumes(mock_env): }, 'other': {}, } - value = interpolate_environment_variables(V2_0, volumes, 'volume', mock_env) + value = interpolate_environment_variables(VERSION, volumes, 'volume', mock_env) assert value == expected @@ -116,7 +110,7 @@ def test_interpolate_environment_variables_in_secrets(mock_env): }, 'other': {}, } - value = interpolate_environment_variables(V3_4, secrets, 'secret', mock_env) + value = interpolate_environment_variables(VERSION, secrets, 'secret', mock_env) assert value == expected @@ -187,7 +181,7 @@ def test_interpolate_environment_services_convert_types_v2(mock_env): } } - value = interpolate_environment_variables(V2_3, entry, 'service', mock_env) + value = interpolate_environment_variables(VERSION, entry, 'service', mock_env) assert value == expected @@ -260,7 +254,7 @@ def test_interpolate_environment_services_convert_types_v3(mock_env): } } - value = interpolate_environment_variables(V3_4, entry, 'service', mock_env) + value = interpolate_environment_variables(VERSION, entry, 'service', mock_env) assert value == expected @@ -268,21 +262,21 @@ def test_interpolate_environment_services_convert_types_invalid(mock_env): entry = {'service1': {'privileged': '${POSINT}'}} with pytest.raises(ConfigurationError) as exc: - interpolate_environment_variables(V2_3, entry, 'service', mock_env) + interpolate_environment_variables(VERSION, entry, 'service', mock_env) assert 'Error while attempting to convert service.service1.privileged to '\ 'appropriate type: "50" is not a valid boolean value' in exc.exconly() entry = {'service1': {'cpus': '${TRUE}'}} with pytest.raises(ConfigurationError) as exc: - interpolate_environment_variables(V2_3, entry, 'service', mock_env) + interpolate_environment_variables(VERSION, entry, 'service', mock_env) assert 'Error while attempting to convert service.service1.cpus to '\ 'appropriate type: "True" is not a valid float' in exc.exconly() entry = {'service1': {'ulimits': {'nproc': '${FLOAT}'}}} with pytest.raises(ConfigurationError) as exc: - interpolate_environment_variables(V2_3, entry, 'service', mock_env) + interpolate_environment_variables(VERSION, entry, 'service', mock_env) assert 'Error while attempting to convert service.service1.ulimits.nproc to '\ 'appropriate type: "0.145" is not a valid integer' in exc.exconly() @@ -305,7 +299,7 @@ def test_interpolate_environment_network_convert_types(mock_env): } } - value = interpolate_environment_variables(V3_4, entry, 'network', mock_env) + value = interpolate_environment_variables(VERSION, entry, 'network', mock_env) assert value == expected @@ -322,13 +316,13 @@ def test_interpolate_environment_external_resource_convert_types(mock_env): } } - value = interpolate_environment_variables(V3_4, entry, 'network', mock_env) + value = interpolate_environment_variables(VERSION, entry, 'network', mock_env) assert value == expected - value = interpolate_environment_variables(V3_4, entry, 'volume', mock_env) + value = interpolate_environment_variables(VERSION, entry, 'volume', mock_env) assert value == expected - value = interpolate_environment_variables(V3_4, entry, 'secret', mock_env) + value = interpolate_environment_variables(VERSION, entry, 'secret', mock_env) assert value == expected - value = interpolate_environment_variables(V3_4, entry, 'config', mock_env) + value = interpolate_environment_variables(VERSION, entry, 'config', mock_env) assert value == expected @@ -359,7 +353,7 @@ def test_interpolate_service_name_uses_dot(mock_env): } } - value = interpolate_environment_variables(V3_4, entry, 'service', mock_env) + value = interpolate_environment_variables(VERSION, entry, 'service', mock_env) assert value == expected @@ -422,7 +416,7 @@ def test_interpolate_mandatory_no_err_msg(defaults_interpolator): with pytest.raises(UnsetRequiredSubstitution) as e: defaults_interpolator("not ok ${BAZ?}") - assert e.value.err == '' + assert e.value.err == 'BAZ' def test_interpolate_mixed_separators(defaults_interpolator): @@ -444,7 +438,7 @@ def test_unbraced_separators(defaults_interpolator): def test_interpolate_unicode_values(): variable_mapping = { - 'FOO': '十六夜 咲夜'.encode('utf-8'), + 'FOO': '十六夜 咲夜'.encode(), 'BAR': '十六夜 咲夜' } interpol = Interpolator(TemplateWithDefaults, variable_mapping).interpolate diff --git a/tests/unit/config/sort_services_test.py b/tests/unit/config/sort_services_test.py index c39ac022562..508c4bba190 100644 --- a/tests/unit/config/sort_services_test.py +++ b/tests/unit/config/sort_services_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import pytest from compose.config.errors import DependencyError @@ -8,7 +5,7 @@ from compose.config.types import VolumeFromSpec -class TestSortService(object): +class TestSortService: def test_sort_service_dicts_1(self): services = [ { diff --git a/tests/unit/config/types_test.py b/tests/unit/config/types_test.py index e7cc67b0422..e5fcde1a621 100644 --- a/tests/unit/config/types_test.py +++ b/tests/unit/config/types_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import pytest from compose.config.errors import ConfigurationError @@ -8,8 +5,8 @@ from compose.config.types import ServicePort from compose.config.types import VolumeFromSpec from compose.config.types import VolumeSpec +from compose.const import COMPOSE_SPEC as VERSION from compose.const import COMPOSEFILE_V1 as V1 -from compose.const import COMPOSEFILE_V2_0 as V2_0 def test_parse_extra_hosts_list(): @@ -42,7 +39,7 @@ def test_parse_extra_hosts_dict(): } -class TestServicePort(object): +class TestServicePort: def test_parse_dict(self): data = { 'target': 8000, @@ -132,7 +129,7 @@ def test_parse_invalid_publish_range(self): ServicePort.parse(port_def) -class TestVolumeSpec(object): +class TestVolumeSpec: def test_parse_volume_spec_only_one_path(self): spec = VolumeSpec.parse('/the/volume') @@ -219,7 +216,7 @@ def test_parse_volume_windows_mixed_notations_native(self): ) -class TestVolumesFromSpec(object): +class TestVolumesFromSpec: services = ['servicea', 'serviceb'] @@ -236,26 +233,26 @@ def test_parse_v1_invalid(self): VolumeFromSpec.parse('unknown:format:ro', self.services, V1) def test_parse_v2_from_service(self): - volume_from = VolumeFromSpec.parse('servicea', self.services, V2_0) + volume_from = VolumeFromSpec.parse('servicea', self.services, VERSION) assert volume_from == VolumeFromSpec('servicea', 'rw', 'service') def test_parse_v2_from_service_with_mode(self): - volume_from = VolumeFromSpec.parse('servicea:ro', self.services, V2_0) + volume_from = VolumeFromSpec.parse('servicea:ro', self.services, VERSION) assert volume_from == VolumeFromSpec('servicea', 'ro', 'service') def test_parse_v2_from_container(self): - volume_from = VolumeFromSpec.parse('container:foo', self.services, V2_0) + volume_from = VolumeFromSpec.parse('container:foo', self.services, VERSION) assert volume_from == VolumeFromSpec('foo', 'rw', 'container') def test_parse_v2_from_container_with_mode(self): - volume_from = VolumeFromSpec.parse('container:foo:ro', self.services, V2_0) + volume_from = VolumeFromSpec.parse('container:foo:ro', self.services, VERSION) assert volume_from == VolumeFromSpec('foo', 'ro', 'container') def test_parse_v2_invalid_type(self): with pytest.raises(ConfigurationError) as exc: - VolumeFromSpec.parse('bogus:foo:ro', self.services, V2_0) + VolumeFromSpec.parse('bogus:foo:ro', self.services, VERSION) assert "Unknown volumes_from type 'bogus'" in exc.exconly() def test_parse_v2_invalid(self): with pytest.raises(ConfigurationError): - VolumeFromSpec.parse('unknown:format:ro', self.services, V2_0) + VolumeFromSpec.parse('unknown:format:ro', self.services, VERSION) diff --git a/tests/unit/container_test.py b/tests/unit/container_test.py index 626b466d4b3..fe1e1078794 100644 --- a/tests/unit/container_test.py +++ b/tests/unit/container_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import docker from .. import mock @@ -224,34 +221,6 @@ def test_short_id(self): container = Container(None, self.container_dict, has_been_inspected=True) assert container.short_id == self.container_id[:12] - def test_has_api_logs(self): - container_dict = { - 'HostConfig': { - 'LogConfig': { - 'Type': 'json-file' - } - } - } - - container = Container(None, container_dict, has_been_inspected=True) - assert container.has_api_logs is True - - container_dict['HostConfig']['LogConfig']['Type'] = 'none' - container = Container(None, container_dict, has_been_inspected=True) - assert container.has_api_logs is False - - container_dict['HostConfig']['LogConfig']['Type'] = 'syslog' - container = Container(None, container_dict, has_been_inspected=True) - assert container.has_api_logs is False - - container_dict['HostConfig']['LogConfig']['Type'] = 'journald' - container = Container(None, container_dict, has_been_inspected=True) - assert container.has_api_logs is True - - container_dict['HostConfig']['LogConfig']['Type'] = 'foobar' - container = Container(None, container_dict, has_been_inspected=True) - assert container.has_api_logs is False - class GetContainerNameTestCase(unittest.TestCase): diff --git a/tests/unit/metrics/__init__.py b/tests/unit/metrics/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/unit/metrics/metrics_test.py b/tests/unit/metrics/metrics_test.py new file mode 100644 index 00000000000..e9f23720a35 --- /dev/null +++ b/tests/unit/metrics/metrics_test.py @@ -0,0 +1,36 @@ +import unittest + +from compose.metrics.client import MetricsCommand +from compose.metrics.client import Status + + +class MetricsTest(unittest.TestCase): + @classmethod + def test_metrics(cls): + assert MetricsCommand('up', 'moby').to_map() == { + 'command': 'compose up', + 'context': 'moby', + 'status': 'success', + 'source': 'docker-compose', + } + + assert MetricsCommand('down', 'local').to_map() == { + 'command': 'compose down', + 'context': 'local', + 'status': 'success', + 'source': 'docker-compose', + } + + assert MetricsCommand('help', 'aci', Status.FAILURE).to_map() == { + 'command': 'compose help', + 'context': 'aci', + 'status': 'failure', + 'source': 'docker-compose', + } + + assert MetricsCommand('run', 'ecs').to_map() == { + 'command': 'compose run', + 'context': 'ecs', + 'status': 'success', + 'source': 'docker-compose', + } diff --git a/tests/unit/network_test.py b/tests/unit/network_test.py index b829de196be..ab7ad59cfbd 100644 --- a/tests/unit/network_test.py +++ b/tests/unit/network_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import pytest from .. import mock diff --git a/tests/unit/parallel_test.py b/tests/unit/parallel_test.py index 0735bfccb48..91914333500 100644 --- a/tests/unit/parallel_test.py +++ b/tests/unit/parallel_test.py @@ -1,12 +1,9 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import unittest from threading import Lock -import six from docker.errors import APIError +from compose.cli.colors import AnsiMode from compose.parallel import GlobalLimit from compose.parallel import parallel_execute from compose.parallel import parallel_execute_iter @@ -39,7 +36,7 @@ def test_parallel_execute(self): results, errors = parallel_execute( objects=[1, 2, 3, 4, 5], func=lambda x: x * 2, - get_name=six.text_type, + get_name=str, msg="Doubling", ) @@ -61,7 +58,7 @@ def f(obj): results, errors = parallel_execute( objects=list(range(tasks)), func=f, - get_name=six.text_type, + get_name=str, msg="Testing", limit=limit, ) @@ -85,7 +82,7 @@ def f(obj): results, errors = parallel_execute( objects=list(range(tasks)), func=f, - get_name=six.text_type, + get_name=str, msg="Testing", ) @@ -147,7 +144,7 @@ def test_parallel_execute_alignment(capsys): results, errors = parallel_execute( objects=["short", "a very long name"], func=lambda x: x, - get_name=six.text_type, + get_name=str, msg="Aligning", ) @@ -160,11 +157,11 @@ def test_parallel_execute_alignment(capsys): def test_parallel_execute_ansi(capsys): ParallelStreamWriter.instance = None - ParallelStreamWriter.set_noansi(value=False) + ParallelStreamWriter.set_default_ansi_mode(AnsiMode.ALWAYS) results, errors = parallel_execute( objects=["something", "something more"], func=lambda x: x, - get_name=six.text_type, + get_name=str, msg="Control characters", ) @@ -176,11 +173,11 @@ def test_parallel_execute_ansi(capsys): def test_parallel_execute_noansi(capsys): ParallelStreamWriter.instance = None - ParallelStreamWriter.set_noansi() + ParallelStreamWriter.set_default_ansi_mode(AnsiMode.NEVER) results, errors = parallel_execute( objects=["something", "something more"], func=lambda x: x, - get_name=six.text_type, + get_name=str, msg="Control characters", ) diff --git a/tests/unit/progress_stream_test.py b/tests/unit/progress_stream_test.py index 6fdb7d92788..288c9b6e448 100644 --- a/tests/unit/progress_stream_test.py +++ b/tests/unit/progress_stream_test.py @@ -1,14 +1,8 @@ -# ~*~ encoding: utf-8 ~*~ -from __future__ import absolute_import -from __future__ import unicode_literals - -import io import os import random import shutil import tempfile - -from six import StringIO +from io import StringIO from compose import progress_stream from tests import unittest @@ -79,7 +73,7 @@ def test_mismatched_encoding_stream_write(self): def mktempfile(encoding): fname = os.path.join(tmpdir, hex(random.getrandbits(128))[2:-1]) - return io.open(fname, mode='w+', encoding=encoding) + return open(fname, mode='w+', encoding=encoding) text = '就吃饭' with mktempfile(encoding='utf-8') as tf: diff --git a/tests/unit/project_test.py b/tests/unit/project_test.py index 6391fac863d..a3ffdb67dae 100644 --- a/tests/unit/project_test.py +++ b/tests/unit/project_test.py @@ -1,7 +1,3 @@ -# encoding: utf-8 -from __future__ import absolute_import -from __future__ import unicode_literals - import datetime import os import tempfile @@ -16,10 +12,8 @@ from compose.config import ConfigurationError from compose.config.config import Config from compose.config.types import VolumeFromSpec +from compose.const import COMPOSE_SPEC as VERSION 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 @@ -32,6 +26,18 @@ from compose.service import Service +def build_config(**kwargs): + return Config( + config_version=kwargs.get('config_version', VERSION), + version=kwargs.get('version', VERSION), + services=kwargs.get('services'), + volumes=kwargs.get('volumes'), + networks=kwargs.get('networks'), + secrets=kwargs.get('secrets'), + configs=kwargs.get('configs'), + ) + + class ProjectTest(unittest.TestCase): def setUp(self): self.mock_client = mock.create_autospec(docker.APIClient) @@ -39,7 +45,7 @@ def setUp(self): self.mock_client.api_version = docker.constants.DEFAULT_DOCKER_API_VERSION def test_from_config_v1(self): - config = Config( + config = build_config( version=V1, services=[ { @@ -70,8 +76,7 @@ def test_from_config_v1(self): @mock.patch('compose.network.Network.true_name', lambda n: n.full_name) def test_from_config_v2(self): - config = Config( - version=V2_0, + config = build_config( services=[ { 'name': 'web', @@ -177,8 +182,7 @@ def test_use_volumes_from_container(self): project = Project.from_config( name='test', client=self.mock_client, - config_data=Config( - version=V2_0, + config_data=build_config( services=[{ 'name': 'test', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -205,8 +209,7 @@ def test_use_volumes_from_service_no_container(self): project = Project.from_config( name='test', client=self.mock_client, - config_data=Config( - version=V2_0, + config_data=build_config( services=[ { 'name': 'vol', @@ -233,8 +236,7 @@ def test_use_volumes_from_service_container(self): project = Project.from_config( name='test', client=None, - config_data=Config( - version=V2_0, + config_data=build_config( services=[ { 'name': 'vol', @@ -543,7 +545,7 @@ def test_net_unset(self): project = Project.from_config( name='test', client=self.mock_client, - config_data=Config( + config_data=build_config( version=V1, services=[ { @@ -568,8 +570,7 @@ def test_use_net_from_container(self): project = Project.from_config( name='test', client=self.mock_client, - config_data=Config( - version=V2_0, + config_data=build_config( services=[ { 'name': 'test', @@ -599,8 +600,7 @@ def test_use_net_from_service(self): project = Project.from_config( name='test', client=self.mock_client, - config_data=Config( - version=V2_0, + config_data=build_config( services=[ { 'name': 'aaa', @@ -626,8 +626,7 @@ def test_uses_default_network_true(self): project = Project.from_config( name='test', client=self.mock_client, - config_data=Config( - version=V2_0, + config_data=build_config( services=[ { 'name': 'foo', @@ -647,8 +646,7 @@ def test_uses_default_network_false(self): project = Project.from_config( name='test', client=self.mock_client, - config_data=Config( - version=V2_0, + config_data=build_config( services=[ { 'name': 'foo', @@ -682,8 +680,7 @@ def test_container_without_name(self): project = Project.from_config( name='test', client=self.mock_client, - config_data=Config( - version=V2_0, + config_data=build_config( services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -700,8 +697,7 @@ def test_down_with_no_resources(self): project = Project.from_config( name='test', client=self.mock_client, - config_data=Config( - version=V2_0, + config_data=build_config( services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, @@ -743,7 +739,7 @@ def test_no_warning_with_no_swarm_info(self): assert fake_log.warn.call_count == 0 def test_no_such_service_unicode(self): - assert NoSuchService('十六夜 咲夜'.encode('utf-8')).msg == 'No such service: 十六夜 咲夜' + assert NoSuchService('十六夜 咲夜'.encode()).msg == 'No such service: 十六夜 咲夜' assert NoSuchService('十六夜 咲夜').msg == 'No such service: 十六夜 咲夜' def test_project_platform_value(self): @@ -751,8 +747,8 @@ def test_project_platform_value(self): 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, } - config_data = Config( - version=V2_4, services=[service_config], networks={}, volumes={}, secrets=None, configs=None + config_data = build_config( + services=[service_config], networks={}, volumes={}, secrets=None, configs=None ) project = Project.from_config(name='test', client=self.mock_client, config_data=config_data) @@ -773,8 +769,7 @@ def test_project_platform_value(self): 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, + config_data = build_config( services=[ {'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG}, {'name': 'db', 'image': BUSYBOX_IMAGE_WITH_TAG, 'stop_grace_period': '1s'}, @@ -805,8 +800,7 @@ def test_error_parallel_pull(self, mock_write): project = Project.from_config( name='test', client=self.mock_client, - config_data=Config( - version=V2_0, + config_data=build_config( services=[{ 'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG, diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index 592e22f759f..36108ee9f28 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import docker import pytest from docker.constants import DEFAULT_DOCKER_API_VERSION @@ -66,9 +63,9 @@ def test_containers_with_containers(self): assert [c.id for c in service.containers()] == list(range(3)) expected_labels = [ - '{0}=myproject'.format(LABEL_PROJECT), - '{0}=db'.format(LABEL_SERVICE), - '{0}=False'.format(LABEL_ONE_OFF), + '{}=myproject'.format(LABEL_PROJECT), + '{}=db'.format(LABEL_SERVICE), + '{}=False'.format(LABEL_ONE_OFF), ] self.mock_client.containers.assert_called_once_with( @@ -333,7 +330,7 @@ def test_get_container_create_options_does_not_mutate_options(self): assert service.options['environment'] == environment assert opts['labels'][LABEL_CONFIG_HASH] == \ - '689149e6041a85f6fb4945a2146a497ed43c8a5cbd8991753d875b165f1b4de4' + '6da0f3ec0d5adf901de304bdc7e0ee44ec5dd7adb08aebc20fe0dd791d4ee5a8' assert opts['environment'] == ['also=real'] def test_get_container_create_options_sets_affinity_with_binds(self): @@ -580,6 +577,24 @@ def test_ensure_image_exists_force_build(self): assert self.mock_client.build.call_count == 1 self.mock_client.build.call_args[1]['tag'] == 'default_foo' + def test_ensure_image_exists_not_always_pull(self): + service = Service('foo', client=self.mock_client, image='someimage:sometag') + + self.mock_client.inspect_image.side_effect = [NoSuchImageError, {'Id': 'abc123'}] + service.ensure_image_exists() + service.ensure_image_exists() + + assert self.mock_client.pull.call_count == 1 + + def test_ensure_image_exists_always_pull(self): + service = Service('foo', client=self.mock_client, image='someimage:sometag') + + self.mock_client.inspect_image.side_effect = [NoSuchImageError, {'Id': 'abc123'}] + service.ensure_image_exists() + service.ensure_image_exists(always_pull=True) + + assert self.mock_client.pull.call_count == 2 + def test_build_does_not_pull(self): self.mock_client.build.return_value = [ b'{"stream": "Successfully built 12345"}', @@ -703,6 +718,7 @@ def test_config_dict(self): config_dict = service.config_dict() expected = { 'image_id': 'abcd', + 'ipc_mode': None, 'options': {'image': 'example.com/foo'}, 'links': [('one', 'one')], 'net': 'other', @@ -726,6 +742,7 @@ def test_config_dict_with_network_mode_from_container(self): config_dict = service.config_dict() expected = { 'image_id': 'abcd', + 'ipc_mode': None, 'options': {'image': 'example.com/foo'}, 'links': [], 'networks': {}, diff --git a/tests/unit/split_buffer_test.py b/tests/unit/split_buffer_test.py index dedd4ee36bb..d6b5b884c87 100644 --- a/tests/unit/split_buffer_test.py +++ b/tests/unit/split_buffer_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - from .. import unittest from compose.utils import split_buffer @@ -39,7 +36,7 @@ def reader(): self.assert_produces(reader, ['abc\n', 'd']) def test_preserves_unicode_sequences_within_lines(self): - string = u"a\u2022c\n" + string = "a\u2022c\n" def reader(): yield string.encode('utf-8') diff --git a/tests/unit/timeparse_test.py b/tests/unit/timeparse_test.py index 9915932c300..e56595f1893 100644 --- a/tests/unit/timeparse_test.py +++ b/tests/unit/timeparse_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - from compose import timeparse diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py index 21b88d962c1..3052e4d8644 100644 --- a/tests/unit/utils_test.py +++ b/tests/unit/utils_test.py @@ -1,11 +1,7 @@ -# encoding: utf-8 -from __future__ import absolute_import -from __future__ import unicode_literals - from compose import utils -class TestJsonSplitter(object): +class TestJsonSplitter: def test_json_splitter_no_object(self): data = '{"foo": "bar' @@ -20,7 +16,7 @@ def test_json_splitter_leading_whitespace(self): assert utils.json_splitter(data) == ({'foo': 'bar'}, '{"next": "obj"}') -class TestStreamAsText(object): +class TestStreamAsText: def test_stream_with_non_utf_unicode_character(self): stream = [b'\xed\xf3\xf3'] @@ -28,12 +24,12 @@ def test_stream_with_non_utf_unicode_character(self): assert output == '���' def test_stream_with_utf_character(self): - stream = ['ěĝ'.encode('utf-8')] + stream = ['ěĝ'.encode()] output, = utils.stream_as_text(stream) assert output == 'ěĝ' -class TestJsonStream(object): +class TestJsonStream: def test_with_falsy_entries(self): stream = [ @@ -62,7 +58,7 @@ def test_with_leading_whitespace(self): ] -class TestParseBytes(object): +class TestParseBytes: def test_parse_bytes(self): assert utils.parse_bytes('123kb') == 123 * 1024 assert utils.parse_bytes(123) == 123 @@ -70,7 +66,7 @@ def test_parse_bytes(self): assert utils.parse_bytes('123') == 123 -class TestMoreItertools(object): +class TestMoreItertools: def test_unique_everseen(self): unique = utils.unique_everseen assert list(unique([2, 1, 2, 1])) == [2, 1] diff --git a/tests/unit/volume_test.py b/tests/unit/volume_test.py index 457d8558174..0dfbfcd40b7 100644 --- a/tests/unit/volume_test.py +++ b/tests/unit/volume_test.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - import docker import pytest @@ -13,7 +10,7 @@ def mock_client(): return mock.create_autospec(docker.APIClient) -class TestVolume(object): +class TestVolume: def test_remove_local_volume(self, mock_client): vol = volume.Volume(mock_client, 'foo', 'project') diff --git a/tox.ini b/tox.ini index 57e57bc635e..12530d19153 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py37,pre-commit +envlist = py37,py39,pre-commit [testenv] usedevelop=True @@ -15,6 +15,7 @@ passenv = setenv = HOME=/tmp deps = + -rrequirements-indirect.txt -rrequirements.txt -rrequirements-dev.txt commands = @@ -32,7 +33,7 @@ deps = pre-commit commands = pre-commit install - pre-commit run --all-files + pre-commit run --all-files --show-diff-on-failure # Coverage configuration [run] @@ -49,7 +50,7 @@ directory = coverage-html [flake8] max-line-length = 105 # Set this high for now -max-complexity = 11 +max-complexity = 12 exclude = compose/packages [pytest]