Compare commits
6 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3f23e2d17 | ||
|
|
a467a8a094 | ||
|
|
78227c3c06 | ||
|
|
e4e802d1f8 | ||
|
|
b24a60ba9f | ||
|
|
461b600068 |
262 changed files with 5600 additions and 24316 deletions
|
|
@ -1,9 +1,2 @@
|
||||||
*.egg-info
|
|
||||||
.coverage
|
|
||||||
.git
|
.git
|
||||||
.tox
|
|
||||||
build
|
|
||||||
coverage-html
|
|
||||||
docs/_site
|
|
||||||
venv
|
venv
|
||||||
.tox
|
|
||||||
|
|
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -1,11 +1,8 @@
|
||||||
*.egg-info
|
*.egg-info
|
||||||
*.pyc
|
*.pyc
|
||||||
.coverage*
|
.tox
|
||||||
/.tox
|
|
||||||
/build
|
/build
|
||||||
/coverage-html
|
|
||||||
/dist
|
/dist
|
||||||
/docs/_site
|
/docs/_site
|
||||||
/venv
|
/venv
|
||||||
README.rst
|
docker-compose.spec
|
||||||
compose/GITSHA
|
|
||||||
|
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
- repo: git://github.com/pre-commit/pre-commit-hooks
|
|
||||||
sha: 'v0.4.2'
|
|
||||||
hooks:
|
|
||||||
- id: check-added-large-files
|
|
||||||
- id: check-docstring-first
|
|
||||||
- id: check-merge-conflict
|
|
||||||
- id: check-yaml
|
|
||||||
- id: check-json
|
|
||||||
- id: debug-statements
|
|
||||||
- id: end-of-file-fixer
|
|
||||||
- id: flake8
|
|
||||||
- id: name-tests-test
|
|
||||||
exclude: 'tests/(integration/testcases\.py|helpers\.py)'
|
|
||||||
- id: requirements-txt-fixer
|
|
||||||
- id: trailing-whitespace
|
|
||||||
- repo: git://github.com/asottile/reorder_python_imports
|
|
||||||
sha: v0.1.0
|
|
||||||
hooks:
|
|
||||||
- id: reorder-python-imports
|
|
||||||
language_version: 'python2.7'
|
|
||||||
args:
|
|
||||||
- --add-import
|
|
||||||
- from __future__ import absolute_import
|
|
||||||
- --add-import
|
|
||||||
- from __future__ import unicode_literals
|
|
||||||
29
.travis.yml
29
.travis.yml
|
|
@ -1,29 +0,0 @@
|
||||||
sudo: required
|
|
||||||
|
|
||||||
language: python
|
|
||||||
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- os: linux
|
|
||||||
services:
|
|
||||||
- docker
|
|
||||||
- os: osx
|
|
||||||
language: generic
|
|
||||||
|
|
||||||
install: ./script/travis/install
|
|
||||||
|
|
||||||
script:
|
|
||||||
- ./script/travis/ci
|
|
||||||
- ./script/travis/build-binary
|
|
||||||
|
|
||||||
before_deploy:
|
|
||||||
- "./script/travis/render-bintray-config.py < ./script/travis/bintray.json.tmpl > ./bintray.json"
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
provider: bintray
|
|
||||||
user: docker-compose-roleuser
|
|
||||||
key: '$BINTRAY_API_KEY'
|
|
||||||
file: ./bintray.json
|
|
||||||
skip_cleanup: true
|
|
||||||
on:
|
|
||||||
all_branches: true
|
|
||||||
1174
CHANGELOG.md
1174
CHANGELOG.md
File diff suppressed because it is too large
Load diff
|
|
@ -1 +0,0 @@
|
||||||
CHANGELOG.md
|
|
||||||
331
CHANGES.md
Normal file
331
CHANGES.md
Normal file
|
|
@ -0,0 +1,331 @@
|
||||||
|
Change log
|
||||||
|
==========
|
||||||
|
|
||||||
|
1.2.0rc4 (2015-04-09)
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
This is a release candidate for Compose 1.2.0.
|
||||||
|
|
||||||
|
On top of the changes listed below for RC1-RC3, the following bugs have been fixed:
|
||||||
|
|
||||||
|
- Environment variables and the user's home directory (~) were not expanding properly in `volumes` host paths.
|
||||||
|
|
||||||
|
1.2.0rc3 (2015-03-23)
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
This is a release candidate for Compose 1.2.0.
|
||||||
|
|
||||||
|
On top of the changes listed below for RC1 and RC2, the following bugs have been fixed:
|
||||||
|
|
||||||
|
- When copying a service's configuration with `extends`, `image` and `build` could come into conflict, resulting in an error, as it makes no sense to have both defined. Each now overwrites the other: if a service with `image` defined is extended and `build` is added, the `image` entry will be removed.
|
||||||
|
|
||||||
|
- When copying a service's configuration with `extends`, if both services defined a multi-value option such as `ports` or `dns`, the original value would be completely discarded. They are now concatenated instead.
|
||||||
|
|
||||||
|
- When a relative path is supplied to `build`, it is treated as relative to the *directory of the configuration file*, not the directory that `docker-compose` is being run in. In the majority of cases, those are the same, but if you use the `-f|--file` argument to specify a configuration file in another directory, **this is a breaking change**.
|
||||||
|
|
||||||
|
1.2.0rc2 (2015-03-24)
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
This is a release candidate for Compose 1.2.0.
|
||||||
|
|
||||||
|
On top of the changes listed below for RC1, a bug has been fixed where containers were being created with blank entries for "Dns" and "DnsSearch", causing DNS lookups from within a container to fail.
|
||||||
|
|
||||||
|
1.2.0rc1 (2015-03-23)
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
This is a release candidate for Compose 1.2.0.
|
||||||
|
|
||||||
|
- `docker-compose.yml` now supports an `extends` option, which enables a service to inherit configuration from another service in another configuration file. This is really good for sharing common configuration between apps, or for configuring the same app for different environments. Here's the [documentation](https://github.com/docker/compose/blob/master/docs/yml.md#extends).
|
||||||
|
|
||||||
|
- When using Compose with a Swarm cluster, containers that depend on one another will be co-scheduled on the same node. This means that most Compose apps will now work out of the box, as long as they don't use `build`.
|
||||||
|
|
||||||
|
- Repeated invocations of `docker-compose up` when using Compose with a Swarm cluster now work reliably.
|
||||||
|
|
||||||
|
- Filenames in `env_file` and volume host paths in `volumes` are now treated as relative to the *directory of the configuration file*, not the directory that `docker-compose` is being run in. In the majority of cases, those are the same, but if you use the `-f|--file` argument to specify a configuration file in another directory, **this is a breaking change**.
|
||||||
|
|
||||||
|
- A service can now share another service's network namespace with `net: container:<service>`.
|
||||||
|
|
||||||
|
- `volumes_from` and `net: container:<service>` entries are taken into account when resolving dependencies, so `docker-compose up <service>` will correctly start all dependencies of `<service>`.
|
||||||
|
|
||||||
|
- Problems with authentication when using images from third-party registries have been fixed.
|
||||||
|
|
||||||
|
- `docker-compose run` now accepts a `--user` argument to specify a user to run the command as, just like `docker run`.
|
||||||
|
|
||||||
|
- The `up`, `stop` and `restart` commands now accept a `--timeout` (or `-t`) argument to specify how long to wait when attempting to gracefully stop containers, just like `docker stop`.
|
||||||
|
|
||||||
|
- `docker-compose rm` now accepts `-f` as a shorthand for `--force`, just like `docker rm`.
|
||||||
|
|
||||||
|
Thanks, @abesto, @albers, @alunduil, @dnephin, @funkyfuture, @gilclark, @IanVS, @KingsleyKelly, @knutwalker, @thaJeztah and @vmalloc!
|
||||||
|
|
||||||
|
1.1.0 (2015-02-25)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Fig has been renamed to Docker Compose, or just Compose for short. This has several implications for you:
|
||||||
|
|
||||||
|
- The command you type is now `docker-compose`, not `fig`.
|
||||||
|
- You should rename your fig.yml to docker-compose.yml.
|
||||||
|
- If you’re installing via PyPi, the package is now `docker-compose`, so install it with `pip install docker-compose`.
|
||||||
|
|
||||||
|
Besides that, there’s a lot of new stuff in this release:
|
||||||
|
|
||||||
|
- We’ve made a few small changes to ensure that Compose will work with Swarm, Docker’s new clustering tool (https://github.com/docker/swarm). Eventually you'll be able to point Compose at a Swarm cluster instead of a standalone Docker host and it’ll run your containers on the cluster with no extra work from you. As Swarm is still developing, integration is rough and lots of Compose features don't work yet.
|
||||||
|
|
||||||
|
- `docker-compose run` now has a `--service-ports` flag for exposing ports on the given service. This is useful for e.g. running your webapp with an interactive debugger.
|
||||||
|
|
||||||
|
- You can now link to containers outside your app with the `external_links` option in docker-compose.yml.
|
||||||
|
|
||||||
|
- You can now prevent `docker-compose up` from automatically building images with the `--no-build` option. This will make fewer API calls and run faster.
|
||||||
|
|
||||||
|
- If you don’t specify a tag when using the `image` key, Compose will default to the `latest` tag, rather than pulling all tags.
|
||||||
|
|
||||||
|
- `docker-compose kill` now supports the `-s` flag, allowing you to specify the exact signal you want to send to a service’s containers.
|
||||||
|
|
||||||
|
- docker-compose.yml now has an `env_file` key, analogous to `docker run --env-file`, letting you specify multiple environment variables in a separate file. This is great if you have a lot of them, or if you want to keep sensitive information out of version control.
|
||||||
|
|
||||||
|
- docker-compose.yml now supports the `dns_search`, `cap_add`, `cap_drop`, `cpu_shares` and `restart` options, analogous to `docker run`’s `--dns-search`, `--cap-add`, `--cap-drop`, `--cpu-shares` and `--restart` options.
|
||||||
|
|
||||||
|
- Compose now ships with Bash tab completion - see the installation and usage docs at https://github.com/docker/compose/blob/1.1.0/docs/completion.md
|
||||||
|
|
||||||
|
- A number of bugs have been fixed - see the milestone for details: https://github.com/docker/compose/issues?q=milestone%3A1.1.0+
|
||||||
|
|
||||||
|
Thanks @dnephin, @squebe, @jbalonso, @raulcd, @benlangfield, @albers, @ggtools, @bersace, @dtenenba, @petercv, @drewkett, @TFenby, @paulRbr, @Aigeruth and @salehe!
|
||||||
|
|
||||||
|
1.0.1 (2014-11-04)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Added an `--allow-insecure-ssl` option to allow `fig up`, `fig run` and `fig pull` to pull from insecure registries.
|
||||||
|
- Fixed `fig run` not showing output in Jenkins.
|
||||||
|
- Fixed a bug where Fig couldn't build Dockerfiles with ADD statements pointing at URLs.
|
||||||
|
|
||||||
|
1.0.0 (2014-10-16)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
The highlights:
|
||||||
|
|
||||||
|
- [Fig has joined Docker.](https://www.orchardup.com/blog/orchard-is-joining-docker) Fig will continue to be maintained, but we'll also be incorporating the best bits of Fig into Docker itself.
|
||||||
|
|
||||||
|
This means the GitHub repository has moved to [https://github.com/docker/fig](https://github.com/docker/fig) and our IRC channel is now #docker-fig on Freenode.
|
||||||
|
|
||||||
|
- Fig can be used with the [official Docker OS X installer](https://docs.docker.com/installation/mac/). Boot2Docker will mount the home directory from your host machine so volumes work as expected.
|
||||||
|
|
||||||
|
- Fig supports Docker 1.3.
|
||||||
|
|
||||||
|
- It is now possible to connect to the Docker daemon using TLS by using the `DOCKER_CERT_PATH` and `DOCKER_TLS_VERIFY` environment variables.
|
||||||
|
|
||||||
|
- There is a new `fig port` command which outputs the host port binding of a service, in a similar way to `docker port`.
|
||||||
|
|
||||||
|
- There is a new `fig pull` command which pulls the latest images for a service.
|
||||||
|
|
||||||
|
- There is a new `fig restart` command which restarts a service's containers.
|
||||||
|
|
||||||
|
- Fig creates multiple containers in service by appending a number to the service name (e.g. `db_1`, `db_2`, etc). As a convenience, Fig will now give the first container an alias of the service name (e.g. `db`).
|
||||||
|
|
||||||
|
This link alias is also a valid hostname and added to `/etc/hosts` so you can connect to linked services using their hostname. For example, instead of resolving the environment variables `DB_PORT_5432_TCP_ADDR` and `DB_PORT_5432_TCP_PORT`, you could just use the hostname `db` and port `5432` directly.
|
||||||
|
|
||||||
|
- Volume definitions now support `ro` mode, expanding `~` and expanding environment variables.
|
||||||
|
|
||||||
|
- `.dockerignore` is supported when building.
|
||||||
|
|
||||||
|
- The project name can be set with the `FIG_PROJECT_NAME` environment variable.
|
||||||
|
|
||||||
|
- The `--env` and `--entrypoint` options have been added to `fig run`.
|
||||||
|
|
||||||
|
- The Fig binary for Linux is now linked against an older version of glibc so it works on CentOS 6 and Debian Wheezy.
|
||||||
|
|
||||||
|
Other things:
|
||||||
|
|
||||||
|
- `fig ps` now works on Jenkins and makes fewer API calls to the Docker daemon.
|
||||||
|
- `--verbose` displays more useful debugging output.
|
||||||
|
- When starting a service where `volumes_from` points to a service without any containers running, that service will now be started.
|
||||||
|
- Lots of docs improvements. Notably, environment variables are documented and official repositories are used throughout.
|
||||||
|
|
||||||
|
Thanks @dnephin, @d11wtq, @marksteve, @rubbish, @jbalonso, @timfreund, @alunduil, @mieciu, @shuron, @moss, @suzaku and @chmouel! Whew.
|
||||||
|
|
||||||
|
0.5.2 (2014-07-28)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Added a `--no-cache` option to `fig build`, which bypasses the cache just like `docker build --no-cache`.
|
||||||
|
- Fixed the `dns:` fig.yml option, which was causing fig to error out.
|
||||||
|
- Fixed a bug where fig couldn't start under Python 2.6.
|
||||||
|
- Fixed a log-streaming bug that occasionally caused fig to exit.
|
||||||
|
|
||||||
|
Thanks @dnephin and @marksteve!
|
||||||
|
|
||||||
|
|
||||||
|
0.5.1 (2014-07-11)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- If a service has a command defined, `fig run [service]` with no further arguments will run it.
|
||||||
|
- The project name now defaults to the directory containing fig.yml, not the current working directory (if they're different)
|
||||||
|
- `volumes_from` now works properly with containers as well as services
|
||||||
|
- Fixed a race condition when recreating containers in `fig up`
|
||||||
|
|
||||||
|
Thanks @ryanbrainard and @d11wtq!
|
||||||
|
|
||||||
|
|
||||||
|
0.5.0 (2014-07-11)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Fig now starts links when you run `fig run` or `fig up`.
|
||||||
|
|
||||||
|
For example, if you have a `web` service which depends on a `db` service, `fig run web ...` will start the `db` service.
|
||||||
|
|
||||||
|
- Environment variables can now be resolved from the environment that Fig is running in. Just specify it as a blank variable in your `fig.yml` and, if set, it'll be resolved:
|
||||||
|
```
|
||||||
|
environment:
|
||||||
|
RACK_ENV: development
|
||||||
|
SESSION_SECRET:
|
||||||
|
```
|
||||||
|
|
||||||
|
- `volumes_from` is now supported in `fig.yml`. All of the volumes from the specified services and containers will be mounted:
|
||||||
|
|
||||||
|
```
|
||||||
|
volumes_from:
|
||||||
|
- service_name
|
||||||
|
- container_name
|
||||||
|
```
|
||||||
|
|
||||||
|
- A host address can now be specified in `ports`:
|
||||||
|
|
||||||
|
```
|
||||||
|
ports:
|
||||||
|
- "0.0.0.0:8000:8000"
|
||||||
|
- "127.0.0.1:8001:8001"
|
||||||
|
```
|
||||||
|
|
||||||
|
- The `net` and `workdir` options are now supported in `fig.yml`.
|
||||||
|
- The `hostname` option now works in the same way as the Docker CLI, splitting out into a `domainname` option.
|
||||||
|
- TTY behaviour is far more robust, and resizes are supported correctly.
|
||||||
|
- Load YAML files safely.
|
||||||
|
|
||||||
|
Thanks to @d11wtq, @ryanbrainard, @rail44, @j0hnsmith, @binarin, @Elemecca, @mozz100 and @marksteve for their help with this release!
|
||||||
|
|
||||||
|
|
||||||
|
0.4.2 (2014-06-18)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Fix various encoding errors when using `fig run`, `fig up` and `fig build`.
|
||||||
|
|
||||||
|
0.4.1 (2014-05-08)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Add support for Docker 0.11.0. (Thanks @marksteve!)
|
||||||
|
- Make project name configurable. (Thanks @jefmathiot!)
|
||||||
|
- Return correct exit code from `fig run`.
|
||||||
|
|
||||||
|
0.4.0 (2014-04-29)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Support Docker 0.9 and 0.10
|
||||||
|
- Display progress bars correctly when pulling images (no more ski slopes)
|
||||||
|
- `fig up` now stops all services when any container exits
|
||||||
|
- Added support for the `privileged` config option in fig.yml (thanks @kvz!)
|
||||||
|
- Shortened and aligned log prefixes in `fig up` output
|
||||||
|
- Only containers started with `fig run` link back to their own service
|
||||||
|
- Handle UTF-8 correctly when streaming `fig build/run/up` output (thanks @mauvm and @shanejonas!)
|
||||||
|
- Error message improvements
|
||||||
|
|
||||||
|
0.3.2 (2014-03-05)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Added an `--rm` option to `fig run`. (Thanks @marksteve!)
|
||||||
|
- Added an `expose` option to `fig.yml`.
|
||||||
|
|
||||||
|
0.3.1 (2014-03-04)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Added contribution instructions. (Thanks @kvz!)
|
||||||
|
- Fixed `fig rm` throwing an error.
|
||||||
|
- Fixed a bug in `fig ps` on Docker 0.8.1 when there is a container with no command.
|
||||||
|
|
||||||
|
0.3.0 (2014-03-03)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- We now ship binaries for OS X and Linux. No more having to install with Pip!
|
||||||
|
- Add `-f` flag to specify alternate `fig.yml` files
|
||||||
|
- Add support for custom link names
|
||||||
|
- Fix a bug where recreating would sometimes hang
|
||||||
|
- Update docker-py to support Docker 0.8.0.
|
||||||
|
- Various documentation improvements
|
||||||
|
- Various error message improvements
|
||||||
|
|
||||||
|
Thanks @marksteve, @Gazler and @teozkr!
|
||||||
|
|
||||||
|
0.2.2 (2014-02-17)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Resolve dependencies using Cormen/Tarjan topological sort
|
||||||
|
- Fix `fig up` not printing log output
|
||||||
|
- Stop containers in reverse order to starting
|
||||||
|
- Fix scale command not binding ports
|
||||||
|
|
||||||
|
Thanks to @barnybug and @dustinlacewell for their work on this release.
|
||||||
|
|
||||||
|
0.2.1 (2014-02-04)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- General improvements to error reporting (#77, #79)
|
||||||
|
|
||||||
|
0.2.0 (2014-01-31)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Link services to themselves so run commands can access the running service. (#67)
|
||||||
|
- Much better documentation.
|
||||||
|
- Make service dependency resolution more reliable. (#48)
|
||||||
|
- Load Fig configurations with a `.yaml` extension. (#58)
|
||||||
|
|
||||||
|
Big thanks to @cameronmaske, @mrchrisadams and @damianmoore for their help with this release.
|
||||||
|
|
||||||
|
0.1.4 (2014-01-27)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Add a link alias without the project name. This makes the environment variables a little shorter: `REDIS_1_PORT_6379_TCP_ADDR`. (#54)
|
||||||
|
|
||||||
|
0.1.3 (2014-01-23)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Fix ports sometimes being configured incorrectly. (#46)
|
||||||
|
- Fix log output sometimes not displaying. (#47)
|
||||||
|
|
||||||
|
0.1.2 (2014-01-22)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Add `-T` option to `fig run` to disable pseudo-TTY. (#34)
|
||||||
|
- Fix `fig up` requiring the ubuntu image to be pulled to recreate containers. (#33) Thanks @cameronmaske!
|
||||||
|
- Improve reliability, fix arrow keys and fix a race condition in `fig run`. (#34, #39, #40)
|
||||||
|
|
||||||
|
0.1.1 (2014-01-17)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Fix bug where ports were not exposed correctly (#29). Thanks @dustinlacewell!
|
||||||
|
|
||||||
|
0.1.0 (2014-01-16)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Containers are recreated on each `fig up`, ensuring config is up-to-date with `fig.yml` (#2)
|
||||||
|
- Add `fig scale` command (#9)
|
||||||
|
- Use `DOCKER_HOST` environment variable to find Docker daemon, for consistency with the official Docker client (was previously `DOCKER_URL`) (#19)
|
||||||
|
- Truncate long commands in `fig ps` (#18)
|
||||||
|
- Fill out CLI help banners for commands (#15, #16)
|
||||||
|
- Show a friendlier error when `fig.yml` is missing (#4)
|
||||||
|
- Fix bug with `fig build` logging (#3)
|
||||||
|
- Fix bug where builds would time out if a step took a long time without generating output (#6)
|
||||||
|
- Fix bug where streaming container output over the Unix socket raised an error (#7)
|
||||||
|
|
||||||
|
Big thanks to @tomstuart, @EnTeQuAk, @schickling, @aronasorman and @GeoffreyPlitt.
|
||||||
|
|
||||||
|
0.0.2 (2014-01-02)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Improve documentation
|
||||||
|
- Try to connect to Docker on `tcp://localdocker:4243` and a UNIX socket in addition to `localhost`.
|
||||||
|
- Improve `fig up` behaviour
|
||||||
|
- Add confirmation prompt to `fig rm`
|
||||||
|
- Add `fig build` command
|
||||||
|
|
||||||
|
0.0.1 (2013-12-20)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Initial release.
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
# Contributing to Compose
|
# Contributing to Compose
|
||||||
|
|
||||||
Compose is a part of the Docker project, and follows the same rules and
|
Compose is a part of the Docker project, and follows the same rules and principles. Take a read of [Docker's contributing guidelines](https://github.com/docker/docker/blob/master/CONTRIBUTING.md) to get an overview.
|
||||||
principles. Take a read of [Docker's contributing guidelines](https://github.com/docker/docker/blob/master/CONTRIBUTING.md)
|
|
||||||
to get an overview.
|
|
||||||
|
|
||||||
## TL;DR
|
## TL;DR
|
||||||
|
|
||||||
|
|
@ -19,56 +17,60 @@ If you're looking contribute to Compose
|
||||||
but you're new to the project or maybe even to Python, here are the steps
|
but you're new to the project or maybe even to Python, here are the steps
|
||||||
that should get you started.
|
that should get you started.
|
||||||
|
|
||||||
1. Fork [https://github.com/docker/compose](https://github.com/docker/compose)
|
1. Fork [https://github.com/docker/compose](https://github.com/docker/compose) to your username.
|
||||||
to your username.
|
1. Clone your forked repository locally `git clone git@github.com:yourusername/compose.git`.
|
||||||
2. Clone your forked repository locally `git clone git@github.com:yourusername/compose.git`.
|
1. Enter the local directory `cd compose`.
|
||||||
3. You must [configure a remote](https://help.github.com/articles/configuring-a-remote-for-a-fork/) for your fork so that you can [sync changes you make](https://help.github.com/articles/syncing-a-fork/) with the original repository.
|
1. Set up a development environment by running `python setup.py develop`. This will install the dependencies and set up a symlink from your `docker-compose` executable to the checkout of the repository. When you now run `docker-compose` from anywhere on your machine, it will run your development version of Compose.
|
||||||
4. Enter the local directory `cd compose`.
|
|
||||||
5. Set up a development environment by running `python setup.py develop`. This
|
|
||||||
will install the dependencies and set up a symlink from your `docker-compose`
|
|
||||||
executable to the checkout of the repository. When you now run
|
|
||||||
`docker-compose` from anywhere on your machine, it will run your development
|
|
||||||
version of Compose.
|
|
||||||
|
|
||||||
## Install pre-commit hooks
|
|
||||||
|
|
||||||
This step is optional, but recommended. Pre-commit hooks will run style checks
|
|
||||||
and in some cases fix style issues for you, when you commit code.
|
|
||||||
|
|
||||||
Install the git pre-commit hooks using [tox](https://tox.readthedocs.io) by
|
|
||||||
running `tox -e pre-commit` or by following the
|
|
||||||
[pre-commit install guide](http://pre-commit.com/#install).
|
|
||||||
|
|
||||||
To run the style checks at any time run `tox -e pre-commit`.
|
|
||||||
|
|
||||||
## Submitting a pull request
|
|
||||||
|
|
||||||
See Docker's [basic contribution workflow](https://docs.docker.com/opensource/workflow/make-a-contribution/#the-basic-contribution-workflow) for a guide on how to submit a pull request for code or documentation.
|
|
||||||
|
|
||||||
## Running the test suite
|
## Running the test suite
|
||||||
|
|
||||||
Use the test script to run linting checks and then the full test suite against
|
Use the test script to run linting checks and then the full test suite:
|
||||||
different Python interpreters:
|
|
||||||
|
|
||||||
$ script/test/default
|
$ script/test
|
||||||
|
|
||||||
Tests are run against a Docker daemon inside a container, so that we can test
|
Tests are run against a Docker daemon inside a container, so that we can test against multiple Docker versions. By default they'll run against only the latest Docker version - set the `DOCKER_VERSIONS` environment variable to "all" to run against all supported versions:
|
||||||
against multiple Docker versions. By default they'll run against only the latest
|
|
||||||
Docker version - set the `DOCKER_VERSIONS` environment variable to "all" to run
|
|
||||||
against all supported versions:
|
|
||||||
|
|
||||||
$ DOCKER_VERSIONS=all script/test/default
|
$ DOCKER_VERSIONS=all script/test
|
||||||
|
|
||||||
Arguments to `script/test/default` are passed through to the `tox` executable, so
|
Arguments to `script/test` are passed through to the `nosetests` executable, so you can specify a test directory, file, module, class or method:
|
||||||
you can specify a test directory, file, module, class or method:
|
|
||||||
|
|
||||||
$ script/test/default tests/unit
|
$ script/test tests/unit
|
||||||
$ script/test/default tests/unit/cli_test.py
|
$ script/test tests/unit/cli_test.py
|
||||||
$ script/test/default tests/unit/config_test.py::ConfigTest
|
$ script/test tests.integration.service_test
|
||||||
$ script/test/default tests/unit/config_test.py::ConfigTest::test_load
|
$ script/test tests.integration.service_test:ServiceTest.test_containers
|
||||||
|
|
||||||
## Finding things to work on
|
## Building binaries
|
||||||
|
|
||||||
We use a [ZenHub board](https://www.zenhub.io/) to keep track of specific things we are working on and planning to work on. If you're looking for things to work on, stuff in the backlog is a great place to start.
|
Linux:
|
||||||
|
|
||||||
For more information about our project planning, take a look at our [GitHub wiki](https://github.com/docker/compose/wiki).
|
$ script/build-linux
|
||||||
|
|
||||||
|
OS X:
|
||||||
|
|
||||||
|
$ script/build-osx
|
||||||
|
|
||||||
|
Note that this only works on Mountain Lion, not Mavericks, due to a [bug in PyInstaller](http://www.pyinstaller.org/ticket/807).
|
||||||
|
|
||||||
|
## Release process
|
||||||
|
|
||||||
|
1. Open pull request that:
|
||||||
|
|
||||||
|
- Updates the version in `compose/__init__.py`
|
||||||
|
- Updates the binary URL in `docs/install.md`
|
||||||
|
- Updates the script URL in `docs/completion.md`
|
||||||
|
- Adds release notes to `CHANGES.md`
|
||||||
|
|
||||||
|
2. Create unpublished GitHub release with release notes
|
||||||
|
|
||||||
|
3. Build Linux version on any Docker host with `script/build-linux` and attach to release
|
||||||
|
|
||||||
|
4. Build OS X version on Mountain Lion with `script/build-osx` and attach to release as `docker-compose-Darwin-x86_64` and `docker-compose-Linux-x86_64`.
|
||||||
|
|
||||||
|
5. Publish GitHub release, creating tag
|
||||||
|
|
||||||
|
6. Update website with `script/deploy-docs`
|
||||||
|
|
||||||
|
7. Upload PyPi package
|
||||||
|
|
||||||
|
$ git checkout $VERSION
|
||||||
|
$ python setup.py sdist upload
|
||||||
|
|
|
||||||
66
Dockerfile
66
Dockerfile
|
|
@ -3,69 +3,41 @@ FROM debian:wheezy
|
||||||
RUN set -ex; \
|
RUN set -ex; \
|
||||||
apt-get update -qq; \
|
apt-get update -qq; \
|
||||||
apt-get install -y \
|
apt-get install -y \
|
||||||
locales \
|
python \
|
||||||
gcc \
|
python-pip \
|
||||||
make \
|
python-dev \
|
||||||
zlib1g \
|
|
||||||
zlib1g-dev \
|
|
||||||
libssl-dev \
|
|
||||||
git \
|
git \
|
||||||
|
apt-transport-https \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
libsqlite3-dev \
|
lxc \
|
||||||
libbz2-dev \
|
iptables \
|
||||||
; \
|
; \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN curl https://get.docker.com/builds/Linux/x86_64/docker-1.8.3 \
|
ENV ALL_DOCKER_VERSIONS 1.3.3 1.4.1 1.5.0
|
||||||
-o /usr/local/bin/docker && \
|
|
||||||
chmod +x /usr/local/bin/docker
|
|
||||||
|
|
||||||
# Build Python 2.7.13 from source
|
|
||||||
RUN set -ex; \
|
RUN set -ex; \
|
||||||
curl -L https://www.python.org/ftp/python/2.7.13/Python-2.7.13.tgz | tar -xz; \
|
for v in ${ALL_DOCKER_VERSIONS}; do \
|
||||||
cd Python-2.7.13; \
|
curl https://get.docker.com/builds/Linux/x86_64/docker-$v -o /usr/local/bin/docker-$v; \
|
||||||
./configure --enable-shared; \
|
chmod +x /usr/local/bin/docker-$v; \
|
||||||
make; \
|
done
|
||||||
make install; \
|
|
||||||
cd ..; \
|
|
||||||
rm -rf /Python-2.7.13
|
|
||||||
|
|
||||||
# Build python 3.4 from source
|
# Set the default Docker to be run
|
||||||
RUN set -ex; \
|
RUN ln -s /usr/local/bin/docker-1.3.3 /usr/local/bin/docker
|
||||||
curl -L https://www.python.org/ftp/python/3.4.6/Python-3.4.6.tgz | tar -xz; \
|
|
||||||
cd Python-3.4.6; \
|
|
||||||
./configure --enable-shared; \
|
|
||||||
make; \
|
|
||||||
make install; \
|
|
||||||
cd ..; \
|
|
||||||
rm -rf /Python-3.4.6
|
|
||||||
|
|
||||||
# Make libpython findable
|
|
||||||
ENV LD_LIBRARY_PATH /usr/local/lib
|
|
||||||
|
|
||||||
# Install pip
|
|
||||||
RUN set -ex; \
|
|
||||||
curl -L https://bootstrap.pypa.io/get-pip.py | python
|
|
||||||
|
|
||||||
# Python3 requires a valid locale
|
|
||||||
RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && locale-gen
|
|
||||||
ENV LANG en_US.UTF-8
|
|
||||||
|
|
||||||
RUN useradd -d /home/user -m -s /bin/bash user
|
RUN useradd -d /home/user -m -s /bin/bash user
|
||||||
WORKDIR /code/
|
WORKDIR /code/
|
||||||
|
|
||||||
RUN pip install tox==2.1.1
|
|
||||||
|
|
||||||
ADD requirements.txt /code/
|
ADD requirements.txt /code/
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
ADD requirements-dev.txt /code/
|
ADD requirements-dev.txt /code/
|
||||||
ADD .pre-commit-config.yaml /code/
|
RUN pip install -r requirements-dev.txt
|
||||||
ADD setup.py /code/
|
|
||||||
ADD tox.ini /code/
|
|
||||||
ADD compose /code/compose/
|
|
||||||
RUN tox --notest
|
|
||||||
|
|
||||||
ADD . /code/
|
ADD . /code/
|
||||||
|
RUN python setup.py install
|
||||||
|
|
||||||
RUN chown -R user /code/
|
RUN chown -R user /code/
|
||||||
|
|
||||||
ENTRYPOINT ["/code/.tox/py27/bin/docker-compose"]
|
ENTRYPOINT ["/usr/local/bin/docker-compose"]
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
|
|
||||||
FROM alpine:3.4
|
|
||||||
ARG version
|
|
||||||
RUN apk -U add \
|
|
||||||
python \
|
|
||||||
py-pip
|
|
||||||
|
|
||||||
COPY requirements.txt /code/requirements.txt
|
|
||||||
RUN pip install -r /code/requirements.txt
|
|
||||||
|
|
||||||
COPY dist/docker_compose-${version}-py2.py3-none-any.whl /code/
|
|
||||||
RUN pip install --no-deps /code/docker_compose-${version}-py2.py3-none-any.whl
|
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/bin/docker-compose"]
|
|
||||||
64
Jenkinsfile
vendored
64
Jenkinsfile
vendored
|
|
@ -1,64 +0,0 @@
|
||||||
#!groovy
|
|
||||||
|
|
||||||
def image
|
|
||||||
|
|
||||||
def buildImage = { ->
|
|
||||||
wrappedNode(label: "ubuntu && !zfs", cleanWorkspace: true) {
|
|
||||||
stage("build image") {
|
|
||||||
checkout(scm)
|
|
||||||
def imageName = "dockerbuildbot/compose:${gitCommit()}"
|
|
||||||
image = docker.image(imageName)
|
|
||||||
try {
|
|
||||||
image.pull()
|
|
||||||
} catch (Exception exc) {
|
|
||||||
image = docker.build(imageName, ".")
|
|
||||||
image.push()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def runTests = { Map settings ->
|
|
||||||
def dockerVersions = settings.get("dockerVersions", null)
|
|
||||||
def pythonVersions = settings.get("pythonVersions", null)
|
|
||||||
|
|
||||||
if (!pythonVersions) {
|
|
||||||
throw new Exception("Need Python versions to test. e.g.: `runTests(pythonVersions: 'py27,py34')`")
|
|
||||||
}
|
|
||||||
if (!dockerVersions) {
|
|
||||||
throw new Exception("Need Docker versions to test. e.g.: `runTests(dockerVersions: 'all')`")
|
|
||||||
}
|
|
||||||
|
|
||||||
{ ->
|
|
||||||
wrappedNode(label: "ubuntu && !zfs", cleanWorkspace: true) {
|
|
||||||
stage("test python=${pythonVersions} / docker=${dockerVersions}") {
|
|
||||||
checkout(scm)
|
|
||||||
def storageDriver = sh(script: 'docker info | awk -F \': \' \'$1 == "Storage Driver" { print $2; exit }\'', returnStdout: true).trim()
|
|
||||||
echo "Using local system's storage driver: ${storageDriver}"
|
|
||||||
sh """docker run \\
|
|
||||||
-t \\
|
|
||||||
--rm \\
|
|
||||||
--privileged \\
|
|
||||||
--volume="\$(pwd)/.git:/code/.git" \\
|
|
||||||
--volume="/var/run/docker.sock:/var/run/docker.sock" \\
|
|
||||||
-e "TAG=${image.id}" \\
|
|
||||||
-e "STORAGE_DRIVER=${storageDriver}" \\
|
|
||||||
-e "DOCKER_VERSIONS=${dockerVersions}" \\
|
|
||||||
-e "BUILD_NUMBER=\$BUILD_TAG" \\
|
|
||||||
-e "PY_TEST_VERSIONS=${pythonVersions}" \\
|
|
||||||
--entrypoint="script/ci" \\
|
|
||||||
${image.id} \\
|
|
||||||
--verbose
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildImage()
|
|
||||||
// TODO: break this out into meaningful "DOCKER_VERSIONS" values instead of all
|
|
||||||
parallel(
|
|
||||||
failFast: true,
|
|
||||||
all_py27: runTests(pythonVersions: "py27", dockerVersions: "all"),
|
|
||||||
all_py34: runTests(pythonVersions: "py34", dockerVersions: "all"),
|
|
||||||
)
|
|
||||||
49
MAINTAINERS
49
MAINTAINERS
|
|
@ -1,46 +1,3 @@
|
||||||
# Compose maintainers file
|
Aanand Prasad <aanand.prasad@gmail.com> (@aanand)
|
||||||
#
|
Ben Firshman <ben@firshman.co.uk> (@bfirsh)
|
||||||
# This file describes who runs the docker/compose project and how.
|
Daniel Nephin <dnephin@gmail.com> (@dnephin)
|
||||||
# This is a living document - if you see something out of date or missing, speak up!
|
|
||||||
#
|
|
||||||
# It is structured to be consumable by both humans and programs.
|
|
||||||
# To extract its contents programmatically, use any TOML-compliant parser.
|
|
||||||
#
|
|
||||||
# This file is compiled into the MAINTAINERS file in docker/opensource.
|
|
||||||
#
|
|
||||||
[Org]
|
|
||||||
[Org."Core maintainers"]
|
|
||||||
people = [
|
|
||||||
"aanand",
|
|
||||||
"bfirsh",
|
|
||||||
"dnephin",
|
|
||||||
"mnowster",
|
|
||||||
]
|
|
||||||
|
|
||||||
[people]
|
|
||||||
|
|
||||||
# A reference list of all people associated with the project.
|
|
||||||
# All other sections should refer to people by their canonical key
|
|
||||||
# in the people section.
|
|
||||||
|
|
||||||
# ADD YOURSELF HERE IN ALPHABETICAL ORDER
|
|
||||||
|
|
||||||
[people.aanand]
|
|
||||||
Name = "Aanand Prasad"
|
|
||||||
Email = "aanand.prasad@gmail.com"
|
|
||||||
GitHub = "aanand"
|
|
||||||
|
|
||||||
[people.bfirsh]
|
|
||||||
Name = "Ben Firshman"
|
|
||||||
Email = "ben@firshman.co.uk"
|
|
||||||
GitHub = "bfirsh"
|
|
||||||
|
|
||||||
[people.dnephin]
|
|
||||||
Name = "Daniel Nephin"
|
|
||||||
Email = "dnephin@gmail.com"
|
|
||||||
GitHub = "dnephin"
|
|
||||||
|
|
||||||
[people.mnowster]
|
|
||||||
Name = "Mazz Mosley"
|
|
||||||
Email = "mazz@houseofmnowster.com"
|
|
||||||
GitHub = "mnowster"
|
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,7 @@ include requirements.txt
|
||||||
include requirements-dev.txt
|
include requirements-dev.txt
|
||||||
include tox.ini
|
include tox.ini
|
||||||
include *.md
|
include *.md
|
||||||
exclude README.md
|
include contrib/completion/bash/docker-compose
|
||||||
include README.rst
|
|
||||||
include compose/config/*.json
|
|
||||||
include compose/GITSHA
|
|
||||||
recursive-include contrib/completion *
|
|
||||||
recursive-include tests *
|
recursive-include tests *
|
||||||
global-exclude *.pyc
|
global-exclude *.pyc
|
||||||
global-exclude *.pyo
|
global-exclude *.pyo
|
||||||
|
|
|
||||||
72
README.md
72
README.md
|
|
@ -1,41 +1,45 @@
|
||||||
Docker Compose
|
Docker Compose
|
||||||
==============
|
==============
|
||||||

|
[](http://jenkins.dockerproject.com/job/Compose%20Master/)
|
||||||
|
*(Previously known as Fig)*
|
||||||
|
|
||||||
Compose is a tool for defining and running multi-container Docker applications.
|
Compose is a tool for defining and running complex applications with Docker.
|
||||||
With Compose, you use a Compose file to configure your application's services.
|
With Compose, you define a multi-container application in a single file, then
|
||||||
Then, using a single command, you create and start all the services
|
spin your application up in a single command which does everything that needs to
|
||||||
from your configuration. To learn more about all the features of Compose
|
be done to get it running.
|
||||||
see [the list of features](https://github.com/docker/docker.github.io/blob/master/compose/overview.md#features).
|
|
||||||
|
|
||||||
Compose is great for development, testing, and staging environments, as well as
|
Compose is great for development environments, staging servers, and CI. We don't
|
||||||
CI workflows. You can learn more about each case in
|
recommend that you use it in production yet.
|
||||||
[Common Use Cases](https://github.com/docker/docker.github.io/blob/master/compose/overview.md#common-use-cases).
|
|
||||||
|
|
||||||
Using Compose is basically a three-step process.
|
Using Compose is basically a three-step process.
|
||||||
|
|
||||||
1. Define your app's environment with a `Dockerfile` so it can be
|
First, you define your app's environment with a `Dockerfile` so it can be
|
||||||
reproduced anywhere.
|
reproduced anywhere:
|
||||||
2. Define the services that make up your app in `docker-compose.yml` so
|
|
||||||
|
```Dockerfile
|
||||||
|
FROM python:2.7
|
||||||
|
WORKDIR /code
|
||||||
|
ADD requirements.txt /code/
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
ADD . /code
|
||||||
|
CMD python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, you define the services that make up your app in `docker-compose.yml` so
|
||||||
they can be run together in an isolated environment:
|
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 `docker-compose.yml` looks like this:
|
```yaml
|
||||||
|
|
||||||
version: '2'
|
|
||||||
|
|
||||||
services:
|
|
||||||
web:
|
web:
|
||||||
build: .
|
build: .
|
||||||
|
links:
|
||||||
|
- db
|
||||||
ports:
|
ports:
|
||||||
- "5000:5000"
|
- "8000:8000"
|
||||||
volumes:
|
db:
|
||||||
- .:/code
|
image: postgres
|
||||||
redis:
|
```
|
||||||
image: redis
|
|
||||||
|
|
||||||
For more information about the Compose file, see the
|
Lastly, run `docker-compose up` and Compose will start and run your entire app.
|
||||||
[Compose file reference](https://github.com/docker/docker.github.io/blob/master/compose/compose-file/compose-versioning.md)
|
|
||||||
|
|
||||||
Compose has commands for managing the whole lifecycle of your application:
|
Compose has commands for managing the whole lifecycle of your application:
|
||||||
|
|
||||||
|
|
@ -47,19 +51,5 @@ Compose has commands for managing the whole lifecycle of your application:
|
||||||
Installation and documentation
|
Installation and documentation
|
||||||
------------------------------
|
------------------------------
|
||||||
|
|
||||||
- Full documentation is available on [Docker's website](https://docs.docker.com/compose/).
|
- Full documentation is available on [Docker's website](http://docs.docker.com/compose/).
|
||||||
- If you have any questions, you can talk in real-time with other developers in the #docker-compose IRC channel on Freenode. [Click here to join using IRCCloud.](https://www.irccloud.com/invite?hostname=irc.freenode.net&channel=%23docker-compose)
|
- Hop into #docker-compose on Freenode if you have any questions.
|
||||||
- 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)
|
|
||||||
|
|
||||||
Contributing
|
|
||||||
------------
|
|
||||||
|
|
||||||
[](http://jenkins.dockerproject.org/job/Compose%20Master/)
|
|
||||||
|
|
||||||
Want to help build Compose? Check out our [contributing documentation](https://github.com/docker/compose/blob/master/CONTRIBUTING.md).
|
|
||||||
|
|
||||||
Releasing
|
|
||||||
---------
|
|
||||||
|
|
||||||
Releases are built by maintainers, following an outline of the [release process](https://github.com/docker/compose/blob/master/project/RELEASE-PROCESS.md).
|
|
||||||
|
|
|
||||||
24
ROADMAP.md
24
ROADMAP.md
|
|
@ -1,23 +1,12 @@
|
||||||
# Roadmap
|
# Roadmap
|
||||||
|
|
||||||
## An even better tool for development environments
|
|
||||||
|
|
||||||
Compose is a great tool for development environments, but it could be even better. For example:
|
|
||||||
|
|
||||||
- It should be possible to define hostnames for containers which work from the host machine, e.g. “mywebcontainer.local”. This is needed by apps comprising multiple web services which generate links to one another (e.g. a frontend website and a separate admin webapp)
|
|
||||||
|
|
||||||
## More than just development environments
|
## More than just development environments
|
||||||
|
|
||||||
Compose currently works really well in development, but we want to make the Compose file format better for test, staging, and production environments. To support these use cases, there will need to be improvements to the file format, improvements to the command-line tool, integrations with other tools, and perhaps new tools altogether.
|
Over time we will extend Compose's remit to cover test, staging and production environments. This is not a simple task, and will take many incremental improvements such as:
|
||||||
|
|
||||||
Some specific things we are considering:
|
- Compose’s brute-force “delete and recreate everything” approach is great for dev and testing, but it not sufficient for production environments. You should be able to define a "desired" state that Compose will intelligently converge to.
|
||||||
|
- It should be possible to partially modify the config file for different environments (dev/test/staging/prod), passing in e.g. custom ports or volume mount paths. ([#426](https://github.com/docker/fig/issues/426))
|
||||||
- Compose currently will attempt to get your application into the correct state when running `up`, but it has a number of shortcomings:
|
|
||||||
- It should roll back to a known good state if it fails.
|
|
||||||
- It should allow a user to check the actions it is about to perform before running them.
|
|
||||||
- It should be possible to partially modify the config file for different environments (dev/test/staging/prod), passing in e.g. custom ports, volume mount paths, or volume drivers. ([#1377](https://github.com/docker/compose/issues/1377))
|
|
||||||
- Compose should recommend a technique for zero-downtime deploys.
|
- Compose should recommend a technique for zero-downtime deploys.
|
||||||
- It should be possible to continuously attempt to keep an application in the correct state, instead of just performing `up` a single time.
|
|
||||||
|
|
||||||
## Integration with Swarm
|
## Integration with Swarm
|
||||||
|
|
||||||
|
|
@ -30,3 +19,10 @@ The current state of integration is documented in [SWARM.md](SWARM.md).
|
||||||
Compose works well for applications that are in a single repository and depend on services that are hosted on Docker Hub. If your application depends on another application within your organisation, Compose doesn't work as well.
|
Compose works well for applications that are in a single repository and depend on services that are hosted on Docker Hub. If your application depends on another application within your organisation, Compose doesn't work as well.
|
||||||
|
|
||||||
There are several ideas about how this could work, such as [including external files](https://github.com/docker/fig/issues/318).
|
There are several ideas about how this could work, such as [including external files](https://github.com/docker/fig/issues/318).
|
||||||
|
|
||||||
|
## An even better tool for development environments
|
||||||
|
|
||||||
|
Compose is a great tool for development environments, but it could be even better. For example:
|
||||||
|
|
||||||
|
- [Compose could watch your code and automatically kick off builds when something changes.](https://github.com/docker/fig/issues/184)
|
||||||
|
- It should be possible to define hostnames for containers which work from the host machine, e.g. “mywebcontainer.local”. This is needed by apps comprising multiple web services which generate links to one another (e.g. a frontend website and a separate admin webapp)
|
||||||
|
|
|
||||||
52
SWARM.md
52
SWARM.md
|
|
@ -1 +1,51 @@
|
||||||
This file has moved to: https://docs.docker.com/compose/swarm/
|
Docker Compose/Swarm integration
|
||||||
|
================================
|
||||||
|
|
||||||
|
Eventually, Compose and Swarm aim to have full integration, meaning you can point a Compose app at a Swarm cluster and have it all just work as if you were using a single Docker host.
|
||||||
|
|
||||||
|
However, the current extent of integration is minimal: Compose can create containers on a Swarm cluster, but the majority of Compose apps won’t work out of the box unless all containers are scheduled on one host, defeating much of the purpose of using Swarm in the first place.
|
||||||
|
|
||||||
|
Still, Compose and Swarm can be useful in a “batch processing” scenario (where a large number of containers need to be spun up and down to do independent computation) or a “shared cluster” scenario (where multiple teams want to deploy apps on a cluster without worrying about where to put them).
|
||||||
|
|
||||||
|
A number of things need to happen before full integration is achieved, which are documented below.
|
||||||
|
|
||||||
|
Re-deploying containers with `docker-compose up`
|
||||||
|
------------------------------------------------
|
||||||
|
|
||||||
|
Repeated invocations of `docker-compose up` will not work reliably when used against a Swarm cluster because of an under-the-hood design problem; [this will be fixed](https://github.com/docker/fig/pull/972) in the next version of Compose. For now, containers must be completely removed and re-created:
|
||||||
|
|
||||||
|
$ docker-compose kill
|
||||||
|
$ docker-compose rm --force
|
||||||
|
$ docker-compose up
|
||||||
|
|
||||||
|
Links and networking
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
The primary thing stopping multi-container apps from working seamlessly on Swarm is getting them to talk to one another: enabling private communication between containers on different hosts hasn’t been solved in a non-hacky way.
|
||||||
|
|
||||||
|
Long-term, networking is [getting overhauled](https://github.com/docker/docker/issues/9983) in such a way that it’ll fit the multi-host model much better. For now, containers on different hosts cannot be linked. In the next version of Compose, linked services will be automatically scheduled on the same host; for now, this must be done manually (see “Co-scheduling containers” below).
|
||||||
|
|
||||||
|
`volumes_from` and `net: container`
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
For containers to share volumes or a network namespace, they must be scheduled on the same host - this is, after all, inherent to how both volumes and network namespaces work. In the next version of Compose, this co-scheduling will be automatic whenever `volumes_from` or `net: "container:..."` is specified; for now, containers which share volumes or a network namespace must be co-scheduled manually (see “Co-scheduling containers” below).
|
||||||
|
|
||||||
|
Co-scheduling containers
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
For now, containers can be manually scheduled on the same host using Swarm’s [affinity filters](https://github.com/docker/swarm/blob/master/scheduler/filter/README.md#affinity-filter). Here’s a simple example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
web:
|
||||||
|
image: my-web-image
|
||||||
|
links: ["db"]
|
||||||
|
environment:
|
||||||
|
- "affinity:container==myproject_db_*"
|
||||||
|
db:
|
||||||
|
image: postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, we express an affinity filter on all web containers, saying that each one must run alongside a container whose name begins with `myproject_db_`.
|
||||||
|
|
||||||
|
- `myproject` is the common prefix Compose gives to all containers in your project, which is either generated from the name of the current directory or specified with `-p` or the `DOCKER_COMPOSE_PROJECT_NAME` environment variable.
|
||||||
|
- `*` is a wildcard, which works just like filename wildcards in a Unix shell.
|
||||||
|
|
|
||||||
24
appveyor.yml
24
appveyor.yml
|
|
@ -1,24 +0,0 @@
|
||||||
|
|
||||||
version: '{branch}-{build}'
|
|
||||||
|
|
||||||
install:
|
|
||||||
- "SET PATH=C:\\Python27-x64;C:\\Python27-x64\\Scripts;%PATH%"
|
|
||||||
- "python --version"
|
|
||||||
- "pip install tox==2.1.1 virtualenv==13.1.2"
|
|
||||||
|
|
||||||
# Build the binary after tests
|
|
||||||
build: false
|
|
||||||
|
|
||||||
test_script:
|
|
||||||
- "tox -e py27,py34 -- tests/unit"
|
|
||||||
- ps: ".\\script\\build\\windows.ps1"
|
|
||||||
|
|
||||||
artifacts:
|
|
||||||
- path: .\dist\docker-compose-Windows-x86_64.exe
|
|
||||||
name: "Compose Windows binary"
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
- provider: Environment
|
|
||||||
name: master-builds
|
|
||||||
on:
|
|
||||||
branch: master
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
from .service import Service # noqa:flake8
|
||||||
|
|
||||||
__version__ = '1.12.0dev'
|
__version__ = '1.2.0rc4'
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from compose.cli.main import main
|
|
||||||
|
|
||||||
main()
|
|
||||||
|
|
@ -1,258 +0,0 @@
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import six
|
|
||||||
from docker.utils import split_command
|
|
||||||
from docker.utils.ports import split_port
|
|
||||||
|
|
||||||
from .cli.errors import UserError
|
|
||||||
from .config.serialize import denormalize_config
|
|
||||||
from .network import get_network_defs_for_service
|
|
||||||
from .service import format_environment
|
|
||||||
from .service import NoSuchImageError
|
|
||||||
from .service import parse_repository_tag
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
SERVICE_KEYS = {
|
|
||||||
'working_dir': 'WorkingDir',
|
|
||||||
'user': 'User',
|
|
||||||
'labels': 'Labels',
|
|
||||||
}
|
|
||||||
|
|
||||||
IGNORED_KEYS = {'build'}
|
|
||||||
|
|
||||||
SUPPORTED_KEYS = {
|
|
||||||
'image',
|
|
||||||
'ports',
|
|
||||||
'expose',
|
|
||||||
'networks',
|
|
||||||
'command',
|
|
||||||
'environment',
|
|
||||||
'entrypoint',
|
|
||||||
} | set(SERVICE_KEYS)
|
|
||||||
|
|
||||||
VERSION = '0.1'
|
|
||||||
|
|
||||||
|
|
||||||
class NeedsPush(Exception):
|
|
||||||
def __init__(self, image_name):
|
|
||||||
self.image_name = image_name
|
|
||||||
|
|
||||||
|
|
||||||
class NeedsPull(Exception):
|
|
||||||
def __init__(self, image_name, service_name):
|
|
||||||
self.image_name = image_name
|
|
||||||
self.service_name = service_name
|
|
||||||
|
|
||||||
|
|
||||||
class MissingDigests(Exception):
|
|
||||||
def __init__(self, needs_push, needs_pull):
|
|
||||||
self.needs_push = needs_push
|
|
||||||
self.needs_pull = needs_pull
|
|
||||||
|
|
||||||
|
|
||||||
def serialize_bundle(config, image_digests):
|
|
||||||
return json.dumps(to_bundle(config, image_digests), indent=2, sort_keys=True)
|
|
||||||
|
|
||||||
|
|
||||||
def get_image_digests(project, allow_push=False):
|
|
||||||
digests = {}
|
|
||||||
needs_push = set()
|
|
||||||
needs_pull = set()
|
|
||||||
|
|
||||||
for service in project.services:
|
|
||||||
try:
|
|
||||||
digests[service.name] = get_image_digest(
|
|
||||||
service,
|
|
||||||
allow_push=allow_push,
|
|
||||||
)
|
|
||||||
except NeedsPush as e:
|
|
||||||
needs_push.add(e.image_name)
|
|
||||||
except NeedsPull as e:
|
|
||||||
needs_pull.add(e.service_name)
|
|
||||||
|
|
||||||
if needs_push or needs_pull:
|
|
||||||
raise MissingDigests(needs_push, needs_pull)
|
|
||||||
|
|
||||||
return digests
|
|
||||||
|
|
||||||
|
|
||||||
def get_image_digest(service, allow_push=False):
|
|
||||||
if 'image' not in service.options:
|
|
||||||
raise UserError(
|
|
||||||
"Service '{s.name}' doesn't define an image tag. An image name is "
|
|
||||||
"required to generate a proper image digest for the bundle. Specify "
|
|
||||||
"an image repo and tag with the 'image' option.".format(s=service))
|
|
||||||
|
|
||||||
_, _, separator = parse_repository_tag(service.options['image'])
|
|
||||||
# Compose file already uses a digest, no lookup required
|
|
||||||
if separator == '@':
|
|
||||||
return service.options['image']
|
|
||||||
|
|
||||||
try:
|
|
||||||
image = service.image()
|
|
||||||
except NoSuchImageError:
|
|
||||||
action = 'build' if 'build' in service.options else 'pull'
|
|
||||||
raise UserError(
|
|
||||||
"Image not found for service '{service}'. "
|
|
||||||
"You might need to run `docker-compose {action} {service}`."
|
|
||||||
.format(service=service.name, action=action))
|
|
||||||
|
|
||||||
if image['RepoDigests']:
|
|
||||||
# TODO: pick a digest based on the image tag if there are multiple
|
|
||||||
# digests
|
|
||||||
return image['RepoDigests'][0]
|
|
||||||
|
|
||||||
if 'build' not in service.options:
|
|
||||||
raise NeedsPull(service.image_name, service.name)
|
|
||||||
|
|
||||||
if not allow_push:
|
|
||||||
raise NeedsPush(service.image_name)
|
|
||||||
|
|
||||||
return push_image(service)
|
|
||||||
|
|
||||||
|
|
||||||
def push_image(service):
|
|
||||||
try:
|
|
||||||
digest = service.push()
|
|
||||||
except:
|
|
||||||
log.error(
|
|
||||||
"Failed to push image for service '{s.name}'. Please use an "
|
|
||||||
"image tag that can be pushed to a Docker "
|
|
||||||
"registry.".format(s=service))
|
|
||||||
raise
|
|
||||||
|
|
||||||
if not digest:
|
|
||||||
raise ValueError("Failed to get digest for %s" % service.name)
|
|
||||||
|
|
||||||
repo, _, _ = parse_repository_tag(service.options['image'])
|
|
||||||
identifier = '{repo}@{digest}'.format(repo=repo, digest=digest)
|
|
||||||
|
|
||||||
# only do this if RepoDigests isn't already populated
|
|
||||||
image = service.image()
|
|
||||||
if not image['RepoDigests']:
|
|
||||||
# Pull by digest so that image['RepoDigests'] is populated for next time
|
|
||||||
# and we don't have to pull/push again
|
|
||||||
service.client.pull(identifier)
|
|
||||||
log.info("Stored digest for {}".format(service.image_name))
|
|
||||||
|
|
||||||
return identifier
|
|
||||||
|
|
||||||
|
|
||||||
def to_bundle(config, image_digests):
|
|
||||||
if config.networks:
|
|
||||||
log.warn("Unsupported top level key 'networks' - ignoring")
|
|
||||||
|
|
||||||
if config.volumes:
|
|
||||||
log.warn("Unsupported top level key 'volumes' - ignoring")
|
|
||||||
|
|
||||||
config = denormalize_config(config)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'Version': VERSION,
|
|
||||||
'Services': {
|
|
||||||
name: convert_service_to_bundle(
|
|
||||||
name,
|
|
||||||
service_dict,
|
|
||||||
image_digests[name],
|
|
||||||
)
|
|
||||||
for name, service_dict in config['services'].items()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def convert_service_to_bundle(name, service_dict, image_digest):
|
|
||||||
container_config = {'Image': image_digest}
|
|
||||||
|
|
||||||
for key, value in service_dict.items():
|
|
||||||
if key in IGNORED_KEYS:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if key not in SUPPORTED_KEYS:
|
|
||||||
log.warn("Unsupported key '{}' in services.{} - ignoring".format(key, name))
|
|
||||||
continue
|
|
||||||
|
|
||||||
if key == 'environment':
|
|
||||||
container_config['Env'] = format_environment({
|
|
||||||
envkey: envvalue for envkey, envvalue in value.items()
|
|
||||||
if envvalue
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
|
|
||||||
if key in SERVICE_KEYS:
|
|
||||||
container_config[SERVICE_KEYS[key]] = value
|
|
||||||
continue
|
|
||||||
|
|
||||||
set_command_and_args(
|
|
||||||
container_config,
|
|
||||||
service_dict.get('entrypoint', []),
|
|
||||||
service_dict.get('command', []))
|
|
||||||
container_config['Networks'] = make_service_networks(name, service_dict)
|
|
||||||
|
|
||||||
ports = make_port_specs(service_dict)
|
|
||||||
if ports:
|
|
||||||
container_config['Ports'] = ports
|
|
||||||
|
|
||||||
return container_config
|
|
||||||
|
|
||||||
|
|
||||||
# See https://github.com/docker/swarmkit/blob/agent/exec/container/container.go#L95
|
|
||||||
def set_command_and_args(config, entrypoint, command):
|
|
||||||
if isinstance(entrypoint, six.string_types):
|
|
||||||
entrypoint = split_command(entrypoint)
|
|
||||||
if isinstance(command, six.string_types):
|
|
||||||
command = split_command(command)
|
|
||||||
|
|
||||||
if entrypoint:
|
|
||||||
config['Command'] = entrypoint + command
|
|
||||||
return
|
|
||||||
|
|
||||||
if command:
|
|
||||||
config['Args'] = command
|
|
||||||
|
|
||||||
|
|
||||||
def make_service_networks(name, service_dict):
|
|
||||||
networks = []
|
|
||||||
|
|
||||||
for network_name, network_def in get_network_defs_for_service(service_dict).items():
|
|
||||||
for key in network_def.keys():
|
|
||||||
log.warn(
|
|
||||||
"Unsupported key '{}' in services.{}.networks.{} - ignoring"
|
|
||||||
.format(key, name, network_name))
|
|
||||||
|
|
||||||
networks.append(network_name)
|
|
||||||
|
|
||||||
return networks
|
|
||||||
|
|
||||||
|
|
||||||
def make_port_specs(service_dict):
|
|
||||||
ports = []
|
|
||||||
|
|
||||||
internal_ports = [
|
|
||||||
internal_port
|
|
||||||
for port_def in service_dict.get('ports', [])
|
|
||||||
for internal_port in split_port(port_def)[0]
|
|
||||||
]
|
|
||||||
|
|
||||||
internal_ports += service_dict.get('expose', [])
|
|
||||||
|
|
||||||
for internal_port in internal_ports:
|
|
||||||
spec = make_port_spec(internal_port)
|
|
||||||
if spec not in ports:
|
|
||||||
ports.append(spec)
|
|
||||||
|
|
||||||
return ports
|
|
||||||
|
|
||||||
|
|
||||||
def make_port_spec(value):
|
|
||||||
components = six.text_type(value).partition('/')
|
|
||||||
return {
|
|
||||||
'Protocol': components[2] or 'tcp',
|
|
||||||
'Port': int(components[0]),
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import print_function
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Attempt to detect https://github.com/docker/compose/issues/4344
|
|
||||||
try:
|
|
||||||
# We don't try importing pip because it messes with package imports
|
|
||||||
# on some Linux distros (Ubuntu, Fedora)
|
|
||||||
# https://github.com/docker/compose/issues/4425
|
|
||||||
# https://github.com/docker/compose/issues/4481
|
|
||||||
# https://github.com/pypa/pip/blob/master/pip/_vendor/__init__.py
|
|
||||||
s_cmd = subprocess.Popen(
|
|
||||||
['pip', 'freeze'], stderr=subprocess.PIPE, stdout=subprocess.PIPE
|
|
||||||
)
|
|
||||||
packages = s_cmd.communicate()[0].splitlines()
|
|
||||||
dockerpy_installed = len(
|
|
||||||
list(filter(lambda p: p.startswith(b'docker-py=='), packages))
|
|
||||||
) > 0
|
|
||||||
if dockerpy_installed:
|
|
||||||
from .colors import red
|
|
||||||
print(
|
|
||||||
red('ERROR:'),
|
|
||||||
"Dependency conflict: an older version of the 'docker-py' package "
|
|
||||||
"is polluting the namespace. "
|
|
||||||
"Run the following command to remedy the issue:\n"
|
|
||||||
"pip uninstall docker docker-py; pip install docker",
|
|
||||||
file=sys.stderr
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
except OSError:
|
|
||||||
# pip command is not available, which indicates it's probably the binary
|
|
||||||
# distribution of Compose which is not affected
|
|
||||||
pass
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import colorama
|
|
||||||
|
|
||||||
NAMES = [
|
NAMES = [
|
||||||
'grey',
|
'grey',
|
||||||
'red',
|
'red',
|
||||||
|
|
@ -33,7 +29,6 @@ def make_color_fn(code):
|
||||||
return lambda s: ansi_color(code, s)
|
return lambda s: ansi_color(code, s)
|
||||||
|
|
||||||
|
|
||||||
colorama.init(strip=False)
|
|
||||||
for (name, code) in get_pairs():
|
for (name, code) in get_pairs():
|
||||||
globals()[name] = make_color_fn(code)
|
globals()[name] = make_color_fn(code)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,132 +1,133 @@
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from requests.exceptions import ConnectionError, SSLError
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import ssl
|
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from . import errors
|
|
||||||
from . import verbose_proxy
|
|
||||||
from .. import config
|
from .. import config
|
||||||
from ..config.environment import Environment
|
|
||||||
from ..const import API_VERSIONS
|
|
||||||
from ..project import Project
|
from ..project import Project
|
||||||
|
from ..service import ConfigError
|
||||||
|
from .docopt_command import DocoptCommand
|
||||||
|
from .utils import call_silently, is_mac, is_ubuntu
|
||||||
from .docker_client import docker_client
|
from .docker_client import docker_client
|
||||||
from .docker_client import tls_config_from_options
|
from . import verbose_proxy
|
||||||
from .utils import get_version_info
|
from . import errors
|
||||||
|
from .. import __version__
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def project_from_options(project_dir, options):
|
class Command(DocoptCommand):
|
||||||
environment = Environment.from_env_file(project_dir)
|
base_dir = '.'
|
||||||
host = options.get('--host')
|
|
||||||
if host is not None:
|
def dispatch(self, *args, **kwargs):
|
||||||
host = host.lstrip('=')
|
try:
|
||||||
return get_project(
|
super(Command, self).dispatch(*args, **kwargs)
|
||||||
project_dir,
|
except SSLError as e:
|
||||||
get_config_path_from_options(project_dir, options, environment),
|
raise errors.UserError('SSL error: %s' % e)
|
||||||
|
except ConnectionError:
|
||||||
|
if call_silently(['which', 'docker']) != 0:
|
||||||
|
if is_mac():
|
||||||
|
raise errors.DockerNotFoundMac()
|
||||||
|
elif is_ubuntu():
|
||||||
|
raise errors.DockerNotFoundUbuntu()
|
||||||
|
else:
|
||||||
|
raise errors.DockerNotFoundGeneric()
|
||||||
|
elif call_silently(['which', 'boot2docker']) == 0:
|
||||||
|
raise errors.ConnectionErrorBoot2Docker()
|
||||||
|
else:
|
||||||
|
raise errors.ConnectionErrorGeneric(self.get_client().base_url)
|
||||||
|
|
||||||
|
def perform_command(self, options, handler, command_options):
|
||||||
|
if options['COMMAND'] == 'help':
|
||||||
|
# Skip looking up the compose file.
|
||||||
|
handler(None, command_options)
|
||||||
|
return
|
||||||
|
|
||||||
|
if 'FIG_FILE' in os.environ:
|
||||||
|
log.warn('The FIG_FILE environment variable is deprecated.')
|
||||||
|
log.warn('Please use COMPOSE_FILE instead.')
|
||||||
|
|
||||||
|
explicit_config_path = options.get('--file') or os.environ.get('COMPOSE_FILE') or os.environ.get('FIG_FILE')
|
||||||
|
project = self.get_project(
|
||||||
|
self.get_config_path(explicit_config_path),
|
||||||
project_name=options.get('--project-name'),
|
project_name=options.get('--project-name'),
|
||||||
verbose=options.get('--verbose'),
|
verbose=options.get('--verbose'))
|
||||||
host=host,
|
|
||||||
tls_config=tls_config_from_options(options),
|
|
||||||
environment=environment
|
|
||||||
)
|
|
||||||
|
|
||||||
|
handler(project, command_options)
|
||||||
|
|
||||||
def get_config_from_options(base_dir, options):
|
def get_client(self, verbose=False):
|
||||||
environment = Environment.from_env_file(base_dir)
|
client = docker_client()
|
||||||
config_path = get_config_path_from_options(
|
|
||||||
base_dir, options, environment
|
|
||||||
)
|
|
||||||
return config.load(
|
|
||||||
config.find(base_dir, config_path, environment)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_config_path_from_options(base_dir, options, environment):
|
|
||||||
file_option = options.get('--file')
|
|
||||||
if file_option:
|
|
||||||
return file_option
|
|
||||||
|
|
||||||
config_files = environment.get('COMPOSE_FILE')
|
|
||||||
if config_files:
|
|
||||||
return config_files.split(os.pathsep)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_tls_version(environment):
|
|
||||||
compose_tls_version = environment.get('COMPOSE_TLS_VERSION', None)
|
|
||||||
if not compose_tls_version:
|
|
||||||
return None
|
|
||||||
|
|
||||||
tls_attr_name = "PROTOCOL_{}".format(compose_tls_version)
|
|
||||||
if not hasattr(ssl, tls_attr_name):
|
|
||||||
log.warn(
|
|
||||||
'The "{}" protocol is unavailable. You may need to update your '
|
|
||||||
'version of Python or OpenSSL. Falling back to TLSv1 (default).'
|
|
||||||
.format(compose_tls_version)
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
return getattr(ssl, tls_attr_name)
|
|
||||||
|
|
||||||
|
|
||||||
def get_client(environment, verbose=False, version=None, tls_config=None, host=None,
|
|
||||||
tls_version=None):
|
|
||||||
|
|
||||||
client = docker_client(
|
|
||||||
version=version, tls_config=tls_config, host=host,
|
|
||||||
environment=environment, tls_version=get_tls_version(environment)
|
|
||||||
)
|
|
||||||
if verbose:
|
if verbose:
|
||||||
version_info = six.iteritems(client.version())
|
version_info = six.iteritems(client.version())
|
||||||
log.info(get_version_info('full'))
|
log.info("Compose version %s", __version__)
|
||||||
log.info("Docker base_url: %s", client.base_url)
|
log.info("Docker base_url: %s", client.base_url)
|
||||||
log.info("Docker version: %s",
|
log.info("Docker version: %s",
|
||||||
", ".join("%s=%s" % item for item in version_info))
|
", ".join("%s=%s" % item for item in version_info))
|
||||||
return verbose_proxy.VerboseProxy('docker', client)
|
return verbose_proxy.VerboseProxy('docker', client)
|
||||||
return client
|
return client
|
||||||
|
|
||||||
|
def get_project(self, config_path, project_name=None, verbose=False):
|
||||||
|
try:
|
||||||
|
return Project.from_dicts(
|
||||||
|
self.get_project_name(config_path, project_name),
|
||||||
|
config.load(config_path),
|
||||||
|
self.get_client(verbose=verbose))
|
||||||
|
except ConfigError as e:
|
||||||
|
raise errors.UserError(six.text_type(e))
|
||||||
|
|
||||||
def get_project(project_dir, config_path=None, project_name=None, verbose=False,
|
def get_project_name(self, config_path, project_name=None):
|
||||||
host=None, tls_config=None, environment=None):
|
|
||||||
if not environment:
|
|
||||||
environment = Environment.from_env_file(project_dir)
|
|
||||||
config_details = config.find(project_dir, config_path, environment)
|
|
||||||
project_name = get_project_name(
|
|
||||||
config_details.working_dir, project_name, environment
|
|
||||||
)
|
|
||||||
config_data = config.load(config_details)
|
|
||||||
|
|
||||||
api_version = environment.get(
|
|
||||||
'COMPOSE_API_VERSION',
|
|
||||||
API_VERSIONS[config_data.version])
|
|
||||||
|
|
||||||
client = get_client(
|
|
||||||
verbose=verbose, version=api_version, tls_config=tls_config,
|
|
||||||
host=host, environment=environment
|
|
||||||
)
|
|
||||||
|
|
||||||
with errors.handle_connection_errors(client):
|
|
||||||
return Project.from_config(project_name, config_data, client)
|
|
||||||
|
|
||||||
|
|
||||||
def get_project_name(working_dir, project_name=None, environment=None):
|
|
||||||
def normalize_name(name):
|
def normalize_name(name):
|
||||||
return re.sub(r'[^a-z0-9]', '', name.lower())
|
return re.sub(r'[^a-z0-9]', '', name.lower())
|
||||||
|
|
||||||
if not environment:
|
if 'FIG_PROJECT_NAME' in os.environ:
|
||||||
environment = Environment.from_env_file(working_dir)
|
log.warn('The FIG_PROJECT_NAME environment variable is deprecated.')
|
||||||
project_name = project_name or environment.get('COMPOSE_PROJECT_NAME')
|
log.warn('Please use COMPOSE_PROJECT_NAME instead.')
|
||||||
if project_name:
|
|
||||||
|
project_name = project_name or os.environ.get('COMPOSE_PROJECT_NAME') or os.environ.get('FIG_PROJECT_NAME')
|
||||||
|
if project_name is not None:
|
||||||
return normalize_name(project_name)
|
return normalize_name(project_name)
|
||||||
|
|
||||||
project = os.path.basename(os.path.abspath(working_dir))
|
project = os.path.basename(os.path.dirname(os.path.abspath(config_path)))
|
||||||
if project:
|
if project:
|
||||||
return normalize_name(project)
|
return normalize_name(project)
|
||||||
|
|
||||||
return 'default'
|
return 'default'
|
||||||
|
|
||||||
|
def get_config_path(self, file_path=None):
|
||||||
|
if file_path:
|
||||||
|
return os.path.join(self.base_dir, file_path)
|
||||||
|
|
||||||
|
supported_filenames = [
|
||||||
|
'docker-compose.yml',
|
||||||
|
'docker-compose.yaml',
|
||||||
|
'fig.yml',
|
||||||
|
'fig.yaml',
|
||||||
|
]
|
||||||
|
|
||||||
|
def expand(filename):
|
||||||
|
return os.path.join(self.base_dir, filename)
|
||||||
|
|
||||||
|
candidates = [filename for filename in supported_filenames if os.path.exists(expand(filename))]
|
||||||
|
|
||||||
|
if len(candidates) == 0:
|
||||||
|
raise errors.ComposeFileNotFound(supported_filenames)
|
||||||
|
|
||||||
|
winner = candidates[0]
|
||||||
|
|
||||||
|
if len(candidates) > 1:
|
||||||
|
log.warning("Found multiple config files with supported names: %s", ", ".join(candidates))
|
||||||
|
log.warning("Using %s\n", winner)
|
||||||
|
|
||||||
|
if winner == 'docker-compose.yaml':
|
||||||
|
log.warning("Please be aware that .yml is the expected extension "
|
||||||
|
"in most cases, and using .yaml can cause compatibility "
|
||||||
|
"issues in future.\n")
|
||||||
|
|
||||||
|
if winner.startswith("fig."):
|
||||||
|
log.warning("%s is deprecated and will not be supported in future. "
|
||||||
|
"Please rename your config file to docker-compose.yml\n" % winner)
|
||||||
|
|
||||||
|
return expand(winner)
|
||||||
|
|
|
||||||
|
|
@ -1,74 +1,35 @@
|
||||||
from __future__ import absolute_import
|
from docker import Client
|
||||||
from __future__ import unicode_literals
|
from docker import tls
|
||||||
|
import ssl
|
||||||
import logging
|
import os
|
||||||
|
|
||||||
from docker import APIClient
|
|
||||||
from docker.errors import TLSParameterError
|
|
||||||
from docker.tls import TLSConfig
|
|
||||||
from docker.utils import kwargs_from_env
|
|
||||||
|
|
||||||
from ..const import HTTP_TIMEOUT
|
|
||||||
from .errors import UserError
|
|
||||||
from .utils import generate_user_agent
|
|
||||||
from .utils import unquote_path
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def tls_config_from_options(options):
|
def docker_client():
|
||||||
tls = options.get('--tls', False)
|
|
||||||
ca_cert = unquote_path(options.get('--tlscacert'))
|
|
||||||
cert = unquote_path(options.get('--tlscert'))
|
|
||||||
key = unquote_path(options.get('--tlskey'))
|
|
||||||
verify = options.get('--tlsverify')
|
|
||||||
skip_hostname_check = options.get('--skip-hostname-check', False)
|
|
||||||
|
|
||||||
advanced_opts = any([ca_cert, cert, key, verify])
|
|
||||||
|
|
||||||
if tls is True and not advanced_opts:
|
|
||||||
return True
|
|
||||||
elif advanced_opts: # --tls is a noop
|
|
||||||
client_cert = None
|
|
||||||
if cert or key:
|
|
||||||
client_cert = (cert, key)
|
|
||||||
|
|
||||||
return TLSConfig(
|
|
||||||
client_cert=client_cert, verify=verify, ca_cert=ca_cert,
|
|
||||||
assert_hostname=False if skip_hostname_check else None
|
|
||||||
)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def docker_client(environment, version=None, tls_config=None, host=None,
|
|
||||||
tls_version=None):
|
|
||||||
"""
|
"""
|
||||||
Returns a docker-py client configured using environment variables
|
Returns a docker-py client configured using environment variables
|
||||||
according to the same logic as the official Docker client.
|
according to the same logic as the official Docker client.
|
||||||
"""
|
"""
|
||||||
try:
|
cert_path = os.environ.get('DOCKER_CERT_PATH', '')
|
||||||
kwargs = kwargs_from_env(environment=environment, ssl_version=tls_version)
|
if cert_path == '':
|
||||||
except TLSParameterError:
|
cert_path = os.path.join(os.environ.get('HOME', ''), '.docker')
|
||||||
raise UserError(
|
|
||||||
"TLS configuration is invalid - make sure your DOCKER_TLS_VERIFY "
|
|
||||||
"and DOCKER_CERT_PATH are set correctly.\n"
|
|
||||||
"You might need to run `eval \"$(docker-machine env default)\"`")
|
|
||||||
|
|
||||||
if host:
|
base_url = os.environ.get('DOCKER_HOST')
|
||||||
kwargs['base_url'] = host
|
tls_config = None
|
||||||
if tls_config:
|
|
||||||
kwargs['tls'] = tls_config
|
|
||||||
|
|
||||||
if version:
|
if os.environ.get('DOCKER_TLS_VERIFY', '') != '':
|
||||||
kwargs['version'] = version
|
parts = base_url.split('://', 1)
|
||||||
|
base_url = '%s://%s' % ('https', parts[1])
|
||||||
|
|
||||||
timeout = environment.get('COMPOSE_HTTP_TIMEOUT')
|
client_cert = (os.path.join(cert_path, 'cert.pem'), os.path.join(cert_path, 'key.pem'))
|
||||||
if timeout:
|
ca_cert = os.path.join(cert_path, 'ca.pem')
|
||||||
kwargs['timeout'] = int(timeout)
|
|
||||||
else:
|
|
||||||
kwargs['timeout'] = HTTP_TIMEOUT
|
|
||||||
|
|
||||||
kwargs['user_agent'] = generate_user_agent()
|
tls_config = tls.TLSConfig(
|
||||||
|
ssl_version=ssl.PROTOCOL_TLSv1,
|
||||||
|
verify=True,
|
||||||
|
assert_hostname=False,
|
||||||
|
client_cert=client_cert,
|
||||||
|
ca_cert=ca_cert,
|
||||||
|
)
|
||||||
|
|
||||||
return APIClient(**kwargs)
|
timeout = int(os.environ.get('DOCKER_CLIENT_TIMEOUT', 60))
|
||||||
|
return Client(base_url=base_url, tls=tls_config, version='1.15', timeout=timeout)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
from __future__ import absolute_import
|
||||||
|
import sys
|
||||||
|
|
||||||
from inspect import getdoc
|
from inspect import getdoc
|
||||||
|
from docopt import docopt, DocoptExit
|
||||||
from docopt import docopt
|
|
||||||
from docopt import DocoptExit
|
|
||||||
|
|
||||||
|
|
||||||
def docopt_full_help(docstring, *args, **kwargs):
|
def docopt_full_help(docstring, *args, **kwargs):
|
||||||
|
|
@ -14,21 +13,30 @@ def docopt_full_help(docstring, *args, **kwargs):
|
||||||
raise SystemExit(docstring)
|
raise SystemExit(docstring)
|
||||||
|
|
||||||
|
|
||||||
class DocoptDispatcher(object):
|
class DocoptCommand(object):
|
||||||
|
def docopt_options(self):
|
||||||
|
return {'options_first': True}
|
||||||
|
|
||||||
def __init__(self, command_class, options):
|
def sys_dispatch(self):
|
||||||
self.command_class = command_class
|
self.dispatch(sys.argv[1:], None)
|
||||||
self.options = options
|
|
||||||
|
|
||||||
def parse(self, argv):
|
def dispatch(self, argv, global_options):
|
||||||
command_help = getdoc(self.command_class)
|
self.perform_command(*self.parse(argv, global_options))
|
||||||
options = docopt_full_help(command_help, argv, **self.options)
|
|
||||||
|
def perform_command(self, options, handler, command_options):
|
||||||
|
handler(command_options)
|
||||||
|
|
||||||
|
def parse(self, argv, global_options):
|
||||||
|
options = docopt_full_help(getdoc(self), argv, **self.docopt_options())
|
||||||
command = options['COMMAND']
|
command = options['COMMAND']
|
||||||
|
|
||||||
if command is None:
|
if command is None:
|
||||||
raise SystemExit(command_help)
|
raise SystemExit(getdoc(self))
|
||||||
|
|
||||||
handler = get_handler(self.command_class, command)
|
if not hasattr(self, command):
|
||||||
|
raise NoSuchCommand(command, self)
|
||||||
|
|
||||||
|
handler = getattr(self, command)
|
||||||
docstring = getdoc(handler)
|
docstring = getdoc(handler)
|
||||||
|
|
||||||
if docstring is None:
|
if docstring is None:
|
||||||
|
|
@ -38,19 +46,6 @@ class DocoptDispatcher(object):
|
||||||
return options, handler, command_options
|
return options, handler, command_options
|
||||||
|
|
||||||
|
|
||||||
def get_handler(command_class, command):
|
|
||||||
command = command.replace('-', '_')
|
|
||||||
# we certainly want to have "exec" command, since that's what docker client has
|
|
||||||
# but in python exec is a keyword
|
|
||||||
if command == "exec":
|
|
||||||
command = "exec_command"
|
|
||||||
|
|
||||||
if not hasattr(command_class, command):
|
|
||||||
raise NoSuchCommand(command, command_class)
|
|
||||||
|
|
||||||
return getattr(command_class, command)
|
|
||||||
|
|
||||||
|
|
||||||
class NoSuchCommand(Exception):
|
class NoSuchCommand(Exception):
|
||||||
def __init__(self, command, supercommand):
|
def __init__(self, command, supercommand):
|
||||||
super(NoSuchCommand, self).__init__("No such command: %s" % command)
|
super(NoSuchCommand, self).__init__("No such command: %s" % command)
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,8 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import contextlib
|
|
||||||
import logging
|
|
||||||
import socket
|
|
||||||
from distutils.spawn import find_executable
|
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
|
||||||
from docker.errors import APIError
|
|
||||||
from requests.exceptions import ConnectionError as RequestsConnectionError
|
|
||||||
from requests.exceptions import ReadTimeout
|
|
||||||
from requests.exceptions import SSLError
|
|
||||||
from requests.packages.urllib3.exceptions import ReadTimeoutError
|
|
||||||
|
|
||||||
from ..const import API_VERSION_TO_ENGINE_VERSION
|
|
||||||
from .utils import is_docker_for_mac_installed
|
|
||||||
from .utils import is_mac
|
|
||||||
from .utils import is_ubuntu
|
|
||||||
from .utils import is_windows
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class UserError(Exception):
|
class UserError(Exception):
|
||||||
|
|
||||||
def __init__(self, msg):
|
def __init__(self, msg):
|
||||||
self.msg = dedent(msg).strip()
|
self.msg = dedent(msg).strip()
|
||||||
|
|
||||||
|
|
@ -34,104 +12,53 @@ class UserError(Exception):
|
||||||
__str__ = __unicode__
|
__str__ = __unicode__
|
||||||
|
|
||||||
|
|
||||||
class ConnectionError(Exception):
|
class DockerNotFoundMac(UserError):
|
||||||
pass
|
def __init__(self):
|
||||||
|
super(DockerNotFoundMac, self).__init__("""
|
||||||
|
Couldn't connect to Docker daemon. You might need to install docker-osx:
|
||||||
|
|
||||||
|
https://github.com/noplay/docker-osx
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
class DockerNotFoundUbuntu(UserError):
|
||||||
def handle_connection_errors(client):
|
def __init__(self):
|
||||||
try:
|
super(DockerNotFoundUbuntu, self).__init__("""
|
||||||
yield
|
Couldn't connect to Docker daemon. You might need to install Docker:
|
||||||
except SSLError as e:
|
|
||||||
log.error('SSL error: %s' % e)
|
http://docs.docker.io/en/latest/installation/ubuntulinux/
|
||||||
raise ConnectionError()
|
""")
|
||||||
except RequestsConnectionError as e:
|
|
||||||
if e.args and isinstance(e.args[0], ReadTimeoutError):
|
|
||||||
log_timeout_error(client.timeout)
|
|
||||||
raise ConnectionError()
|
|
||||||
exit_with_error(get_conn_error_message(client.base_url))
|
|
||||||
except APIError as e:
|
|
||||||
log_api_error(e, client.api_version)
|
|
||||||
raise ConnectionError()
|
|
||||||
except (ReadTimeout, socket.timeout) as e:
|
|
||||||
log_timeout_error(client.timeout)
|
|
||||||
raise ConnectionError()
|
|
||||||
|
|
||||||
|
|
||||||
def log_timeout_error(timeout):
|
class DockerNotFoundGeneric(UserError):
|
||||||
log.error(
|
def __init__(self):
|
||||||
"An HTTP request took too long to complete. Retry with --verbose to "
|
super(DockerNotFoundGeneric, self).__init__("""
|
||||||
"obtain debug information.\n"
|
Couldn't connect to Docker daemon. You might need to install Docker:
|
||||||
"If you encounter this issue regularly because of slow network "
|
|
||||||
"conditions, consider setting COMPOSE_HTTP_TIMEOUT to a higher "
|
http://docs.docker.io/en/latest/installation/
|
||||||
"value (current value: %s)." % timeout)
|
""")
|
||||||
|
|
||||||
|
|
||||||
def log_api_error(e, client_version):
|
class ConnectionErrorBoot2Docker(UserError):
|
||||||
if b'client is newer than server' not in e.explanation:
|
def __init__(self):
|
||||||
log.error(e.explanation)
|
super(ConnectionErrorBoot2Docker, self).__init__("""
|
||||||
return
|
Couldn't connect to Docker daemon - you might need to run `boot2docker up`.
|
||||||
|
""")
|
||||||
version = API_VERSION_TO_ENGINE_VERSION.get(client_version)
|
|
||||||
if not version:
|
|
||||||
# They've set a custom API version
|
|
||||||
log.error(e.explanation)
|
|
||||||
return
|
|
||||||
|
|
||||||
log.error(
|
|
||||||
"The Docker Engine version is less than the minimum required by "
|
|
||||||
"Compose. Your current project requires a Docker Engine of "
|
|
||||||
"version {version} or greater.".format(version=version))
|
|
||||||
|
|
||||||
|
|
||||||
def exit_with_error(msg):
|
class ConnectionErrorGeneric(UserError):
|
||||||
log.error(dedent(msg).strip())
|
def __init__(self, url):
|
||||||
raise ConnectionError()
|
super(ConnectionErrorGeneric, self).__init__("""
|
||||||
|
Couldn't connect to Docker daemon at %s - is it running?
|
||||||
|
|
||||||
def get_conn_error_message(url):
|
|
||||||
if find_executable('docker') is None:
|
|
||||||
return docker_not_found_msg("Couldn't connect to Docker daemon.")
|
|
||||||
if is_docker_for_mac_installed():
|
|
||||||
return conn_error_docker_for_mac
|
|
||||||
if find_executable('docker-machine') is not None:
|
|
||||||
return conn_error_docker_machine
|
|
||||||
return conn_error_generic.format(url=url)
|
|
||||||
|
|
||||||
|
|
||||||
def docker_not_found_msg(problem):
|
|
||||||
return "{} You might need to install Docker:\n\n{}".format(
|
|
||||||
problem, docker_install_url())
|
|
||||||
|
|
||||||
|
|
||||||
def docker_install_url():
|
|
||||||
if is_mac():
|
|
||||||
return docker_install_url_mac
|
|
||||||
elif is_ubuntu():
|
|
||||||
return docker_install_url_ubuntu
|
|
||||||
elif is_windows():
|
|
||||||
return docker_install_url_windows
|
|
||||||
else:
|
|
||||||
return docker_install_url_generic
|
|
||||||
|
|
||||||
|
|
||||||
docker_install_url_mac = "https://docs.docker.com/engine/installation/mac/"
|
|
||||||
docker_install_url_ubuntu = "https://docs.docker.com/engine/installation/ubuntulinux/"
|
|
||||||
docker_install_url_windows = "https://docs.docker.com/engine/installation/windows/"
|
|
||||||
docker_install_url_generic = "https://docs.docker.com/engine/installation/"
|
|
||||||
|
|
||||||
|
|
||||||
conn_error_docker_machine = """
|
|
||||||
Couldn't connect to Docker daemon - you might need to run `docker-machine start default`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
conn_error_docker_for_mac = """
|
|
||||||
Couldn't connect to Docker daemon. You might need to start Docker for Mac.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
conn_error_generic = """
|
|
||||||
Couldn't connect to Docker daemon at {url} - is it running?
|
|
||||||
|
|
||||||
If it's at a non-standard location, specify the URL with the DOCKER_HOST environment variable.
|
If it's at a non-standard location, specify the URL with the DOCKER_HOST environment variable.
|
||||||
"""
|
""" % url)
|
||||||
|
|
||||||
|
|
||||||
|
class ComposeFileNotFound(UserError):
|
||||||
|
def __init__(self, supported_filenames):
|
||||||
|
super(ComposeFileNotFound, self).__init__("""
|
||||||
|
Can't find a suitable configuration file. Are you in the right directory?
|
||||||
|
|
||||||
|
Supported filenames: %s
|
||||||
|
""" % ", ".join(supported_filenames))
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,18 @@
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
from __future__ import absolute_import
|
||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import six
|
|
||||||
import texttable
|
import texttable
|
||||||
|
|
||||||
from compose.cli import colors
|
|
||||||
|
|
||||||
|
|
||||||
def get_tty_width():
|
def get_tty_width():
|
||||||
tty_size = os.popen('stty size', 'r').read().split()
|
tty_size = os.popen('stty size', 'r').read().split()
|
||||||
if len(tty_size) != 2:
|
if len(tty_size) != 2:
|
||||||
return 0
|
return 80
|
||||||
_, width = tty_size
|
_, width = tty_size
|
||||||
return int(width)
|
return int(width)
|
||||||
|
|
||||||
|
|
||||||
class Formatter(object):
|
class Formatter(object):
|
||||||
"""Format tabular data for printing."""
|
|
||||||
def table(self, headers, rows):
|
def table(self, headers, rows):
|
||||||
table = texttable.Texttable(max_width=get_tty_width())
|
table = texttable.Texttable(max_width=get_tty_width())
|
||||||
table.set_cols_dtype(['t' for h in headers])
|
table.set_cols_dtype(['t' for h in headers])
|
||||||
|
|
@ -28,24 +21,3 @@ class Formatter(object):
|
||||||
table.set_chars(['-', '|', '+', '-'])
|
table.set_chars(['-', '|', '+', '-'])
|
||||||
|
|
||||||
return table.draw()
|
return table.draw()
|
||||||
|
|
||||||
|
|
||||||
class ConsoleWarningFormatter(logging.Formatter):
|
|
||||||
"""A logging.Formatter which prints WARNING and ERROR messages with
|
|
||||||
a prefix of the log level colored appropriate for the log level.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_level_message(self, record):
|
|
||||||
separator = ': '
|
|
||||||
if record.levelno == logging.WARNING:
|
|
||||||
return colors.yellow(record.levelname) + separator
|
|
||||||
if record.levelno == logging.ERROR:
|
|
||||||
return colors.red(record.levelname) + separator
|
|
||||||
|
|
||||||
return ''
|
|
||||||
|
|
||||||
def format(self, record):
|
|
||||||
if isinstance(record.msg, six.binary_type):
|
|
||||||
record.msg = record.msg.decode('utf-8')
|
|
||||||
message = super(ConsoleWarningFormatter, self).format(record)
|
|
||||||
return '{0}{1}'.format(self.get_level_message(record), message)
|
|
||||||
|
|
|
||||||
|
|
@ -1,237 +1,82 @@
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
from __future__ import absolute_import
|
||||||
import sys
|
import sys
|
||||||
from collections import namedtuple
|
|
||||||
from itertools import cycle
|
from itertools import cycle
|
||||||
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 .multiplexer import Multiplexer, STOP
|
||||||
from . import colors
|
from . import colors
|
||||||
from compose import utils
|
from .utils import split_buffer
|
||||||
from compose.cli.signals import ShutdownException
|
|
||||||
from compose.utils import split_buffer
|
|
||||||
|
|
||||||
|
|
||||||
class LogPresenter(object):
|
class LogPrinter(object):
|
||||||
|
def __init__(self, containers, attach_params=None, output=sys.stdout, monochrome=False):
|
||||||
|
self.containers = containers
|
||||||
|
self.attach_params = attach_params or {}
|
||||||
|
self.prefix_width = self._calculate_prefix_width(containers)
|
||||||
|
self.generators = self._make_log_generators(monochrome)
|
||||||
|
self.output = output
|
||||||
|
|
||||||
def __init__(self, prefix_width, color_func):
|
def run(self):
|
||||||
self.prefix_width = prefix_width
|
mux = Multiplexer(self.generators)
|
||||||
self.color_func = color_func
|
for line in mux.loop():
|
||||||
|
self.output.write(line)
|
||||||
|
|
||||||
def present(self, container, line):
|
def _calculate_prefix_width(self, containers):
|
||||||
prefix = container.name_without_project.ljust(self.prefix_width)
|
|
||||||
return '{prefix} {line}'.format(
|
|
||||||
prefix=self.color_func(prefix + ' |'),
|
|
||||||
line=line)
|
|
||||||
|
|
||||||
|
|
||||||
def build_log_presenters(service_names, monochrome):
|
|
||||||
"""Return an iterable of functions.
|
|
||||||
|
|
||||||
Each function can be used to format the logs output of a container.
|
|
||||||
"""
|
"""
|
||||||
prefix_width = max_name_width(service_names)
|
Calculate the maximum width of container names so we can make the log
|
||||||
|
|
||||||
def no_color(text):
|
|
||||||
return text
|
|
||||||
|
|
||||||
for color_func in cycle([no_color] if monochrome else colors.rainbow()):
|
|
||||||
yield LogPresenter(prefix_width, color_func)
|
|
||||||
|
|
||||||
|
|
||||||
def max_name_width(service_names, max_index_width=3):
|
|
||||||
"""Calculate the maximum width of container names so we can make the log
|
|
||||||
prefixes line up like so:
|
prefixes line up like so:
|
||||||
|
|
||||||
db_1 | Listening
|
db_1 | Listening
|
||||||
web_1 | Listening
|
web_1 | Listening
|
||||||
"""
|
"""
|
||||||
return max(len(name) for name in service_names) + max_index_width
|
prefix_width = 0
|
||||||
|
for container in containers:
|
||||||
|
prefix_width = max(prefix_width, len(container.name_without_project))
|
||||||
|
return prefix_width
|
||||||
|
|
||||||
|
def _make_log_generators(self, monochrome):
|
||||||
|
color_fns = cycle(colors.rainbow())
|
||||||
|
generators = []
|
||||||
|
|
||||||
class LogPrinter(object):
|
def no_color(text):
|
||||||
"""Print logs from many containers to a single output stream."""
|
return text
|
||||||
|
|
||||||
def __init__(self,
|
for container in self.containers:
|
||||||
containers,
|
if monochrome:
|
||||||
presenters,
|
color_fn = no_color
|
||||||
event_stream,
|
|
||||||
output=sys.stdout,
|
|
||||||
cascade_stop=False,
|
|
||||||
log_args=None):
|
|
||||||
self.containers = containers
|
|
||||||
self.presenters = presenters
|
|
||||||
self.event_stream = event_stream
|
|
||||||
self.output = utils.get_output_stream(output)
|
|
||||||
self.cascade_stop = cascade_stop
|
|
||||||
self.log_args = log_args or {}
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
if not self.containers:
|
|
||||||
return
|
|
||||||
|
|
||||||
queue = Queue()
|
|
||||||
thread_args = queue, self.log_args
|
|
||||||
thread_map = build_thread_map(self.containers, self.presenters, thread_args)
|
|
||||||
start_producer_thread((
|
|
||||||
thread_map,
|
|
||||||
self.event_stream,
|
|
||||||
self.presenters,
|
|
||||||
thread_args))
|
|
||||||
|
|
||||||
for line in consume_queue(queue, self.cascade_stop):
|
|
||||||
remove_stopped_threads(thread_map)
|
|
||||||
|
|
||||||
if not line:
|
|
||||||
if not thread_map:
|
|
||||||
# There are no running containers left to tail, so exit
|
|
||||||
return
|
|
||||||
# We got an empty line because of a timeout, but there are still
|
|
||||||
# active containers to tail, so continue
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.output.write(line)
|
|
||||||
self.output.flush()
|
|
||||||
|
|
||||||
|
|
||||||
def remove_stopped_threads(thread_map):
|
|
||||||
for container_id, tailer_thread in list(thread_map.items()):
|
|
||||||
if not tailer_thread.is_alive():
|
|
||||||
thread_map.pop(container_id, None)
|
|
||||||
|
|
||||||
|
|
||||||
def build_thread(container, presenter, queue, log_args):
|
|
||||||
tailer = Thread(
|
|
||||||
target=tail_container_logs,
|
|
||||||
args=(container, presenter, queue, log_args))
|
|
||||||
tailer.daemon = True
|
|
||||||
tailer.start()
|
|
||||||
return tailer
|
|
||||||
|
|
||||||
|
|
||||||
def build_thread_map(initial_containers, presenters, thread_args):
|
|
||||||
return {
|
|
||||||
container.id: build_thread(container, next(presenters), *thread_args)
|
|
||||||
for container in initial_containers
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class QueueItem(namedtuple('_QueueItem', 'item is_stop exc')):
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def new(cls, item):
|
|
||||||
return cls(item, None, None)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def exception(cls, exc):
|
|
||||||
return cls(None, None, exc)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def stop(cls):
|
|
||||||
return cls(None, True, None)
|
|
||||||
|
|
||||||
|
|
||||||
def tail_container_logs(container, presenter, queue, log_args):
|
|
||||||
generator = get_log_generator(container)
|
|
||||||
|
|
||||||
try:
|
|
||||||
for item in generator(container, log_args):
|
|
||||||
queue.put(QueueItem.new(presenter.present(container, item)))
|
|
||||||
except Exception as e:
|
|
||||||
queue.put(QueueItem.exception(e))
|
|
||||||
return
|
|
||||||
|
|
||||||
if log_args.get('follow'):
|
|
||||||
queue.put(QueueItem.new(presenter.color_func(wait_on_exit(container))))
|
|
||||||
queue.put(QueueItem.stop())
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
if container.log_stream is None:
|
|
||||||
stream = container.logs(stdout=True, stderr=True, stream=True, **log_args)
|
|
||||||
else:
|
else:
|
||||||
stream = container.log_stream
|
color_fn = next(color_fns)
|
||||||
|
generators.append(self._make_log_generator(container, color_fn))
|
||||||
|
|
||||||
return split_buffer(stream)
|
return generators
|
||||||
|
|
||||||
|
def _make_log_generator(self, container, color_fn):
|
||||||
|
prefix = color_fn(self._generate_prefix(container)).encode('utf-8')
|
||||||
|
# Attach to container before log printer starts running
|
||||||
|
line_generator = split_buffer(self._attach(container), '\n')
|
||||||
|
|
||||||
|
for line in line_generator:
|
||||||
|
yield prefix + line
|
||||||
|
|
||||||
def wait_on_exit(container):
|
|
||||||
try:
|
|
||||||
exit_code = container.wait()
|
exit_code = container.wait()
|
||||||
return "%s exited with code %s\n" % (container.name, exit_code)
|
yield color_fn("%s exited with code %s\n" % (container.name, exit_code))
|
||||||
except APIError as e:
|
yield STOP
|
||||||
return "Unexpected API error for %s (HTTP code %s)\nResponse body:\n%s\n" % (
|
|
||||||
container.name, e.response.status_code,
|
|
||||||
e.response.text or '[empty]'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
def _generate_prefix(self, container):
|
||||||
|
"""
|
||||||
|
Generate the prefix for a log line without colour
|
||||||
|
"""
|
||||||
|
name = container.name_without_project
|
||||||
|
padding = ' ' * (self.prefix_width - len(name))
|
||||||
|
return ''.join([name, padding, ' | '])
|
||||||
|
|
||||||
def start_producer_thread(thread_args):
|
def _attach(self, container):
|
||||||
producer = Thread(target=watch_events, args=thread_args)
|
params = {
|
||||||
producer.daemon = True
|
'stdout': True,
|
||||||
producer.start()
|
'stderr': True,
|
||||||
|
'stream': True,
|
||||||
|
}
|
||||||
def watch_events(thread_map, event_stream, presenters, thread_args):
|
params.update(self.attach_params)
|
||||||
for event in event_stream:
|
params = dict((name, 1 if value else 0) for (name, value) in list(params.items()))
|
||||||
if event['action'] == 'stop':
|
return container.attach(**params)
|
||||||
thread_map.pop(event['id'], None)
|
|
||||||
|
|
||||||
if event['action'] != 'start':
|
|
||||||
continue
|
|
||||||
|
|
||||||
if event['id'] in thread_map:
|
|
||||||
if thread_map[event['id']].is_alive():
|
|
||||||
continue
|
|
||||||
# Container was stopped and started, we need a new thread
|
|
||||||
thread_map.pop(event['id'], None)
|
|
||||||
|
|
||||||
thread_map[event['id']] = build_thread(
|
|
||||||
event['container'],
|
|
||||||
next(presenters),
|
|
||||||
*thread_args)
|
|
||||||
|
|
||||||
|
|
||||||
def consume_queue(queue, cascade_stop):
|
|
||||||
"""Consume the queue by reading lines off of it and yielding them."""
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
item = queue.get(timeout=0.1)
|
|
||||||
except Empty:
|
|
||||||
yield None
|
|
||||||
continue
|
|
||||||
# See https://github.com/docker/compose/issues/189
|
|
||||||
except thread.error:
|
|
||||||
raise ShutdownException()
|
|
||||||
|
|
||||||
if item.exc:
|
|
||||||
raise item.exc
|
|
||||||
|
|
||||||
if item.is_stop:
|
|
||||||
if cascade_stop:
|
|
||||||
raise StopIteration
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
|
|
||||||
yield item.item
|
|
||||||
|
|
|
||||||
1019
compose/cli/main.py
1019
compose/cli/main.py
File diff suppressed because it is too large
Load diff
42
compose/cli/multiplexer.py
Normal file
42
compose/cli/multiplexer.py
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
try:
|
||||||
|
from Queue import Queue, Empty
|
||||||
|
except ImportError:
|
||||||
|
from queue import Queue, Empty # Python 3.x
|
||||||
|
|
||||||
|
|
||||||
|
# Yield STOP from an input generator to stop the
|
||||||
|
# top-level loop without processing any more input.
|
||||||
|
STOP = object()
|
||||||
|
|
||||||
|
|
||||||
|
class Multiplexer(object):
|
||||||
|
def __init__(self, generators):
|
||||||
|
self.generators = generators
|
||||||
|
self.queue = Queue()
|
||||||
|
|
||||||
|
def loop(self):
|
||||||
|
self._init_readers()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
item = self.queue.get(timeout=0.1)
|
||||||
|
if item is STOP:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
yield item
|
||||||
|
except Empty:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _init_readers(self):
|
||||||
|
for generator in self.generators:
|
||||||
|
t = Thread(target=_enqueue_output, args=(generator, self.queue))
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
|
||||||
|
def _enqueue_output(generator, queue):
|
||||||
|
for item in generator:
|
||||||
|
queue.put(item)
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import signal
|
|
||||||
|
|
||||||
|
|
||||||
class ShutdownException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def shutdown(signal, frame):
|
|
||||||
raise ShutdownException()
|
|
||||||
|
|
||||||
|
|
||||||
def set_signal_handler(handler):
|
|
||||||
signal.signal(signal.SIGINT, handler)
|
|
||||||
signal.signal(signal.SIGTERM, handler)
|
|
||||||
|
|
||||||
|
|
||||||
def set_signal_handler_to_shutdown():
|
|
||||||
set_signal_handler(shutdown)
|
|
||||||
|
|
@ -1,24 +1,10 @@
|
||||||
|
from __future__ import unicode_literals
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from __future__ import division
|
from __future__ import division
|
||||||
from __future__ import unicode_literals
|
import datetime
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import platform
|
|
||||||
import ssl
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import platform
|
||||||
|
|
||||||
import docker
|
|
||||||
|
|
||||||
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):
|
def yesno(prompt, default=None):
|
||||||
|
|
@ -31,7 +17,7 @@ def yesno(prompt, default=None):
|
||||||
Unrecognised input (anything other than "y", "n", "yes",
|
Unrecognised input (anything other than "y", "n", "yes",
|
||||||
"no" or "") will return None.
|
"no" or "") will return None.
|
||||||
"""
|
"""
|
||||||
answer = input(prompt).strip().lower()
|
answer = raw_input(prompt).strip().lower()
|
||||||
|
|
||||||
if answer == "y" or answer == "yes":
|
if answer == "y" or answer == "yes":
|
||||||
return True
|
return True
|
||||||
|
|
@ -43,14 +29,62 @@ def yesno(prompt, default=None):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def input(prompt):
|
# http://stackoverflow.com/a/5164027
|
||||||
|
def prettydate(d):
|
||||||
|
diff = datetime.datetime.utcnow() - d
|
||||||
|
s = diff.seconds
|
||||||
|
if diff.days > 7 or diff.days < 0:
|
||||||
|
return d.strftime('%d %b %y')
|
||||||
|
elif diff.days == 1:
|
||||||
|
return '1 day ago'
|
||||||
|
elif diff.days > 1:
|
||||||
|
return '{0} days ago'.format(diff.days)
|
||||||
|
elif s <= 1:
|
||||||
|
return 'just now'
|
||||||
|
elif s < 60:
|
||||||
|
return '{0} seconds ago'.format(s)
|
||||||
|
elif s < 120:
|
||||||
|
return '1 minute ago'
|
||||||
|
elif s < 3600:
|
||||||
|
return '{0} minutes ago'.format(s / 60)
|
||||||
|
elif s < 7200:
|
||||||
|
return '1 hour ago'
|
||||||
|
else:
|
||||||
|
return '{0} hours ago'.format(s / 3600)
|
||||||
|
|
||||||
|
|
||||||
|
def mkdir(path, permissions=0o700):
|
||||||
|
if not os.path.exists(path):
|
||||||
|
os.mkdir(path)
|
||||||
|
|
||||||
|
os.chmod(path, permissions)
|
||||||
|
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def split_buffer(reader, separator):
|
||||||
"""
|
"""
|
||||||
Version of input (raw_input in Python 2) which forces a flush of sys.stdout
|
Given a generator which yields strings and a separator string,
|
||||||
to avoid problems where the prompt fails to appear due to line buffering
|
joins all input, splits on the separator and yields each chunk.
|
||||||
|
|
||||||
|
Unlike string.split(), each chunk includes the trailing
|
||||||
|
separator, except for the last one if none was found on the end
|
||||||
|
of the input.
|
||||||
"""
|
"""
|
||||||
sys.stdout.write(prompt)
|
buffered = str('')
|
||||||
sys.stdout.flush()
|
separator = str(separator)
|
||||||
return sys.stdin.readline().rstrip('\n')
|
|
||||||
|
for data in reader:
|
||||||
|
buffered += data
|
||||||
|
while True:
|
||||||
|
index = buffered.find(separator)
|
||||||
|
if index == -1:
|
||||||
|
break
|
||||||
|
yield buffered[:index + 1]
|
||||||
|
buffered = buffered[index + 1:]
|
||||||
|
|
||||||
|
if len(buffered) > 0:
|
||||||
|
yield buffered
|
||||||
|
|
||||||
|
|
||||||
def call_silently(*args, **kwargs):
|
def call_silently(*args, **kwargs):
|
||||||
|
|
@ -58,12 +92,7 @@ def call_silently(*args, **kwargs):
|
||||||
Like subprocess.call(), but redirects stdout and stderr to /dev/null.
|
Like subprocess.call(), but redirects stdout and stderr to /dev/null.
|
||||||
"""
|
"""
|
||||||
with open(os.devnull, 'w') as shutup:
|
with open(os.devnull, 'w') as shutup:
|
||||||
try:
|
|
||||||
return subprocess.call(*args, stdout=shutup, stderr=shutup, **kwargs)
|
return subprocess.call(*args, stdout=shutup, stderr=shutup, **kwargs)
|
||||||
except WindowsError:
|
|
||||||
# On Windows, subprocess.call() can still raise exceptions. Normalize
|
|
||||||
# to POSIXy behaviour by returning a nonzero exit code.
|
|
||||||
return 1
|
|
||||||
|
|
||||||
|
|
||||||
def is_mac():
|
def is_mac():
|
||||||
|
|
@ -72,66 +101,3 @@ def is_mac():
|
||||||
|
|
||||||
def is_ubuntu():
|
def is_ubuntu():
|
||||||
return platform.system() == 'Linux' and platform.linux_distribution()[0] == 'Ubuntu'
|
return platform.system() == 'Linux' and platform.linux_distribution()[0] == 'Ubuntu'
|
||||||
|
|
||||||
|
|
||||||
def is_windows():
|
|
||||||
return IS_WINDOWS_PLATFORM
|
|
||||||
|
|
||||||
|
|
||||||
def get_version_info(scope):
|
|
||||||
versioninfo = 'docker-compose version {}, build {}'.format(
|
|
||||||
compose.__version__,
|
|
||||||
get_build_version())
|
|
||||||
|
|
||||||
if scope == 'compose':
|
|
||||||
return versioninfo
|
|
||||||
if scope == 'full':
|
|
||||||
return (
|
|
||||||
"{}\n"
|
|
||||||
"docker-py version: {}\n"
|
|
||||||
"{} version: {}\n"
|
|
||||||
"OpenSSL version: {}"
|
|
||||||
).format(
|
|
||||||
versioninfo,
|
|
||||||
docker.version,
|
|
||||||
platform.python_implementation(),
|
|
||||||
platform.python_version(),
|
|
||||||
ssl.OPENSSL_VERSION)
|
|
||||||
|
|
||||||
raise ValueError("{} is not a valid version scope".format(scope))
|
|
||||||
|
|
||||||
|
|
||||||
def get_build_version():
|
|
||||||
filename = os.path.join(os.path.dirname(compose.__file__), 'GITSHA')
|
|
||||||
if not os.path.exists(filename):
|
|
||||||
return 'unknown'
|
|
||||||
|
|
||||||
with open(filename) as fh:
|
|
||||||
return fh.read().strip()
|
|
||||||
|
|
||||||
|
|
||||||
def is_docker_for_mac_installed():
|
|
||||||
return is_mac() and os.path.isdir('/Applications/Docker.app')
|
|
||||||
|
|
||||||
|
|
||||||
def generate_user_agent():
|
|
||||||
parts = [
|
|
||||||
"docker-compose/{}".format(compose.__version__),
|
|
||||||
"docker-py/{}".format(docker.__version__),
|
|
||||||
]
|
|
||||||
try:
|
|
||||||
p_system = platform.system()
|
|
||||||
p_release = platform.release()
|
|
||||||
except IOError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
parts.append("{}/{}".format(p_system, p_release))
|
|
||||||
return " ".join(parts)
|
|
||||||
|
|
||||||
|
|
||||||
def unquote_path(s):
|
|
||||||
if not s:
|
|
||||||
return s
|
|
||||||
if s[0] == '"' and s[-1] == '"':
|
|
||||||
return s[1:-1]
|
|
||||||
return s
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
|
from itertools import chain
|
||||||
import logging
|
import logging
|
||||||
import pprint
|
import pprint
|
||||||
from itertools import chain
|
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
|
|
||||||
432
compose/config.py
Normal file
432
compose/config.py
Normal file
|
|
@ -0,0 +1,432 @@
|
||||||
|
import os
|
||||||
|
import yaml
|
||||||
|
import six
|
||||||
|
|
||||||
|
|
||||||
|
DOCKER_CONFIG_KEYS = [
|
||||||
|
'cap_add',
|
||||||
|
'cap_drop',
|
||||||
|
'cpu_shares',
|
||||||
|
'command',
|
||||||
|
'detach',
|
||||||
|
'dns',
|
||||||
|
'dns_search',
|
||||||
|
'domainname',
|
||||||
|
'entrypoint',
|
||||||
|
'env_file',
|
||||||
|
'environment',
|
||||||
|
'hostname',
|
||||||
|
'image',
|
||||||
|
'links',
|
||||||
|
'mem_limit',
|
||||||
|
'net',
|
||||||
|
'ports',
|
||||||
|
'privileged',
|
||||||
|
'restart',
|
||||||
|
'stdin_open',
|
||||||
|
'tty',
|
||||||
|
'user',
|
||||||
|
'volumes',
|
||||||
|
'volumes_from',
|
||||||
|
'working_dir',
|
||||||
|
]
|
||||||
|
|
||||||
|
ALLOWED_KEYS = DOCKER_CONFIG_KEYS + [
|
||||||
|
'build',
|
||||||
|
'expose',
|
||||||
|
'external_links',
|
||||||
|
'name',
|
||||||
|
]
|
||||||
|
|
||||||
|
DOCKER_CONFIG_HINTS = {
|
||||||
|
'cpu_share' : 'cpu_shares',
|
||||||
|
'link' : 'links',
|
||||||
|
'port' : 'ports',
|
||||||
|
'privilege' : 'privileged',
|
||||||
|
'priviliged': 'privileged',
|
||||||
|
'privilige' : 'privileged',
|
||||||
|
'volume' : 'volumes',
|
||||||
|
'workdir' : 'working_dir',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load(filename):
|
||||||
|
working_dir = os.path.dirname(filename)
|
||||||
|
return from_dictionary(load_yaml(filename), working_dir=working_dir, filename=filename)
|
||||||
|
|
||||||
|
|
||||||
|
def from_dictionary(dictionary, working_dir=None, filename=None):
|
||||||
|
service_dicts = []
|
||||||
|
|
||||||
|
for service_name, service_dict in list(dictionary.items()):
|
||||||
|
if not isinstance(service_dict, dict):
|
||||||
|
raise ConfigurationError('Service "%s" doesn\'t have any configuration options. All top level keys in your docker-compose.yml must map to a dictionary of configuration options.' % service_name)
|
||||||
|
loader = ServiceLoader(working_dir=working_dir, filename=filename)
|
||||||
|
service_dict = loader.make_service_dict(service_name, service_dict)
|
||||||
|
service_dicts.append(service_dict)
|
||||||
|
|
||||||
|
return service_dicts
|
||||||
|
|
||||||
|
|
||||||
|
def make_service_dict(name, service_dict, working_dir=None):
|
||||||
|
return ServiceLoader(working_dir=working_dir).make_service_dict(name, service_dict)
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceLoader(object):
|
||||||
|
def __init__(self, working_dir, filename=None, already_seen=None):
|
||||||
|
self.working_dir = working_dir
|
||||||
|
self.filename = filename
|
||||||
|
self.already_seen = already_seen or []
|
||||||
|
|
||||||
|
def make_service_dict(self, name, service_dict):
|
||||||
|
if self.signature(name) in self.already_seen:
|
||||||
|
raise CircularReference(self.already_seen)
|
||||||
|
|
||||||
|
service_dict = service_dict.copy()
|
||||||
|
service_dict['name'] = name
|
||||||
|
service_dict = resolve_environment(service_dict, working_dir=self.working_dir)
|
||||||
|
service_dict = self.resolve_extends(service_dict)
|
||||||
|
return process_container_options(service_dict, working_dir=self.working_dir)
|
||||||
|
|
||||||
|
def resolve_extends(self, service_dict):
|
||||||
|
if 'extends' not in service_dict:
|
||||||
|
return service_dict
|
||||||
|
|
||||||
|
extends_options = process_extends_options(service_dict['name'], service_dict['extends'])
|
||||||
|
|
||||||
|
if self.working_dir is None:
|
||||||
|
raise Exception("No working_dir passed to ServiceLoader()")
|
||||||
|
|
||||||
|
other_config_path = expand_path(self.working_dir, extends_options['file'])
|
||||||
|
other_working_dir = os.path.dirname(other_config_path)
|
||||||
|
other_already_seen = self.already_seen + [self.signature(service_dict['name'])]
|
||||||
|
other_loader = ServiceLoader(
|
||||||
|
working_dir=other_working_dir,
|
||||||
|
filename=other_config_path,
|
||||||
|
already_seen=other_already_seen,
|
||||||
|
)
|
||||||
|
|
||||||
|
other_config = load_yaml(other_config_path)
|
||||||
|
other_service_dict = other_config[extends_options['service']]
|
||||||
|
other_service_dict = other_loader.make_service_dict(
|
||||||
|
service_dict['name'],
|
||||||
|
other_service_dict,
|
||||||
|
)
|
||||||
|
validate_extended_service_dict(
|
||||||
|
other_service_dict,
|
||||||
|
filename=other_config_path,
|
||||||
|
service=extends_options['service'],
|
||||||
|
)
|
||||||
|
|
||||||
|
return merge_service_dicts(other_service_dict, service_dict)
|
||||||
|
|
||||||
|
def signature(self, name):
|
||||||
|
return (self.filename, name)
|
||||||
|
|
||||||
|
|
||||||
|
def process_extends_options(service_name, extends_options):
|
||||||
|
error_prefix = "Invalid 'extends' configuration for %s:" % service_name
|
||||||
|
|
||||||
|
if not isinstance(extends_options, dict):
|
||||||
|
raise ConfigurationError("%s must be a dictionary" % error_prefix)
|
||||||
|
|
||||||
|
if 'service' not in extends_options:
|
||||||
|
raise ConfigurationError(
|
||||||
|
"%s you need to specify a service, e.g. 'service: web'" % error_prefix
|
||||||
|
)
|
||||||
|
|
||||||
|
for k, _ in extends_options.items():
|
||||||
|
if k not in ['file', 'service']:
|
||||||
|
raise ConfigurationError(
|
||||||
|
"%s unsupported configuration option '%s'" % (error_prefix, k)
|
||||||
|
)
|
||||||
|
|
||||||
|
return extends_options
|
||||||
|
|
||||||
|
|
||||||
|
def validate_extended_service_dict(service_dict, filename, service):
|
||||||
|
error_prefix = "Cannot extend service '%s' in %s:" % (service, filename)
|
||||||
|
|
||||||
|
if 'links' in service_dict:
|
||||||
|
raise ConfigurationError("%s services with 'links' cannot be extended" % error_prefix)
|
||||||
|
|
||||||
|
if 'volumes_from' in service_dict:
|
||||||
|
raise ConfigurationError("%s services with 'volumes_from' cannot be extended" % error_prefix)
|
||||||
|
|
||||||
|
if 'net' in service_dict:
|
||||||
|
if get_service_name_from_net(service_dict['net']) is not None:
|
||||||
|
raise ConfigurationError("%s services with 'net: container' cannot be extended" % error_prefix)
|
||||||
|
|
||||||
|
|
||||||
|
def process_container_options(service_dict, working_dir=None):
|
||||||
|
for k in service_dict:
|
||||||
|
if k not in ALLOWED_KEYS:
|
||||||
|
msg = "Unsupported config option for %s service: '%s'" % (service_dict['name'], k)
|
||||||
|
if k in DOCKER_CONFIG_HINTS:
|
||||||
|
msg += " (did you mean '%s'?)" % DOCKER_CONFIG_HINTS[k]
|
||||||
|
raise ConfigurationError(msg)
|
||||||
|
|
||||||
|
service_dict = service_dict.copy()
|
||||||
|
|
||||||
|
if 'volumes' in service_dict:
|
||||||
|
service_dict['volumes'] = resolve_host_paths(service_dict['volumes'], working_dir=working_dir)
|
||||||
|
|
||||||
|
if 'build' in service_dict:
|
||||||
|
service_dict['build'] = resolve_build_path(service_dict['build'], working_dir=working_dir)
|
||||||
|
|
||||||
|
return service_dict
|
||||||
|
|
||||||
|
|
||||||
|
def merge_service_dicts(base, override):
|
||||||
|
d = base.copy()
|
||||||
|
|
||||||
|
if 'environment' in base or 'environment' in override:
|
||||||
|
d['environment'] = merge_environment(
|
||||||
|
base.get('environment'),
|
||||||
|
override.get('environment'),
|
||||||
|
)
|
||||||
|
|
||||||
|
if 'volumes' in base or 'volumes' in override:
|
||||||
|
d['volumes'] = merge_volumes(
|
||||||
|
base.get('volumes'),
|
||||||
|
override.get('volumes'),
|
||||||
|
)
|
||||||
|
|
||||||
|
if 'image' in override and 'build' in d:
|
||||||
|
del d['build']
|
||||||
|
|
||||||
|
if 'build' in override and 'image' in d:
|
||||||
|
del d['image']
|
||||||
|
|
||||||
|
list_keys = ['ports', 'expose', 'external_links']
|
||||||
|
|
||||||
|
for key in list_keys:
|
||||||
|
if key in base or key in override:
|
||||||
|
d[key] = base.get(key, []) + override.get(key, [])
|
||||||
|
|
||||||
|
list_or_string_keys = ['dns', 'dns_search']
|
||||||
|
|
||||||
|
for key in list_or_string_keys:
|
||||||
|
if key in base or key in override:
|
||||||
|
d[key] = to_list(base.get(key)) + to_list(override.get(key))
|
||||||
|
|
||||||
|
already_merged_keys = ['environment', 'volumes'] + list_keys + list_or_string_keys
|
||||||
|
|
||||||
|
for k in set(ALLOWED_KEYS) - set(already_merged_keys):
|
||||||
|
if k in override:
|
||||||
|
d[k] = override[k]
|
||||||
|
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def merge_environment(base, override):
|
||||||
|
env = parse_environment(base)
|
||||||
|
env.update(parse_environment(override))
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
def parse_links(links):
|
||||||
|
return dict(parse_link(l) for l in links)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_link(link):
|
||||||
|
if ':' in link:
|
||||||
|
source, alias = link.split(':', 1)
|
||||||
|
return (alias, source)
|
||||||
|
else:
|
||||||
|
return (link, link)
|
||||||
|
|
||||||
|
|
||||||
|
def get_env_files(options, working_dir=None):
|
||||||
|
if 'env_file' not in options:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
if working_dir is None:
|
||||||
|
raise Exception("No working_dir passed to get_env_files()")
|
||||||
|
|
||||||
|
env_files = options.get('env_file', [])
|
||||||
|
if not isinstance(env_files, list):
|
||||||
|
env_files = [env_files]
|
||||||
|
|
||||||
|
return [expand_path(working_dir, path) for path in env_files]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_environment(service_dict, working_dir=None):
|
||||||
|
service_dict = service_dict.copy()
|
||||||
|
|
||||||
|
if 'environment' not in service_dict and 'env_file' not in service_dict:
|
||||||
|
return service_dict
|
||||||
|
|
||||||
|
env = {}
|
||||||
|
|
||||||
|
if 'env_file' in service_dict:
|
||||||
|
for f in get_env_files(service_dict, working_dir=working_dir):
|
||||||
|
env.update(env_vars_from_file(f))
|
||||||
|
del service_dict['env_file']
|
||||||
|
|
||||||
|
env.update(parse_environment(service_dict.get('environment')))
|
||||||
|
env = dict(resolve_env_var(k, v) for k, v in six.iteritems(env))
|
||||||
|
|
||||||
|
service_dict['environment'] = env
|
||||||
|
return service_dict
|
||||||
|
|
||||||
|
|
||||||
|
def parse_environment(environment):
|
||||||
|
if not environment:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
if isinstance(environment, list):
|
||||||
|
return dict(split_env(e) for e in environment)
|
||||||
|
|
||||||
|
if isinstance(environment, dict):
|
||||||
|
return environment
|
||||||
|
|
||||||
|
raise ConfigurationError(
|
||||||
|
"environment \"%s\" must be a list or mapping," %
|
||||||
|
environment
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def split_env(env):
|
||||||
|
if '=' in env:
|
||||||
|
return env.split('=', 1)
|
||||||
|
else:
|
||||||
|
return env, None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_env_var(key, val):
|
||||||
|
if val is not None:
|
||||||
|
return key, val
|
||||||
|
elif key in os.environ:
|
||||||
|
return key, os.environ[key]
|
||||||
|
else:
|
||||||
|
return key, ''
|
||||||
|
|
||||||
|
|
||||||
|
def env_vars_from_file(filename):
|
||||||
|
"""
|
||||||
|
Read in a line delimited file of environment variables.
|
||||||
|
"""
|
||||||
|
if not os.path.exists(filename):
|
||||||
|
raise ConfigurationError("Couldn't find env file: %s" % filename)
|
||||||
|
env = {}
|
||||||
|
for line in open(filename, 'r'):
|
||||||
|
line = line.strip()
|
||||||
|
if line and not line.startswith('#'):
|
||||||
|
k, v = split_env(line)
|
||||||
|
env[k] = v
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_host_paths(volumes, working_dir=None):
|
||||||
|
if working_dir is None:
|
||||||
|
raise Exception("No working_dir passed to resolve_host_paths()")
|
||||||
|
|
||||||
|
return [resolve_host_path(v, working_dir) for v in volumes]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_host_path(volume, working_dir):
|
||||||
|
container_path, host_path = split_volume(volume)
|
||||||
|
if host_path is not None:
|
||||||
|
host_path = os.path.expanduser(host_path)
|
||||||
|
host_path = os.path.expandvars(host_path)
|
||||||
|
return "%s:%s" % (expand_path(working_dir, host_path), container_path)
|
||||||
|
else:
|
||||||
|
return container_path
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_build_path(build_path, working_dir=None):
|
||||||
|
if working_dir is None:
|
||||||
|
raise Exception("No working_dir passed to resolve_build_path")
|
||||||
|
|
||||||
|
_path = expand_path(working_dir, build_path)
|
||||||
|
if not os.path.exists(_path) or not os.access(_path, os.R_OK):
|
||||||
|
raise ConfigurationError("build path %s either does not exist or is not accessible." % _path)
|
||||||
|
else:
|
||||||
|
return _path
|
||||||
|
|
||||||
|
|
||||||
|
def merge_volumes(base, override):
|
||||||
|
d = dict_from_volumes(base)
|
||||||
|
d.update(dict_from_volumes(override))
|
||||||
|
return volumes_from_dict(d)
|
||||||
|
|
||||||
|
|
||||||
|
def dict_from_volumes(volumes):
|
||||||
|
if volumes:
|
||||||
|
return dict(split_volume(v) for v in volumes)
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def volumes_from_dict(d):
|
||||||
|
return [join_volume(v) for v in d.items()]
|
||||||
|
|
||||||
|
|
||||||
|
def split_volume(string):
|
||||||
|
if ':' in string:
|
||||||
|
(host, container) = string.split(':', 1)
|
||||||
|
return (container, host)
|
||||||
|
else:
|
||||||
|
return (string, None)
|
||||||
|
|
||||||
|
|
||||||
|
def join_volume(pair):
|
||||||
|
(container, host) = pair
|
||||||
|
if host is None:
|
||||||
|
return container
|
||||||
|
else:
|
||||||
|
return ":".join((host, container))
|
||||||
|
|
||||||
|
|
||||||
|
def expand_path(working_dir, path):
|
||||||
|
return os.path.abspath(os.path.join(working_dir, path))
|
||||||
|
|
||||||
|
|
||||||
|
def to_list(value):
|
||||||
|
if value is None:
|
||||||
|
return []
|
||||||
|
elif isinstance(value, six.string_types):
|
||||||
|
return [value]
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def get_service_name_from_net(net_config):
|
||||||
|
if not net_config:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not net_config.startswith('container:'):
|
||||||
|
return
|
||||||
|
|
||||||
|
_, net_name = net_config.split(':', 1)
|
||||||
|
return net_name
|
||||||
|
|
||||||
|
|
||||||
|
def load_yaml(filename):
|
||||||
|
try:
|
||||||
|
with open(filename, 'r') as fh:
|
||||||
|
return yaml.safe_load(fh)
|
||||||
|
except IOError as e:
|
||||||
|
raise ConfigurationError(six.text_type(e))
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigurationError(Exception):
|
||||||
|
def __init__(self, msg):
|
||||||
|
self.msg = msg
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.msg
|
||||||
|
|
||||||
|
|
||||||
|
class CircularReference(ConfigurationError):
|
||||||
|
def __init__(self, trail):
|
||||||
|
self.trail = trail
|
||||||
|
|
||||||
|
@property
|
||||||
|
def msg(self):
|
||||||
|
lines = [
|
||||||
|
"{} in {}".format(service_name, filename)
|
||||||
|
for (filename, service_name) in self.trail
|
||||||
|
]
|
||||||
|
return "Circular reference:\n {}".format("\n extends ".join(lines))
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
# 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
|
|
||||||
from .config import find
|
|
||||||
from .config import load
|
|
||||||
from .config import merge_environment
|
|
||||||
from .config import parse_environment
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,188 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
|
||||||
"id": "config_schema_v1.json",
|
|
||||||
|
|
||||||
"type": "object",
|
|
||||||
|
|
||||||
"patternProperties": {
|
|
||||||
"^[a-zA-Z0-9._-]+$": {
|
|
||||||
"$ref": "#/definitions/service"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"additionalProperties": false,
|
|
||||||
|
|
||||||
"definitions": {
|
|
||||||
"service": {
|
|
||||||
"id": "#/definitions/service",
|
|
||||||
"type": "object",
|
|
||||||
|
|
||||||
"properties": {
|
|
||||||
"build": {"type": "string"},
|
|
||||||
"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"},
|
|
||||||
"devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
|
||||||
"dns": {"$ref": "#/definitions/string_or_list"},
|
|
||||||
"dns_search": {"$ref": "#/definitions/string_or_list"},
|
|
||||||
"dockerfile": {"type": "string"},
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
"extra_hosts": {"$ref": "#/definitions/list_or_dict"},
|
|
||||||
"external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
|
||||||
"hostname": {"type": "string"},
|
|
||||||
"image": {"type": "string"},
|
|
||||||
"ipc": {"type": "string"},
|
|
||||||
"labels": {"$ref": "#/definitions/list_or_dict"},
|
|
||||||
"links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
|
||||||
"log_driver": {"type": "string"},
|
|
||||||
"log_opt": {"type": "object"},
|
|
||||||
"mac_address": {"type": "string"},
|
|
||||||
"mem_limit": {"type": ["number", "string"]},
|
|
||||||
"memswap_limit": {"type": ["number", "string"]},
|
|
||||||
"mem_swappiness": {"type": "integer"},
|
|
||||||
"net": {"type": "string"},
|
|
||||||
"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_signal": {"type": "string"},
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
|
|
||||||
"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"],
|
|
||||||
"not": {"required": ["image"]}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"required": ["image"],
|
|
||||||
"not": {"anyOf": [
|
|
||||||
{"required": ["build"]},
|
|
||||||
{"required": ["dockerfile"]}
|
|
||||||
]}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,329 +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
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"additionalProperties": false,
|
|
||||||
|
|
||||||
"definitions": {
|
|
||||||
|
|
||||||
"service": {
|
|
||||||
"id": "#/definitions/service",
|
|
||||||
"type": "object",
|
|
||||||
|
|
||||||
"properties": {
|
|
||||||
"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_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/list_or_dict"},
|
|
||||||
"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"]},
|
|
||||||
"memswap_limit": {"type": ["number", "string"]},
|
|
||||||
"mem_swappiness": {"type": "integer"},
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"external": {
|
|
||||||
"type": ["boolean", "object"],
|
|
||||||
"properties": {
|
|
||||||
"name": {"type": "string"}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"internal": {"type": "boolean"}
|
|
||||||
},
|
|
||||||
"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}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
"constraints": {
|
|
||||||
"service": {
|
|
||||||
"id": "#/definitions/constraints/service",
|
|
||||||
"anyOf": [
|
|
||||||
{"required": ["build"]},
|
|
||||||
{"required": ["image"]}
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"build": {
|
|
||||||
"required": ["context"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,376 +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
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"additionalProperties": false,
|
|
||||||
|
|
||||||
"definitions": {
|
|
||||||
|
|
||||||
"service": {
|
|
||||||
"id": "#/definitions/service",
|
|
||||||
"type": "object",
|
|
||||||
|
|
||||||
"properties": {
|
|
||||||
"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": {
|
|
||||||
"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": {"$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/list_or_dict"},
|
|
||||||
"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"]},
|
|
||||||
"memswap_limit": {"type": ["number", "string"]},
|
|
||||||
"mem_swappiness": {"type": "integer"},
|
|
||||||
"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"}
|
|
||||||
},
|
|
||||||
"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"]},
|
|
||||||
"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},
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"external": {
|
|
||||||
"type": ["boolean", "object"],
|
|
||||||
"properties": {
|
|
||||||
"name": {"type": "string"}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"internal": {"type": "boolean"},
|
|
||||||
"enable_ipv6": {"type": "boolean"},
|
|
||||||
"labels": {"$ref": "#/definitions/list_or_dict"}
|
|
||||||
},
|
|
||||||
"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/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"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,383 +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/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": {
|
|
||||||
"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/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"}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"resources": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"limits": {"$ref": "#/definitions/resource"},
|
|
||||||
"reservations": {"$ref": "#/definitions/resource"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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"}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"external": {
|
|
||||||
"type": ["boolean", "object"],
|
|
||||||
"properties": {
|
|
||||||
"name": {"type": "string"}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"internal": {"type": "boolean"},
|
|
||||||
"labels": {"$ref": "#/definitions/list_or_dict"}
|
|
||||||
},
|
|
||||||
"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/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"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,428 +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/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": {
|
|
||||||
"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/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"}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"resources": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"limits": {"$ref": "#/definitions/resource"},
|
|
||||||
"reservations": {"$ref": "#/definitions/resource"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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"}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"external": {
|
|
||||||
"type": ["boolean", "object"],
|
|
||||||
"properties": {
|
|
||||||
"name": {"type": "string"}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"internal": {"type": "boolean"},
|
|
||||||
"labels": {"$ref": "#/definitions/list_or_dict"}
|
|
||||||
},
|
|
||||||
"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/list_or_dict"}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
|
|
||||||
"secret": {
|
|
||||||
"id": "#/definitions/secret",
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"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"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import codecs
|
|
||||||
import contextlib
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from ..const import IS_WINDOWS_PLATFORM
|
|
||||||
from .errors import ConfigurationError
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def split_env(env):
|
|
||||||
if isinstance(env, six.binary_type):
|
|
||||||
env = env.decode('utf-8', 'replace')
|
|
||||||
if '=' in env:
|
|
||||||
return env.split('=', 1)
|
|
||||||
else:
|
|
||||||
return env, None
|
|
||||||
|
|
||||||
|
|
||||||
def env_vars_from_file(filename):
|
|
||||||
"""
|
|
||||||
Read in a line delimited file of environment variables.
|
|
||||||
"""
|
|
||||||
if not os.path.exists(filename):
|
|
||||||
raise ConfigurationError("Couldn't find env file: %s" % filename)
|
|
||||||
elif not os.path.isfile(filename):
|
|
||||||
raise ConfigurationError("%s is not a file." % (filename))
|
|
||||||
env = {}
|
|
||||||
with contextlib.closing(codecs.open(filename, 'r', 'utf-8')) as fileobj:
|
|
||||||
for line in fileobj:
|
|
||||||
line = line.strip()
|
|
||||||
if line and not line.startswith('#'):
|
|
||||||
k, v = split_env(line)
|
|
||||||
env[k] = v
|
|
||||||
return env
|
|
||||||
|
|
||||||
|
|
||||||
class Environment(dict):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(Environment, self).__init__(*args, **kwargs)
|
|
||||||
self.missing_keys = []
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_env_file(cls, base_dir):
|
|
||||||
def _initialize():
|
|
||||||
result = cls()
|
|
||||||
if base_dir is None:
|
|
||||||
return result
|
|
||||||
env_file_path = os.path.join(base_dir, '.env')
|
|
||||||
try:
|
|
||||||
return cls(env_vars_from_file(env_file_path))
|
|
||||||
except ConfigurationError:
|
|
||||||
pass
|
|
||||||
return result
|
|
||||||
instance = _initialize()
|
|
||||||
instance.update(os.environ)
|
|
||||||
return instance
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_command_line(cls, parsed_env_opts):
|
|
||||||
result = cls()
|
|
||||||
for k, v in parsed_env_opts.items():
|
|
||||||
# Values from the command line take priority, unless they're unset
|
|
||||||
# in which case they take the value from the system's environment
|
|
||||||
if v is None and k in os.environ:
|
|
||||||
result[k] = os.environ[k]
|
|
||||||
else:
|
|
||||||
result[k] = v
|
|
||||||
return result
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
try:
|
|
||||||
return super(Environment, self).__getitem__(key)
|
|
||||||
except KeyError:
|
|
||||||
if IS_WINDOWS_PLATFORM:
|
|
||||||
try:
|
|
||||||
return super(Environment, self).__getitem__(key.upper())
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
if key not in self.missing_keys:
|
|
||||||
log.warn(
|
|
||||||
"The {} variable is not set. Defaulting to a blank string."
|
|
||||||
.format(key)
|
|
||||||
)
|
|
||||||
self.missing_keys.append(key)
|
|
||||||
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def __contains__(self, key):
|
|
||||||
result = super(Environment, self).__contains__(key)
|
|
||||||
if IS_WINDOWS_PLATFORM:
|
|
||||||
return (
|
|
||||||
result or super(Environment, self).__contains__(key.upper())
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get(self, key, *args, **kwargs):
|
|
||||||
if IS_WINDOWS_PLATFORM:
|
|
||||||
return super(Environment, self).get(
|
|
||||||
key,
|
|
||||||
super(Environment, self).get(key.upper(), *args, **kwargs)
|
|
||||||
)
|
|
||||||
return super(Environment, self).get(key, *args, **kwargs)
|
|
||||||
|
|
||||||
def get_boolean(self, key):
|
|
||||||
# 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
|
|
||||||
if value.lower() in ['0', 'false']:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
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 ("2.0", "2.1", "3.0") and place your '
|
|
||||||
'service definitions under the `services` key, or omit the `version` key '
|
|
||||||
'and place your service definitions at the root of the file to use '
|
|
||||||
'version 1.\nFor more on the Compose file format versions, see '
|
|
||||||
'https://docs.docker.com/compose/compose-file/')
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigurationError(Exception):
|
|
||||||
def __init__(self, msg):
|
|
||||||
self.msg = msg
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.msg
|
|
||||||
|
|
||||||
|
|
||||||
class DependencyError(ConfigurationError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class CircularReference(ConfigurationError):
|
|
||||||
def __init__(self, trail):
|
|
||||||
self.trail = trail
|
|
||||||
|
|
||||||
@property
|
|
||||||
def msg(self):
|
|
||||||
lines = [
|
|
||||||
"{} in {}".format(service_name, filename)
|
|
||||||
for (filename, service_name) in self.trail
|
|
||||||
]
|
|
||||||
return "Circular reference:\n {}".format("\n extends ".join(lines))
|
|
||||||
|
|
||||||
|
|
||||||
class ComposeFileNotFound(ConfigurationError):
|
|
||||||
def __init__(self, supported_filenames):
|
|
||||||
super(ComposeFileNotFound, self).__init__("""
|
|
||||||
Can't find a suitable configuration file in this directory or any
|
|
||||||
parent. Are you in the right directory?
|
|
||||||
|
|
||||||
Supported filenames: %s
|
|
||||||
""" % ", ".join(supported_filenames))
|
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from string import Template
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from .errors import ConfigurationError
|
|
||||||
from compose.const import COMPOSEFILE_V1 as V1
|
|
||||||
from compose.const import COMPOSEFILE_V2_0 as V2_0
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Interpolator(object):
|
|
||||||
|
|
||||||
def __init__(self, templater, mapping):
|
|
||||||
self.templater = templater
|
|
||||||
self.mapping = mapping
|
|
||||||
|
|
||||||
def interpolate(self, string):
|
|
||||||
try:
|
|
||||||
return self.templater(string).substitute(self.mapping)
|
|
||||||
except ValueError:
|
|
||||||
raise InvalidInterpolation(string)
|
|
||||||
|
|
||||||
|
|
||||||
def interpolate_environment_variables(version, config, section, environment):
|
|
||||||
if version in (V2_0, 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))
|
|
||||||
for key, val in (config_dict or {}).items()
|
|
||||||
)
|
|
||||||
|
|
||||||
return dict(
|
|
||||||
(name, process_item(name, config_dict or {}))
|
|
||||||
for name, config_dict in config.items()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def interpolate_value(name, config_key, value, section, interpolator):
|
|
||||||
try:
|
|
||||||
return recursive_interpolate(value, interpolator)
|
|
||||||
except InvalidInterpolation as e:
|
|
||||||
raise ConfigurationError(
|
|
||||||
'Invalid interpolation format for "{config_key}" option '
|
|
||||||
'in {section} "{name}": "{string}"'.format(
|
|
||||||
config_key=config_key,
|
|
||||||
name=name,
|
|
||||||
section=section,
|
|
||||||
string=e.string))
|
|
||||||
|
|
||||||
|
|
||||||
def recursive_interpolate(obj, interpolator):
|
|
||||||
if isinstance(obj, six.string_types):
|
|
||||||
return interpolator.interpolate(obj)
|
|
||||||
if isinstance(obj, dict):
|
|
||||||
return dict(
|
|
||||||
(key, recursive_interpolate(val, interpolator))
|
|
||||||
for (key, val) in obj.items()
|
|
||||||
)
|
|
||||||
if isinstance(obj, list):
|
|
||||||
return [recursive_interpolate(val, interpolator) for val in obj]
|
|
||||||
return obj
|
|
||||||
|
|
||||||
|
|
||||||
class TemplateWithDefaults(Template):
|
|
||||||
idpattern = r'[_a-z][_a-z0-9]*(?::?-[^}]+)?'
|
|
||||||
|
|
||||||
# Modified from python2.7/string.py
|
|
||||||
def substitute(self, mapping):
|
|
||||||
# Helper function for .sub()
|
|
||||||
def convert(mo):
|
|
||||||
# Check the most common path first.
|
|
||||||
named = mo.group('named') or mo.group('braced')
|
|
||||||
if named is not None:
|
|
||||||
if ':-' in named:
|
|
||||||
var, _, default = named.partition(':-')
|
|
||||||
return mapping.get(var) or default
|
|
||||||
if '-' in named:
|
|
||||||
var, _, default = named.partition('-')
|
|
||||||
return mapping.get(var, default)
|
|
||||||
val = mapping[named]
|
|
||||||
return '%s' % (val,)
|
|
||||||
if mo.group('escaped') is not None:
|
|
||||||
return self.delimiter
|
|
||||||
if mo.group('invalid') is not None:
|
|
||||||
self._invalid(mo)
|
|
||||||
raise ValueError('Unrecognized named group in pattern',
|
|
||||||
self.pattern)
|
|
||||||
return self.pattern.sub(convert, self.template)
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidInterpolation(Exception):
|
|
||||||
def __init__(self, string):
|
|
||||||
self.string = string
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import six
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
from compose.config import types
|
|
||||||
from compose.config.config import V1
|
|
||||||
from compose.config.config import V2_1
|
|
||||||
|
|
||||||
|
|
||||||
def serialize_config_type(dumper, data):
|
|
||||||
representer = dumper.represent_str if six.PY3 else dumper.represent_unicode
|
|
||||||
return representer(data.repr())
|
|
||||||
|
|
||||||
|
|
||||||
yaml.SafeDumper.add_representer(types.VolumeFromSpec, serialize_config_type)
|
|
||||||
yaml.SafeDumper.add_representer(types.VolumeSpec, serialize_config_type)
|
|
||||||
|
|
||||||
|
|
||||||
def denormalize_config(config):
|
|
||||||
denormalized_services = [
|
|
||||||
denormalize_service_dict(service_dict, config.version)
|
|
||||||
for service_dict in config.services
|
|
||||||
]
|
|
||||||
services = {
|
|
||||||
service_dict.pop('name'): service_dict
|
|
||||||
for service_dict in denormalized_services
|
|
||||||
}
|
|
||||||
networks = config.networks.copy()
|
|
||||||
for net_name, net_conf in networks.items():
|
|
||||||
if 'external_name' in net_conf:
|
|
||||||
del net_conf['external_name']
|
|
||||||
|
|
||||||
volumes = config.volumes.copy()
|
|
||||||
for vol_name, vol_conf in volumes.items():
|
|
||||||
if 'external_name' in vol_conf:
|
|
||||||
del vol_conf['external_name']
|
|
||||||
|
|
||||||
version = config.version
|
|
||||||
if version == V1:
|
|
||||||
version = V2_1
|
|
||||||
|
|
||||||
return {
|
|
||||||
'version': version,
|
|
||||||
'services': services,
|
|
||||||
'networks': networks,
|
|
||||||
'volumes': volumes,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def serialize_config(config):
|
|
||||||
return yaml.safe_dump(
|
|
||||||
denormalize_config(config),
|
|
||||||
default_flow_style=False,
|
|
||||||
indent=2,
|
|
||||||
width=80)
|
|
||||||
|
|
||||||
|
|
||||||
def serialize_ns_time_value(value):
|
|
||||||
result = (value, 'ns')
|
|
||||||
table = [
|
|
||||||
(1000., 'us'),
|
|
||||||
(1000., 'ms'),
|
|
||||||
(1000., 's'),
|
|
||||||
(60., 'm'),
|
|
||||||
(60., 'h')
|
|
||||||
]
|
|
||||||
for stage in table:
|
|
||||||
tmp = value / stage[0]
|
|
||||||
if tmp == int(value / stage[0]):
|
|
||||||
value = tmp
|
|
||||||
result = (int(value), stage[1])
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
return '{0}{1}'.format(*result)
|
|
||||||
|
|
||||||
|
|
||||||
def denormalize_service_dict(service_dict, version):
|
|
||||||
service_dict = service_dict.copy()
|
|
||||||
|
|
||||||
if 'restart' in service_dict:
|
|
||||||
service_dict['restart'] = types.serialize_restart_spec(
|
|
||||||
service_dict['restart']
|
|
||||||
)
|
|
||||||
|
|
||||||
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:
|
|
||||||
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(
|
|
||||||
service_dict['healthcheck']['interval']
|
|
||||||
)
|
|
||||||
if 'timeout' in service_dict['healthcheck']:
|
|
||||||
service_dict['healthcheck']['timeout'] = serialize_ns_time_value(
|
|
||||||
service_dict['healthcheck']['timeout']
|
|
||||||
)
|
|
||||||
|
|
||||||
if 'secrets' in service_dict:
|
|
||||||
service_dict['secrets'] = map(lambda s: s.repr(), service_dict['secrets'])
|
|
||||||
|
|
||||||
return service_dict
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from compose.config.errors import DependencyError
|
|
||||||
|
|
||||||
|
|
||||||
def get_service_name_from_network_mode(network_mode):
|
|
||||||
return get_source_name_from_network_mode(network_mode, 'service')
|
|
||||||
|
|
||||||
|
|
||||||
def get_container_name_from_network_mode(network_mode):
|
|
||||||
return get_source_name_from_network_mode(network_mode, 'container')
|
|
||||||
|
|
||||||
|
|
||||||
def get_source_name_from_network_mode(network_mode, source_type):
|
|
||||||
if not network_mode:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not network_mode.startswith(source_type+':'):
|
|
||||||
return
|
|
||||||
|
|
||||||
_, net_name = network_mode.split(':', 1)
|
|
||||||
return net_name
|
|
||||||
|
|
||||||
|
|
||||||
def get_service_names(links):
|
|
||||||
return [link.split(':')[0] for link in links]
|
|
||||||
|
|
||||||
|
|
||||||
def get_service_names_from_volumes_from(volumes_from):
|
|
||||||
return [volume_from.source for volume_from in volumes_from]
|
|
||||||
|
|
||||||
|
|
||||||
def get_service_dependents(service_dict, services):
|
|
||||||
name = service_dict['name']
|
|
||||||
return [
|
|
||||||
service for service in services
|
|
||||||
if (name in get_service_names(service.get('links', [])) or
|
|
||||||
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 in service.get('depends_on', []))
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def sort_service_dicts(services):
|
|
||||||
# Topological sort (Cormen/Tarjan algorithm).
|
|
||||||
unmarked = services[:]
|
|
||||||
temporary_marked = set()
|
|
||||||
sorted_services = []
|
|
||||||
|
|
||||||
def visit(n):
|
|
||||||
if n['name'] in temporary_marked:
|
|
||||||
if n['name'] in get_service_names(n.get('links', [])):
|
|
||||||
raise DependencyError('A service can not link to itself: %s' % n['name'])
|
|
||||||
if n['name'] in n.get('volumes_from', []):
|
|
||||||
raise DependencyError('A service can not mount itself as volume: %s' % n['name'])
|
|
||||||
if n['name'] in n.get('depends_on', []):
|
|
||||||
raise DependencyError('A service can not depend on itself: %s' % n['name'])
|
|
||||||
raise DependencyError('Circular dependency between %s' % ' and '.join(temporary_marked))
|
|
||||||
|
|
||||||
if n in unmarked:
|
|
||||||
temporary_marked.add(n['name'])
|
|
||||||
for m in get_service_dependents(n, services):
|
|
||||||
visit(m)
|
|
||||||
temporary_marked.remove(n['name'])
|
|
||||||
unmarked.remove(n)
|
|
||||||
sorted_services.insert(0, n)
|
|
||||||
|
|
||||||
while unmarked:
|
|
||||||
visit(unmarked[-1])
|
|
||||||
|
|
||||||
return sorted_services
|
|
||||||
|
|
@ -1,260 +0,0 @@
|
||||||
"""
|
|
||||||
Types for objects parsed from the configuration.
|
|
||||||
"""
|
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
from collections import namedtuple
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from ..const import COMPOSEFILE_V1 as V1
|
|
||||||
from .errors import ConfigurationError
|
|
||||||
from compose.const import IS_WINDOWS_PLATFORM
|
|
||||||
from compose.utils import splitdrive
|
|
||||||
|
|
||||||
win32_root_path_pattern = re.compile(r'^[A-Za-z]\:\\.*')
|
|
||||||
|
|
||||||
|
|
||||||
class VolumeFromSpec(namedtuple('_VolumeFromSpec', 'source mode type')):
|
|
||||||
|
|
||||||
# TODO: drop service_names arg when v1 is removed
|
|
||||||
@classmethod
|
|
||||||
def parse(cls, volume_from_config, service_names, version):
|
|
||||||
func = cls.parse_v1 if version == V1 else cls.parse_v2
|
|
||||||
return func(service_names, volume_from_config)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parse_v1(cls, service_names, volume_from_config):
|
|
||||||
parts = volume_from_config.split(':')
|
|
||||||
if len(parts) > 2:
|
|
||||||
raise ConfigurationError(
|
|
||||||
"volume_from {} has incorrect format, should be "
|
|
||||||
"service[:mode]".format(volume_from_config))
|
|
||||||
|
|
||||||
if len(parts) == 1:
|
|
||||||
source = parts[0]
|
|
||||||
mode = 'rw'
|
|
||||||
else:
|
|
||||||
source, mode = parts
|
|
||||||
|
|
||||||
type = 'service' if source in service_names else 'container'
|
|
||||||
return cls(source, mode, type)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parse_v2(cls, service_names, volume_from_config):
|
|
||||||
parts = volume_from_config.split(':')
|
|
||||||
if len(parts) > 3:
|
|
||||||
raise ConfigurationError(
|
|
||||||
"volume_from {} has incorrect format, should be one of "
|
|
||||||
"'<service name>[:<mode>]' or "
|
|
||||||
"'container:<container name>[:<mode>]'".format(volume_from_config))
|
|
||||||
|
|
||||||
if len(parts) == 1:
|
|
||||||
source = parts[0]
|
|
||||||
return cls(source, 'rw', 'service')
|
|
||||||
|
|
||||||
if len(parts) == 2:
|
|
||||||
if parts[0] == 'container':
|
|
||||||
type, source = parts
|
|
||||||
return cls(source, 'rw', type)
|
|
||||||
|
|
||||||
source, mode = parts
|
|
||||||
return cls(source, mode, 'service')
|
|
||||||
|
|
||||||
if len(parts) == 3:
|
|
||||||
type, source, mode = parts
|
|
||||||
if type not in ('service', 'container'):
|
|
||||||
raise ConfigurationError(
|
|
||||||
"Unknown volumes_from type '{}' in '{}'".format(
|
|
||||||
type,
|
|
||||||
volume_from_config))
|
|
||||||
|
|
||||||
return cls(source, mode, type)
|
|
||||||
|
|
||||||
def repr(self):
|
|
||||||
return '{v.type}:{v.source}:{v.mode}'.format(v=self)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_restart_spec(restart_config):
|
|
||||||
if not restart_config:
|
|
||||||
return None
|
|
||||||
parts = restart_config.split(':')
|
|
||||||
if len(parts) > 2:
|
|
||||||
raise ConfigurationError(
|
|
||||||
"Restart %s has incorrect format, should be "
|
|
||||||
"mode[:max_retry]" % restart_config)
|
|
||||||
if len(parts) == 2:
|
|
||||||
name, max_retry_count = parts
|
|
||||||
else:
|
|
||||||
name, = parts
|
|
||||||
max_retry_count = 0
|
|
||||||
|
|
||||||
return {'Name': name, 'MaximumRetryCount': int(max_retry_count)}
|
|
||||||
|
|
||||||
|
|
||||||
def serialize_restart_spec(restart_spec):
|
|
||||||
if not restart_spec:
|
|
||||||
return ''
|
|
||||||
parts = [restart_spec['Name']]
|
|
||||||
if restart_spec['MaximumRetryCount']:
|
|
||||||
parts.append(six.text_type(restart_spec['MaximumRetryCount']))
|
|
||||||
return ':'.join(parts)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_extra_hosts(extra_hosts_config):
|
|
||||||
if not extra_hosts_config:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
if isinstance(extra_hosts_config, dict):
|
|
||||||
return dict(extra_hosts_config)
|
|
||||||
|
|
||||||
if isinstance(extra_hosts_config, list):
|
|
||||||
extra_hosts_dict = {}
|
|
||||||
for extra_hosts_line in extra_hosts_config:
|
|
||||||
# TODO: validate string contains ':' ?
|
|
||||||
host, ip = extra_hosts_line.split(':', 1)
|
|
||||||
extra_hosts_dict[host.strip()] = ip.strip()
|
|
||||||
return extra_hosts_dict
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_path_for_engine(path):
|
|
||||||
"""Windows paths, c:\my\path\shiny, need to be changed to be compatible with
|
|
||||||
the Engine. Volume paths are expected to be linux style /c/my/path/shiny/
|
|
||||||
"""
|
|
||||||
drive, tail = splitdrive(path)
|
|
||||||
|
|
||||||
if drive:
|
|
||||||
path = '/' + drive.lower().rstrip(':') + tail
|
|
||||||
|
|
||||||
return path.replace('\\', '/')
|
|
||||||
|
|
||||||
|
|
||||||
class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')):
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _parse_unix(cls, volume_config):
|
|
||||||
parts = volume_config.split(':')
|
|
||||||
|
|
||||||
if len(parts) > 3:
|
|
||||||
raise ConfigurationError(
|
|
||||||
"Volume %s has incorrect format, should be "
|
|
||||||
"external:internal[:mode]" % volume_config)
|
|
||||||
|
|
||||||
if len(parts) == 1:
|
|
||||||
external = None
|
|
||||||
internal = os.path.normpath(parts[0])
|
|
||||||
else:
|
|
||||||
external = os.path.normpath(parts[0])
|
|
||||||
internal = os.path.normpath(parts[1])
|
|
||||||
|
|
||||||
mode = 'rw'
|
|
||||||
if len(parts) == 3:
|
|
||||||
mode = parts[2]
|
|
||||||
|
|
||||||
return cls(external, internal, mode)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _parse_win32(cls, volume_config, normalize):
|
|
||||||
# relative paths in windows expand to include the drive, eg C:\
|
|
||||||
# so we join the first 2 parts back together to count as one
|
|
||||||
mode = 'rw'
|
|
||||||
|
|
||||||
def separate_next_section(volume_config):
|
|
||||||
drive, tail = splitdrive(volume_config)
|
|
||||||
parts = tail.split(':', 1)
|
|
||||||
if drive:
|
|
||||||
parts[0] = drive + parts[0]
|
|
||||||
return parts
|
|
||||||
|
|
||||||
parts = separate_next_section(volume_config)
|
|
||||||
if len(parts) == 1:
|
|
||||||
internal = parts[0]
|
|
||||||
external = None
|
|
||||||
else:
|
|
||||||
external = parts[0]
|
|
||||||
parts = separate_next_section(parts[1])
|
|
||||||
external = os.path.normpath(external)
|
|
||||||
internal = parts[0]
|
|
||||||
if len(parts) > 1:
|
|
||||||
if ':' in parts[1]:
|
|
||||||
raise ConfigurationError(
|
|
||||||
"Volume %s has incorrect format, should be "
|
|
||||||
"external:internal[:mode]" % volume_config
|
|
||||||
)
|
|
||||||
mode = parts[1]
|
|
||||||
|
|
||||||
if normalize:
|
|
||||||
external = normalize_path_for_engine(external) if external else None
|
|
||||||
|
|
||||||
return cls(external, internal, mode)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parse(cls, volume_config, normalize=False):
|
|
||||||
"""Parse a volume_config path and split it into external:internal[:mode]
|
|
||||||
parts to be returned as a valid VolumeSpec.
|
|
||||||
"""
|
|
||||||
if IS_WINDOWS_PLATFORM:
|
|
||||||
return cls._parse_win32(volume_config, normalize)
|
|
||||||
else:
|
|
||||||
return cls._parse_unix(volume_config)
|
|
||||||
|
|
||||||
def repr(self):
|
|
||||||
external = self.external + ':' if self.external else ''
|
|
||||||
return '{ext}{v.internal}:{v.mode}'.format(ext=external, v=self)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_named_volume(self):
|
|
||||||
res = self.external and not self.external.startswith(('.', '/', '~'))
|
|
||||||
if not IS_WINDOWS_PLATFORM:
|
|
||||||
return res
|
|
||||||
|
|
||||||
return (
|
|
||||||
res and not self.external.startswith('\\') and
|
|
||||||
not win32_root_path_pattern.match(self.external)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ServiceLink(namedtuple('_ServiceLink', 'target alias')):
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parse(cls, link_spec):
|
|
||||||
target, _, alias = link_spec.partition(':')
|
|
||||||
if not alias:
|
|
||||||
alias = target
|
|
||||||
return cls(target, alias)
|
|
||||||
|
|
||||||
def repr(self):
|
|
||||||
if self.target == self.alias:
|
|
||||||
return self.target
|
|
||||||
return '{s.target}:{s.alias}'.format(s=self)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def merge_field(self):
|
|
||||||
return self.alias
|
|
||||||
|
|
||||||
|
|
||||||
class ServiceSecret(namedtuple('_ServiceSecret', 'source target uid gid mode')):
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parse(cls, spec):
|
|
||||||
if isinstance(spec, six.string_types):
|
|
||||||
return cls(spec, None, None, None, None)
|
|
||||||
return cls(
|
|
||||||
spec.get('source'),
|
|
||||||
spec.get('target'),
|
|
||||||
spec.get('uid'),
|
|
||||||
spec.get('gid'),
|
|
||||||
spec.get('mode'),
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def merge_field(self):
|
|
||||||
return self.source
|
|
||||||
|
|
||||||
def repr(self):
|
|
||||||
return dict(
|
|
||||||
[(k, v) for k, v in self._asdict().items() if v is not None]
|
|
||||||
)
|
|
||||||
|
|
@ -1,423 +0,0 @@
|
||||||
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
|
|
||||||
from jsonschema import RefResolver
|
|
||||||
from jsonschema import ValidationError
|
|
||||||
|
|
||||||
from ..const import COMPOSEFILE_V1 as V1
|
|
||||||
from .errors import ConfigurationError
|
|
||||||
from .errors import VERSION_EXPLANATION
|
|
||||||
from .sort_services import get_service_name_from_network_mode
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
DOCKER_CONFIG_HINTS = {
|
|
||||||
'cpu_share': 'cpu_shares',
|
|
||||||
'add_host': 'extra_hosts',
|
|
||||||
'hosts': 'extra_hosts',
|
|
||||||
'extra_host': 'extra_hosts',
|
|
||||||
'device': 'devices',
|
|
||||||
'link': 'links',
|
|
||||||
'memory_swap': 'memswap_limit',
|
|
||||||
'port': 'ports',
|
|
||||||
'privilege': 'privileged',
|
|
||||||
'priviliged': 'privileged',
|
|
||||||
'privilige': 'privileged',
|
|
||||||
'volume': 'volumes',
|
|
||||||
'workdir': 'working_dir',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
VALID_NAME_CHARS = '[a-zA-Z0-9\._\-]'
|
|
||||||
VALID_EXPOSE_FORMAT = r'^\d+(\-\d+)?(\/[a-zA-Z]+)?$'
|
|
||||||
|
|
||||||
|
|
||||||
@FormatChecker.cls_checks(format="ports", raises=ValidationError)
|
|
||||||
def format_ports(instance):
|
|
||||||
try:
|
|
||||||
split_port(instance)
|
|
||||||
except ValueError as e:
|
|
||||||
raise ValidationError(six.text_type(e))
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
@FormatChecker.cls_checks(format="expose", raises=ValidationError)
|
|
||||||
def format_expose(instance):
|
|
||||||
if isinstance(instance, six.string_types):
|
|
||||||
if not re.match(VALID_EXPOSE_FORMAT, instance):
|
|
||||||
raise ValidationError(
|
|
||||||
"should be of the format 'PORT[/PROTOCOL]'")
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def match_named_volumes(service_dict, project_volumes):
|
|
||||||
service_volumes = service_dict.get('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'
|
|
||||||
' declaration was found in the volumes section.'.format(
|
|
||||||
volume_spec.repr(), service_dict.get('name')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def python_type_to_yaml_type(type_):
|
|
||||||
type_name = type(type_).__name__
|
|
||||||
return {
|
|
||||||
'dict': 'mapping',
|
|
||||||
'list': 'array',
|
|
||||||
'int': 'number',
|
|
||||||
'float': 'number',
|
|
||||||
'bool': 'boolean',
|
|
||||||
'unicode': 'string',
|
|
||||||
'str': 'string',
|
|
||||||
'bytes': 'string',
|
|
||||||
}.get(type_name, type_name)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_config_section(filename, config, section):
|
|
||||||
"""Validate the structure of a configuration section. This must be done
|
|
||||||
before interpolation so it's separate from schema validation.
|
|
||||||
"""
|
|
||||||
if not isinstance(config, dict):
|
|
||||||
raise ConfigurationError(
|
|
||||||
"In file '{filename}', {section} must be a mapping, not "
|
|
||||||
"{type}.".format(
|
|
||||||
filename=filename,
|
|
||||||
section=section,
|
|
||||||
type=anglicize_json_type(python_type_to_yaml_type(config))))
|
|
||||||
|
|
||||||
for key, value in config.items():
|
|
||||||
if not isinstance(key, six.string_types):
|
|
||||||
raise ConfigurationError(
|
|
||||||
"In file '{filename}', the {section} name {name} must be a "
|
|
||||||
"quoted string, i.e. '{name}'.".format(
|
|
||||||
filename=filename,
|
|
||||||
section=section,
|
|
||||||
name=key))
|
|
||||||
|
|
||||||
if not isinstance(value, (dict, type(None))):
|
|
||||||
raise ConfigurationError(
|
|
||||||
"In file '{filename}', {section} '{name}' must be a mapping not "
|
|
||||||
"{type}.".format(
|
|
||||||
filename=filename,
|
|
||||||
section=section,
|
|
||||||
name=key,
|
|
||||||
type=anglicize_json_type(python_type_to_yaml_type(value))))
|
|
||||||
|
|
||||||
|
|
||||||
def validate_top_level_object(config_file):
|
|
||||||
if not isinstance(config_file.config, dict):
|
|
||||||
raise ConfigurationError(
|
|
||||||
"Top level object in '{}' needs to be an object not '{}'.".format(
|
|
||||||
config_file.filename,
|
|
||||||
type(config_file.config)))
|
|
||||||
|
|
||||||
|
|
||||||
def validate_ulimits(service_config):
|
|
||||||
ulimit_config = service_config.config.get('ulimits', {})
|
|
||||||
for limit_name, soft_hard_values in six.iteritems(ulimit_config):
|
|
||||||
if isinstance(soft_hard_values, dict):
|
|
||||||
if not soft_hard_values['soft'] <= soft_hard_values['hard']:
|
|
||||||
raise ConfigurationError(
|
|
||||||
"Service '{s.name}' has invalid ulimit '{ulimit}'. "
|
|
||||||
"'soft' value can not be greater than 'hard' value ".format(
|
|
||||||
s=service_config,
|
|
||||||
ulimit=ulimit_config))
|
|
||||||
|
|
||||||
|
|
||||||
def validate_extends_file_path(service_name, extends_options, filename):
|
|
||||||
"""
|
|
||||||
The service to be extended must either be defined in the config key 'file',
|
|
||||||
or within 'filename'.
|
|
||||||
"""
|
|
||||||
error_prefix = "Invalid 'extends' configuration for %s:" % service_name
|
|
||||||
|
|
||||||
if 'file' not in extends_options and filename is None:
|
|
||||||
raise ConfigurationError(
|
|
||||||
"%s you need to specify a 'file', e.g. 'file: something.yml'" % error_prefix
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_network_mode(service_config, service_names):
|
|
||||||
network_mode = service_config.config.get('network_mode')
|
|
||||||
if not network_mode:
|
|
||||||
return
|
|
||||||
|
|
||||||
if 'networks' in service_config.config:
|
|
||||||
raise ConfigurationError("'network_mode' and 'networks' cannot be combined")
|
|
||||||
|
|
||||||
dependency = get_service_name_from_network_mode(network_mode)
|
|
||||||
if not dependency:
|
|
||||||
return
|
|
||||||
|
|
||||||
if dependency not in service_names:
|
|
||||||
raise ConfigurationError(
|
|
||||||
"Service '{s.name}' uses the network stack 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:
|
|
||||||
raise ConfigurationError(
|
|
||||||
"Service '{s.name}' has a link to service '{link}' which is "
|
|
||||||
"undefined.".format(s=service_config, link=link))
|
|
||||||
|
|
||||||
|
|
||||||
def validate_depends_on(service_config, service_names):
|
|
||||||
deps = service_config.config.get('depends_on', {})
|
|
||||||
for dependency in deps.keys():
|
|
||||||
if dependency not in service_names:
|
|
||||||
raise ConfigurationError(
|
|
||||||
"Service '{s.name}' depends on service '{dep}' which is "
|
|
||||||
"undefined.".format(s=service_config, dep=dependency)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_unsupported_config_msg(path, error_key):
|
|
||||||
msg = "Unsupported config option for {}: '{}'".format(path_string(path), error_key)
|
|
||||||
if error_key in DOCKER_CONFIG_HINTS:
|
|
||||||
msg += " (did you mean '{}'?)".format(DOCKER_CONFIG_HINTS[error_key])
|
|
||||||
return msg
|
|
||||||
|
|
||||||
|
|
||||||
def anglicize_json_type(json_type):
|
|
||||||
if json_type.startswith(('a', 'e', 'i', 'o', 'u')):
|
|
||||||
return 'an ' + json_type
|
|
||||||
return 'a ' + json_type
|
|
||||||
|
|
||||||
|
|
||||||
def is_service_dict_schema(schema_id):
|
|
||||||
return schema_id in ('config_schema_v1.json', '#/properties/services')
|
|
||||||
|
|
||||||
|
|
||||||
def handle_error_for_schema_with_id(error, path):
|
|
||||||
schema_id = error.schema['id']
|
|
||||||
|
|
||||||
if is_service_dict_schema(schema_id) and error.validator == 'additionalProperties':
|
|
||||||
return "Invalid service name '{}' - only {} characters are allowed".format(
|
|
||||||
# The service_name is the key to the json object
|
|
||||||
list(error.instance)[0],
|
|
||||||
VALID_NAME_CHARS)
|
|
||||||
|
|
||||||
if error.validator == 'additionalProperties':
|
|
||||||
if schema_id == '#/definitions/service':
|
|
||||||
invalid_config_key = parse_key_from_error_msg(error)
|
|
||||||
return get_unsupported_config_msg(path, invalid_config_key)
|
|
||||||
|
|
||||||
if not error.path:
|
|
||||||
return '{}\n\n{}'.format(error.message, VERSION_EXPLANATION)
|
|
||||||
|
|
||||||
|
|
||||||
def handle_generic_error(error, path):
|
|
||||||
msg_format = None
|
|
||||||
error_msg = error.message
|
|
||||||
|
|
||||||
if error.validator == 'oneOf':
|
|
||||||
msg_format = "{path} {msg}"
|
|
||||||
config_key, error_msg = _parse_oneof_validator(error)
|
|
||||||
if config_key:
|
|
||||||
path.append(config_key)
|
|
||||||
|
|
||||||
elif error.validator == 'type':
|
|
||||||
msg_format = "{path} contains an invalid type, it should be {msg}"
|
|
||||||
error_msg = _parse_valid_types_from_validator(error.validator_value)
|
|
||||||
|
|
||||||
elif error.validator == 'required':
|
|
||||||
error_msg = ", ".join(error.validator_value)
|
|
||||||
msg_format = "{path} is invalid, {msg} is required."
|
|
||||||
|
|
||||||
elif error.validator == 'dependencies':
|
|
||||||
config_key = list(error.validator_value.keys())[0]
|
|
||||||
required_keys = ",".join(error.validator_value[config_key])
|
|
||||||
|
|
||||||
msg_format = "{path} is invalid: {msg}"
|
|
||||||
path.append(config_key)
|
|
||||||
error_msg = "when defining '{}' you must set '{}' as well".format(
|
|
||||||
config_key,
|
|
||||||
required_keys)
|
|
||||||
|
|
||||||
elif error.cause:
|
|
||||||
error_msg = six.text_type(error.cause)
|
|
||||||
msg_format = "{path} is invalid: {msg}"
|
|
||||||
|
|
||||||
elif error.path:
|
|
||||||
msg_format = "{path} value {msg}"
|
|
||||||
|
|
||||||
if msg_format:
|
|
||||||
return msg_format.format(path=path_string(path), msg=error_msg)
|
|
||||||
|
|
||||||
return error.message
|
|
||||||
|
|
||||||
|
|
||||||
def parse_key_from_error_msg(error):
|
|
||||||
return error.message.split("'")[1]
|
|
||||||
|
|
||||||
|
|
||||||
def path_string(path):
|
|
||||||
return ".".join(c for c in path if isinstance(c, six.string_types))
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_valid_types_from_validator(validator):
|
|
||||||
"""A validator value can be either an array of valid types or a string of
|
|
||||||
a valid type. Parse the valid types and prefix with the correct article.
|
|
||||||
"""
|
|
||||||
if not isinstance(validator, list):
|
|
||||||
return anglicize_json_type(validator)
|
|
||||||
|
|
||||||
if len(validator) == 1:
|
|
||||||
return anglicize_json_type(validator[0])
|
|
||||||
|
|
||||||
return "{}, or {}".format(
|
|
||||||
", ".join([anglicize_json_type(validator[0])] + validator[1:-1]),
|
|
||||||
anglicize_json_type(validator[-1]))
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_oneof_validator(error):
|
|
||||||
"""oneOf has multiple schemas, so we need to reason about which schema, sub
|
|
||||||
schema or constraint the validation is failing on.
|
|
||||||
Inspecting the context value of a ValidationError gives us information about
|
|
||||||
which sub schema failed and which kind of error it is.
|
|
||||||
"""
|
|
||||||
types = []
|
|
||||||
for context in error.context:
|
|
||||||
|
|
||||||
if context.validator == 'oneOf':
|
|
||||||
_, error_msg = _parse_oneof_validator(context)
|
|
||||||
return path_string(context.path), error_msg
|
|
||||||
|
|
||||||
if context.validator == 'required':
|
|
||||||
return (None, context.message)
|
|
||||||
|
|
||||||
if context.validator == 'additionalProperties':
|
|
||||||
invalid_config_key = parse_key_from_error_msg(context)
|
|
||||||
return (None, "contains unsupported option: '{}'".format(invalid_config_key))
|
|
||||||
|
|
||||||
if context.path:
|
|
||||||
return (
|
|
||||||
path_string(context.path),
|
|
||||||
"contains {}, which is an invalid type, it should be {}".format(
|
|
||||||
json.dumps(context.instance),
|
|
||||||
_parse_valid_types_from_validator(context.validator_value)),
|
|
||||||
)
|
|
||||||
|
|
||||||
if context.validator == 'uniqueItems':
|
|
||||||
return (
|
|
||||||
None,
|
|
||||||
"contains non unique items, please remove duplicates from {}".format(
|
|
||||||
context.instance),
|
|
||||||
)
|
|
||||||
|
|
||||||
if context.validator == 'type':
|
|
||||||
types.append(context.validator_value)
|
|
||||||
|
|
||||||
valid_types = _parse_valid_types_from_validator(types)
|
|
||||||
return (None, "contains an invalid type, it should be {}".format(valid_types))
|
|
||||||
|
|
||||||
|
|
||||||
def process_service_constraint_errors(error, service_name, version):
|
|
||||||
if version == V1:
|
|
||||||
if 'image' in error.instance and 'build' in error.instance:
|
|
||||||
return (
|
|
||||||
"Service {} has both an image and build path specified. "
|
|
||||||
"A service can either be built to image or use an existing "
|
|
||||||
"image, not both.".format(service_name))
|
|
||||||
|
|
||||||
if 'image' in error.instance and 'dockerfile' in error.instance:
|
|
||||||
return (
|
|
||||||
"Service {} has both an image and alternate Dockerfile. "
|
|
||||||
"A service can either be built to image or use an existing "
|
|
||||||
"image, not both.".format(service_name))
|
|
||||||
|
|
||||||
if 'image' not in error.instance and 'build' not in error.instance:
|
|
||||||
return (
|
|
||||||
"Service {} has neither an image nor a build context specified. "
|
|
||||||
"At least one must be provided.".format(service_name))
|
|
||||||
|
|
||||||
|
|
||||||
def process_config_schema_errors(error):
|
|
||||||
path = list(error.path)
|
|
||||||
|
|
||||||
if 'id' in error.schema:
|
|
||||||
error_msg = handle_error_for_schema_with_id(error, path)
|
|
||||||
if error_msg:
|
|
||||||
return error_msg
|
|
||||||
|
|
||||||
return handle_generic_error(error, path)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_against_config_schema(config_file):
|
|
||||||
schema = load_jsonschema(config_file.version)
|
|
||||||
format_checker = FormatChecker(["ports", "expose"])
|
|
||||||
validator = Draft4Validator(
|
|
||||||
schema,
|
|
||||||
resolver=RefResolver(get_resolver_path(), schema),
|
|
||||||
format_checker=format_checker)
|
|
||||||
handle_errors(
|
|
||||||
validator.iter_errors(config_file.config),
|
|
||||||
process_config_schema_errors,
|
|
||||||
config_file.filename)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_service_constraints(config, service_name, version):
|
|
||||||
def handler(errors):
|
|
||||||
return process_service_constraint_errors(errors, service_name, version)
|
|
||||||
|
|
||||||
schema = load_jsonschema(version)
|
|
||||||
validator = Draft4Validator(schema['definitions']['constraints']['service'])
|
|
||||||
handle_errors(validator.iter_errors(config), handler, None)
|
|
||||||
|
|
||||||
|
|
||||||
def get_schema_path():
|
|
||||||
return os.path.dirname(os.path.abspath(__file__))
|
|
||||||
|
|
||||||
|
|
||||||
def load_jsonschema(version):
|
|
||||||
filename = os.path.join(
|
|
||||||
get_schema_path(),
|
|
||||||
"config_schema_v{0}.json".format(version))
|
|
||||||
|
|
||||||
with open(filename, "r") as fh:
|
|
||||||
return json.load(fh)
|
|
||||||
|
|
||||||
|
|
||||||
def get_resolver_path():
|
|
||||||
schema_path = get_schema_path()
|
|
||||||
if sys.platform == "win32":
|
|
||||||
scheme = "///"
|
|
||||||
# TODO: why is this necessary?
|
|
||||||
schema_path = schema_path.replace('\\', '/')
|
|
||||||
else:
|
|
||||||
scheme = "//"
|
|
||||||
return "file:{}{}/".format(scheme, schema_path)
|
|
||||||
|
|
||||||
|
|
||||||
def handle_errors(errors, format_error_func, filename):
|
|
||||||
"""jsonschema returns an error tree full of information to explain what has
|
|
||||||
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))
|
|
||||||
if not errors:
|
|
||||||
return
|
|
||||||
|
|
||||||
error_msg = '\n'.join(format_error_func(error) for error in errors)
|
|
||||||
raise ConfigurationError(
|
|
||||||
"The Compose file{file_msg} is invalid because:\n{error_msg}".format(
|
|
||||||
file_msg=" '{}'".format(filename) if filename else "",
|
|
||||||
error_msg=error_msg))
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
DEFAULT_TIMEOUT = 10
|
|
||||||
HTTP_TIMEOUT = 60
|
|
||||||
IMAGE_EVENTS = ['delete', 'import', 'load', 'pull', 'push', 'save', 'tag', 'untag']
|
|
||||||
IS_WINDOWS_PLATFORM = (sys.platform == "win32")
|
|
||||||
LABEL_CONTAINER_NUMBER = 'com.docker.compose.container-number'
|
|
||||||
LABEL_ONE_OFF = 'com.docker.compose.oneoff'
|
|
||||||
LABEL_PROJECT = 'com.docker.compose.project'
|
|
||||||
LABEL_SERVICE = 'com.docker.compose.service'
|
|
||||||
LABEL_NETWORK = 'com.docker.compose.network'
|
|
||||||
LABEL_VERSION = 'com.docker.compose.version'
|
|
||||||
LABEL_VOLUME = 'com.docker.compose.volume'
|
|
||||||
LABEL_CONFIG_HASH = 'com.docker.compose.config-hash'
|
|
||||||
|
|
||||||
SECRETS_PATH = '/run/secrets'
|
|
||||||
|
|
||||||
COMPOSEFILE_V1 = '1'
|
|
||||||
COMPOSEFILE_V2_0 = '2.0'
|
|
||||||
COMPOSEFILE_V2_1 = '2.1'
|
|
||||||
COMPOSEFILE_V3_0 = '3.0'
|
|
||||||
COMPOSEFILE_V3_1 = '3.1'
|
|
||||||
|
|
||||||
API_VERSIONS = {
|
|
||||||
COMPOSEFILE_V1: '1.21',
|
|
||||||
COMPOSEFILE_V2_0: '1.22',
|
|
||||||
COMPOSEFILE_V2_1: '1.24',
|
|
||||||
COMPOSEFILE_V3_0: '1.25',
|
|
||||||
COMPOSEFILE_V3_1: '1.25',
|
|
||||||
}
|
|
||||||
|
|
||||||
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_V3_0]: '1.13.0',
|
|
||||||
API_VERSIONS[COMPOSEFILE_V3_1]: '1.13.0',
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +1,8 @@
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
from __future__ import absolute_import
|
||||||
from functools import reduce
|
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
from functools import reduce
|
||||||
from .const import LABEL_CONTAINER_NUMBER
|
|
||||||
from .const import LABEL_PROJECT
|
|
||||||
from .const import LABEL_SERVICE
|
|
||||||
|
|
||||||
|
|
||||||
class Container(object):
|
class Container(object):
|
||||||
|
|
@ -19,27 +14,22 @@ class Container(object):
|
||||||
self.client = client
|
self.client = client
|
||||||
self.dictionary = dictionary
|
self.dictionary = dictionary
|
||||||
self.has_been_inspected = has_been_inspected
|
self.has_been_inspected = has_been_inspected
|
||||||
self.log_stream = None
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_ps(cls, client, dictionary, **kwargs):
|
def from_ps(cls, client, dictionary, **kwargs):
|
||||||
"""
|
"""
|
||||||
Construct a container object from the output of GET /containers/json.
|
Construct a container object from the output of GET /containers/json.
|
||||||
"""
|
"""
|
||||||
name = get_container_name(dictionary)
|
|
||||||
if name is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
new_dictionary = {
|
new_dictionary = {
|
||||||
'Id': dictionary['Id'],
|
'Id': dictionary['Id'],
|
||||||
'Image': dictionary['Image'],
|
'Image': dictionary['Image'],
|
||||||
'Name': '/' + name,
|
'Name': '/' + get_container_name(dictionary),
|
||||||
}
|
}
|
||||||
return cls(client, new_dictionary, **kwargs)
|
return cls(client, new_dictionary, **kwargs)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_id(cls, client, id):
|
def from_id(cls, client, id):
|
||||||
return cls(client, client.inspect_container(id), has_been_inspected=True)
|
return cls(client, client.inspect_container(id))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, client, **options):
|
def create(cls, client, **options):
|
||||||
|
|
@ -54,38 +44,24 @@ class Container(object):
|
||||||
def image(self):
|
def image(self):
|
||||||
return self.dictionary['Image']
|
return self.dictionary['Image']
|
||||||
|
|
||||||
@property
|
|
||||||
def image_config(self):
|
|
||||||
return self.client.inspect_image(self.image)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def short_id(self):
|
def short_id(self):
|
||||||
return self.id[:12]
|
return self.id[:10]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
return self.dictionary['Name'][1:]
|
return self.dictionary['Name'][1:]
|
||||||
|
|
||||||
@property
|
|
||||||
def service(self):
|
|
||||||
return self.labels.get(LABEL_SERVICE)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name_without_project(self):
|
def name_without_project(self):
|
||||||
project = self.labels.get(LABEL_PROJECT)
|
return '_'.join(self.dictionary['Name'].split('_')[1:])
|
||||||
|
|
||||||
if self.name.startswith('{0}_{1}'.format(project, self.service)):
|
|
||||||
return '{0}_{1}'.format(self.service, self.number)
|
|
||||||
else:
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def number(self):
|
def number(self):
|
||||||
number = self.labels.get(LABEL_CONTAINER_NUMBER)
|
try:
|
||||||
if not number:
|
return int(self.name.split('_')[-1])
|
||||||
raise ValueError("Container {0} does not have a {1} label".format(
|
except ValueError:
|
||||||
self.short_id, LABEL_CONTAINER_NUMBER))
|
return None
|
||||||
return int(number)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ports(self):
|
def ports(self):
|
||||||
|
|
@ -103,24 +79,8 @@ class Container(object):
|
||||||
return ', '.join(format_port(*item)
|
return ', '.join(format_port(*item)
|
||||||
for item in sorted(six.iteritems(self.ports)))
|
for item in sorted(six.iteritems(self.ports)))
|
||||||
|
|
||||||
@property
|
|
||||||
def labels(self):
|
|
||||||
return self.get('Config.Labels') or {}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def stop_signal(self):
|
|
||||||
return self.get('Config.StopSignal')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def log_config(self):
|
|
||||||
return self.get('HostConfig.LogConfig') or None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def human_readable_state(self):
|
def human_readable_state(self):
|
||||||
if self.is_paused:
|
|
||||||
return 'Paused'
|
|
||||||
if self.is_restarting:
|
|
||||||
return 'Restarting'
|
|
||||||
if self.is_running:
|
if self.is_running:
|
||||||
return 'Ghost' if self.get('State.Ghost') else 'Up'
|
return 'Ghost' if self.get('State.Ghost') else 'Up'
|
||||||
else:
|
else:
|
||||||
|
|
@ -134,44 +94,12 @@ class Container(object):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def environment(self):
|
def environment(self):
|
||||||
def parse_env(var):
|
return dict(var.split("=", 1) for var in self.get('Config.Env') or [])
|
||||||
if '=' in var:
|
|
||||||
return var.split("=", 1)
|
|
||||||
return var, None
|
|
||||||
return dict(parse_env(var) for var in self.get('Config.Env') or [])
|
|
||||||
|
|
||||||
@property
|
|
||||||
def exit_code(self):
|
|
||||||
return self.get('State.ExitCode')
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_running(self):
|
def is_running(self):
|
||||||
return self.get('State.Running')
|
return self.get('State.Running')
|
||||||
|
|
||||||
@property
|
|
||||||
def is_restarting(self):
|
|
||||||
return self.get('State.Restarting')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_paused(self):
|
|
||||||
return self.get('State.Paused')
|
|
||||||
|
|
||||||
@property
|
|
||||||
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')
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
def get(self, key):
|
def get(self, key):
|
||||||
"""Return a value from the container or None if the value is not set.
|
"""Return a value from the container or None if the value is not set.
|
||||||
|
|
||||||
|
|
@ -189,48 +117,21 @@ class Container(object):
|
||||||
port = self.ports.get("%s/%s" % (port, protocol))
|
port = self.ports.get("%s/%s" % (port, protocol))
|
||||||
return "{HostIp}:{HostPort}".format(**port[0]) if port else None
|
return "{HostIp}:{HostPort}".format(**port[0]) if port else None
|
||||||
|
|
||||||
def get_mount(self, mount_dest):
|
|
||||||
for mount in self.get('Mounts'):
|
|
||||||
if mount['Destination'] == mount_dest:
|
|
||||||
return mount
|
|
||||||
return None
|
|
||||||
|
|
||||||
def start(self, **options):
|
def start(self, **options):
|
||||||
return self.client.start(self.id, **options)
|
return self.client.start(self.id, **options)
|
||||||
|
|
||||||
def stop(self, **options):
|
def stop(self, **options):
|
||||||
return self.client.stop(self.id, **options)
|
return self.client.stop(self.id, **options)
|
||||||
|
|
||||||
def pause(self, **options):
|
|
||||||
return self.client.pause(self.id, **options)
|
|
||||||
|
|
||||||
def unpause(self, **options):
|
|
||||||
return self.client.unpause(self.id, **options)
|
|
||||||
|
|
||||||
def kill(self, **options):
|
def kill(self, **options):
|
||||||
return self.client.kill(self.id, **options)
|
return self.client.kill(self.id, **options)
|
||||||
|
|
||||||
def restart(self, **options):
|
def restart(self):
|
||||||
return self.client.restart(self.id, **options)
|
return self.client.restart(self.id)
|
||||||
|
|
||||||
def remove(self, **options):
|
def remove(self, **options):
|
||||||
return self.client.remove_container(self.id, **options)
|
return self.client.remove_container(self.id, **options)
|
||||||
|
|
||||||
def create_exec(self, command, **options):
|
|
||||||
return self.client.exec_create(self.id, command, **options)
|
|
||||||
|
|
||||||
def start_exec(self, exec_id, **options):
|
|
||||||
return self.client.exec_start(exec_id, **options)
|
|
||||||
|
|
||||||
def rename_to_tmp_name(self):
|
|
||||||
"""Rename the container to a hopefully unique temporary container name
|
|
||||||
by prepending the short id.
|
|
||||||
"""
|
|
||||||
self.client.rename(
|
|
||||||
self.id,
|
|
||||||
'%s_%s' % (self.short_id, self.name)
|
|
||||||
)
|
|
||||||
|
|
||||||
def inspect_if_not_inspected(self):
|
def inspect_if_not_inspected(self):
|
||||||
if not self.has_been_inspected:
|
if not self.has_been_inspected:
|
||||||
self.inspect()
|
self.inspect()
|
||||||
|
|
@ -246,20 +147,29 @@ class Container(object):
|
||||||
self.has_been_inspected = True
|
self.has_been_inspected = True
|
||||||
return self.dictionary
|
return self.dictionary
|
||||||
|
|
||||||
|
def links(self):
|
||||||
|
links = []
|
||||||
|
for container in self.client.containers():
|
||||||
|
for name in container['Names']:
|
||||||
|
bits = name.split('/')
|
||||||
|
if len(bits) > 2 and bits[1] == self.name:
|
||||||
|
links.append(bits[2])
|
||||||
|
return links
|
||||||
|
|
||||||
def attach(self, *args, **kwargs):
|
def attach(self, *args, **kwargs):
|
||||||
return self.client.attach(self.id, *args, **kwargs)
|
return self.client.attach(self.id, *args, **kwargs)
|
||||||
|
|
||||||
|
def attach_socket(self, **kwargs):
|
||||||
|
return self.client.attach_socket(self.id, **kwargs)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<Container: %s (%s)>' % (self.name, self.id[:6])
|
return '<Container: %s>' % self.name
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
if type(self) != type(other):
|
if type(self) != type(other):
|
||||||
return False
|
return False
|
||||||
return self.id == other.id
|
return self.id == other.id
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return self.id.__hash__()
|
|
||||||
|
|
||||||
|
|
||||||
def get_container_name(container):
|
def get_container_name(container):
|
||||||
if not container.get('Name') and not container.get('Names'):
|
if not container.get('Name') and not container.get('Names'):
|
||||||
|
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
|
|
||||||
class OperationFailedError(Exception):
|
|
||||||
def __init__(self, reason):
|
|
||||||
self.msg = reason
|
|
||||||
|
|
||||||
|
|
||||||
class StreamParseError(RuntimeError):
|
|
||||||
def __init__(self, reason):
|
|
||||||
self.msg = reason
|
|
||||||
|
|
||||||
|
|
||||||
class HealthCheckException(Exception):
|
|
||||||
def __init__(self, reason):
|
|
||||||
self.msg = reason
|
|
||||||
|
|
||||||
|
|
||||||
class HealthCheckFailed(HealthCheckException):
|
|
||||||
def __init__(self, container_id):
|
|
||||||
super(HealthCheckFailed, self).__init__(
|
|
||||||
'Container "{}" is unhealthy.'.format(container_id)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class NoHealthCheckConfigured(HealthCheckException):
|
|
||||||
def __init__(self, service_name):
|
|
||||||
super(NoHealthCheckConfigured, self).__init__(
|
|
||||||
'Service "{}" is missing a healthcheck configuration'.format(
|
|
||||||
service_name
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
@ -1,231 +0,0 @@
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from docker.errors import NotFound
|
|
||||||
from docker.types import IPAMConfig
|
|
||||||
from docker.types import IPAMPool
|
|
||||||
from docker.utils import version_gte
|
|
||||||
from docker.utils import version_lt
|
|
||||||
|
|
||||||
from .config import ConfigurationError
|
|
||||||
from .const import LABEL_NETWORK
|
|
||||||
from .const import LABEL_PROJECT
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
OPTS_EXCEPTIONS = [
|
|
||||||
'com.docker.network.driver.overlay.vxlanid_list',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class Network(object):
|
|
||||||
def __init__(self, client, project, name, driver=None, driver_opts=None,
|
|
||||||
ipam=None, external_name=None, internal=False, enable_ipv6=False,
|
|
||||||
labels=None):
|
|
||||||
self.client = client
|
|
||||||
self.project = project
|
|
||||||
self.name = name
|
|
||||||
self.driver = driver
|
|
||||||
self.driver_opts = driver_opts
|
|
||||||
self.ipam = create_ipam_config_from_dict(ipam)
|
|
||||||
self.external_name = external_name
|
|
||||||
self.internal = internal
|
|
||||||
self.enable_ipv6 = enable_ipv6
|
|
||||||
self.labels = labels
|
|
||||||
|
|
||||||
def ensure(self):
|
|
||||||
if self.external_name:
|
|
||||||
try:
|
|
||||||
self.inspect()
|
|
||||||
log.debug(
|
|
||||||
'Network {0} declared as external. No new '
|
|
||||||
'network will be created.'.format(self.name)
|
|
||||||
)
|
|
||||||
except NotFound:
|
|
||||||
raise ConfigurationError(
|
|
||||||
'Network {name} declared as external, but could'
|
|
||||||
' not be found. Please create the network manually'
|
|
||||||
' using `{command} {name}` and try again.'.format(
|
|
||||||
name=self.external_name,
|
|
||||||
command='docker network create'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = self.inspect()
|
|
||||||
check_remote_network_config(data, self)
|
|
||||||
except NotFound:
|
|
||||||
driver_name = 'the default driver'
|
|
||||||
if self.driver:
|
|
||||||
driver_name = 'driver "{}"'.format(self.driver)
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
'Creating network "{}" with {}'
|
|
||||||
.format(self.full_name, driver_name)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.client.create_network(
|
|
||||||
name=self.full_name,
|
|
||||||
driver=self.driver,
|
|
||||||
options=self.driver_opts,
|
|
||||||
ipam=self.ipam,
|
|
||||||
internal=self.internal,
|
|
||||||
enable_ipv6=self.enable_ipv6,
|
|
||||||
labels=self._labels,
|
|
||||||
attachable=version_gte(self.client._version, '1.24') or None,
|
|
||||||
)
|
|
||||||
|
|
||||||
def remove(self):
|
|
||||||
if self.external_name:
|
|
||||||
log.info("Network %s is external, skipping", self.full_name)
|
|
||||||
return
|
|
||||||
|
|
||||||
log.info("Removing network {}".format(self.full_name))
|
|
||||||
self.client.remove_network(self.full_name)
|
|
||||||
|
|
||||||
def inspect(self):
|
|
||||||
return self.client.inspect_network(self.full_name)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def full_name(self):
|
|
||||||
if self.external_name:
|
|
||||||
return self.external_name
|
|
||||||
return '{0}_{1}'.format(self.project, self.name)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _labels(self):
|
|
||||||
if version_lt(self.client._version, '1.23'):
|
|
||||||
return None
|
|
||||||
labels = self.labels.copy() if self.labels else {}
|
|
||||||
labels.update({
|
|
||||||
LABEL_PROJECT: self.project,
|
|
||||||
LABEL_NETWORK: self.name,
|
|
||||||
})
|
|
||||||
return labels
|
|
||||||
|
|
||||||
|
|
||||||
def create_ipam_config_from_dict(ipam_dict):
|
|
||||||
if not ipam_dict:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return IPAMConfig(
|
|
||||||
driver=ipam_dict.get('driver'),
|
|
||||||
pool_configs=[
|
|
||||||
IPAMPool(
|
|
||||||
subnet=config.get('subnet'),
|
|
||||||
iprange=config.get('ip_range'),
|
|
||||||
gateway=config.get('gateway'),
|
|
||||||
aux_addresses=config.get('aux_addresses'),
|
|
||||||
)
|
|
||||||
for config in ipam_dict.get('config', [])
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def check_remote_network_config(remote, local):
|
|
||||||
if local.driver and remote.get('Driver') != local.driver:
|
|
||||||
raise ConfigurationError(
|
|
||||||
'Network "{}" needs to be recreated - driver has changed'
|
|
||||||
.format(local.full_name)
|
|
||||||
)
|
|
||||||
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())):
|
|
||||||
if k in OPTS_EXCEPTIONS:
|
|
||||||
continue
|
|
||||||
if remote_opts.get(k) != local_opts.get(k):
|
|
||||||
raise ConfigurationError(
|
|
||||||
'Network "{}" needs to be recreated - options have changed'
|
|
||||||
.format(local.full_name)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def build_networks(name, config_data, client):
|
|
||||||
network_config = config_data.networks or {}
|
|
||||||
networks = {
|
|
||||||
network_name: Network(
|
|
||||||
client=client, project=name, name=network_name,
|
|
||||||
driver=data.get('driver'),
|
|
||||||
driver_opts=data.get('driver_opts'),
|
|
||||||
ipam=data.get('ipam'),
|
|
||||||
external_name=data.get('external_name'),
|
|
||||||
internal=data.get('internal'),
|
|
||||||
enable_ipv6=data.get('enable_ipv6'),
|
|
||||||
labels=data.get('labels'),
|
|
||||||
)
|
|
||||||
for network_name, data in network_config.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
if 'default' not in networks:
|
|
||||||
networks['default'] = Network(client, name, 'default')
|
|
||||||
|
|
||||||
return networks
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectNetworks(object):
|
|
||||||
|
|
||||||
def __init__(self, networks, use_networking):
|
|
||||||
self.networks = networks or {}
|
|
||||||
self.use_networking = use_networking
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_services(cls, services, networks, use_networking):
|
|
||||||
service_networks = {
|
|
||||||
network: networks.get(network)
|
|
||||||
for service in services
|
|
||||||
for network in get_network_names_for_service(service)
|
|
||||||
}
|
|
||||||
unused = set(networks) - set(service_networks) - {'default'}
|
|
||||||
if unused:
|
|
||||||
log.warn(
|
|
||||||
"Some networks were defined but are not used by any service: "
|
|
||||||
"{}".format(", ".join(unused)))
|
|
||||||
return cls(service_networks, use_networking)
|
|
||||||
|
|
||||||
def remove(self):
|
|
||||||
if not self.use_networking:
|
|
||||||
return
|
|
||||||
for network in self.networks.values():
|
|
||||||
try:
|
|
||||||
network.remove()
|
|
||||||
except NotFound:
|
|
||||||
log.warn("Network %s not found.", network.full_name)
|
|
||||||
|
|
||||||
def initialize(self):
|
|
||||||
if not self.use_networking:
|
|
||||||
return
|
|
||||||
|
|
||||||
for network in self.networks.values():
|
|
||||||
network.ensure()
|
|
||||||
|
|
||||||
|
|
||||||
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 {}))
|
|
||||||
for net, config in networks.items()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_network_names_for_service(service_dict):
|
|
||||||
return get_network_defs_for_service(service_dict).keys()
|
|
||||||
|
|
||||||
|
|
||||||
def get_networks(service_dict, network_definitions):
|
|
||||||
networks = {}
|
|
||||||
for name, netdef in get_network_defs_for_service(service_dict).items():
|
|
||||||
network = network_definitions.get(name)
|
|
||||||
if network:
|
|
||||||
networks[network.full_name] = netdef
|
|
||||||
else:
|
|
||||||
raise ConfigurationError(
|
|
||||||
'Service "{}" uses an undefined network "{}"'
|
|
||||||
.format(service_dict['name'], name))
|
|
||||||
|
|
||||||
return networks
|
|
||||||
|
|
@ -1,259 +0,0 @@
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import operator
|
|
||||||
import sys
|
|
||||||
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 compose.cli.signals import ShutdownException
|
|
||||||
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__)
|
|
||||||
|
|
||||||
STOP = object()
|
|
||||||
|
|
||||||
|
|
||||||
def parallel_execute(objects, func, get_name, msg, get_deps=None):
|
|
||||||
"""Runs func on objects in parallel while ensuring that func is
|
|
||||||
ran on object only after it is ran on all its dependencies.
|
|
||||||
|
|
||||||
get_deps called on object must return a collection with its dependencies.
|
|
||||||
get_name called on object must return its name.
|
|
||||||
"""
|
|
||||||
objects = list(objects)
|
|
||||||
stream = get_output_stream(sys.stderr)
|
|
||||||
|
|
||||||
writer = ParallelStreamWriter(stream, msg)
|
|
||||||
for obj in objects:
|
|
||||||
writer.initialize(get_name(obj))
|
|
||||||
|
|
||||||
events = parallel_execute_iter(objects, func, get_deps)
|
|
||||||
|
|
||||||
errors = {}
|
|
||||||
results = []
|
|
||||||
error_to_reraise = None
|
|
||||||
|
|
||||||
for obj, result, exception in events:
|
|
||||||
if exception is None:
|
|
||||||
writer.write(get_name(obj), 'done')
|
|
||||||
results.append(result)
|
|
||||||
elif isinstance(exception, APIError):
|
|
||||||
errors[get_name(obj)] = exception.explanation
|
|
||||||
writer.write(get_name(obj), 'error')
|
|
||||||
elif isinstance(exception, (OperationFailedError, HealthCheckFailed, NoHealthCheckConfigured)):
|
|
||||||
errors[get_name(obj)] = exception.msg
|
|
||||||
writer.write(get_name(obj), 'error')
|
|
||||||
elif isinstance(exception, UpstreamError):
|
|
||||||
writer.write(get_name(obj), 'error')
|
|
||||||
else:
|
|
||||||
errors[get_name(obj)] = exception
|
|
||||||
error_to_reraise = exception
|
|
||||||
|
|
||||||
for obj_name, error in errors.items():
|
|
||||||
stream.write("\nERROR: for {} {}\n".format(obj_name, error))
|
|
||||||
|
|
||||||
if error_to_reraise:
|
|
||||||
raise error_to_reraise
|
|
||||||
|
|
||||||
return results, errors
|
|
||||||
|
|
||||||
|
|
||||||
def _no_deps(x):
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
class State(object):
|
|
||||||
"""
|
|
||||||
Holds the state of a partially-complete parallel operation.
|
|
||||||
|
|
||||||
state.started: objects being processed
|
|
||||||
state.finished: objects which have been processed
|
|
||||||
state.failed: objects which either failed or whose dependencies failed
|
|
||||||
"""
|
|
||||||
def __init__(self, objects):
|
|
||||||
self.objects = objects
|
|
||||||
|
|
||||||
self.started = set()
|
|
||||||
self.finished = set()
|
|
||||||
self.failed = set()
|
|
||||||
|
|
||||||
def is_done(self):
|
|
||||||
return len(self.finished) + len(self.failed) >= len(self.objects)
|
|
||||||
|
|
||||||
def pending(self):
|
|
||||||
return set(self.objects) - self.started - self.finished - self.failed
|
|
||||||
|
|
||||||
|
|
||||||
def parallel_execute_iter(objects, func, get_deps):
|
|
||||||
"""
|
|
||||||
Runs func on objects in parallel while ensuring that func is
|
|
||||||
ran on object only after it is ran on all its dependencies.
|
|
||||||
|
|
||||||
Returns an iterator of tuples which look like:
|
|
||||||
|
|
||||||
# if func returned normally when run on object
|
|
||||||
(object, result, None)
|
|
||||||
|
|
||||||
# if func raised an exception when run on object
|
|
||||||
(object, None, exception)
|
|
||||||
|
|
||||||
# if func raised an exception when run on one of object's dependencies
|
|
||||||
(object, None, UpstreamError())
|
|
||||||
"""
|
|
||||||
if get_deps is None:
|
|
||||||
get_deps = _no_deps
|
|
||||||
|
|
||||||
results = Queue()
|
|
||||||
state = State(objects)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
feed_queue(objects, func, get_deps, results, state)
|
|
||||||
|
|
||||||
try:
|
|
||||||
event = results.get(timeout=0.1)
|
|
||||||
except Empty:
|
|
||||||
continue
|
|
||||||
# See https://github.com/docker/compose/issues/189
|
|
||||||
except thread.error:
|
|
||||||
raise ShutdownException()
|
|
||||||
|
|
||||||
if event is STOP:
|
|
||||||
break
|
|
||||||
|
|
||||||
obj, _, exception = event
|
|
||||||
if exception is None:
|
|
||||||
log.debug('Finished processing: {}'.format(obj))
|
|
||||||
state.finished.add(obj)
|
|
||||||
else:
|
|
||||||
log.debug('Failed: {}'.format(obj))
|
|
||||||
state.failed.add(obj)
|
|
||||||
|
|
||||||
yield event
|
|
||||||
|
|
||||||
|
|
||||||
def producer(obj, func, results):
|
|
||||||
"""
|
|
||||||
The entry point for a producer thread which runs func on a single object.
|
|
||||||
Places a tuple on the results queue once func has either returned or raised.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
result = func(obj)
|
|
||||||
results.put((obj, result, None))
|
|
||||||
except Exception as e:
|
|
||||||
results.put((obj, None, e))
|
|
||||||
|
|
||||||
|
|
||||||
def feed_queue(objects, func, get_deps, results, state):
|
|
||||||
"""
|
|
||||||
Starts producer threads for any objects which are ready to be processed
|
|
||||||
(i.e. they have no dependencies which haven't been successfully processed).
|
|
||||||
|
|
||||||
Shortcuts any objects whose dependencies have failed and places an
|
|
||||||
(object, None, UpstreamError()) tuple on the results queue.
|
|
||||||
"""
|
|
||||||
pending = state.pending()
|
|
||||||
log.debug('Pending: {}'.format(pending))
|
|
||||||
|
|
||||||
for obj in pending:
|
|
||||||
deps = get_deps(obj)
|
|
||||||
try:
|
|
||||||
if any(dep[0] in state.failed for dep in deps):
|
|
||||||
log.debug('{} has upstream errors - not processing'.format(obj))
|
|
||||||
results.put((obj, None, UpstreamError()))
|
|
||||||
state.failed.add(obj)
|
|
||||||
elif all(
|
|
||||||
dep not in objects or (
|
|
||||||
dep in state.finished and (not ready_check or ready_check(dep))
|
|
||||||
) for dep, ready_check in deps
|
|
||||||
):
|
|
||||||
log.debug('Starting producer thread for {}'.format(obj))
|
|
||||||
t = Thread(target=producer, args=(obj, func, results))
|
|
||||||
t.daemon = True
|
|
||||||
t.start()
|
|
||||||
state.started.add(obj)
|
|
||||||
except (HealthCheckFailed, NoHealthCheckConfigured) as e:
|
|
||||||
log.debug(
|
|
||||||
'Healthcheck for service(s) upstream of {} failed - '
|
|
||||||
'not processing'.format(obj)
|
|
||||||
)
|
|
||||||
results.put((obj, None, e))
|
|
||||||
|
|
||||||
if state.is_done():
|
|
||||||
results.put(STOP)
|
|
||||||
|
|
||||||
|
|
||||||
class UpstreamError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ParallelStreamWriter(object):
|
|
||||||
"""Write out messages for operations happening in parallel.
|
|
||||||
|
|
||||||
Each operation has it's own line, and ANSI code characters are used
|
|
||||||
to jump to the correct line, and write over the line.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, stream, msg):
|
|
||||||
self.stream = stream
|
|
||||||
self.msg = msg
|
|
||||||
self.lines = []
|
|
||||||
|
|
||||||
def initialize(self, obj_index):
|
|
||||||
if self.msg is None:
|
|
||||||
return
|
|
||||||
self.lines.append(obj_index)
|
|
||||||
self.stream.write("{} {} ... \r\n".format(self.msg, obj_index))
|
|
||||||
self.stream.flush()
|
|
||||||
|
|
||||||
def write(self, obj_index, status):
|
|
||||||
if self.msg is None:
|
|
||||||
return
|
|
||||||
position = self.lines.index(obj_index)
|
|
||||||
diff = len(self.lines) - position
|
|
||||||
# move up
|
|
||||||
self.stream.write("%c[%dA" % (27, diff))
|
|
||||||
# erase
|
|
||||||
self.stream.write("%c[2K\r" % 27)
|
|
||||||
self.stream.write("{} {} ... {}\r".format(self.msg, obj_index, status))
|
|
||||||
# move back down
|
|
||||||
self.stream.write("%c[%dB" % (27, diff))
|
|
||||||
self.stream.flush()
|
|
||||||
|
|
||||||
|
|
||||||
def parallel_operation(containers, operation, options, message):
|
|
||||||
parallel_execute(
|
|
||||||
containers,
|
|
||||||
operator.methodcaller(operation, **options),
|
|
||||||
operator.attrgetter('name'),
|
|
||||||
message)
|
|
||||||
|
|
||||||
|
|
||||||
def parallel_remove(containers, options):
|
|
||||||
stopped_containers = [c for c in containers if not c.is_running]
|
|
||||||
parallel_operation(stopped_containers, 'remove', options, 'Removing')
|
|
||||||
|
|
||||||
|
|
||||||
def parallel_start(containers, options):
|
|
||||||
parallel_operation(containers, 'start', options, 'Starting')
|
|
||||||
|
|
||||||
|
|
||||||
def parallel_pause(containers, options):
|
|
||||||
parallel_operation(containers, 'pause', options, 'Pausing')
|
|
||||||
|
|
||||||
|
|
||||||
def parallel_unpause(containers, options):
|
|
||||||
parallel_operation(containers, 'unpause', options, 'Unpausing')
|
|
||||||
|
|
||||||
|
|
||||||
def parallel_kill(containers, options):
|
|
||||||
parallel_operation(containers, 'kill', options, 'Killing')
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
from __future__ import absolute_import
|
import json
|
||||||
from __future__ import unicode_literals
|
import os
|
||||||
|
import codecs
|
||||||
from compose import utils
|
|
||||||
|
|
||||||
|
|
||||||
class StreamOutputError(Exception):
|
class StreamOutputError(Exception):
|
||||||
|
|
@ -9,41 +8,35 @@ class StreamOutputError(Exception):
|
||||||
|
|
||||||
|
|
||||||
def stream_output(output, stream):
|
def stream_output(output, stream):
|
||||||
is_terminal = hasattr(stream, 'isatty') and stream.isatty()
|
is_terminal = hasattr(stream, 'fileno') and os.isatty(stream.fileno())
|
||||||
stream = utils.get_output_stream(stream)
|
stream = codecs.getwriter('utf-8')(stream)
|
||||||
all_events = []
|
all_events = []
|
||||||
lines = {}
|
lines = {}
|
||||||
diff = 0
|
diff = 0
|
||||||
|
|
||||||
for event in utils.json_stream(output):
|
for chunk in output:
|
||||||
|
event = json.loads(chunk)
|
||||||
all_events.append(event)
|
all_events.append(event)
|
||||||
is_progress_event = 'progress' in event or 'progressDetail' in event
|
|
||||||
|
|
||||||
if not is_progress_event:
|
if 'progress' in event or 'progressDetail' in event:
|
||||||
print_output_event(event, stream, is_terminal)
|
|
||||||
stream.flush()
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not is_terminal:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# if it's a progress event and we have a terminal, then display the progress bars
|
|
||||||
image_id = event.get('id')
|
image_id = event.get('id')
|
||||||
if not image_id:
|
if not image_id:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if image_id not in lines:
|
if image_id in lines:
|
||||||
|
diff = len(lines) - lines[image_id]
|
||||||
|
else:
|
||||||
lines[image_id] = len(lines)
|
lines[image_id] = len(lines)
|
||||||
stream.write("\n")
|
stream.write("\n")
|
||||||
|
diff = 0
|
||||||
|
|
||||||
diff = len(lines) - lines[image_id]
|
if is_terminal:
|
||||||
|
|
||||||
# move cursor up `diff` rows
|
# move cursor up `diff` rows
|
||||||
stream.write("%c[%dA" % (27, diff))
|
stream.write("%c[%dA" % (27, diff))
|
||||||
|
|
||||||
print_output_event(event, stream, is_terminal)
|
print_output_event(event, stream, is_terminal)
|
||||||
|
|
||||||
if 'id' in event:
|
if 'id' in event and is_terminal:
|
||||||
# move cursor back down
|
# move cursor back down
|
||||||
stream.write("%c[%dB" % (27, diff))
|
stream.write("%c[%dB" % (27, diff))
|
||||||
|
|
||||||
|
|
@ -62,6 +55,7 @@ def print_output_event(event, stream, is_terminal):
|
||||||
# erase current line
|
# erase current line
|
||||||
stream.write("%c[2K\r" % 27)
|
stream.write("%c[2K\r" % 27)
|
||||||
terminator = "\r"
|
terminator = "\r"
|
||||||
|
pass
|
||||||
elif 'progressDetail' in event:
|
elif 'progressDetail' in event:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -80,9 +74,8 @@ def print_output_event(event, stream, is_terminal):
|
||||||
stream.write("%s %s%s" % (status, event['progress'], terminator))
|
stream.write("%s %s%s" % (status, event['progress'], terminator))
|
||||||
elif 'progressDetail' in event:
|
elif 'progressDetail' in event:
|
||||||
detail = event['progressDetail']
|
detail = event['progressDetail']
|
||||||
total = detail.get('total')
|
if 'current' in detail:
|
||||||
if 'current' in detail and total:
|
percentage = float(detail['current']) / float(detail['total']) * 100
|
||||||
percentage = float(detail['current']) / float(total) * 100
|
|
||||||
stream.write('%s (%.1f%%)%s' % (status, percentage, terminator))
|
stream.write('%s (%.1f%%)%s' % (status, percentage, terminator))
|
||||||
else:
|
else:
|
||||||
stream.write('%s%s' % (status, terminator))
|
stream.write('%s%s' % (status, terminator))
|
||||||
|
|
@ -90,22 +83,3 @@ def print_output_event(event, stream, is_terminal):
|
||||||
stream.write("%s%s" % (event['stream'], terminator))
|
stream.write("%s%s" % (event['stream'], terminator))
|
||||||
else:
|
else:
|
||||||
stream.write("%s%s\n" % (status, terminator))
|
stream.write("%s%s\n" % (status, terminator))
|
||||||
|
|
||||||
|
|
||||||
def get_digest_from_pull(events):
|
|
||||||
for event in events:
|
|
||||||
status = event.get('status')
|
|
||||||
if not status or 'Digest' not in status:
|
|
||||||
continue
|
|
||||||
|
|
||||||
_, digest = status.split(':', 1)
|
|
||||||
return digest.strip()
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_digest_from_push(events):
|
|
||||||
for event in events:
|
|
||||||
digest = event.get('aux', {}).get('Digest')
|
|
||||||
if digest:
|
|
||||||
return digest
|
|
||||||
return None
|
|
||||||
|
|
|
||||||
|
|
@ -1,134 +1,80 @@
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
from __future__ import absolute_import
|
||||||
import datetime
|
|
||||||
import logging
|
import logging
|
||||||
import operator
|
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
from .config import get_service_name_from_net, ConfigurationError
|
||||||
import enum
|
|
||||||
from docker.errors import APIError
|
|
||||||
|
|
||||||
from . import parallel
|
|
||||||
from .config import ConfigurationError
|
|
||||||
from .config.config import V1
|
|
||||||
from .config.sort_services import get_container_name_from_network_mode
|
|
||||||
from .config.sort_services import get_service_name_from_network_mode
|
|
||||||
from .const import IMAGE_EVENTS
|
|
||||||
from .const import LABEL_ONE_OFF
|
|
||||||
from .const import LABEL_PROJECT
|
|
||||||
from .const import LABEL_SERVICE
|
|
||||||
from .container import Container
|
|
||||||
from .network import build_networks
|
|
||||||
from .network import get_networks
|
|
||||||
from .network import ProjectNetworks
|
|
||||||
from .service import BuildAction
|
|
||||||
from .service import ContainerNetworkMode
|
|
||||||
from .service import ConvergenceStrategy
|
|
||||||
from .service import NetworkMode
|
|
||||||
from .service import Service
|
from .service import Service
|
||||||
from .service import ServiceNetworkMode
|
from .container import Container
|
||||||
from .utils import microseconds_from_time_nano
|
from docker.errors import APIError
|
||||||
from .volume import ProjectVolumes
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@enum.unique
|
def sort_service_dicts(services):
|
||||||
class OneOffFilter(enum.Enum):
|
# Topological sort (Cormen/Tarjan algorithm).
|
||||||
include = 0
|
unmarked = services[:]
|
||||||
exclude = 1
|
temporary_marked = set()
|
||||||
only = 2
|
sorted_services = []
|
||||||
|
|
||||||
@classmethod
|
def get_service_names(links):
|
||||||
def update_labels(cls, value, labels):
|
return [link.split(':')[0] for link in links]
|
||||||
if value == cls.only:
|
|
||||||
labels.append('{0}={1}'.format(LABEL_ONE_OFF, "True"))
|
def get_service_dependents(service_dict, services):
|
||||||
elif value == cls.exclude:
|
name = service_dict['name']
|
||||||
labels.append('{0}={1}'.format(LABEL_ONE_OFF, "False"))
|
return [
|
||||||
elif value == cls.include:
|
service for service in services
|
||||||
pass
|
if (name in get_service_names(service.get('links', [])) or
|
||||||
|
name in service.get('volumes_from', []) or
|
||||||
|
name == get_service_name_from_net(service.get('net')))
|
||||||
|
]
|
||||||
|
|
||||||
|
def visit(n):
|
||||||
|
if n['name'] in temporary_marked:
|
||||||
|
if n['name'] in get_service_names(n.get('links', [])):
|
||||||
|
raise DependencyError('A service can not link to itself: %s' % n['name'])
|
||||||
|
if n['name'] in n.get('volumes_from', []):
|
||||||
|
raise DependencyError('A service can not mount itself as volume: %s' % n['name'])
|
||||||
else:
|
else:
|
||||||
raise ValueError("Invalid value for one_off: {}".format(repr(value)))
|
raise DependencyError('Circular import between %s' % ' and '.join(temporary_marked))
|
||||||
|
if n in unmarked:
|
||||||
|
temporary_marked.add(n['name'])
|
||||||
|
for m in get_service_dependents(n, services):
|
||||||
|
visit(m)
|
||||||
|
temporary_marked.remove(n['name'])
|
||||||
|
unmarked.remove(n)
|
||||||
|
sorted_services.insert(0, n)
|
||||||
|
|
||||||
|
while unmarked:
|
||||||
|
visit(unmarked[-1])
|
||||||
|
|
||||||
|
return sorted_services
|
||||||
|
|
||||||
|
|
||||||
class Project(object):
|
class Project(object):
|
||||||
"""
|
"""
|
||||||
A collection of services.
|
A collection of services.
|
||||||
"""
|
"""
|
||||||
def __init__(self, name, services, client, networks=None, volumes=None):
|
def __init__(self, name, services, client):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.services = services
|
self.services = services
|
||||||
self.client = client
|
self.client = client
|
||||||
self.volumes = volumes or ProjectVolumes({})
|
|
||||||
self.networks = networks or ProjectNetworks({}, False)
|
|
||||||
|
|
||||||
def labels(self, one_off=OneOffFilter.exclude):
|
|
||||||
labels = ['{0}={1}'.format(LABEL_PROJECT, self.name)]
|
|
||||||
|
|
||||||
OneOffFilter.update_labels(one_off, labels)
|
|
||||||
return labels
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_config(cls, name, config_data, client):
|
def from_dicts(cls, name, service_dicts, client):
|
||||||
"""
|
"""
|
||||||
Construct a Project from a config.Config object.
|
Construct a ServiceCollection from a list of dicts representing services.
|
||||||
"""
|
"""
|
||||||
use_networking = (config_data.version and config_data.version != V1)
|
project = cls(name, [], client)
|
||||||
networks = build_networks(name, config_data, client)
|
for service_dict in sort_service_dicts(service_dicts):
|
||||||
project_networks = ProjectNetworks.from_services(
|
|
||||||
config_data.services,
|
|
||||||
networks,
|
|
||||||
use_networking)
|
|
||||||
volumes = ProjectVolumes.from_config(name, config_data, client)
|
|
||||||
project = cls(name, [], client, project_networks, volumes)
|
|
||||||
|
|
||||||
for service_dict in config_data.services:
|
|
||||||
service_dict = dict(service_dict)
|
|
||||||
if use_networking:
|
|
||||||
service_networks = get_networks(service_dict, networks)
|
|
||||||
else:
|
|
||||||
service_networks = {}
|
|
||||||
|
|
||||||
service_dict.pop('networks', None)
|
|
||||||
links = project.get_links(service_dict)
|
links = project.get_links(service_dict)
|
||||||
network_mode = project.get_network_mode(
|
volumes_from = project.get_volumes_from(service_dict)
|
||||||
service_dict, list(service_networks.keys())
|
net = project.get_net(service_dict)
|
||||||
)
|
|
||||||
volumes_from = get_volumes_from(project, service_dict)
|
|
||||||
|
|
||||||
if config_data.version != V1:
|
|
||||||
service_dict['volumes'] = [
|
|
||||||
volumes.namespace_spec(volume_spec)
|
|
||||||
for volume_spec in service_dict.get('volumes', [])
|
|
||||||
]
|
|
||||||
|
|
||||||
secrets = get_secrets(
|
|
||||||
service_dict['name'],
|
|
||||||
service_dict.pop('secrets', None) or [],
|
|
||||||
config_data.secrets)
|
|
||||||
|
|
||||||
project.services.append(
|
|
||||||
Service(
|
|
||||||
service_dict.pop('name'),
|
|
||||||
client=client,
|
|
||||||
project=name,
|
|
||||||
use_networking=use_networking,
|
|
||||||
networks=service_networks,
|
|
||||||
links=links,
|
|
||||||
network_mode=network_mode,
|
|
||||||
volumes_from=volumes_from,
|
|
||||||
secrets=secrets,
|
|
||||||
**service_dict)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
project.services.append(Service(client=client, project=name, links=links, net=net,
|
||||||
|
volumes_from=volumes_from, **service_dict))
|
||||||
return project
|
return project
|
||||||
|
|
||||||
@property
|
|
||||||
def service_names(self):
|
|
||||||
return [service.name for service in self.services]
|
|
||||||
|
|
||||||
def get_service(self, name):
|
def get_service(self, name):
|
||||||
"""
|
"""
|
||||||
Retrieve a service by name. Raises NoSuchService
|
Retrieve a service by name. Raises NoSuchService
|
||||||
|
|
@ -140,16 +86,6 @@ class Project(object):
|
||||||
|
|
||||||
raise NoSuchService(name)
|
raise NoSuchService(name)
|
||||||
|
|
||||||
def validate_service_names(self, service_names):
|
|
||||||
"""
|
|
||||||
Validate that the given list of service names only contains valid
|
|
||||||
services. Raises NoSuchService if one of the names is invalid.
|
|
||||||
"""
|
|
||||||
valid_names = self.service_names
|
|
||||||
for name in 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):
|
||||||
"""
|
"""
|
||||||
Returns a list of this project's services filtered
|
Returns a list of this project's services filtered
|
||||||
|
|
@ -165,8 +101,11 @@ class Project(object):
|
||||||
Raises NoSuchService if any of the named services do not exist.
|
Raises NoSuchService if any of the named services do not exist.
|
||||||
"""
|
"""
|
||||||
if service_names is None or len(service_names) == 0:
|
if service_names is None or len(service_names) == 0:
|
||||||
service_names = self.service_names
|
return self.get_services(
|
||||||
|
service_names=[s.name for s in self.services],
|
||||||
|
include_deps=include_deps
|
||||||
|
)
|
||||||
|
else:
|
||||||
unsorted = [self.get_service(name) for name in service_names]
|
unsorted = [self.get_service(name) for name in service_names]
|
||||||
services = [s for s in self.services if s in unsorted]
|
services = [s for s in self.services if s in unsorted]
|
||||||
|
|
||||||
|
|
@ -175,15 +114,8 @@ class Project(object):
|
||||||
|
|
||||||
uniques = []
|
uniques = []
|
||||||
[uniques.append(s) for s in services if s not in uniques]
|
[uniques.append(s) for s in services if s not in uniques]
|
||||||
|
|
||||||
return uniques
|
return uniques
|
||||||
|
|
||||||
def get_services_without_duplicate(self, service_names=None, include_deps=False):
|
|
||||||
services = self.get_services(service_names, include_deps)
|
|
||||||
for service in services:
|
|
||||||
service.remove_duplicate_containers()
|
|
||||||
return services
|
|
||||||
|
|
||||||
def get_links(self, service_dict):
|
def get_links(self, service_dict):
|
||||||
links = []
|
links = []
|
||||||
if 'links' in service_dict:
|
if 'links' in service_dict:
|
||||||
|
|
@ -195,321 +127,114 @@ class Project(object):
|
||||||
try:
|
try:
|
||||||
links.append((self.get_service(service_name), link_name))
|
links.append((self.get_service(service_name), link_name))
|
||||||
except NoSuchService:
|
except NoSuchService:
|
||||||
raise ConfigurationError(
|
raise ConfigurationError('Service "%s" has a link to service "%s" which does not exist.' % (service_dict['name'], service_name))
|
||||||
'Service "%s" has a link to service "%s" which does not '
|
|
||||||
'exist.' % (service_dict['name'], service_name))
|
|
||||||
del service_dict['links']
|
del service_dict['links']
|
||||||
return links
|
return links
|
||||||
|
|
||||||
def get_network_mode(self, service_dict, networks):
|
def get_volumes_from(self, service_dict):
|
||||||
network_mode = service_dict.pop('network_mode', None)
|
volumes_from = []
|
||||||
if not network_mode:
|
if 'volumes_from' in service_dict:
|
||||||
if self.networks.use_networking:
|
for volume_name in service_dict.get('volumes_from', []):
|
||||||
return NetworkMode(networks[0]) if networks else NetworkMode('none')
|
|
||||||
return NetworkMode(None)
|
|
||||||
|
|
||||||
service_name = get_service_name_from_network_mode(network_mode)
|
|
||||||
if service_name:
|
|
||||||
return ServiceNetworkMode(self.get_service(service_name))
|
|
||||||
|
|
||||||
container_name = get_container_name_from_network_mode(network_mode)
|
|
||||||
if container_name:
|
|
||||||
try:
|
try:
|
||||||
return ContainerNetworkMode(Container.from_id(self.client, container_name))
|
service = self.get_service(volume_name)
|
||||||
|
volumes_from.append(service)
|
||||||
|
except NoSuchService:
|
||||||
|
try:
|
||||||
|
container = Container.from_id(self.client, volume_name)
|
||||||
|
volumes_from.append(container)
|
||||||
except APIError:
|
except APIError:
|
||||||
raise ConfigurationError(
|
raise ConfigurationError('Service "%s" mounts volumes from "%s", which is not the name of a service or container.' % (service_dict['name'], volume_name))
|
||||||
"Service '{name}' uses the network stack of container '{dep}' which "
|
del service_dict['volumes_from']
|
||||||
"does not exist.".format(name=service_dict['name'], dep=container_name))
|
return volumes_from
|
||||||
|
|
||||||
return NetworkMode(network_mode)
|
def get_net(self, service_dict):
|
||||||
|
if 'net' in service_dict:
|
||||||
|
net_name = get_service_name_from_net(service_dict.get('net'))
|
||||||
|
|
||||||
|
if net_name:
|
||||||
|
try:
|
||||||
|
net = self.get_service(net_name)
|
||||||
|
except NoSuchService:
|
||||||
|
try:
|
||||||
|
net = Container.from_id(self.client, net_name)
|
||||||
|
except APIError:
|
||||||
|
raise ConfigurationError('Serivce "%s" is trying to use the network of "%s", which is not the name of a service or container.' % (service_dict['name'], net_name))
|
||||||
|
else:
|
||||||
|
net = service_dict['net']
|
||||||
|
|
||||||
|
del service_dict['net']
|
||||||
|
|
||||||
|
else:
|
||||||
|
net = 'bridge'
|
||||||
|
|
||||||
|
return net
|
||||||
|
|
||||||
def start(self, service_names=None, **options):
|
def start(self, service_names=None, **options):
|
||||||
containers = []
|
for service in self.get_services(service_names):
|
||||||
|
service.start(**options)
|
||||||
|
|
||||||
def start_service(service):
|
def stop(self, service_names=None, **options):
|
||||||
service_containers = service.start(quiet=True, **options)
|
for service in reversed(self.get_services(service_names)):
|
||||||
containers.extend(service_containers)
|
service.stop(**options)
|
||||||
|
|
||||||
services = self.get_services(service_names)
|
|
||||||
|
|
||||||
def get_deps(service):
|
|
||||||
return {
|
|
||||||
(self.get_service(dep), config)
|
|
||||||
for dep, config in service.get_dependency_configs().items()
|
|
||||||
}
|
|
||||||
|
|
||||||
parallel.parallel_execute(
|
|
||||||
services,
|
|
||||||
start_service,
|
|
||||||
operator.attrgetter('name'),
|
|
||||||
'Starting',
|
|
||||||
get_deps)
|
|
||||||
|
|
||||||
return containers
|
|
||||||
|
|
||||||
def stop(self, service_names=None, one_off=OneOffFilter.exclude, **options):
|
|
||||||
containers = self.containers(service_names, one_off=one_off)
|
|
||||||
|
|
||||||
def get_deps(container):
|
|
||||||
# actually returning inversed dependencies
|
|
||||||
return {(other, None) for other in containers
|
|
||||||
if container.service in
|
|
||||||
self.get_service(other.service).get_dependency_names()}
|
|
||||||
|
|
||||||
parallel.parallel_execute(
|
|
||||||
containers,
|
|
||||||
self.build_container_operation_with_timeout_func('stop', options),
|
|
||||||
operator.attrgetter('name'),
|
|
||||||
'Stopping',
|
|
||||||
get_deps)
|
|
||||||
|
|
||||||
def pause(self, service_names=None, **options):
|
|
||||||
containers = self.containers(service_names)
|
|
||||||
parallel.parallel_pause(reversed(containers), options)
|
|
||||||
return containers
|
|
||||||
|
|
||||||
def unpause(self, service_names=None, **options):
|
|
||||||
containers = self.containers(service_names)
|
|
||||||
parallel.parallel_unpause(containers, options)
|
|
||||||
return containers
|
|
||||||
|
|
||||||
def kill(self, service_names=None, **options):
|
def kill(self, service_names=None, **options):
|
||||||
parallel.parallel_kill(self.containers(service_names), options)
|
for service in reversed(self.get_services(service_names)):
|
||||||
|
service.kill(**options)
|
||||||
def remove_stopped(self, service_names=None, one_off=OneOffFilter.exclude, **options):
|
|
||||||
parallel.parallel_remove(self.containers(
|
|
||||||
service_names, stopped=True, one_off=one_off
|
|
||||||
), options)
|
|
||||||
|
|
||||||
def down(self, remove_image_type, include_volumes, remove_orphans=False):
|
|
||||||
self.stop(one_off=OneOffFilter.include)
|
|
||||||
self.find_orphan_containers(remove_orphans)
|
|
||||||
self.remove_stopped(v=include_volumes, one_off=OneOffFilter.include)
|
|
||||||
|
|
||||||
self.networks.remove()
|
|
||||||
|
|
||||||
if include_volumes:
|
|
||||||
self.volumes.remove()
|
|
||||||
|
|
||||||
self.remove_images(remove_image_type)
|
|
||||||
|
|
||||||
def remove_images(self, remove_image_type):
|
|
||||||
for service in self.get_services():
|
|
||||||
service.remove_image(remove_image_type)
|
|
||||||
|
|
||||||
def restart(self, service_names=None, **options):
|
def restart(self, service_names=None, **options):
|
||||||
containers = self.containers(service_names, stopped=True)
|
for service in self.get_services(service_names):
|
||||||
|
service.restart(**options)
|
||||||
|
|
||||||
parallel.parallel_execute(
|
def build(self, service_names=None, no_cache=False):
|
||||||
containers,
|
|
||||||
self.build_container_operation_with_timeout_func('restart', options),
|
|
||||||
operator.attrgetter('name'),
|
|
||||||
'Restarting')
|
|
||||||
return containers
|
|
||||||
|
|
||||||
def build(self, service_names=None, no_cache=False, pull=False, force_rm=False):
|
|
||||||
for service in self.get_services(service_names):
|
for service in self.get_services(service_names):
|
||||||
if service.can_be_built():
|
if service.can_be_built():
|
||||||
service.build(no_cache, pull, force_rm)
|
service.build(no_cache)
|
||||||
else:
|
else:
|
||||||
log.info('%s uses an image, skipping' % service.name)
|
log.info('%s uses an image, skipping' % service.name)
|
||||||
|
|
||||||
def create(
|
|
||||||
self,
|
|
||||||
service_names=None,
|
|
||||||
strategy=ConvergenceStrategy.changed,
|
|
||||||
do_build=BuildAction.none,
|
|
||||||
):
|
|
||||||
services = self.get_services_without_duplicate(service_names, include_deps=True)
|
|
||||||
|
|
||||||
for svc in services:
|
|
||||||
svc.ensure_image_exists(do_build=do_build)
|
|
||||||
plans = self._get_convergence_plans(services, strategy)
|
|
||||||
|
|
||||||
for service in services:
|
|
||||||
service.execute_convergence_plan(
|
|
||||||
plans[service.name],
|
|
||||||
detached=True,
|
|
||||||
start=False)
|
|
||||||
|
|
||||||
def events(self, service_names=None):
|
|
||||||
def build_container_event(event, container):
|
|
||||||
time = datetime.datetime.fromtimestamp(event['time'])
|
|
||||||
time = time.replace(
|
|
||||||
microsecond=microseconds_from_time_nano(event['timeNano']))
|
|
||||||
return {
|
|
||||||
'time': time,
|
|
||||||
'type': 'container',
|
|
||||||
'action': event['status'],
|
|
||||||
'id': container.id,
|
|
||||||
'service': container.service,
|
|
||||||
'attributes': {
|
|
||||||
'name': container.name,
|
|
||||||
'image': event['from'],
|
|
||||||
},
|
|
||||||
'container': container,
|
|
||||||
}
|
|
||||||
|
|
||||||
service_names = set(service_names or self.service_names)
|
|
||||||
for event in self.client.events(
|
|
||||||
filters={'label': self.labels()},
|
|
||||||
decode=True
|
|
||||||
):
|
|
||||||
# The first part of this condition is a guard against some events
|
|
||||||
# broadcasted by swarm that don't have a status field.
|
|
||||||
# See https://github.com/docker/compose/issues/3316
|
|
||||||
if 'status' not in event or event['status'] in IMAGE_EVENTS:
|
|
||||||
# We don't receive any image events because labels aren't applied
|
|
||||||
# to images
|
|
||||||
continue
|
|
||||||
|
|
||||||
# TODO: get labels from the API v1.22 , see github issue 2618
|
|
||||||
try:
|
|
||||||
# this can fail if the container has been removed
|
|
||||||
container = Container.from_id(self.client, event['id'])
|
|
||||||
except APIError:
|
|
||||||
continue
|
|
||||||
if container.service not in service_names:
|
|
||||||
continue
|
|
||||||
yield build_container_event(event, container)
|
|
||||||
|
|
||||||
def up(self,
|
def up(self,
|
||||||
service_names=None,
|
service_names=None,
|
||||||
start_deps=True,
|
start_deps=True,
|
||||||
strategy=ConvergenceStrategy.changed,
|
recreate=True,
|
||||||
do_build=BuildAction.none,
|
insecure_registry=False,
|
||||||
timeout=None,
|
detach=False,
|
||||||
detached=False,
|
do_build=True):
|
||||||
remove_orphans=False):
|
running_containers = []
|
||||||
|
for service in self.get_services(service_names, include_deps=start_deps):
|
||||||
warn_for_swarm_mode(self.client)
|
if recreate:
|
||||||
|
for (_, container) in service.recreate_containers(
|
||||||
self.initialize()
|
insecure_registry=insecure_registry,
|
||||||
self.find_orphan_containers(remove_orphans)
|
detach=detach,
|
||||||
|
do_build=do_build):
|
||||||
services = self.get_services_without_duplicate(
|
running_containers.append(container)
|
||||||
service_names,
|
|
||||||
include_deps=start_deps)
|
|
||||||
|
|
||||||
for svc in services:
|
|
||||||
svc.ensure_image_exists(do_build=do_build)
|
|
||||||
plans = self._get_convergence_plans(services, strategy)
|
|
||||||
|
|
||||||
def do(service):
|
|
||||||
return service.execute_convergence_plan(
|
|
||||||
plans[service.name],
|
|
||||||
timeout=timeout,
|
|
||||||
detached=detached
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_deps(service):
|
|
||||||
return {
|
|
||||||
(self.get_service(dep), config)
|
|
||||||
for dep, config in service.get_dependency_configs().items()
|
|
||||||
}
|
|
||||||
|
|
||||||
results, errors = parallel.parallel_execute(
|
|
||||||
services,
|
|
||||||
do,
|
|
||||||
operator.attrgetter('name'),
|
|
||||||
None,
|
|
||||||
get_deps
|
|
||||||
)
|
|
||||||
if errors:
|
|
||||||
raise ProjectError(
|
|
||||||
'Encountered errors while bringing up the project.'
|
|
||||||
)
|
|
||||||
|
|
||||||
return [
|
|
||||||
container
|
|
||||||
for svc_containers in results
|
|
||||||
if svc_containers is not None
|
|
||||||
for container in svc_containers
|
|
||||||
]
|
|
||||||
|
|
||||||
def initialize(self):
|
|
||||||
self.networks.initialize()
|
|
||||||
self.volumes.initialize()
|
|
||||||
|
|
||||||
def _get_convergence_plans(self, services, strategy):
|
|
||||||
plans = {}
|
|
||||||
|
|
||||||
for service in services:
|
|
||||||
updated_dependencies = [
|
|
||||||
name
|
|
||||||
for name in service.get_dependency_names()
|
|
||||||
if name in plans and
|
|
||||||
plans[name].action in ('recreate', 'create')
|
|
||||||
]
|
|
||||||
|
|
||||||
if updated_dependencies and strategy.allows_recreate:
|
|
||||||
log.debug('%s has upstream changes (%s)',
|
|
||||||
service.name,
|
|
||||||
", ".join(updated_dependencies))
|
|
||||||
plan = service.convergence_plan(ConvergenceStrategy.always)
|
|
||||||
else:
|
else:
|
||||||
plan = service.convergence_plan(strategy)
|
for container in service.start_or_create_containers(
|
||||||
|
insecure_registry=insecure_registry,
|
||||||
|
detach=detach,
|
||||||
|
do_build=do_build):
|
||||||
|
running_containers.append(container)
|
||||||
|
|
||||||
plans[service.name] = plan
|
return running_containers
|
||||||
|
|
||||||
return plans
|
def pull(self, service_names=None, insecure_registry=False):
|
||||||
|
for service in self.get_services(service_names, include_deps=True):
|
||||||
|
service.pull(insecure_registry=insecure_registry)
|
||||||
|
|
||||||
def pull(self, service_names=None, ignore_pull_failures=False):
|
def remove_stopped(self, service_names=None, **options):
|
||||||
for service in self.get_services(service_names, include_deps=False):
|
for service in self.get_services(service_names):
|
||||||
service.pull(ignore_pull_failures)
|
service.remove_stopped(**options)
|
||||||
|
|
||||||
def push(self, service_names=None, ignore_push_failures=False):
|
def containers(self, service_names=None, stopped=False, one_off=False):
|
||||||
for service in self.get_services(service_names, include_deps=False):
|
return [Container.from_ps(self.client, container)
|
||||||
service.push(ignore_push_failures)
|
for container in self.client.containers(all=stopped)
|
||||||
|
for service in self.get_services(service_names)
|
||||||
def _labeled_containers(self, stopped=False, one_off=OneOffFilter.exclude):
|
if service.has_container(container, one_off=one_off)]
|
||||||
return list(filter(None, [
|
|
||||||
Container.from_ps(self.client, container)
|
|
||||||
for container in self.client.containers(
|
|
||||||
all=stopped,
|
|
||||||
filters={'label': self.labels(one_off=one_off)})])
|
|
||||||
)
|
|
||||||
|
|
||||||
def containers(self, service_names=None, stopped=False, one_off=OneOffFilter.exclude):
|
|
||||||
if service_names:
|
|
||||||
self.validate_service_names(service_names)
|
|
||||||
else:
|
|
||||||
service_names = self.service_names
|
|
||||||
|
|
||||||
containers = self._labeled_containers(stopped, one_off)
|
|
||||||
|
|
||||||
def matches_service_names(container):
|
|
||||||
return container.labels.get(LABEL_SERVICE) in service_names
|
|
||||||
|
|
||||||
return [c for c in containers if matches_service_names(c)]
|
|
||||||
|
|
||||||
def find_orphan_containers(self, remove_orphans):
|
|
||||||
def _find():
|
|
||||||
containers = self._labeled_containers()
|
|
||||||
for ctnr in containers:
|
|
||||||
service_name = ctnr.labels.get(LABEL_SERVICE)
|
|
||||||
if service_name not in self.service_names:
|
|
||||||
yield ctnr
|
|
||||||
orphans = list(_find())
|
|
||||||
if not orphans:
|
|
||||||
return
|
|
||||||
if remove_orphans:
|
|
||||||
for ctnr in orphans:
|
|
||||||
log.info('Removing orphan container "{0}"'.format(ctnr.name))
|
|
||||||
ctnr.kill()
|
|
||||||
ctnr.remove(force=True)
|
|
||||||
else:
|
|
||||||
log.warning(
|
|
||||||
'Found orphan containers ({0}) 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(
|
|
||||||
', '.join(["{}".format(ctnr.name) for ctnr in orphans])
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def _inject_deps(self, acc, service):
|
def _inject_deps(self, acc, service):
|
||||||
dep_names = service.get_dependency_names()
|
net_name = service.get_net_name()
|
||||||
|
dep_names = (service.get_linked_names() +
|
||||||
|
service.get_volumes_from_names() +
|
||||||
|
([net_name] if net_name else []))
|
||||||
|
|
||||||
if len(dep_names) > 0:
|
if len(dep_names) > 0:
|
||||||
dep_services = self.get_services(
|
dep_services = self.get_services(
|
||||||
|
|
@ -522,85 +247,6 @@ class Project(object):
|
||||||
dep_services.append(service)
|
dep_services.append(service)
|
||||||
return acc + dep_services
|
return acc + dep_services
|
||||||
|
|
||||||
def build_container_operation_with_timeout_func(self, operation, options):
|
|
||||||
def container_operation_with_timeout(container):
|
|
||||||
if options.get('timeout') is None:
|
|
||||||
service = self.get_service(container.service)
|
|
||||||
options['timeout'] = service.stop_timeout(None)
|
|
||||||
return getattr(container, operation)(**options)
|
|
||||||
return container_operation_with_timeout
|
|
||||||
|
|
||||||
|
|
||||||
def get_volumes_from(project, service_dict):
|
|
||||||
volumes_from = service_dict.pop('volumes_from', None)
|
|
||||||
if not volumes_from:
|
|
||||||
return []
|
|
||||||
|
|
||||||
def build_volume_from(spec):
|
|
||||||
if spec.type == 'service':
|
|
||||||
try:
|
|
||||||
return spec._replace(source=project.get_service(spec.source))
|
|
||||||
except NoSuchService:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if spec.type == 'container':
|
|
||||||
try:
|
|
||||||
container = Container.from_id(project.client, spec.source)
|
|
||||||
return spec._replace(source=container)
|
|
||||||
except APIError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
raise ConfigurationError(
|
|
||||||
"Service \"{}\" mounts volumes from \"{}\", which is not the name "
|
|
||||||
"of a service or container.".format(
|
|
||||||
service_dict['name'],
|
|
||||||
spec.source))
|
|
||||||
|
|
||||||
return [build_volume_from(vf) for vf in volumes_from]
|
|
||||||
|
|
||||||
|
|
||||||
def get_secrets(service, service_secrets, secret_defs):
|
|
||||||
secrets = []
|
|
||||||
|
|
||||||
for secret in service_secrets:
|
|
||||||
secret_def = secret_defs.get(secret.source)
|
|
||||||
if not secret_def:
|
|
||||||
raise ConfigurationError(
|
|
||||||
"Service \"{service}\" uses an undefined secret \"{secret}\" "
|
|
||||||
.format(service=service, secret=secret.source))
|
|
||||||
|
|
||||||
if secret_def.get('external_name'):
|
|
||||||
log.warn("Service \"{service}\" uses secret \"{secret}\" which is external. "
|
|
||||||
"External secrets are not available to containers created by "
|
|
||||||
"docker-compose.".format(service=service, secret=secret.source))
|
|
||||||
continue
|
|
||||||
|
|
||||||
if secret.uid or secret.gid or secret.mode:
|
|
||||||
log.warn("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))
|
|
||||||
|
|
||||||
secrets.append({'secret': secret, 'file': secret_def.get('file')})
|
|
||||||
|
|
||||||
return secrets
|
|
||||||
|
|
||||||
|
|
||||||
def warn_for_swarm_mode(client):
|
|
||||||
info = client.info()
|
|
||||||
if info.get('Swarm', {}).get('LocalNodeState') == 'active':
|
|
||||||
if info.get('ServerVersion', '').startswith('ucp'):
|
|
||||||
# UCP does multi-node scheduling with traditional Compose files.
|
|
||||||
return
|
|
||||||
|
|
||||||
log.warn(
|
|
||||||
"The Docker Engine you're using is running in swarm mode.\n\n"
|
|
||||||
"Compose does not use swarm mode to deploy services to multiple nodes in a swarm. "
|
|
||||||
"All containers will be scheduled on the current node.\n\n"
|
|
||||||
"To deploy your application across the swarm, "
|
|
||||||
"use `docker stack deploy`.\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class NoSuchService(Exception):
|
class NoSuchService(Exception):
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
|
|
@ -611,6 +257,5 @@ class NoSuchService(Exception):
|
||||||
return self.msg
|
return self.msg
|
||||||
|
|
||||||
|
|
||||||
class ProjectError(Exception):
|
class DependencyError(ConfigurationError):
|
||||||
def __init__(self, msg):
|
pass
|
||||||
self.msg = msg
|
|
||||||
|
|
|
||||||
1424
compose/service.py
1424
compose/service.py
File diff suppressed because it is too large
Load diff
|
|
@ -1,96 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
'''
|
|
||||||
timeparse.py
|
|
||||||
(c) Will Roberts <wildwilhelm@gmail.com> 1 February, 2014
|
|
||||||
|
|
||||||
This is a vendored and modified copy of:
|
|
||||||
github.com/wroberts/pytimeparse @ cc0550d
|
|
||||||
|
|
||||||
It has been modified to mimic the behaviour of
|
|
||||||
https://golang.org/pkg/time/#ParseDuration
|
|
||||||
'''
|
|
||||||
# MIT LICENSE
|
|
||||||
#
|
|
||||||
# Permission is hereby granted, free of charge, to any person
|
|
||||||
# obtaining a copy of this software and associated documentation files
|
|
||||||
# (the "Software"), to deal in the Software without restriction,
|
|
||||||
# including without limitation the rights to use, copy, modify, merge,
|
|
||||||
# publish, distribute, sublicense, and/or sell copies of the Software,
|
|
||||||
# and to permit persons to whom the Software is furnished to do so,
|
|
||||||
# subject to the following conditions:
|
|
||||||
#
|
|
||||||
# The above copyright notice and this permission notice shall be
|
|
||||||
# included in all copies or substantial portions of the Software.
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
||||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
||||||
# 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<hours>[\d.]+)h'
|
|
||||||
MINS = r'(?P<mins>[\d.]+)m'
|
|
||||||
SECS = r'(?P<secs>[\d.]+)s'
|
|
||||||
MILLI = r'(?P<milli>[\d.]+)ms'
|
|
||||||
MICRO = r'(?P<micro>[\d.]+)(?:us|µs)'
|
|
||||||
NANO = r'(?P<nano>[\d.]+)ns'
|
|
||||||
|
|
||||||
|
|
||||||
def opt(x):
|
|
||||||
return r'(?:{x})?'.format(x=x)
|
|
||||||
|
|
||||||
|
|
||||||
TIMEFORMAT = r'{HOURS}{MINS}{SECS}{MILLI}{MICRO}{NANO}'.format(
|
|
||||||
HOURS=opt(HOURS),
|
|
||||||
MINS=opt(MINS),
|
|
||||||
SECS=opt(SECS),
|
|
||||||
MILLI=opt(MILLI),
|
|
||||||
MICRO=opt(MICRO),
|
|
||||||
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),
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
def timeparse(sval):
|
|
||||||
"""Parse a time expression, returning it as a number of seconds. If
|
|
||||||
possible, the return value will be an `int`; if this is not
|
|
||||||
possible, the return will be a `float`. Returns `None` if a time
|
|
||||||
expression cannot be parsed from the given string.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
- `sval`: the string value to parse
|
|
||||||
|
|
||||||
>>> timeparse('1m24s')
|
|
||||||
84
|
|
||||||
>>> timeparse('1.2 minutes')
|
|
||||||
72
|
|
||||||
>>> timeparse('1.2 seconds')
|
|
||||||
1.2
|
|
||||||
"""
|
|
||||||
match = re.match(r'\s*' + TIMEFORMAT + r'\s*$', sval, re.I)
|
|
||||||
if not match or not match.group(0).strip():
|
|
||||||
return
|
|
||||||
|
|
||||||
mdict = match.groupdict()
|
|
||||||
return sum(
|
|
||||||
MULTIPLIERS[k] * cast(v) for (k, v) in mdict.items() if v is not None)
|
|
||||||
|
|
||||||
|
|
||||||
def cast(value):
|
|
||||||
return int(value, 10) if value.isdigit() else float(value)
|
|
||||||
135
compose/utils.py
135
compose/utils.py
|
|
@ -1,135 +0,0 @@
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import codecs
|
|
||||||
import hashlib
|
|
||||||
import json
|
|
||||||
import json.decoder
|
|
||||||
import logging
|
|
||||||
import ntpath
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from .errors import StreamParseError
|
|
||||||
from .timeparse import timeparse
|
|
||||||
|
|
||||||
|
|
||||||
json_decoder = json.JSONDecoder()
|
|
||||||
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.
|
|
||||||
|
|
||||||
This function can be removed once docker-py returns text streams instead
|
|
||||||
of byte streams.
|
|
||||||
"""
|
|
||||||
for data in stream:
|
|
||||||
if not isinstance(data, six.text_type):
|
|
||||||
data = data.decode('utf-8', 'replace')
|
|
||||||
yield data
|
|
||||||
|
|
||||||
|
|
||||||
def line_splitter(buffer, separator=u'\n'):
|
|
||||||
index = buffer.find(six.text_type(separator))
|
|
||||||
if index == -1:
|
|
||||||
return None
|
|
||||||
return buffer[:index + 1], buffer[index + 1:]
|
|
||||||
|
|
||||||
|
|
||||||
def split_buffer(stream, splitter=None, decoder=lambda a: a):
|
|
||||||
"""Given a generator which yields strings and a splitter function,
|
|
||||||
joins all input, splits on the separator and yields each chunk.
|
|
||||||
|
|
||||||
Unlike string.split(), each chunk includes the trailing
|
|
||||||
separator, except for the last one if none was found on the end
|
|
||||||
of the input.
|
|
||||||
"""
|
|
||||||
splitter = splitter or line_splitter
|
|
||||||
buffered = six.text_type('')
|
|
||||||
|
|
||||||
for data in stream_as_text(stream):
|
|
||||||
buffered += data
|
|
||||||
while True:
|
|
||||||
buffer_split = splitter(buffered)
|
|
||||||
if buffer_split is None:
|
|
||||||
break
|
|
||||||
|
|
||||||
item, buffered = buffer_split
|
|
||||||
yield item
|
|
||||||
|
|
||||||
if buffered:
|
|
||||||
try:
|
|
||||||
yield decoder(buffered)
|
|
||||||
except Exception as e:
|
|
||||||
log.error(
|
|
||||||
'Compose tried decoding the following data chunk, but failed:'
|
|
||||||
'\n%s' % repr(buffered)
|
|
||||||
)
|
|
||||||
raise StreamParseError(e)
|
|
||||||
|
|
||||||
|
|
||||||
def json_splitter(buffer):
|
|
||||||
"""Attempt to parse a json object from a buffer. If there is at least one
|
|
||||||
object, return it and the rest of the buffer, otherwise return None.
|
|
||||||
"""
|
|
||||||
buffer = buffer.strip()
|
|
||||||
try:
|
|
||||||
obj, index = json_decoder.raw_decode(buffer)
|
|
||||||
rest = buffer[json.decoder.WHITESPACE.match(buffer, index).end():]
|
|
||||||
return obj, rest
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def json_stream(stream):
|
|
||||||
"""Given a stream of text, return a stream of json objects.
|
|
||||||
This handles streams which are inconsistently buffered (some entries may
|
|
||||||
be newline delimited, and others are not).
|
|
||||||
"""
|
|
||||||
return split_buffer(stream, json_splitter, json_decoder.decode)
|
|
||||||
|
|
||||||
|
|
||||||
def json_hash(obj):
|
|
||||||
dump = json.dumps(obj, sort_keys=True, separators=(',', ':'))
|
|
||||||
h = hashlib.sha256()
|
|
||||||
h.update(dump.encode('utf8'))
|
|
||||||
return h.hexdigest()
|
|
||||||
|
|
||||||
|
|
||||||
def microseconds_from_time_nano(time_nano):
|
|
||||||
return int(time_nano % 1000000000 / 1000)
|
|
||||||
|
|
||||||
|
|
||||||
def nanoseconds_from_time_seconds(time_seconds):
|
|
||||||
return time_seconds * 1000000000
|
|
||||||
|
|
||||||
|
|
||||||
def parse_seconds_float(value):
|
|
||||||
return timeparse(value or '')
|
|
||||||
|
|
||||||
|
|
||||||
def parse_nanoseconds_int(value):
|
|
||||||
parsed = timeparse(value or '')
|
|
||||||
if parsed is None:
|
|
||||||
return None
|
|
||||||
return int(parsed * 1000000000)
|
|
||||||
|
|
||||||
|
|
||||||
def build_string_dict(source_dict):
|
|
||||||
return dict((k, str(v if v is not None else '')) for k, v in source_dict.items())
|
|
||||||
|
|
||||||
|
|
||||||
def splitdrive(path):
|
|
||||||
if len(path) == 0:
|
|
||||||
return ('', '')
|
|
||||||
if path[0] in ['.', '\\', '/', '~']:
|
|
||||||
return ('', path)
|
|
||||||
return ntpath.splitdrive(path)
|
|
||||||
|
|
@ -1,151 +0,0 @@
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from docker.errors import NotFound
|
|
||||||
from docker.utils import version_lt
|
|
||||||
|
|
||||||
from .config import ConfigurationError
|
|
||||||
from .const import LABEL_PROJECT
|
|
||||||
from .const import LABEL_VOLUME
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Volume(object):
|
|
||||||
def __init__(self, client, project, name, driver=None, driver_opts=None,
|
|
||||||
external_name=None, labels=None):
|
|
||||||
self.client = client
|
|
||||||
self.project = project
|
|
||||||
self.name = name
|
|
||||||
self.driver = driver
|
|
||||||
self.driver_opts = driver_opts
|
|
||||||
self.external_name = external_name
|
|
||||||
self.labels = labels
|
|
||||||
|
|
||||||
def create(self):
|
|
||||||
return self.client.create_volume(
|
|
||||||
self.full_name, self.driver, self.driver_opts, labels=self._labels
|
|
||||||
)
|
|
||||||
|
|
||||||
def remove(self):
|
|
||||||
if self.external:
|
|
||||||
log.info("Volume %s is external, skipping", self.full_name)
|
|
||||||
return
|
|
||||||
log.info("Removing volume %s", self.full_name)
|
|
||||||
return self.client.remove_volume(self.full_name)
|
|
||||||
|
|
||||||
def inspect(self):
|
|
||||||
return self.client.inspect_volume(self.full_name)
|
|
||||||
|
|
||||||
def exists(self):
|
|
||||||
try:
|
|
||||||
self.inspect()
|
|
||||||
except NotFound:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def external(self):
|
|
||||||
return bool(self.external_name)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def full_name(self):
|
|
||||||
if self.external_name:
|
|
||||||
return self.external_name
|
|
||||||
return '{0}_{1}'.format(self.project, self.name)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _labels(self):
|
|
||||||
if version_lt(self.client._version, '1.23'):
|
|
||||||
return None
|
|
||||||
labels = self.labels.copy() if self.labels else {}
|
|
||||||
labels.update({
|
|
||||||
LABEL_PROJECT: self.project,
|
|
||||||
LABEL_VOLUME: self.name,
|
|
||||||
})
|
|
||||||
return labels
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectVolumes(object):
|
|
||||||
|
|
||||||
def __init__(self, volumes):
|
|
||||||
self.volumes = volumes
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_config(cls, name, config_data, client):
|
|
||||||
config_volumes = config_data.volumes or {}
|
|
||||||
volumes = {
|
|
||||||
vol_name: Volume(
|
|
||||||
client=client,
|
|
||||||
project=name,
|
|
||||||
name=vol_name,
|
|
||||||
driver=data.get('driver'),
|
|
||||||
driver_opts=data.get('driver_opts'),
|
|
||||||
external_name=data.get('external_name'),
|
|
||||||
labels=data.get('labels')
|
|
||||||
)
|
|
||||||
for vol_name, data in config_volumes.items()
|
|
||||||
}
|
|
||||||
return cls(volumes)
|
|
||||||
|
|
||||||
def remove(self):
|
|
||||||
for volume in self.volumes.values():
|
|
||||||
try:
|
|
||||||
volume.remove()
|
|
||||||
except NotFound:
|
|
||||||
log.warn("Volume %s not found.", volume.full_name)
|
|
||||||
|
|
||||||
def initialize(self):
|
|
||||||
try:
|
|
||||||
for volume in self.volumes.values():
|
|
||||||
volume_exists = volume.exists()
|
|
||||||
if volume.external:
|
|
||||||
log.debug(
|
|
||||||
'Volume {0} declared as external. No new '
|
|
||||||
'volume will be created.'.format(volume.name)
|
|
||||||
)
|
|
||||||
if not volume_exists:
|
|
||||||
raise ConfigurationError(
|
|
||||||
'Volume {name} declared as external, but could'
|
|
||||||
' not be found. Please create the volume manually'
|
|
||||||
' using `{command}{name}` and try again.'.format(
|
|
||||||
name=volume.full_name,
|
|
||||||
command='docker volume create --name='
|
|
||||||
)
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not volume_exists:
|
|
||||||
log.info(
|
|
||||||
'Creating volume "{0}" with {1} driver'.format(
|
|
||||||
volume.full_name, volume.driver or 'default'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
volume.create()
|
|
||||||
else:
|
|
||||||
driver = volume.inspect()['Driver']
|
|
||||||
if volume.driver is not None and driver != volume.driver:
|
|
||||||
raise ConfigurationError(
|
|
||||||
'Configuration for volume {0} specifies driver '
|
|
||||||
'{1}, but a volume with the same name uses a '
|
|
||||||
'different driver ({3}). If you wish to use the '
|
|
||||||
'new configuration, please remove the existing '
|
|
||||||
'volume "{2}" first:\n'
|
|
||||||
'$ docker volume rm {2}'.format(
|
|
||||||
volume.name, volume.driver, volume.full_name,
|
|
||||||
volume.inspect()['Driver']
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except NotFound:
|
|
||||||
raise ConfigurationError(
|
|
||||||
'Volume %s specifies nonexistent driver %s' % (volume.name, volume.driver)
|
|
||||||
)
|
|
||||||
|
|
||||||
def namespace_spec(self, volume_spec):
|
|
||||||
if not volume_spec.is_named_volume:
|
|
||||||
return volume_spec
|
|
||||||
|
|
||||||
volume = self.volumes[volume_spec.external]
|
|
||||||
return volume_spec._replace(external=volume.full_name)
|
|
||||||
|
|
@ -17,145 +17,94 @@
|
||||||
# . ~/.docker-compose-completion.sh
|
# . ~/.docker-compose-completion.sh
|
||||||
|
|
||||||
|
|
||||||
__docker_compose_q() {
|
# For compatibility reasons, Compose and therefore its completion supports several
|
||||||
docker-compose 2>/dev/null $daemon_options "$@"
|
# stack compositon files as listed here, in descending priority.
|
||||||
|
# Support for these filenames might be dropped in some future version.
|
||||||
|
__docker-compose_compose_file() {
|
||||||
|
local file
|
||||||
|
for file in docker-compose.y{,a}ml fig.y{,a}ml ; do
|
||||||
|
[ -e $file ] && {
|
||||||
|
echo $file
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
done
|
||||||
# Transforms a multiline list of strings into a single line string
|
echo docker-compose.yml
|
||||||
# with the words separated by "|".
|
|
||||||
__docker_compose_to_alternatives() {
|
|
||||||
local parts=( $1 )
|
|
||||||
local IFS='|'
|
|
||||||
echo "${parts[*]}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Transforms a multiline list of options into an extglob pattern
|
|
||||||
# suitable for use in case statements.
|
|
||||||
__docker_compose_to_extglob() {
|
|
||||||
local extglob=$( __docker_compose_to_alternatives "$1" )
|
|
||||||
echo "@($extglob)"
|
|
||||||
}
|
|
||||||
|
|
||||||
# suppress trailing whitespace
|
|
||||||
__docker_compose_nospace() {
|
|
||||||
# compopt is not available in ancient bash versions
|
|
||||||
type compopt &>/dev/null && compopt -o nospace
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Extracts all service names from the compose file.
|
# Extracts all service names from the compose file.
|
||||||
___docker_compose_all_services_in_compose_file() {
|
___docker-compose_all_services_in_compose_file() {
|
||||||
__docker_compose_q config --services
|
awk -F: '/^[a-zA-Z0-9]/{print $1}' "${compose_file:-$(__docker-compose_compose_file)}" 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
# All services, even those without an existing container
|
# All services, even those without an existing container
|
||||||
__docker_compose_services_all() {
|
__docker-compose_services_all() {
|
||||||
COMPREPLY=( $(compgen -W "$(___docker_compose_all_services_in_compose_file)" -- "$cur") )
|
COMPREPLY=( $(compgen -W "$(___docker-compose_all_services_in_compose_file)" -- "$cur") )
|
||||||
}
|
}
|
||||||
|
|
||||||
# All services that have an entry with the given key in their compose_file section
|
# All services that have an entry with the given key in their compose_file section
|
||||||
___docker_compose_services_with_key() {
|
___docker-compose_services_with_key() {
|
||||||
# flatten sections under "services" to one line, then filter lines containing the key and return section name
|
# flatten sections to one line, then filter lines containing the key and return section name.
|
||||||
__docker_compose_q config \
|
awk '/^[a-zA-Z0-9]/{printf "\n"};{printf $0;next;}' "${compose_file:-$(__docker-compose_compose_file)}" | awk -F: -v key=": +$1:" '$0 ~ key {print $1}'
|
||||||
| sed -n -e '/^services:/,/^[^ ]/p' \
|
|
||||||
| sed -n 's/^ //p' \
|
|
||||||
| awk '/^[a-zA-Z0-9]/{printf "\n"};{printf $0;next;}' \
|
|
||||||
| awk -F: -v key=": +$1:" '$0 ~ key {print $1}'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# All services that are defined by a Dockerfile reference
|
# All services that are defined by a Dockerfile reference
|
||||||
__docker_compose_services_from_build() {
|
__docker-compose_services_from_build() {
|
||||||
COMPREPLY=( $(compgen -W "$(___docker_compose_services_with_key build)" -- "$cur") )
|
COMPREPLY=( $(compgen -W "$(___docker-compose_services_with_key build)" -- "$cur") )
|
||||||
}
|
}
|
||||||
|
|
||||||
# All services that are defined by an image
|
# All services that are defined by an image
|
||||||
__docker_compose_services_from_image() {
|
__docker-compose_services_from_image() {
|
||||||
COMPREPLY=( $(compgen -W "$(___docker_compose_services_with_key image)" -- "$cur") )
|
COMPREPLY=( $(compgen -W "$(___docker-compose_services_with_key image)" -- "$cur") )
|
||||||
}
|
}
|
||||||
|
|
||||||
# The services for which containers have been created, optionally filtered
|
# The services for which containers have been created, optionally filtered
|
||||||
# by a boolean expression passed in as argument.
|
# by a boolean expression passed in as argument.
|
||||||
__docker_compose_services_with() {
|
__docker-compose_services_with() {
|
||||||
local containers names
|
local containers names
|
||||||
containers="$(__docker_compose_q ps -q)"
|
containers="$(docker-compose 2>/dev/null ${compose_file:+-f $compose_file} ${compose_project:+-p $compose_project} ps -q)"
|
||||||
names=$(docker 2>/dev/null inspect -f "{{if ${1:-true}}}{{range \$k, \$v := .Config.Labels}}{{if eq \$k \"com.docker.compose.service\"}}{{\$v}}{{end}}{{end}}{{end}}" $containers)
|
names=( $(docker 2>/dev/null inspect --format "{{if ${1:-true}}} {{ .Name }} {{end}}" $containers) )
|
||||||
COMPREPLY=( $(compgen -W "$names" -- "$cur") )
|
names=( ${names[@]%_*} ) # strip trailing numbers
|
||||||
}
|
names=( ${names[@]#*_} ) # strip project name
|
||||||
|
COMPREPLY=( $(compgen -W "${names[*]}" -- "$cur") )
|
||||||
# The services for which at least one paused container exists
|
|
||||||
__docker_compose_services_paused() {
|
|
||||||
__docker_compose_services_with '.State.Paused'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# The services for which at least one running container exists
|
# The services for which at least one running container exists
|
||||||
__docker_compose_services_running() {
|
__docker-compose_services_running() {
|
||||||
__docker_compose_services_with '.State.Running'
|
__docker-compose_services_with '.State.Running'
|
||||||
}
|
}
|
||||||
|
|
||||||
# The services for which at least one stopped container exists
|
# The services for which at least one stopped container exists
|
||||||
__docker_compose_services_stopped() {
|
__docker-compose_services_stopped() {
|
||||||
__docker_compose_services_with 'not .State.Running'
|
__docker-compose_services_with 'not .State.Running'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_docker_compose_build() {
|
_docker-compose_build() {
|
||||||
case "$cur" in
|
case "$cur" in
|
||||||
-*)
|
-*)
|
||||||
COMPREPLY=( $( compgen -W "--force-rm --help --no-cache --pull" -- "$cur" ) )
|
COMPREPLY=( $( compgen -W "--no-cache" -- "$cur" ) )
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
__docker_compose_services_from_build
|
__docker-compose_services_from_build
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_docker_compose_bundle() {
|
_docker-compose_docker-compose() {
|
||||||
case "$prev" in
|
case "$prev" in
|
||||||
--output|-o)
|
|
||||||
_filedir
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
COMPREPLY=( $( compgen -W "--push-images --help --output -o" -- "$cur" ) )
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
_docker_compose_config() {
|
|
||||||
COMPREPLY=( $( compgen -W "--help --quiet -q --services" -- "$cur" ) )
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
_docker_compose_create() {
|
|
||||||
case "$cur" in
|
|
||||||
-*)
|
|
||||||
COMPREPLY=( $( compgen -W "--force-recreate --help --no-build --no-recreate" -- "$cur" ) )
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
__docker_compose_services_all
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
_docker_compose_docker_compose() {
|
|
||||||
case "$prev" in
|
|
||||||
--tlscacert|--tlscert|--tlskey)
|
|
||||||
_filedir
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
--file|-f)
|
--file|-f)
|
||||||
_filedir "y?(a)ml"
|
_filedir y?(a)ml
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
$(__docker_compose_to_extglob "$daemon_options_with_args") )
|
--project-name|-p)
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
case "$cur" in
|
case "$cur" in
|
||||||
-*)
|
-*)
|
||||||
COMPREPLY=( $( compgen -W "$daemon_boolean_options $daemon_options_with_args --help -h --verbose --version -v" -- "$cur" ) )
|
COMPREPLY=( $( compgen -W "--help -h --verbose --version --file -f --project-name -p" -- "$cur" ) )
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=( $( compgen -W "${commands[*]}" -- "$cur" ) )
|
COMPREPLY=( $( compgen -W "${commands[*]}" -- "$cur" ) )
|
||||||
|
|
@ -164,64 +113,12 @@ _docker_compose_docker_compose() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_docker_compose_down() {
|
_docker-compose_help() {
|
||||||
case "$prev" in
|
|
||||||
--rmi)
|
|
||||||
COMPREPLY=( $( compgen -W "all local" -- "$cur" ) )
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
case "$cur" in
|
|
||||||
-*)
|
|
||||||
COMPREPLY=( $( compgen -W "--help --rmi --volumes -v --remove-orphans" -- "$cur" ) )
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
_docker_compose_events() {
|
|
||||||
case "$prev" in
|
|
||||||
--json)
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
case "$cur" in
|
|
||||||
-*)
|
|
||||||
COMPREPLY=( $( compgen -W "--help --json" -- "$cur" ) )
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
__docker_compose_services_all
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
_docker_compose_exec() {
|
|
||||||
case "$prev" in
|
|
||||||
--index|--user)
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
case "$cur" in
|
|
||||||
-*)
|
|
||||||
COMPREPLY=( $( compgen -W "-d --help --index --privileged -T --user" -- "$cur" ) )
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
__docker_compose_services_running
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
_docker_compose_help() {
|
|
||||||
COMPREPLY=( $( compgen -W "${commands[*]}" -- "$cur" ) )
|
COMPREPLY=( $( compgen -W "${commands[*]}" -- "$cur" ) )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_docker_compose_kill() {
|
_docker-compose_kill() {
|
||||||
case "$prev" in
|
case "$prev" in
|
||||||
-s)
|
-s)
|
||||||
COMPREPLY=( $( compgen -W "SIGHUP SIGINT SIGKILL SIGUSR1 SIGUSR2" -- "$(echo $cur | tr '[:lower:]' '[:upper:]')" ) )
|
COMPREPLY=( $( compgen -W "SIGHUP SIGINT SIGKILL SIGUSR1 SIGUSR2" -- "$(echo $cur | tr '[:lower:]' '[:upper:]')" ) )
|
||||||
|
|
@ -231,46 +128,28 @@ _docker_compose_kill() {
|
||||||
|
|
||||||
case "$cur" in
|
case "$cur" in
|
||||||
-*)
|
-*)
|
||||||
COMPREPLY=( $( compgen -W "--help -s" -- "$cur" ) )
|
COMPREPLY=( $( compgen -W "-s" -- "$cur" ) )
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
__docker_compose_services_running
|
__docker-compose_services_running
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_docker_compose_logs() {
|
_docker-compose_logs() {
|
||||||
case "$prev" in
|
|
||||||
--tail)
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
case "$cur" in
|
case "$cur" in
|
||||||
-*)
|
-*)
|
||||||
COMPREPLY=( $( compgen -W "--follow -f --help --no-color --tail --timestamps -t" -- "$cur" ) )
|
COMPREPLY=( $( compgen -W "--no-color" -- "$cur" ) )
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
__docker_compose_services_all
|
__docker-compose_services_all
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_docker_compose_pause() {
|
_docker-compose_port() {
|
||||||
case "$cur" in
|
|
||||||
-*)
|
|
||||||
COMPREPLY=( $( compgen -W "--help" -- "$cur" ) )
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
__docker_compose_services_running
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
_docker_compose_port() {
|
|
||||||
case "$prev" in
|
case "$prev" in
|
||||||
--protocol)
|
--protocol)
|
||||||
COMPREPLY=( $( compgen -W "tcp udp" -- "$cur" ) )
|
COMPREPLY=( $( compgen -W "tcp udp" -- "$cur" ) )
|
||||||
|
|
@ -283,276 +162,182 @@ _docker_compose_port() {
|
||||||
|
|
||||||
case "$cur" in
|
case "$cur" in
|
||||||
-*)
|
-*)
|
||||||
COMPREPLY=( $( compgen -W "--help --index --protocol" -- "$cur" ) )
|
COMPREPLY=( $( compgen -W "--protocol --index" -- "$cur" ) )
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
__docker_compose_services_all
|
__docker-compose_services_all
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_docker_compose_ps() {
|
_docker-compose_ps() {
|
||||||
case "$cur" in
|
case "$cur" in
|
||||||
-*)
|
-*)
|
||||||
COMPREPLY=( $( compgen -W "--help -q" -- "$cur" ) )
|
COMPREPLY=( $( compgen -W "-q" -- "$cur" ) )
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
__docker_compose_services_all
|
__docker-compose_services_all
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_docker_compose_pull() {
|
_docker-compose_pull() {
|
||||||
case "$cur" in
|
case "$cur" in
|
||||||
-*)
|
-*)
|
||||||
COMPREPLY=( $( compgen -W "--help --ignore-pull-failures" -- "$cur" ) )
|
COMPREPLY=( $( compgen -W "--allow-insecure-ssl" -- "$cur" ) )
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
__docker_compose_services_from_image
|
__docker-compose_services_from_image
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_docker_compose_push() {
|
_docker-compose_restart() {
|
||||||
case "$cur" in
|
|
||||||
-*)
|
|
||||||
COMPREPLY=( $( compgen -W "--help --ignore-push-failures" -- "$cur" ) )
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
__docker_compose_services_all
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
_docker_compose_restart() {
|
|
||||||
case "$prev" in
|
case "$prev" in
|
||||||
--timeout|-t)
|
-t | --timeout)
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
case "$cur" in
|
case "$cur" in
|
||||||
-*)
|
-*)
|
||||||
COMPREPLY=( $( compgen -W "--help --timeout -t" -- "$cur" ) )
|
COMPREPLY=( $( compgen -W "-t --timeout" -- "$cur" ) )
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
__docker_compose_services_running
|
__docker-compose_services_running
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_docker_compose_rm() {
|
_docker-compose_rm() {
|
||||||
case "$cur" in
|
case "$cur" in
|
||||||
-*)
|
-*)
|
||||||
COMPREPLY=( $( compgen -W "--force -f --help -v" -- "$cur" ) )
|
COMPREPLY=( $( compgen -W "--force -f -v" -- "$cur" ) )
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
__docker_compose_services_stopped
|
__docker-compose_services_stopped
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_docker_compose_run() {
|
_docker-compose_run() {
|
||||||
case "$prev" in
|
case "$prev" in
|
||||||
-e)
|
-e)
|
||||||
COMPREPLY=( $( compgen -e -- "$cur" ) )
|
COMPREPLY=( $( compgen -e -- "$cur" ) )
|
||||||
__docker_compose_nospace
|
compopt -o nospace
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
--entrypoint|--name|--user|-u|--workdir|-w)
|
--entrypoint)
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
case "$cur" in
|
case "$cur" in
|
||||||
-*)
|
-*)
|
||||||
COMPREPLY=( $( compgen -W "-d --entrypoint -e --help --name --no-deps --publish -p --rm --service-ports -T --user -u --workdir -w" -- "$cur" ) )
|
COMPREPLY=( $( compgen -W "--allow-insecure-ssl -d --entrypoint -e --no-deps --rm --service-ports -T" -- "$cur" ) )
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
__docker_compose_services_all
|
__docker-compose_services_all
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_docker_compose_scale() {
|
_docker-compose_scale() {
|
||||||
case "$prev" in
|
case "$prev" in
|
||||||
=)
|
=)
|
||||||
COMPREPLY=("$cur")
|
COMPREPLY=("$cur")
|
||||||
return
|
|
||||||
;;
|
|
||||||
--timeout|-t)
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
case "$cur" in
|
|
||||||
-*)
|
|
||||||
COMPREPLY=( $( compgen -W "--help --timeout -t" -- "$cur" ) )
|
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=( $(compgen -S "=" -W "$(___docker_compose_all_services_in_compose_file)" -- "$cur") )
|
COMPREPLY=( $(compgen -S "=" -W "$(___docker-compose_all_services_in_compose_file)" -- "$cur") )
|
||||||
__docker_compose_nospace
|
compopt -o nospace
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_docker_compose_start() {
|
_docker-compose_start() {
|
||||||
case "$cur" in
|
__docker-compose_services_stopped
|
||||||
-*)
|
|
||||||
COMPREPLY=( $( compgen -W "--help" -- "$cur" ) )
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
__docker_compose_services_stopped
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_docker_compose_stop() {
|
_docker-compose_stop() {
|
||||||
case "$prev" in
|
case "$prev" in
|
||||||
--timeout|-t)
|
-t | --timeout)
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
case "$cur" in
|
case "$cur" in
|
||||||
-*)
|
-*)
|
||||||
COMPREPLY=( $( compgen -W "--help --timeout -t" -- "$cur" ) )
|
COMPREPLY=( $( compgen -W "-t --timeout" -- "$cur" ) )
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
__docker_compose_services_running
|
__docker-compose_services_running
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_docker_compose_top() {
|
_docker-compose_up() {
|
||||||
case "$cur" in
|
|
||||||
-*)
|
|
||||||
COMPREPLY=( $( compgen -W "--help" -- "$cur" ) )
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
__docker_compose_services_running
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
_docker_compose_unpause() {
|
|
||||||
case "$cur" in
|
|
||||||
-*)
|
|
||||||
COMPREPLY=( $( compgen -W "--help" -- "$cur" ) )
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
__docker_compose_services_paused
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
_docker_compose_up() {
|
|
||||||
case "$prev" in
|
case "$prev" in
|
||||||
--timeout|-t)
|
-t | --timeout)
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
case "$cur" in
|
case "$cur" in
|
||||||
-*)
|
-*)
|
||||||
COMPREPLY=( $( compgen -W "--abort-on-container-exit --build -d --force-recreate --help --no-build --no-color --no-deps --no-recreate --timeout -t --remove-orphans" -- "$cur" ) )
|
COMPREPLY=( $( compgen -W "--allow-insecure-ssl -d --no-build --no-color --no-deps --no-recreate -t --timeout" -- "$cur" ) )
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
__docker_compose_services_all
|
__docker-compose_services_all
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_docker_compose_version() {
|
_docker-compose() {
|
||||||
case "$cur" in
|
|
||||||
-*)
|
|
||||||
COMPREPLY=( $( compgen -W "--short" -- "$cur" ) )
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
_docker_compose() {
|
|
||||||
local previous_extglob_setting=$(shopt -p extglob)
|
|
||||||
shopt -s extglob
|
|
||||||
|
|
||||||
local commands=(
|
local commands=(
|
||||||
build
|
build
|
||||||
bundle
|
|
||||||
config
|
|
||||||
create
|
|
||||||
down
|
|
||||||
events
|
|
||||||
exec
|
|
||||||
help
|
help
|
||||||
kill
|
kill
|
||||||
logs
|
logs
|
||||||
pause
|
|
||||||
port
|
port
|
||||||
ps
|
ps
|
||||||
pull
|
pull
|
||||||
push
|
|
||||||
restart
|
restart
|
||||||
rm
|
rm
|
||||||
run
|
run
|
||||||
scale
|
scale
|
||||||
start
|
start
|
||||||
stop
|
stop
|
||||||
top
|
|
||||||
unpause
|
|
||||||
up
|
up
|
||||||
version
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# options for the docker daemon that have to be passed to secondary calls to
|
|
||||||
# docker-compose executed by this script
|
|
||||||
local daemon_boolean_options="
|
|
||||||
--skip-hostname-check
|
|
||||||
--tls
|
|
||||||
--tlsverify
|
|
||||||
"
|
|
||||||
local daemon_options_with_args="
|
|
||||||
--file -f
|
|
||||||
--host -H
|
|
||||||
--project-name -p
|
|
||||||
--tlscacert
|
|
||||||
--tlscert
|
|
||||||
--tlskey
|
|
||||||
"
|
|
||||||
|
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
local cur prev words cword
|
local cur prev words cword
|
||||||
_get_comp_words_by_ref -n : cur prev words cword
|
_get_comp_words_by_ref -n : cur prev words cword
|
||||||
|
|
||||||
# search subcommand and invoke its handler.
|
# search subcommand and invoke its handler.
|
||||||
# special treatment of some top-level options
|
# special treatment of some top-level options
|
||||||
local command='docker_compose'
|
local command='docker-compose'
|
||||||
local daemon_options=()
|
|
||||||
local counter=1
|
local counter=1
|
||||||
|
local compose_file compose_project
|
||||||
while [ $counter -lt $cword ]; do
|
while [ $counter -lt $cword ]; do
|
||||||
case "${words[$counter]}" in
|
case "${words[$counter]}" in
|
||||||
$(__docker_compose_to_extglob "$daemon_boolean_options") )
|
-f|--file)
|
||||||
local opt=${words[counter]}
|
(( counter++ ))
|
||||||
daemon_options+=($opt)
|
compose_file="${words[$counter]}"
|
||||||
;;
|
;;
|
||||||
$(__docker_compose_to_extglob "$daemon_options_with_args") )
|
-p|--project-name)
|
||||||
local opt=${words[counter]}
|
(( counter++ ))
|
||||||
local arg=${words[++counter]}
|
compose_project="${words[$counter]}"
|
||||||
daemon_options+=($opt $arg)
|
|
||||||
;;
|
;;
|
||||||
-*)
|
-*)
|
||||||
;;
|
;;
|
||||||
|
|
@ -564,11 +349,10 @@ _docker_compose() {
|
||||||
(( counter++ ))
|
(( counter++ ))
|
||||||
done
|
done
|
||||||
|
|
||||||
local completions_func=_docker_compose_${command//-/_}
|
local completions_func=_docker-compose_${command}
|
||||||
declare -F $completions_func >/dev/null && $completions_func
|
declare -F $completions_func >/dev/null && $completions_func
|
||||||
|
|
||||||
eval "$previous_extglob_setting"
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
complete -F _docker_compose docker-compose
|
complete -F _docker-compose docker-compose
|
||||||
|
|
|
||||||
|
|
@ -1,469 +0,0 @@
|
||||||
#compdef docker-compose
|
|
||||||
|
|
||||||
# Description
|
|
||||||
# -----------
|
|
||||||
# zsh completion for docker-compose
|
|
||||||
# https://github.com/sdurrheimer/docker-compose-zsh-completion
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
# Version
|
|
||||||
# -------
|
|
||||||
# 1.5.0
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
# Authors
|
|
||||||
# -------
|
|
||||||
# * Steve Durrheimer <s.durrheimer@gmail.com>
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
# Inspiration
|
|
||||||
# -----------
|
|
||||||
# * @albers docker-compose bash completion script
|
|
||||||
# * @felixr docker zsh completion script : https://github.com/felixr/docker-zsh-completion
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
__docker-compose_q() {
|
|
||||||
docker-compose 2>/dev/null $compose_options "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
# All services defined in docker-compose.yml
|
|
||||||
__docker-compose_all_services_in_compose_file() {
|
|
||||||
local already_selected
|
|
||||||
local -a services
|
|
||||||
already_selected=$(echo $words | tr " " "|")
|
|
||||||
__docker-compose_q config --services \
|
|
||||||
| grep -Ev "^(${already_selected})$"
|
|
||||||
}
|
|
||||||
|
|
||||||
# All services, even those without an existing container
|
|
||||||
__docker-compose_services_all() {
|
|
||||||
[[ $PREFIX = -* ]] && return 1
|
|
||||||
integer ret=1
|
|
||||||
services=$(__docker-compose_all_services_in_compose_file)
|
|
||||||
_alternative "args:services:($services)" && ret=0
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
# All services that have an entry with the given key in their docker-compose.yml section
|
|
||||||
__docker-compose_services_with_key() {
|
|
||||||
local already_selected
|
|
||||||
local -a buildable
|
|
||||||
already_selected=$(echo $words | tr " " "|")
|
|
||||||
# flatten sections to one line, then filter lines containing the key and return section name.
|
|
||||||
__docker-compose_q config \
|
|
||||||
| sed -n -e '/^services:/,/^[^ ]/p' \
|
|
||||||
| sed -n 's/^ //p' \
|
|
||||||
| awk '/^[a-zA-Z0-9]/{printf "\n"};{printf $0;next;}' \
|
|
||||||
| grep " \+$1:" \
|
|
||||||
| cut -d: -f1 \
|
|
||||||
| grep -Ev "^(${already_selected})$"
|
|
||||||
}
|
|
||||||
|
|
||||||
# All services that are defined by a Dockerfile reference
|
|
||||||
__docker-compose_services_from_build() {
|
|
||||||
[[ $PREFIX = -* ]] && return 1
|
|
||||||
integer ret=1
|
|
||||||
buildable=$(__docker-compose_services_with_key build)
|
|
||||||
_alternative "args:buildable services:($buildable)" && ret=0
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
# All services that are defined by an image
|
|
||||||
__docker-compose_services_from_image() {
|
|
||||||
[[ $PREFIX = -* ]] && return 1
|
|
||||||
integer ret=1
|
|
||||||
pullable=$(__docker-compose_services_with_key image)
|
|
||||||
_alternative "args:pullable services:($pullable)" && ret=0
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
__docker-compose_get_services() {
|
|
||||||
[[ $PREFIX = -* ]] && return 1
|
|
||||||
integer ret=1
|
|
||||||
local kind
|
|
||||||
declare -a running paused stopped lines args services
|
|
||||||
|
|
||||||
docker_status=$(docker ps > /dev/null 2>&1)
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
_message "Error! Docker is not running."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
kind=$1
|
|
||||||
shift
|
|
||||||
[[ $kind =~ (stopped|all) ]] && args=($args -a)
|
|
||||||
|
|
||||||
lines=(${(f)"$(_call_program commands docker $docker_options ps $args)"})
|
|
||||||
services=(${(f)"$(_call_program commands docker-compose 2>/dev/null $compose_options ps -q)"})
|
|
||||||
|
|
||||||
# Parse header line to find columns
|
|
||||||
local i=1 j=1 k header=${lines[1]}
|
|
||||||
declare -A begin end
|
|
||||||
while (( j < ${#header} - 1 )); do
|
|
||||||
i=$(( j + ${${header[$j,-1]}[(i)[^ ]]} - 1 ))
|
|
||||||
j=$(( i + ${${header[$i,-1]}[(i) ]} - 1 ))
|
|
||||||
k=$(( j + ${${header[$j,-1]}[(i)[^ ]]} - 2 ))
|
|
||||||
begin[${header[$i,$((j-1))]}]=$i
|
|
||||||
end[${header[$i,$((j-1))]}]=$k
|
|
||||||
done
|
|
||||||
lines=(${lines[2,-1]})
|
|
||||||
|
|
||||||
# Container ID
|
|
||||||
local line s name
|
|
||||||
local -a names
|
|
||||||
for line in $lines; do
|
|
||||||
if [[ ${services[@]} == *"${line[${begin[CONTAINER ID]},${end[CONTAINER ID]}]%% ##}"* ]]; then
|
|
||||||
names=(${(ps:,:)${${line[${begin[NAMES]},-1]}%% *}})
|
|
||||||
for name in $names; do
|
|
||||||
s="${${name%_*}#*_}:${(l:15:: :::)${${line[${begin[CREATED]},${end[CREATED]}]/ ago/}%% ##}}"
|
|
||||||
s="$s, ${line[${begin[CONTAINER ID]},${end[CONTAINER ID]}]%% ##}"
|
|
||||||
s="$s, ${${${line[${begin[IMAGE]},${end[IMAGE]}]}/:/\\:}%% ##}"
|
|
||||||
if [[ ${line[${begin[STATUS]},${end[STATUS]}]} = Exit* ]]; then
|
|
||||||
stopped=($stopped $s)
|
|
||||||
else
|
|
||||||
if [[ ${line[${begin[STATUS]},${end[STATUS]}]} = *\(Paused\)* ]]; then
|
|
||||||
paused=($paused $s)
|
|
||||||
fi
|
|
||||||
running=($running $s)
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
[[ $kind =~ (running|all) ]] && _describe -t services-running "running services" running "$@" && ret=0
|
|
||||||
[[ $kind =~ (paused|all) ]] && _describe -t services-paused "paused services" paused "$@" && ret=0
|
|
||||||
[[ $kind =~ (stopped|all) ]] && _describe -t services-stopped "stopped services" stopped "$@" && ret=0
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
__docker-compose_pausedservices() {
|
|
||||||
[[ $PREFIX = -* ]] && return 1
|
|
||||||
__docker-compose_get_services paused "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
__docker-compose_stoppedservices() {
|
|
||||||
[[ $PREFIX = -* ]] && return 1
|
|
||||||
__docker-compose_get_services stopped "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
__docker-compose_runningservices() {
|
|
||||||
[[ $PREFIX = -* ]] && return 1
|
|
||||||
__docker-compose_get_services running "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
__docker-compose_services() {
|
|
||||||
[[ $PREFIX = -* ]] && return 1
|
|
||||||
__docker-compose_get_services all "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
__docker-compose_caching_policy() {
|
|
||||||
oldp=( "$1"(Nmh+1) ) # 1 hour
|
|
||||||
(( $#oldp ))
|
|
||||||
}
|
|
||||||
|
|
||||||
__docker-compose_commands() {
|
|
||||||
local cache_policy
|
|
||||||
|
|
||||||
zstyle -s ":completion:${curcontext}:" cache-policy cache_policy
|
|
||||||
if [[ -z "$cache_policy" ]]; then
|
|
||||||
zstyle ":completion:${curcontext}:" cache-policy __docker-compose_caching_policy
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ( [[ ${+_docker_compose_subcommands} -eq 0 ]] || _cache_invalid docker_compose_subcommands) \
|
|
||||||
&& ! _retrieve_cache docker_compose_subcommands;
|
|
||||||
then
|
|
||||||
local -a lines
|
|
||||||
lines=(${(f)"$(_call_program commands docker-compose 2>&1)"})
|
|
||||||
_docker_compose_subcommands=(${${${lines[$((${lines[(i)Commands:]} + 1)),${lines[(I) *]}]}## #}/ ##/:})
|
|
||||||
(( $#_docker_compose_subcommands > 0 )) && _store_cache docker_compose_subcommands _docker_compose_subcommands
|
|
||||||
fi
|
|
||||||
_describe -t docker-compose-commands "docker-compose command" _docker_compose_subcommands
|
|
||||||
}
|
|
||||||
|
|
||||||
__docker-compose_subcommand() {
|
|
||||||
local opts_help opts_force_recreate opts_no_recreate opts_no_build opts_remove_orphans opts_timeout opts_no_color opts_no_deps
|
|
||||||
|
|
||||||
opts_help='(: -)--help[Print usage]'
|
|
||||||
opts_force_recreate="(--no-recreate)--force-recreate[Recreate containers even if their configuration and image haven't changed. Incompatible with --no-recreate.]"
|
|
||||||
opts_no_recreate="(--force-recreate)--no-recreate[If containers already exist, don't recreate them. Incompatible with --force-recreate.]"
|
|
||||||
opts_no_build="(--build)--no-build[Don't build an image, even if it's missing.]"
|
|
||||||
opts_remove_orphans="--remove-orphans[Remove containers for services not defined in the Compose file]"
|
|
||||||
opts_timeout=('(-t --timeout)'{-t,--timeout}"[Specify a shutdown timeout in seconds. (default: 10)]:seconds: ")
|
|
||||||
opts_no_color='--no-color[Produce monochrome output.]'
|
|
||||||
opts_no_deps="--no-deps[Don't start linked services.]"
|
|
||||||
|
|
||||||
integer ret=1
|
|
||||||
|
|
||||||
case "$words[1]" in
|
|
||||||
(build)
|
|
||||||
_arguments \
|
|
||||||
$opts_help \
|
|
||||||
'--force-rm[Always remove intermediate containers.]' \
|
|
||||||
'--no-cache[Do not use cache when building the image.]' \
|
|
||||||
'--pull[Always attempt to pull a newer version of the image.]' \
|
|
||||||
'*:services:__docker-compose_services_from_build' && ret=0
|
|
||||||
;;
|
|
||||||
(bundle)
|
|
||||||
_arguments \
|
|
||||||
$opts_help \
|
|
||||||
'--push-images[Automatically push images for any services which have a `build` option specified.]' \
|
|
||||||
'(--output -o)'{--output,-o}'[Path to write the bundle file to. Defaults to "<project name>.dab".]:file:_files' && ret=0
|
|
||||||
;;
|
|
||||||
(config)
|
|
||||||
_arguments \
|
|
||||||
$opts_help \
|
|
||||||
'(--quiet -q)'{--quiet,-q}"[Only validate the configuration, don't print anything.]" \
|
|
||||||
'--services[Print the service names, one per line.]' && ret=0
|
|
||||||
;;
|
|
||||||
(create)
|
|
||||||
_arguments \
|
|
||||||
$opts_help \
|
|
||||||
$opts_force_recreate \
|
|
||||||
$opts_no_recreate \
|
|
||||||
$opts_no_build \
|
|
||||||
"(--no-build)--build[Build images before creating containers.]" \
|
|
||||||
'*:services:__docker-compose_services_all' && ret=0
|
|
||||||
;;
|
|
||||||
(down)
|
|
||||||
_arguments \
|
|
||||||
$opts_help \
|
|
||||||
"--rmi[Remove images. Type must be one of: 'all': Remove all images used by any service. 'local': Remove only images that don't have a custom tag set by the \`image\` field.]:type:(all local)" \
|
|
||||||
'(-v --volumes)'{-v,--volumes}"[Remove named volumes declared in the \`volumes\` section of the Compose file and anonymous volumes attached to containers.]" \
|
|
||||||
$opts_remove_orphans && ret=0
|
|
||||||
;;
|
|
||||||
(events)
|
|
||||||
_arguments \
|
|
||||||
$opts_help \
|
|
||||||
'--json[Output events as a stream of json objects]' \
|
|
||||||
'*:services:__docker-compose_services_all' && ret=0
|
|
||||||
;;
|
|
||||||
(exec)
|
|
||||||
_arguments \
|
|
||||||
$opts_help \
|
|
||||||
'-d[Detached mode: Run command in the background.]' \
|
|
||||||
'--privileged[Give extended privileges to the process.]' \
|
|
||||||
'--user=[Run the command as this user.]:username:_users' \
|
|
||||||
'-T[Disable pseudo-tty allocation. By default `docker-compose exec` allocates a TTY.]' \
|
|
||||||
'--index=[Index of the container if there are multiple instances of a service \[default: 1\]]:index: ' \
|
|
||||||
'(-):running services:__docker-compose_runningservices' \
|
|
||||||
'(-):command: _command_names -e' \
|
|
||||||
'*::arguments: _normal' && ret=0
|
|
||||||
;;
|
|
||||||
(help)
|
|
||||||
_arguments ':subcommand:__docker-compose_commands' && ret=0
|
|
||||||
;;
|
|
||||||
(kill)
|
|
||||||
_arguments \
|
|
||||||
$opts_help \
|
|
||||||
'-s[SIGNAL to send to the container. Default signal is SIGKILL.]:signal:_signals' \
|
|
||||||
'*:running services:__docker-compose_runningservices' && ret=0
|
|
||||||
;;
|
|
||||||
(logs)
|
|
||||||
_arguments \
|
|
||||||
$opts_help \
|
|
||||||
'(-f --follow)'{-f,--follow}'[Follow log output]' \
|
|
||||||
$opts_no_color \
|
|
||||||
'--tail=[Number of lines to show from the end of the logs for each container.]:number of lines: ' \
|
|
||||||
'(-t --timestamps)'{-t,--timestamps}'[Show timestamps]' \
|
|
||||||
'*:services:__docker-compose_services_all' && ret=0
|
|
||||||
;;
|
|
||||||
(pause)
|
|
||||||
_arguments \
|
|
||||||
$opts_help \
|
|
||||||
'*:running services:__docker-compose_runningservices' && ret=0
|
|
||||||
;;
|
|
||||||
(port)
|
|
||||||
_arguments \
|
|
||||||
$opts_help \
|
|
||||||
'--protocol=[tcp or udp \[default: tcp\]]:protocol:(tcp udp)' \
|
|
||||||
'--index=[index of the container if there are multiple instances of a service \[default: 1\]]:index: ' \
|
|
||||||
'1:running services:__docker-compose_runningservices' \
|
|
||||||
'2:port:_ports' && ret=0
|
|
||||||
;;
|
|
||||||
(ps)
|
|
||||||
_arguments \
|
|
||||||
$opts_help \
|
|
||||||
'-q[Only display IDs]' \
|
|
||||||
'*:services:__docker-compose_services_all' && ret=0
|
|
||||||
;;
|
|
||||||
(pull)
|
|
||||||
_arguments \
|
|
||||||
$opts_help \
|
|
||||||
'--ignore-pull-failures[Pull what it can and ignores images with pull failures.]' \
|
|
||||||
'*:services:__docker-compose_services_from_image' && ret=0
|
|
||||||
;;
|
|
||||||
(push)
|
|
||||||
_arguments \
|
|
||||||
$opts_help \
|
|
||||||
'--ignore-push-failures[Push what it can and ignores images with push failures.]' \
|
|
||||||
'*:services:__docker-compose_services' && ret=0
|
|
||||||
;;
|
|
||||||
(rm)
|
|
||||||
_arguments \
|
|
||||||
$opts_help \
|
|
||||||
'(-f --force)'{-f,--force}"[Don't ask to confirm removal]" \
|
|
||||||
'-v[Remove any anonymous volumes attached to containers]' \
|
|
||||||
'*:stopped services:__docker-compose_stoppedservices' && ret=0
|
|
||||||
;;
|
|
||||||
(run)
|
|
||||||
_arguments \
|
|
||||||
$opts_help \
|
|
||||||
'-d[Detached mode: Run container in the background, print new container name.]' \
|
|
||||||
'*-e[KEY=VAL Set an environment variable (can be used multiple times)]:environment variable KEY=VAL: ' \
|
|
||||||
'--entrypoint[Overwrite the entrypoint of the image.]:entry point: ' \
|
|
||||||
'--name=[Assign a name to the container]:name: ' \
|
|
||||||
$opts_no_deps \
|
|
||||||
'(-p --publish)'{-p,--publish=}"[Publish a container's port(s) to the host]" \
|
|
||||||
'--rm[Remove container after run. Ignored in detached mode.]' \
|
|
||||||
"--service-ports[Run command with the service's ports enabled and mapped to the host.]" \
|
|
||||||
'-T[Disable pseudo-tty allocation. By default `docker-compose run` allocates a TTY.]' \
|
|
||||||
'(-u --user)'{-u,--user=}'[Run as specified username or uid]:username or uid:_users' \
|
|
||||||
'(-w --workdir)'{-w,--workdir=}'[Working directory inside the container]:workdir: ' \
|
|
||||||
'(-):services:__docker-compose_services' \
|
|
||||||
'(-):command: _command_names -e' \
|
|
||||||
'*::arguments: _normal' && ret=0
|
|
||||||
;;
|
|
||||||
(scale)
|
|
||||||
_arguments \
|
|
||||||
$opts_help \
|
|
||||||
$opts_timeout \
|
|
||||||
'*:running services:__docker-compose_runningservices' && ret=0
|
|
||||||
;;
|
|
||||||
(start)
|
|
||||||
_arguments \
|
|
||||||
$opts_help \
|
|
||||||
'*:stopped services:__docker-compose_stoppedservices' && ret=0
|
|
||||||
;;
|
|
||||||
(stop|restart)
|
|
||||||
_arguments \
|
|
||||||
$opts_help \
|
|
||||||
$opts_timeout \
|
|
||||||
'*:running services:__docker-compose_runningservices' && ret=0
|
|
||||||
;;
|
|
||||||
(top)
|
|
||||||
_arguments \
|
|
||||||
$opts_help \
|
|
||||||
'*:running services:__docker-compose_runningservices' && ret=0
|
|
||||||
;;
|
|
||||||
(unpause)
|
|
||||||
_arguments \
|
|
||||||
$opts_help \
|
|
||||||
'*:paused services:__docker-compose_pausedservices' && ret=0
|
|
||||||
;;
|
|
||||||
(up)
|
|
||||||
_arguments \
|
|
||||||
$opts_help \
|
|
||||||
'(--abort-on-container-exit)-d[Detached mode: Run containers in the background, print new container names. Incompatible with --abort-on-container-exit.]' \
|
|
||||||
$opts_no_color \
|
|
||||||
$opts_no_deps \
|
|
||||||
$opts_force_recreate \
|
|
||||||
$opts_no_recreate \
|
|
||||||
$opts_no_build \
|
|
||||||
"(--no-build)--build[Build images before starting containers.]" \
|
|
||||||
"(-d)--abort-on-container-exit[Stops all containers if any container was stopped. Incompatible with -d.]" \
|
|
||||||
'(-t --timeout)'{-t,--timeout}"[Use this timeout in seconds for container shutdown when attached or when containers are already running. (default: 10)]:seconds: " \
|
|
||||||
$opts_remove_orphans \
|
|
||||||
'*:services:__docker-compose_services_all' && ret=0
|
|
||||||
;;
|
|
||||||
(version)
|
|
||||||
_arguments \
|
|
||||||
$opts_help \
|
|
||||||
"--short[Shows only Compose's version number.]" && ret=0
|
|
||||||
;;
|
|
||||||
(*)
|
|
||||||
_message 'Unknown sub command' && ret=1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
_docker-compose() {
|
|
||||||
# Support for subservices, which allows for `compdef _docker docker-shell=_docker_containers`.
|
|
||||||
# Based on /usr/share/zsh/functions/Completion/Unix/_git without support for `ret`.
|
|
||||||
if [[ $service != docker-compose ]]; then
|
|
||||||
_call_function - _$service
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
local curcontext="$curcontext" state line
|
|
||||||
integer ret=1
|
|
||||||
typeset -A opt_args
|
|
||||||
|
|
||||||
local file_description
|
|
||||||
|
|
||||||
if [[ -n ${words[(r)-f]} || -n ${words[(r)--file]} ]] ; then
|
|
||||||
file_description="Specify an override docker-compose file (default: docker-compose.override.yml)"
|
|
||||||
else
|
|
||||||
file_description="Specify an alternate docker-compose file (default: docker-compose.yml)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
_arguments -C \
|
|
||||||
'(- :)'{-h,--help}'[Get help]' \
|
|
||||||
'*'{-f,--file}"[${file_description}]:file:_files -g '*.yml'" \
|
|
||||||
'(-p --project-name)'{-p,--project-name}'[Specify an alternate project name (default: directory name)]:project name:' \
|
|
||||||
'--verbose[Show more output]' \
|
|
||||||
'(- :)'{-v,--version}'[Print version and exit]' \
|
|
||||||
'(-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:' \
|
|
||||||
'--tlscert=[Path to TLS certificate file]:client cert path:' \
|
|
||||||
'--tlskey=[Path to TLS key file]:tls key path:' \
|
|
||||||
'--tlsverify[Use TLS and verify the remote]' \
|
|
||||||
"--skip-hostname-check[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)]" \
|
|
||||||
'(-): :->command' \
|
|
||||||
'(-)*:: :->option-or-argument' && ret=0
|
|
||||||
|
|
||||||
local -a relevant_compose_flags relevant_docker_flags compose_options docker_options
|
|
||||||
|
|
||||||
relevant_compose_flags=(
|
|
||||||
"--file" "-f"
|
|
||||||
"--host" "-H"
|
|
||||||
"--project-name" "-p"
|
|
||||||
"--tls"
|
|
||||||
"--tlscacert"
|
|
||||||
"--tlscert"
|
|
||||||
"--tlskey"
|
|
||||||
"--tlsverify"
|
|
||||||
"--skip-hostname-check"
|
|
||||||
)
|
|
||||||
|
|
||||||
relevant_docker_flags=(
|
|
||||||
"--host" "-H"
|
|
||||||
"--tls"
|
|
||||||
"--tlscacert"
|
|
||||||
"--tlscert"
|
|
||||||
"--tlskey"
|
|
||||||
"--tlsverify"
|
|
||||||
)
|
|
||||||
|
|
||||||
for k in "${(@k)opt_args}"; do
|
|
||||||
if [[ -n "${relevant_docker_flags[(r)$k]}" ]]; then
|
|
||||||
docker_options+=$k
|
|
||||||
if [[ -n "$opt_args[$k]" ]]; then
|
|
||||||
docker_options+=$opt_args[$k]
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if [[ -n "${relevant_compose_flags[(r)$k]}" ]]; then
|
|
||||||
compose_options+=$k
|
|
||||||
if [[ -n "$opt_args[$k]" ]]; then
|
|
||||||
compose_options+=$opt_args[$k]
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
case $state in
|
|
||||||
(command)
|
|
||||||
__docker-compose_commands && ret=0
|
|
||||||
;;
|
|
||||||
(option-or-argument)
|
|
||||||
curcontext=${curcontext%:*:*}:docker-compose-$words[1]:
|
|
||||||
__docker-compose_subcommand && ret=0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
_docker-compose "$@"
|
|
||||||
|
|
@ -1,173 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
"""
|
|
||||||
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
|
|
||||||
|
|
||||||
import ruamel.yaml
|
|
||||||
|
|
||||||
from compose.config.types import VolumeSpec
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger('migrate')
|
|
||||||
|
|
||||||
|
|
||||||
def migrate(content):
|
|
||||||
data = ruamel.yaml.load(content, ruamel.yaml.RoundTripLoader)
|
|
||||||
|
|
||||||
service_names = data.keys()
|
|
||||||
|
|
||||||
for name, service in data.items():
|
|
||||||
warn_for_links(name, service)
|
|
||||||
warn_for_external_links(name, service)
|
|
||||||
rewrite_net(service, service_names)
|
|
||||||
rewrite_build(service)
|
|
||||||
rewrite_logging(service)
|
|
||||||
rewrite_volumes_from(service, service_names)
|
|
||||||
|
|
||||||
services = {name: data.pop(name) for name in data.keys()}
|
|
||||||
|
|
||||||
data['version'] = "2"
|
|
||||||
data['services'] = services
|
|
||||||
create_volumes_section(data)
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def warn_for_links(name, service):
|
|
||||||
links = service.get('links')
|
|
||||||
if links:
|
|
||||||
example_service = links[0].partition(':')[0]
|
|
||||||
log.warn(
|
|
||||||
"Service {name} has links, which no longer create environment "
|
|
||||||
"variables such as {example_service_upper}_PORT. "
|
|
||||||
"If you are using those in your application code, you should "
|
|
||||||
"instead connect directly to the hostname, e.g. "
|
|
||||||
"'{example_service}'."
|
|
||||||
.format(name=name, example_service=example_service,
|
|
||||||
example_service_upper=example_service.upper()))
|
|
||||||
|
|
||||||
|
|
||||||
def warn_for_external_links(name, service):
|
|
||||||
external_links = service.get('external_links')
|
|
||||||
if external_links:
|
|
||||||
log.warn(
|
|
||||||
"Service {name} has external_links: {ext}, which now work "
|
|
||||||
"slightly differently. In particular, two containers must be "
|
|
||||||
"connected to at least one network in common in order to "
|
|
||||||
"communicate, even if explicitly linked together.\n\n"
|
|
||||||
"Either connect the external container to your app's default "
|
|
||||||
"network, or connect both the external container and your "
|
|
||||||
"service's containers to a pre-existing network. See "
|
|
||||||
"https://docs.docker.com/compose/networking/ "
|
|
||||||
"for more on how to do this."
|
|
||||||
.format(name=name, ext=external_links))
|
|
||||||
|
|
||||||
|
|
||||||
def rewrite_net(service, service_names):
|
|
||||||
if 'net' in service:
|
|
||||||
network_mode = service.pop('net')
|
|
||||||
|
|
||||||
# "container:<service name>" is now "service:<service name>"
|
|
||||||
if network_mode.startswith('container:'):
|
|
||||||
name = network_mode.partition(':')[2]
|
|
||||||
if name in service_names:
|
|
||||||
network_mode = 'service:{}'.format(name)
|
|
||||||
|
|
||||||
service['network_mode'] = network_mode
|
|
||||||
|
|
||||||
|
|
||||||
def rewrite_build(service):
|
|
||||||
if 'dockerfile' in service:
|
|
||||||
service['build'] = {
|
|
||||||
'context': service.pop('build'),
|
|
||||||
'dockerfile': service.pop('dockerfile'),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def rewrite_logging(service):
|
|
||||||
if 'log_driver' in service:
|
|
||||||
service['logging'] = {'driver': service.pop('log_driver')}
|
|
||||||
if 'log_opt' in service:
|
|
||||||
service['logging']['options'] = service.pop('log_opt')
|
|
||||||
|
|
||||||
|
|
||||||
def rewrite_volumes_from(service, service_names):
|
|
||||||
for idx, volume_from in enumerate(service.get('volumes_from', [])):
|
|
||||||
if volume_from.split(':', 1)[0] not in service_names:
|
|
||||||
service['volumes_from'][idx] = 'container:%s' % volume_from
|
|
||||||
|
|
||||||
|
|
||||||
def create_volumes_section(data):
|
|
||||||
named_volumes = get_named_volumes(data['services'])
|
|
||||||
if named_volumes:
|
|
||||||
log.warn(
|
|
||||||
"Named volumes ({names}) must be explicitly declared. Creating a "
|
|
||||||
"'volumes' section with declarations.\n\n"
|
|
||||||
"For backwards-compatibility, they've been declared as external. "
|
|
||||||
"If you don't mind the volume names being prefixed with the "
|
|
||||||
"project name, you can remove the 'external' option from each one."
|
|
||||||
.format(names=', '.join(list(named_volumes))))
|
|
||||||
|
|
||||||
data['volumes'] = named_volumes
|
|
||||||
|
|
||||||
|
|
||||||
def get_named_volumes(services):
|
|
||||||
volume_specs = [
|
|
||||||
VolumeSpec.parse(volume)
|
|
||||||
for service in services.values()
|
|
||||||
for volume in service.get('volumes', [])
|
|
||||||
]
|
|
||||||
names = {
|
|
||||||
spec.external
|
|
||||||
for spec in volume_specs
|
|
||||||
if spec.is_named_volume
|
|
||||||
}
|
|
||||||
return {name: {'external': True} for name in names}
|
|
||||||
|
|
||||||
|
|
||||||
def write(stream, new_format, indent, width):
|
|
||||||
ruamel.yaml.dump(
|
|
||||||
new_format,
|
|
||||||
stream,
|
|
||||||
Dumper=ruamel.yaml.RoundTripDumper,
|
|
||||||
indent=indent,
|
|
||||||
width=width)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_opts(args):
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("filename", help="Compose file filename.")
|
|
||||||
parser.add_argument("-i", "--in-place", action='store_true')
|
|
||||||
parser.add_argument(
|
|
||||||
"--indent", type=int, default=2,
|
|
||||||
help="Number of spaces used to indent the output yaml.")
|
|
||||||
parser.add_argument(
|
|
||||||
"--width", type=int, default=80,
|
|
||||||
help="Number of spaces used as the output width.")
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def main(args):
|
|
||||||
logging.basicConfig(format='\033[33m%(levelname)s:\033[37m %(message)s\033[0m\n')
|
|
||||||
|
|
||||||
opts = parse_opts(args)
|
|
||||||
|
|
||||||
with open(opts.filename, 'r') as fh:
|
|
||||||
new_format = migrate(fh.read())
|
|
||||||
|
|
||||||
if opts.in_place:
|
|
||||||
output = open(opts.filename, 'w')
|
|
||||||
else:
|
|
||||||
output = sys.stdout
|
|
||||||
write(output, new_format, opts.indent, opts.width)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main(sys.argv)
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
# -*- mode: python -*-
|
|
||||||
|
|
||||||
block_cipher = None
|
|
||||||
|
|
||||||
a = Analysis(['bin/docker-compose'],
|
|
||||||
pathex=['.'],
|
|
||||||
hiddenimports=[],
|
|
||||||
hookspath=None,
|
|
||||||
runtime_hooks=None,
|
|
||||||
cipher=block_cipher)
|
|
||||||
|
|
||||||
pyz = PYZ(a.pure, cipher=block_cipher)
|
|
||||||
|
|
||||||
exe = EXE(pyz,
|
|
||||||
a.scripts,
|
|
||||||
a.binaries,
|
|
||||||
a.zipfiles,
|
|
||||||
a.datas,
|
|
||||||
[
|
|
||||||
(
|
|
||||||
'compose/config/config_schema_v1.json',
|
|
||||||
'compose/config/config_schema_v1.json',
|
|
||||||
'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_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/GITSHA',
|
|
||||||
'compose/GITSHA',
|
|
||||||
'DATA'
|
|
||||||
)
|
|
||||||
],
|
|
||||||
|
|
||||||
name='docker-compose',
|
|
||||||
debug=False,
|
|
||||||
strip=None,
|
|
||||||
upx=True,
|
|
||||||
console=True)
|
|
||||||
15
docs/Dockerfile
Normal file
15
docs/Dockerfile
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
FROM docs/base:latest
|
||||||
|
MAINTAINER Sven Dowideit <SvenDowideit@docker.com> (@SvenDowideit)
|
||||||
|
|
||||||
|
# to get the git info for this repo
|
||||||
|
COPY . /src
|
||||||
|
|
||||||
|
# Reset the /docs dir so we can replace the theme meta with the new repo's git info
|
||||||
|
RUN git reset --hard
|
||||||
|
|
||||||
|
RUN grep "__version" /src/compose/__init__.py | sed "s/.*'\(.*\)'/\1/" > /docs/VERSION
|
||||||
|
COPY docs/* /docs/sources/compose/
|
||||||
|
COPY docs/mkdocs.yml /docs/mkdocs-compose.yml
|
||||||
|
|
||||||
|
# Then build everything together, ready for mkdocs
|
||||||
|
RUN /docs/build.sh
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
# The docs have been moved!
|
|
||||||
|
|
||||||
The documentation for Compose has been merged into
|
|
||||||
[the general documentation repo](https://github.com/docker/docker.github.io).
|
|
||||||
|
|
||||||
The docs for Compose are now here:
|
|
||||||
https://github.com/docker/docker.github.io/tree/master/compose
|
|
||||||
|
|
||||||
Please submit pull requests for unpublished features on the `vnext-compose` branch (https://github.com/docker/docker.github.io/tree/vnext-compose).
|
|
||||||
|
|
||||||
If you submit a PR to this codebase that has a docs impact, create a second docs PR on `docker.github.io`. Use the docs PR template provided (coming soon - watch this space).
|
|
||||||
|
|
||||||
PRs for typos, additional information, etc. for already-published features should be labeled as `okay-to-publish` (we are still settling on a naming convention, will provide a label soon). You can submit these PRs either to `vnext-compose` or directly to `master` on `docker.github.io`
|
|
||||||
|
|
||||||
As always, the docs remain open-source and we appreciate your feedback and
|
|
||||||
pull requests!
|
|
||||||
181
docs/cli.md
Normal file
181
docs/cli.md
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
page_title: Compose CLI reference
|
||||||
|
page_description: Compose CLI reference
|
||||||
|
page_keywords: fig, composition, compose, docker, orchestration, cli, reference
|
||||||
|
|
||||||
|
|
||||||
|
# CLI reference
|
||||||
|
|
||||||
|
Most Docker Compose commands are run against one or more services. If
|
||||||
|
the service is not specified, the command will apply to all services.
|
||||||
|
|
||||||
|
For full usage information, run `docker-compose [COMMAND] --help`.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### build
|
||||||
|
|
||||||
|
Builds or rebuilds services.
|
||||||
|
|
||||||
|
Services are built once and then tagged as `project_service`, e.g.,
|
||||||
|
`composetest_db`. If you change a service's Dockerfile or the contents of its
|
||||||
|
build directory, run `docker-compose build` to rebuild it.
|
||||||
|
|
||||||
|
### help
|
||||||
|
|
||||||
|
Displays help and usage instructions for a command.
|
||||||
|
|
||||||
|
### kill
|
||||||
|
|
||||||
|
Forces running containers to stop by sending a `SIGKILL` signal. Optionally the
|
||||||
|
signal can be passed, for example:
|
||||||
|
|
||||||
|
$ docker-compose kill -s SIGINT
|
||||||
|
|
||||||
|
### logs
|
||||||
|
|
||||||
|
Displays log output from services.
|
||||||
|
|
||||||
|
### port
|
||||||
|
|
||||||
|
Prints the public port for a port binding
|
||||||
|
|
||||||
|
### ps
|
||||||
|
|
||||||
|
Lists containers.
|
||||||
|
|
||||||
|
### pull
|
||||||
|
|
||||||
|
Pulls service images.
|
||||||
|
|
||||||
|
### rm
|
||||||
|
|
||||||
|
Removes stopped service containers.
|
||||||
|
|
||||||
|
|
||||||
|
### run
|
||||||
|
|
||||||
|
Runs a one-off command on a service.
|
||||||
|
|
||||||
|
For example,
|
||||||
|
|
||||||
|
$ docker-compose run web python manage.py shell
|
||||||
|
|
||||||
|
will start the `web` service and then run `manage.py shell` in python.
|
||||||
|
Note that by default, linked services will also be started, unless they are
|
||||||
|
already running.
|
||||||
|
|
||||||
|
One-off commands are started in new containers with the same configuration as a
|
||||||
|
normal container for that service, so volumes, links, etc will all be created as
|
||||||
|
expected. When using `run`, there are two differences from bringing up a
|
||||||
|
container normally:
|
||||||
|
|
||||||
|
1. the command will be overridden with the one specified. So, if you run
|
||||||
|
`docker-compose run web bash`, the container's web command (which could default
|
||||||
|
to, e.g., `python app.py`) will be overridden to `bash`
|
||||||
|
|
||||||
|
2. by default no ports will be created in case they collide with already opened
|
||||||
|
ports.
|
||||||
|
|
||||||
|
Links are also created between one-off commands and the other containers which
|
||||||
|
are part of that service. So, for example, you could run:
|
||||||
|
|
||||||
|
$ docker-compose run db psql -h db -U docker
|
||||||
|
|
||||||
|
This would open up an interactive PostgreSQL shell for the linked `db` container
|
||||||
|
(which would get created or started as needed).
|
||||||
|
|
||||||
|
If you do not want linked containers to start when running the one-off command,
|
||||||
|
specify the `--no-deps` flag:
|
||||||
|
|
||||||
|
$ docker-compose run --no-deps web python manage.py shell
|
||||||
|
|
||||||
|
Similarly, if you do want the service's ports to be created and mapped to the
|
||||||
|
host, specify the `--service-ports` flag:
|
||||||
|
$ docker-compose run --service-ports web python manage.py shell
|
||||||
|
|
||||||
|
### scale
|
||||||
|
|
||||||
|
Sets the number of containers to run for a service.
|
||||||
|
|
||||||
|
Numbers are specified as arguments in the form `service=num`. For example:
|
||||||
|
|
||||||
|
$ docker-compose scale web=2 worker=3
|
||||||
|
|
||||||
|
### start
|
||||||
|
|
||||||
|
Starts existing containers for a service.
|
||||||
|
|
||||||
|
### stop
|
||||||
|
|
||||||
|
Stops running containers without removing them. They can be started again with
|
||||||
|
`docker-compose start`.
|
||||||
|
|
||||||
|
### up
|
||||||
|
|
||||||
|
Builds, (re)creates, starts, and attaches to containers for a service.
|
||||||
|
|
||||||
|
Linked services will be started, unless they are already running.
|
||||||
|
|
||||||
|
By default, `docker-compose up` will aggregate the output of each container and,
|
||||||
|
when it exits, all containers will be stopped. Running `docker-compose up -d`,
|
||||||
|
will start the containers in the background and leave them running.
|
||||||
|
|
||||||
|
By default, if there are existing containers for a service, `docker-compose up` will stop and recreate them (preserving mounted volumes with [volumes-from]), so that changes in `docker-compose.yml` are picked up. If you do not want containers stopped and recreated, use `docker-compose up --no-recreate`. This will still start any stopped containers, if needed.
|
||||||
|
|
||||||
|
[volumes-from]: http://docs.docker.io/en/latest/use/working_with_volumes/
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
### --verbose
|
||||||
|
|
||||||
|
Shows more output
|
||||||
|
|
||||||
|
### --version
|
||||||
|
|
||||||
|
Prints version and exits
|
||||||
|
|
||||||
|
### -f, --file FILE
|
||||||
|
|
||||||
|
Specifies an alternate Compose yaml file (default: `docker-compose.yml`)
|
||||||
|
|
||||||
|
### -p, --project-name NAME
|
||||||
|
|
||||||
|
Specifies an alternate project name (default: current directory name)
|
||||||
|
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Several environment variables are available for you to configure Compose's behaviour.
|
||||||
|
|
||||||
|
Variables starting with `DOCKER_` are the same as those used to configure the
|
||||||
|
Docker command-line client. If you're using boot2docker, `$(boot2docker shellinit)`
|
||||||
|
will set them to their correct values.
|
||||||
|
|
||||||
|
### COMPOSE\_PROJECT\_NAME
|
||||||
|
|
||||||
|
Sets the project name, which is prepended to the name of every container started by Compose. Defaults to the `basename` of the current working directory.
|
||||||
|
|
||||||
|
### COMPOSE\_FILE
|
||||||
|
|
||||||
|
Sets the path to the `docker-compose.yml` to use. Defaults to `docker-compose.yml` in the current working directory.
|
||||||
|
|
||||||
|
### DOCKER\_HOST
|
||||||
|
|
||||||
|
Sets the URL of the docker daemon. As with the Docker client, defaults to `unix:///var/run/docker.sock`.
|
||||||
|
|
||||||
|
### DOCKER\_TLS\_VERIFY
|
||||||
|
|
||||||
|
When set to anything other than an empty string, enables TLS communication with
|
||||||
|
the daemon.
|
||||||
|
|
||||||
|
### DOCKER\_CERT\_PATH
|
||||||
|
|
||||||
|
Configures the path to the `ca.pem`, `cert.pem`, and `key.pem` files used for TLS verification. Defaults to `~/.docker`.
|
||||||
|
|
||||||
|
## Compose documentation
|
||||||
|
|
||||||
|
- [Installing Compose](install.md)
|
||||||
|
- [User guide](index.md)
|
||||||
|
- [Yaml file reference](yml.md)
|
||||||
|
- [Compose environment variables](env.md)
|
||||||
|
- [Compose command line completion](completion.md)
|
||||||
41
docs/completion.md
Normal file
41
docs/completion.md
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
---
|
||||||
|
layout: default
|
||||||
|
title: Command Completion
|
||||||
|
---
|
||||||
|
|
||||||
|
Command Completion
|
||||||
|
==================
|
||||||
|
|
||||||
|
Compose comes with [command completion](http://en.wikipedia.org/wiki/Command-line_completion)
|
||||||
|
for the bash shell.
|
||||||
|
|
||||||
|
Installing Command Completion
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
Make sure bash completion is installed. If you use a current Linux in a non-minimal installation, bash completion should be available.
|
||||||
|
On a Mac, install with `brew install bash-completion`
|
||||||
|
|
||||||
|
Place the completion script in `/etc/bash_completion.d/` (`/usr/local/etc/bash_completion.d/` on a Mac), using e.g.
|
||||||
|
|
||||||
|
curl -L https://raw.githubusercontent.com/docker/compose/1.2.0rc4/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose
|
||||||
|
|
||||||
|
Completion will be available upon next login.
|
||||||
|
|
||||||
|
Available completions
|
||||||
|
---------------------
|
||||||
|
Depending on what you typed on the command line so far, it will complete
|
||||||
|
|
||||||
|
- available docker-compose commands
|
||||||
|
- options that are available for a particular command
|
||||||
|
- service names that make sense in a given context (e.g. services with running or stopped instances or services based on images vs. services based on Dockerfiles). For `docker-compose scale`, completed service names will automatically have "=" appended.
|
||||||
|
- arguments for selected options, e.g. `docker-compose kill -s` will complete some signals like SIGHUP and SIGUSR1.
|
||||||
|
|
||||||
|
Enjoy working with Compose faster and with less typos!
|
||||||
|
|
||||||
|
## Compose documentation
|
||||||
|
|
||||||
|
- [Installing Compose](install.md)
|
||||||
|
- [User guide](index.md)
|
||||||
|
- [Command line reference](cli.md)
|
||||||
|
- [Yaml file reference](yml.md)
|
||||||
|
- [Compose environment variables](env.md)
|
||||||
127
docs/django.md
Normal file
127
docs/django.md
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
page_title: Quickstart Guide: Compose and Django
|
||||||
|
page_description: Getting started with Docker Compose and Django
|
||||||
|
page_keywords: documentation, docs, docker, compose, orchestration, containers,
|
||||||
|
django
|
||||||
|
|
||||||
|
|
||||||
|
## Getting started with Compose and Django
|
||||||
|
|
||||||
|
|
||||||
|
This Quick-start Guide will demonstrate how to use Compose to set up and run a
|
||||||
|
simple Django/PostgreSQL app. Before starting, you'll need to have
|
||||||
|
[Compose installed](install.md).
|
||||||
|
|
||||||
|
### Define the project
|
||||||
|
|
||||||
|
Start by setting up the three files you'll need to build the app. First, since
|
||||||
|
your app is going to run inside a Docker container containing all of its
|
||||||
|
dependencies, you'll need to define exactly what needs to be included in the
|
||||||
|
container. This is done using a file called `Dockerfile`. To begin with, the
|
||||||
|
Dockerfile consists of:
|
||||||
|
|
||||||
|
FROM python:2.7
|
||||||
|
ENV PYTHONUNBUFFERED 1
|
||||||
|
RUN mkdir /code
|
||||||
|
WORKDIR /code
|
||||||
|
ADD requirements.txt /code/
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
ADD . /code/
|
||||||
|
|
||||||
|
This Dockerfile will define an image that is used to build a container that
|
||||||
|
includes your application and has Python installed alongside all of your Python
|
||||||
|
dependencies. For more information on how to write Dockerfiles, see the
|
||||||
|
[Docker user guide](https://docs.docker.com/userguide/dockerimages/#building-an-image-from-a-dockerfile) and the [Dockerfile reference](http://docs.docker.com/reference/builder/).
|
||||||
|
|
||||||
|
Second, you'll define your Python dependencies in a file called
|
||||||
|
`requirements.txt`:
|
||||||
|
|
||||||
|
Django
|
||||||
|
psycopg2
|
||||||
|
|
||||||
|
Finally, this is all tied together with a file called `docker-compose.yml`. It
|
||||||
|
describes the services that comprise your app (here, a web server and database),
|
||||||
|
which Docker images they use, how they link together, what volumes will be
|
||||||
|
mounted inside the containers, and what ports they expose.
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
command: python manage.py runserver 0.0.0.0:8000
|
||||||
|
volumes:
|
||||||
|
- .:/code
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
links:
|
||||||
|
- db
|
||||||
|
|
||||||
|
See the [`docker-compose.yml` reference](yml.html) for more information on how
|
||||||
|
this file works.
|
||||||
|
|
||||||
|
### Build the project
|
||||||
|
|
||||||
|
You can now start a Django project with `docker-compose run`:
|
||||||
|
|
||||||
|
$ docker-compose run web django-admin.py startproject composeexample .
|
||||||
|
|
||||||
|
First, Compose will build an image for the `web` service using the `Dockerfile`.
|
||||||
|
It will then run `django-admin.py startproject composeexample .` inside a
|
||||||
|
container built using that image.
|
||||||
|
|
||||||
|
This will generate a Django app inside the current directory:
|
||||||
|
|
||||||
|
$ ls
|
||||||
|
Dockerfile docker-compose.yml composeexample manage.py requirements.txt
|
||||||
|
|
||||||
|
### Connect the database
|
||||||
|
|
||||||
|
Now you need to set up the database connection. Replace the `DATABASES = ...`
|
||||||
|
definition in `composeexample/settings.py` to read:
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||||
|
'NAME': 'postgres',
|
||||||
|
'USER': 'postgres',
|
||||||
|
'HOST': 'db',
|
||||||
|
'PORT': 5432,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
These settings are determined by the
|
||||||
|
[postgres](https://registry.hub.docker.com/_/postgres/) Docker image specified
|
||||||
|
in the Dockerfile.
|
||||||
|
|
||||||
|
Then, run `docker-compose up`:
|
||||||
|
|
||||||
|
Recreating myapp_db_1...
|
||||||
|
Recreating myapp_web_1...
|
||||||
|
Attaching to myapp_db_1, myapp_web_1
|
||||||
|
myapp_db_1 |
|
||||||
|
myapp_db_1 | PostgreSQL stand-alone backend 9.1.11
|
||||||
|
myapp_db_1 | 2014-01-27 12:17:03 UTC LOG: database system is ready to accept connections
|
||||||
|
myapp_db_1 | 2014-01-27 12:17:03 UTC LOG: autovacuum launcher started
|
||||||
|
myapp_web_1 | Validating models...
|
||||||
|
myapp_web_1 |
|
||||||
|
myapp_web_1 | 0 errors found
|
||||||
|
myapp_web_1 | January 27, 2014 - 12:12:40
|
||||||
|
myapp_web_1 | Django version 1.6.1, using settings 'composeexample.settings'
|
||||||
|
myapp_web_1 | Starting development server at http://0.0.0.0:8000/
|
||||||
|
myapp_web_1 | Quit the server with CONTROL-C.
|
||||||
|
|
||||||
|
Your Django app should nw be running at port 8000 on your Docker daemon (if
|
||||||
|
you're using Boot2docker, `boot2docker ip` will tell you its address).
|
||||||
|
|
||||||
|
You can also run management commands with Docker. To set up your database, for
|
||||||
|
example, run `docker-compose up` and in another terminal run:
|
||||||
|
|
||||||
|
$ docker-compose run web python manage.py syncdb
|
||||||
|
|
||||||
|
## More Compose documentation
|
||||||
|
|
||||||
|
- [Installing Compose](install.md)
|
||||||
|
- [User guide](index.md)
|
||||||
|
- [Command line reference](cli.md)
|
||||||
|
- [Yaml file reference](yml.md)
|
||||||
|
- [Compose environment variables](env.md)
|
||||||
|
- [Compose command line completion](completion.md)
|
||||||
41
docs/env.md
Normal file
41
docs/env.md
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
---
|
||||||
|
layout: default
|
||||||
|
title: Compose environment variables reference
|
||||||
|
---
|
||||||
|
|
||||||
|
Environment variables reference
|
||||||
|
===============================
|
||||||
|
|
||||||
|
**Note:** Environment variables are no longer the recommended method for connecting to linked services. Instead, you should use the link name (by default, the name of the linked service) as the hostname to connect to. See the [docker-compose.yml documentation](yml.md#links) for details.
|
||||||
|
|
||||||
|
Compose uses [Docker links] to expose services' containers to one another. Each linked container injects a set of environment variables, each of which begins with the uppercase name of the container.
|
||||||
|
|
||||||
|
To see what environment variables are available to a service, run `docker-compose run SERVICE env`.
|
||||||
|
|
||||||
|
<b><i>name</i>\_PORT</b><br>
|
||||||
|
Full URL, e.g. `DB_PORT=tcp://172.17.0.5:5432`
|
||||||
|
|
||||||
|
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i></b><br>
|
||||||
|
Full URL, e.g. `DB_PORT_5432_TCP=tcp://172.17.0.5:5432`
|
||||||
|
|
||||||
|
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i>\_ADDR</b><br>
|
||||||
|
Container's IP address, e.g. `DB_PORT_5432_TCP_ADDR=172.17.0.5`
|
||||||
|
|
||||||
|
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i>\_PORT</b><br>
|
||||||
|
Exposed port number, e.g. `DB_PORT_5432_TCP_PORT=5432`
|
||||||
|
|
||||||
|
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i>\_PROTO</b><br>
|
||||||
|
Protocol (tcp or udp), e.g. `DB_PORT_5432_TCP_PROTO=tcp`
|
||||||
|
|
||||||
|
<b><i>name</i>\_NAME</b><br>
|
||||||
|
Fully qualified container name, e.g. `DB_1_NAME=/myapp_web_1/myapp_db_1`
|
||||||
|
|
||||||
|
[Docker links]: http://docs.docker.com/userguide/dockerlinks/
|
||||||
|
|
||||||
|
## Compose documentation
|
||||||
|
|
||||||
|
- [Installing Compose](install.md)
|
||||||
|
- [User guide](index.md)
|
||||||
|
- [Command line reference](cli.md)
|
||||||
|
- [Yaml file reference](yml.md)
|
||||||
|
- [Compose command line completion](completion.md)
|
||||||
193
docs/index.md
Normal file
193
docs/index.md
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
page_title: Compose: Multi-container orchestration for Docker
|
||||||
|
page_description: Introduction and Overview of Compose
|
||||||
|
page_keywords: documentation, docs, docker, compose, orchestration, containers
|
||||||
|
|
||||||
|
|
||||||
|
# Docker Compose
|
||||||
|
|
||||||
|
Compose is a tool for defining and running complex applications with Docker.
|
||||||
|
With Compose, you define a multi-container application in a single file, then
|
||||||
|
spin your application up in a single command which does everything that needs to
|
||||||
|
be done to get it running.
|
||||||
|
|
||||||
|
Compose is great for development environments, staging servers, and CI. We don't
|
||||||
|
recommend that you use it in production yet.
|
||||||
|
|
||||||
|
Using Compose is basically a three-step process.
|
||||||
|
|
||||||
|
First, you define your app's environment with a `Dockerfile` so it can be
|
||||||
|
reproduced anywhere:
|
||||||
|
|
||||||
|
```Dockerfile
|
||||||
|
FROM python:2.7
|
||||||
|
WORKDIR /code
|
||||||
|
ADD requirements.txt /code/
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
ADD . /code
|
||||||
|
CMD python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, you define the services that make up your app in `docker-compose.yml` so
|
||||||
|
they can be run together in an isolated environment:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
links:
|
||||||
|
- db
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
db:
|
||||||
|
image: postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
Lastly, run `docker-compose up` and Compose will start and run your entire app.
|
||||||
|
|
||||||
|
Compose has commands for managing the whole lifecycle of your application:
|
||||||
|
|
||||||
|
* 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
|
||||||
|
|
||||||
|
## Compose documentation
|
||||||
|
|
||||||
|
- [Installing Compose](install.md)
|
||||||
|
- [Command line reference](cli.md)
|
||||||
|
- [Yaml file reference](yml.md)
|
||||||
|
- [Compose environment variables](env.md)
|
||||||
|
- [Compose command line completion](completion.md)
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
Let's get started with a walkthrough of getting a simple Python web app running
|
||||||
|
on Compose. It assumes a little knowledge of Python, but the concepts
|
||||||
|
demonstrated here should be understandable even if you're not familiar with
|
||||||
|
Python.
|
||||||
|
|
||||||
|
### Installation and set-up
|
||||||
|
|
||||||
|
First, [install Docker and Compose](install.md).
|
||||||
|
|
||||||
|
Next, you'll want to make a directory for the project:
|
||||||
|
|
||||||
|
$ mkdir composetest
|
||||||
|
$ cd composetest
|
||||||
|
|
||||||
|
Inside this directory, create `app.py`, a simple web app that uses the Flask
|
||||||
|
framework and increments a value in Redis:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from flask import Flask
|
||||||
|
from redis import Redis
|
||||||
|
import os
|
||||||
|
app = Flask(__name__)
|
||||||
|
redis = Redis(host='redis', port=6379)
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def hello():
|
||||||
|
redis.incr('hits')
|
||||||
|
return 'Hello World! I have been seen %s times.' % redis.get('hits')
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(host="0.0.0.0", debug=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, define the Python dependencies in a file called `requirements.txt`:
|
||||||
|
|
||||||
|
flask
|
||||||
|
redis
|
||||||
|
|
||||||
|
### Create a Docker image
|
||||||
|
|
||||||
|
Now, create a Docker image containing all of your app's dependencies. You
|
||||||
|
specify how to build the image using a file called
|
||||||
|
[`Dockerfile`](http://docs.docker.com/reference/builder/):
|
||||||
|
|
||||||
|
FROM python:2.7
|
||||||
|
ADD . /code
|
||||||
|
WORKDIR /code
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
|
This tells Docker to include Python, your code, and your Python dependencies in
|
||||||
|
a Docker image. For more information on how to write Dockerfiles, see the
|
||||||
|
[Docker user
|
||||||
|
guide](https://docs.docker.com/userguide/dockerimages/#building-an-image-from-a-dockerfile)
|
||||||
|
and the
|
||||||
|
[Dockerfile reference](http://docs.docker.com/reference/builder/).
|
||||||
|
|
||||||
|
### Define services
|
||||||
|
|
||||||
|
Next, define a set of services using `docker-compose.yml`:
|
||||||
|
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
command: python app.py
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
volumes:
|
||||||
|
- .:/code
|
||||||
|
links:
|
||||||
|
- redis
|
||||||
|
redis:
|
||||||
|
image: redis
|
||||||
|
|
||||||
|
This defines two services:
|
||||||
|
|
||||||
|
- `web`, which is built from the `Dockerfile` in the current directory. It also
|
||||||
|
says to run the command `python app.py` inside the image, forward the exposed
|
||||||
|
port 5000 on the container to port 5000 on the host machine, connect up the
|
||||||
|
Redis service, and mount the current directory inside the container so we can
|
||||||
|
work on code without having to rebuild the image.
|
||||||
|
- `redis`, which uses the public image
|
||||||
|
[redis](https://registry.hub.docker.com/_/redis/), which gets pulled from the
|
||||||
|
Docker Hub registry.
|
||||||
|
|
||||||
|
### Build and run your app with Compose
|
||||||
|
|
||||||
|
Now, when you run `docker-compose up`, Compose will pull a Redis image, build an
|
||||||
|
image for your code, and start everything up:
|
||||||
|
|
||||||
|
$ docker-compose up
|
||||||
|
Pulling image redis...
|
||||||
|
Building web...
|
||||||
|
Starting composetest_redis_1...
|
||||||
|
Starting composetest_web_1...
|
||||||
|
redis_1 | [8] 02 Jan 18:43:35.576 # Server started, Redis version 2.8.3
|
||||||
|
web_1 | * Running on http://0.0.0.0:5000/
|
||||||
|
|
||||||
|
The web app should now be listening on port 5000 on your Docker daemon host (if
|
||||||
|
you're using Boot2docker, `boot2docker ip` will tell you its address).
|
||||||
|
|
||||||
|
If you want to run your services in the background, you can pass the `-d` flag
|
||||||
|
(for daemon mode) to `docker-compose up` and use `docker-compose ps` to see what
|
||||||
|
is currently running:
|
||||||
|
|
||||||
|
$ docker-compose up -d
|
||||||
|
Starting composetest_redis_1...
|
||||||
|
Starting composetest_web_1...
|
||||||
|
$ docker-compose ps
|
||||||
|
Name Command State Ports
|
||||||
|
-------------------------------------------------------------------
|
||||||
|
composetest_redis_1 /usr/local/bin/run Up
|
||||||
|
composetest_web_1 /bin/sh -c python app.py Up 5000->5000/tcp
|
||||||
|
|
||||||
|
The `docker-compose run` command allows you to run one-off commands for your
|
||||||
|
services. For example, to see what environment variables are available to the
|
||||||
|
`web` service:
|
||||||
|
|
||||||
|
$ docker-compose run web env
|
||||||
|
|
||||||
|
See `docker-compose --help` to see other available commands.
|
||||||
|
|
||||||
|
If you started Compose with `docker-compose up -d`, you'll probably want to stop
|
||||||
|
your services once you've finished with them:
|
||||||
|
|
||||||
|
$ docker-compose stop
|
||||||
|
|
||||||
|
At this point, you have seen the basics of how Compose works.
|
||||||
|
|
||||||
|
- Next, try the quick start guide for [Django](django.md),
|
||||||
|
[Rails](rails.md), or [Wordpress](wordpress.md).
|
||||||
|
- See the reference guides for complete details on the [commands](cli.md), the
|
||||||
|
[configuration file](yml.md) and [environment variables](env.md).
|
||||||
43
docs/install.md
Normal file
43
docs/install.md
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
page_title: Installing Compose
|
||||||
|
page_description: How to intall Docker Compose
|
||||||
|
page_keywords: compose, orchestration, install, installation, docker, documentation
|
||||||
|
|
||||||
|
|
||||||
|
## Installing Compose
|
||||||
|
|
||||||
|
To install Compose, you'll need to install Docker first. You'll then install
|
||||||
|
Compose with a `curl` command.
|
||||||
|
|
||||||
|
### Install Docker
|
||||||
|
|
||||||
|
First, install Docker version 1.3 or greater:
|
||||||
|
|
||||||
|
- [Instructions for Mac OS X](http://docs.docker.com/installation/mac/)
|
||||||
|
- [Instructions for Ubuntu](http://docs.docker.com/installation/ubuntulinux/)
|
||||||
|
- [Instructions for other systems](http://docs.docker.com/installation/)
|
||||||
|
|
||||||
|
### Install Compose
|
||||||
|
|
||||||
|
To install Compose, run the following commands:
|
||||||
|
|
||||||
|
curl -L https://github.com/docker/compose/releases/download/1.2.0rc4/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
|
||||||
|
chmod +x /usr/local/bin/docker-compose
|
||||||
|
|
||||||
|
Optionally, you can also install [command completion](completion.md) for the
|
||||||
|
bash shell.
|
||||||
|
|
||||||
|
Compose is available for OS X and 64-bit Linux. If you're on another platform,
|
||||||
|
Compose can also be installed as a Python package:
|
||||||
|
|
||||||
|
$ sudo pip install -U docker-compose
|
||||||
|
|
||||||
|
No further steps are required; Compose should now be successfully installed.
|
||||||
|
You can test the installation by running `docker-compose --version`.
|
||||||
|
|
||||||
|
## Compose documentation
|
||||||
|
|
||||||
|
- [User guide](index.md)
|
||||||
|
- [Command line reference](cli.md)
|
||||||
|
- [Yaml file reference](yml.md)
|
||||||
|
- [Compose environment variables](env.md)
|
||||||
|
- [Compose command line completion](completion.md)
|
||||||
10
docs/mkdocs.yml
Normal file
10
docs/mkdocs.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
|
||||||
|
- ['compose/index.md', 'User Guide', 'Docker Compose' ]
|
||||||
|
- ['compose/install.md', 'Installation', 'Docker Compose']
|
||||||
|
- ['compose/cli.md', 'Reference', 'Compose command line']
|
||||||
|
- ['compose/yml.md', 'Reference', 'Compose yml']
|
||||||
|
- ['compose/env.md', 'Reference', 'Compose ENV variables']
|
||||||
|
- ['compose/completion.md', 'Reference', 'Compose commandline completion']
|
||||||
|
- ['compose/django.md', 'Examples', 'Getting started with Compose and Django']
|
||||||
|
- ['compose/rails.md', 'Examples', 'Getting started with Compose and Rails']
|
||||||
|
- ['compose/wordpress.md', 'Examples', 'Getting started with Compose and Wordpress']
|
||||||
127
docs/rails.md
Normal file
127
docs/rails.md
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
page_title: Quickstart Guide: Compose and Rails
|
||||||
|
page_description: Getting started with Docker Compose and Rails
|
||||||
|
page_keywords: documentation, docs, docker, compose, orchestration, containers,
|
||||||
|
rails
|
||||||
|
|
||||||
|
|
||||||
|
## Getting started with Compose and Rails
|
||||||
|
|
||||||
|
This Quickstart guide will show you how to use Compose to set up and run a Rails/PostgreSQL app. Before starting, you'll need to have [Compose installed](install.md).
|
||||||
|
|
||||||
|
### Define the project
|
||||||
|
|
||||||
|
Start by setting up the three files you'll need to build the app. First, since
|
||||||
|
your app is going to run inside a Docker container containing all of its
|
||||||
|
dependencies, you'll need to define exactly what needs to be included in the
|
||||||
|
container. This is done using a file called `Dockerfile`. To begin with, the
|
||||||
|
Dockerfile consists of:
|
||||||
|
|
||||||
|
FROM ruby:2.2.0
|
||||||
|
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev
|
||||||
|
RUN mkdir /myapp
|
||||||
|
WORKDIR /myapp
|
||||||
|
ADD Gemfile /myapp/Gemfile
|
||||||
|
RUN bundle install
|
||||||
|
ADD . /myapp
|
||||||
|
|
||||||
|
That'll put your application code inside an image that will build a container with Ruby, Bundler and all your dependencies inside it. For more information on how to write Dockerfiles, see the [Docker user guide](https://docs.docker.com/userguide/dockerimages/#building-an-image-from-a-dockerfile) and the [Dockerfile reference](http://docs.docker.com/reference/builder/).
|
||||||
|
|
||||||
|
Next, create a bootstrap `Gemfile` which just loads Rails. It'll be overwritten in a moment by `rails new`.
|
||||||
|
|
||||||
|
source 'https://rubygems.org'
|
||||||
|
gem 'rails', '4.2.0'
|
||||||
|
|
||||||
|
Finally, `docker-compose.yml` is where the magic happens. This file describes the services that comprise your app (a database and a web app), how to get each one's Docker image (the database just runs on a pre-made PostgreSQL image, and the web app is built from the current directory), and the configuration needed to link them together and expose the web app's port.
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres
|
||||||
|
ports:
|
||||||
|
- "5432"
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
command: bundle exec rails s -p 3000 -b '0.0.0.0'
|
||||||
|
volumes:
|
||||||
|
- .:/myapp
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
links:
|
||||||
|
- db
|
||||||
|
|
||||||
|
### Build the project
|
||||||
|
|
||||||
|
With those three files in place, you can now generate the Rails skeleton app
|
||||||
|
using `docker-compose run`:
|
||||||
|
|
||||||
|
$ docker-compose run web rails new . --force --database=postgresql --skip-bundle
|
||||||
|
|
||||||
|
First, Compose will build the image for the `web` service using the
|
||||||
|
`Dockerfile`. Then it'll run `rails new` inside a new container, using that
|
||||||
|
image. Once it's done, you should have generated a fresh app:
|
||||||
|
|
||||||
|
$ ls
|
||||||
|
Dockerfile app docker-compose.yml tmp
|
||||||
|
Gemfile bin lib vendor
|
||||||
|
Gemfile.lock config log
|
||||||
|
README.rdoc config.ru public
|
||||||
|
Rakefile db test
|
||||||
|
|
||||||
|
Uncomment the line in your new `Gemfile` which loads `therubyracer`, so you've
|
||||||
|
got a Javascript runtime:
|
||||||
|
|
||||||
|
gem 'therubyracer', platforms: :ruby
|
||||||
|
|
||||||
|
Now that you've got a new `Gemfile`, you need to build the image again. (This,
|
||||||
|
and changes to the Dockerfile itself, should be the only times you'll need to
|
||||||
|
rebuild.)
|
||||||
|
|
||||||
|
$ docker-compose build
|
||||||
|
|
||||||
|
### Connect the database
|
||||||
|
|
||||||
|
The app is now bootable, but you're not quite there yet. By default, Rails
|
||||||
|
expects a database to be running on `localhost` - so you need to point it at the
|
||||||
|
`db` container instead. You also need to change the database and username to
|
||||||
|
align with the defaults set by the `postgres` image.
|
||||||
|
|
||||||
|
Open up your newly-generated `database.yml` file. Replace its contents with the
|
||||||
|
following:
|
||||||
|
|
||||||
|
development: &default
|
||||||
|
adapter: postgresql
|
||||||
|
encoding: unicode
|
||||||
|
database: postgres
|
||||||
|
pool: 5
|
||||||
|
username: postgres
|
||||||
|
password:
|
||||||
|
host: db
|
||||||
|
|
||||||
|
test:
|
||||||
|
<<: *default
|
||||||
|
database: myapp_test
|
||||||
|
|
||||||
|
You can now boot the app with:
|
||||||
|
|
||||||
|
$ docker-compose up
|
||||||
|
|
||||||
|
If all's well, you should see some PostgreSQL output, and then—after a few
|
||||||
|
seconds—the familiar refrain:
|
||||||
|
|
||||||
|
myapp_web_1 | [2014-01-17 17:16:29] INFO WEBrick 1.3.1
|
||||||
|
myapp_web_1 | [2014-01-17 17:16:29] INFO ruby 2.2.0 (2014-12-25) [x86_64-linux-gnu]
|
||||||
|
myapp_web_1 | [2014-01-17 17:16:29] INFO WEBrick::HTTPServer#start: pid=1 port=3000
|
||||||
|
|
||||||
|
Finally, you need to create the database. In another terminal, run:
|
||||||
|
|
||||||
|
$ docker-compose run web rake db:create
|
||||||
|
|
||||||
|
That's it. Your app should now be running on port 3000 on your Docker daemon (if
|
||||||
|
you're using Boot2docker, `boot2docker ip` will tell you its address).
|
||||||
|
|
||||||
|
## More Compose documentation
|
||||||
|
|
||||||
|
- [Installing Compose](install.md)
|
||||||
|
- [User guide](index.md)
|
||||||
|
- [Command line reference](cli.md)
|
||||||
|
- [Yaml file reference](yml.md)
|
||||||
|
- [Compose environment variables](env.md)
|
||||||
|
- [Compose command line completion](completion.md)
|
||||||
122
docs/wordpress.md
Normal file
122
docs/wordpress.md
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
page_title: Quickstart Guide: Compose and Wordpress
|
||||||
|
page_description: Getting started with Docker Compose and Rails
|
||||||
|
page_keywords: documentation, docs, docker, compose, orchestration, containers,
|
||||||
|
wordpress
|
||||||
|
|
||||||
|
## Getting started with Compose and Wordpress
|
||||||
|
|
||||||
|
You can use Compose to easily run Wordpress in an isolated environment built
|
||||||
|
with Docker containers.
|
||||||
|
|
||||||
|
### Define the project
|
||||||
|
|
||||||
|
First, [Install Compose](install.md) and then download Wordpress into the
|
||||||
|
current directory:
|
||||||
|
|
||||||
|
$ curl https://wordpress.org/latest.tar.gz | tar -xvzf -
|
||||||
|
|
||||||
|
This will create a directory called `wordpress`. If you wish, you can rename it
|
||||||
|
to the name of your project.
|
||||||
|
|
||||||
|
Next, inside that directory, create a `Dockerfile`, a file that defines what
|
||||||
|
environment your app is going to run in. For more information on how to write
|
||||||
|
Dockerfiles, see the
|
||||||
|
[Docker user guide](https://docs.docker.com/userguide/dockerimages/#building-an-image-from-a-dockerfile) and the
|
||||||
|
[Dockerfile reference](http://docs.docker.com/reference/builder/). In this case,
|
||||||
|
your Dockerfile should be:
|
||||||
|
|
||||||
|
```
|
||||||
|
FROM orchardup/php5
|
||||||
|
ADD . /code
|
||||||
|
```
|
||||||
|
|
||||||
|
This tells Docker how to build an image defining a container that contains PHP
|
||||||
|
and Wordpress.
|
||||||
|
|
||||||
|
Next you'll create a `docker-compose.yml` file that will start your web service
|
||||||
|
and a separate MySQL instance:
|
||||||
|
|
||||||
|
```
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
command: php -S 0.0.0.0:8000 -t /code
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
links:
|
||||||
|
- db
|
||||||
|
volumes:
|
||||||
|
- .:/code
|
||||||
|
db:
|
||||||
|
image: orchardup/mysql
|
||||||
|
environment:
|
||||||
|
MYSQL_DATABASE: wordpress
|
||||||
|
```
|
||||||
|
|
||||||
|
Two supporting files are needed to get this working - first, `wp-config.php` is
|
||||||
|
the standard Wordpress config file with a single change to point the database
|
||||||
|
configuration at the `db` container:
|
||||||
|
|
||||||
|
```
|
||||||
|
<?php
|
||||||
|
define('DB_NAME', 'wordpress');
|
||||||
|
define('DB_USER', 'root');
|
||||||
|
define('DB_PASSWORD', '');
|
||||||
|
define('DB_HOST', "db:3306");
|
||||||
|
define('DB_CHARSET', 'utf8');
|
||||||
|
define('DB_COLLATE', '');
|
||||||
|
|
||||||
|
define('AUTH_KEY', 'put your unique phrase here');
|
||||||
|
define('SECURE_AUTH_KEY', 'put your unique phrase here');
|
||||||
|
define('LOGGED_IN_KEY', 'put your unique phrase here');
|
||||||
|
define('NONCE_KEY', 'put your unique phrase here');
|
||||||
|
define('AUTH_SALT', 'put your unique phrase here');
|
||||||
|
define('SECURE_AUTH_SALT', 'put your unique phrase here');
|
||||||
|
define('LOGGED_IN_SALT', 'put your unique phrase here');
|
||||||
|
define('NONCE_SALT', 'put your unique phrase here');
|
||||||
|
|
||||||
|
$table_prefix = 'wp_';
|
||||||
|
define('WPLANG', '');
|
||||||
|
define('WP_DEBUG', false);
|
||||||
|
|
||||||
|
if ( !defined('ABSPATH') )
|
||||||
|
define('ABSPATH', dirname(__FILE__) . '/');
|
||||||
|
|
||||||
|
require_once(ABSPATH . 'wp-settings.php');
|
||||||
|
```
|
||||||
|
|
||||||
|
Second, `router.php` tells PHP's built-in web server how to run Wordpress:
|
||||||
|
|
||||||
|
```
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$root = $_SERVER['DOCUMENT_ROOT'];
|
||||||
|
chdir($root);
|
||||||
|
$path = '/'.ltrim(parse_url($_SERVER['REQUEST_URI'])['path'],'/');
|
||||||
|
set_include_path(get_include_path().':'.__DIR__);
|
||||||
|
if(file_exists($root.$path))
|
||||||
|
{
|
||||||
|
if(is_dir($root.$path) && substr($path,strlen($path) - 1, 1) !== '/')
|
||||||
|
$path = rtrim($path,'/').'/index.php';
|
||||||
|
if(strpos($path,'.php') === false) return false;
|
||||||
|
else {
|
||||||
|
chdir(dirname($root.$path));
|
||||||
|
require_once $root.$path;
|
||||||
|
}
|
||||||
|
}else include_once 'index.php';
|
||||||
|
```
|
||||||
|
### Build the project
|
||||||
|
|
||||||
|
With those four files in place, run `docker-compose up` inside your Wordpress
|
||||||
|
directory and it'll pull and build the needed images, and then start the web and
|
||||||
|
database containers. You'll then be able to visit Wordpress at port 8000 on your
|
||||||
|
Docker daemon (if you're using Boot2docker, `boot2docker ip` will tell you its
|
||||||
|
address).
|
||||||
|
|
||||||
|
## More Compose documentation
|
||||||
|
|
||||||
|
- [Installing Compose](install.md)
|
||||||
|
- [User guide](index.md)
|
||||||
|
- [Command line reference](cli.md)
|
||||||
|
- [Yaml file reference](yml.md)
|
||||||
|
- [Compose environment variables](env.md)
|
||||||
|
- [Compose command line completion](completion.md)
|
||||||
334
docs/yml.md
Normal file
334
docs/yml.md
Normal file
|
|
@ -0,0 +1,334 @@
|
||||||
|
---
|
||||||
|
layout: default
|
||||||
|
title: docker-compose.yml reference
|
||||||
|
page_title: docker-compose.yml reference
|
||||||
|
page_description: docker-compose.yml reference
|
||||||
|
page_keywords: fig, composition, compose, docker
|
||||||
|
---
|
||||||
|
|
||||||
|
# docker-compose.yml reference
|
||||||
|
|
||||||
|
Each service defined in `docker-compose.yml` must specify exactly one of
|
||||||
|
`image` or `build`. Other keys are optional, and are analogous to their
|
||||||
|
`docker run` command-line counterparts.
|
||||||
|
|
||||||
|
As with `docker run`, options specified in the Dockerfile (e.g., `CMD`,
|
||||||
|
`EXPOSE`, `VOLUME`, `ENV`) are respected by default - you don't need to
|
||||||
|
specify them again in `docker-compose.yml`.
|
||||||
|
|
||||||
|
### image
|
||||||
|
|
||||||
|
Tag or partial image ID. Can be local or remote - Compose will attempt to
|
||||||
|
pull if it doesn't exist locally.
|
||||||
|
|
||||||
|
```
|
||||||
|
image: ubuntu
|
||||||
|
image: orchardup/postgresql
|
||||||
|
image: a4bc65fd
|
||||||
|
```
|
||||||
|
|
||||||
|
### build
|
||||||
|
|
||||||
|
Path to a directory containing a Dockerfile. When the value supplied is a
|
||||||
|
relative path, it is interpreted as relative to the location of the yml file
|
||||||
|
itself. This directory is also the build context that is sent to the Docker daemon.
|
||||||
|
|
||||||
|
Compose will build and tag it with a generated name, and use that image thereafter.
|
||||||
|
|
||||||
|
```
|
||||||
|
build: /path/to/build/dir
|
||||||
|
```
|
||||||
|
|
||||||
|
### command
|
||||||
|
|
||||||
|
Override the default command.
|
||||||
|
|
||||||
|
```
|
||||||
|
command: bundle exec thin -p 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
<a name="links"></a>
|
||||||
|
### links
|
||||||
|
|
||||||
|
Link to containers in another service. Either specify both the service name and
|
||||||
|
the link alias (`SERVICE:ALIAS`), or just the service name (which will also be
|
||||||
|
used for the alias).
|
||||||
|
|
||||||
|
```
|
||||||
|
links:
|
||||||
|
- db
|
||||||
|
- db:database
|
||||||
|
- redis
|
||||||
|
```
|
||||||
|
|
||||||
|
An entry with the alias' name will be created in `/etc/hosts` inside containers
|
||||||
|
for this service, e.g:
|
||||||
|
|
||||||
|
```
|
||||||
|
172.17.2.186 db
|
||||||
|
172.17.2.186 database
|
||||||
|
172.17.2.187 redis
|
||||||
|
```
|
||||||
|
|
||||||
|
Environment variables will also be created - see the [environment variable
|
||||||
|
reference](env.md) for details.
|
||||||
|
|
||||||
|
### external_links
|
||||||
|
|
||||||
|
Link to containers started outside this `docker-compose.yml` or even outside
|
||||||
|
of Compose, especially for containers that provide shared or common services.
|
||||||
|
`external_links` follow semantics similar to `links` when specifying both the
|
||||||
|
container name and the link alias (`CONTAINER:ALIAS`).
|
||||||
|
|
||||||
|
```
|
||||||
|
external_links:
|
||||||
|
- redis_1
|
||||||
|
- project_db_1:mysql
|
||||||
|
- project_db_1:postgresql
|
||||||
|
```
|
||||||
|
|
||||||
|
### ports
|
||||||
|
|
||||||
|
Expose ports. Either specify both ports (`HOST:CONTAINER`), or just the container
|
||||||
|
port (a random host port will be chosen).
|
||||||
|
|
||||||
|
> **Note:** When mapping ports in the `HOST:CONTAINER` format, you may experience
|
||||||
|
> erroneous results when using a container port lower than 60, because YAML will
|
||||||
|
> parse numbers in the format `xx:yy` as sexagesimal (base 60). For this reason,
|
||||||
|
> we recommend always explicitly specifying your port mappings as strings.
|
||||||
|
|
||||||
|
```
|
||||||
|
ports:
|
||||||
|
- "3000"
|
||||||
|
- "8000:8000"
|
||||||
|
- "49100:22"
|
||||||
|
- "127.0.0.1:8001:8001"
|
||||||
|
```
|
||||||
|
|
||||||
|
### expose
|
||||||
|
|
||||||
|
Expose ports without publishing them to the host machine - they'll only be
|
||||||
|
accessible to linked services. Only the internal port can be specified.
|
||||||
|
|
||||||
|
```
|
||||||
|
expose:
|
||||||
|
- "3000"
|
||||||
|
- "8000"
|
||||||
|
```
|
||||||
|
|
||||||
|
### volumes
|
||||||
|
|
||||||
|
Mount paths as volumes, optionally specifying a path on the host machine
|
||||||
|
(`HOST:CONTAINER`), or an access mode (`HOST:CONTAINER:ro`).
|
||||||
|
|
||||||
|
```
|
||||||
|
volumes:
|
||||||
|
- /var/lib/mysql
|
||||||
|
- cache/:/tmp/cache
|
||||||
|
- ~/configs:/etc/configs/:ro
|
||||||
|
```
|
||||||
|
|
||||||
|
### volumes_from
|
||||||
|
|
||||||
|
Mount all of the volumes from another service or container.
|
||||||
|
|
||||||
|
```
|
||||||
|
volumes_from:
|
||||||
|
- service_name
|
||||||
|
- container_name
|
||||||
|
```
|
||||||
|
|
||||||
|
### environment
|
||||||
|
|
||||||
|
Add environment variables. You can use either an array or a dictionary.
|
||||||
|
|
||||||
|
Environment variables with only a key are resolved to their values on the
|
||||||
|
machine Compose is running on, which can be helpful for secret or host-specific values.
|
||||||
|
|
||||||
|
```
|
||||||
|
environment:
|
||||||
|
RACK_ENV: development
|
||||||
|
SESSION_SECRET:
|
||||||
|
|
||||||
|
environment:
|
||||||
|
- RACK_ENV=development
|
||||||
|
- SESSION_SECRET
|
||||||
|
```
|
||||||
|
|
||||||
|
### env_file
|
||||||
|
|
||||||
|
Add environment variables from a file. Can be a single value or a list.
|
||||||
|
|
||||||
|
If you have specified a Compose file with `docker-compose -f FILE`, paths in
|
||||||
|
`env_file` are relative to the directory that file is in.
|
||||||
|
|
||||||
|
Environment variables specified in `environment` override these values.
|
||||||
|
|
||||||
|
```
|
||||||
|
env_file: .env
|
||||||
|
|
||||||
|
env_file:
|
||||||
|
- ./common.env
|
||||||
|
- ./apps/web.env
|
||||||
|
- /opt/secrets.env
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
RACK_ENV: development
|
||||||
|
```
|
||||||
|
|
||||||
|
### extends
|
||||||
|
|
||||||
|
Extend another service, in the current file or another, optionally overriding
|
||||||
|
configuration.
|
||||||
|
|
||||||
|
Here's a simple example. Suppose we have 2 files - **common.yml** and
|
||||||
|
**development.yml**. We can use `extends` to define a service in
|
||||||
|
**development.yml** which uses configuration defined in **common.yml**:
|
||||||
|
|
||||||
|
**common.yml**
|
||||||
|
|
||||||
|
```
|
||||||
|
webapp:
|
||||||
|
build: ./webapp
|
||||||
|
environment:
|
||||||
|
- DEBUG=false
|
||||||
|
- SEND_EMAILS=false
|
||||||
|
```
|
||||||
|
|
||||||
|
**development.yml**
|
||||||
|
|
||||||
|
```
|
||||||
|
web:
|
||||||
|
extends:
|
||||||
|
file: common.yml
|
||||||
|
service: webapp
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
links:
|
||||||
|
- db
|
||||||
|
environment:
|
||||||
|
- DEBUG=true
|
||||||
|
db:
|
||||||
|
image: postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, the `web` service in **development.yml** inherits the configuration of
|
||||||
|
the `webapp` service in **common.yml** - the `build` and `environment` keys -
|
||||||
|
and adds `ports` and `links` configuration. It overrides one of the defined
|
||||||
|
environment variables (DEBUG) with a new value, and the other one
|
||||||
|
(SEND_EMAILS) is left untouched. It's exactly as if you defined `web` like
|
||||||
|
this:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
web:
|
||||||
|
build: ./webapp
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
links:
|
||||||
|
- db
|
||||||
|
environment:
|
||||||
|
- DEBUG=true
|
||||||
|
- SEND_EMAILS=false
|
||||||
|
```
|
||||||
|
|
||||||
|
The `extends` option is great for sharing configuration between different
|
||||||
|
apps, or for configuring the same app differently for different environments.
|
||||||
|
You could write a new file for a staging environment, **staging.yml**, which
|
||||||
|
binds to a different port and doesn't turn on debugging:
|
||||||
|
|
||||||
|
```
|
||||||
|
web:
|
||||||
|
extends:
|
||||||
|
file: common.yml
|
||||||
|
service: webapp
|
||||||
|
ports:
|
||||||
|
- "80:8000"
|
||||||
|
links:
|
||||||
|
- db
|
||||||
|
db:
|
||||||
|
image: postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** When you extend a service, `links` and `volumes_from`
|
||||||
|
> configuration options are **not** inherited - you will have to define
|
||||||
|
> those manually each time you extend it.
|
||||||
|
|
||||||
|
### net
|
||||||
|
|
||||||
|
Networking mode. Use the same values as the docker client `--net` parameter.
|
||||||
|
|
||||||
|
```
|
||||||
|
net: "bridge"
|
||||||
|
net: "none"
|
||||||
|
net: "container:[name or id]"
|
||||||
|
net: "host"
|
||||||
|
```
|
||||||
|
|
||||||
|
### dns
|
||||||
|
|
||||||
|
Custom DNS servers. Can be a single value or a list.
|
||||||
|
|
||||||
|
```
|
||||||
|
dns: 8.8.8.8
|
||||||
|
dns:
|
||||||
|
- 8.8.8.8
|
||||||
|
- 9.9.9.9
|
||||||
|
```
|
||||||
|
|
||||||
|
### cap_add, cap_drop
|
||||||
|
|
||||||
|
Add or drop container capabilities.
|
||||||
|
See `man 7 capabilities` for a full list.
|
||||||
|
|
||||||
|
```
|
||||||
|
cap_add:
|
||||||
|
- ALL
|
||||||
|
|
||||||
|
cap_drop:
|
||||||
|
- NET_ADMIN
|
||||||
|
- SYS_ADMIN
|
||||||
|
```
|
||||||
|
|
||||||
|
### dns_search
|
||||||
|
|
||||||
|
Custom DNS search domains. Can be a single value or a list.
|
||||||
|
|
||||||
|
```
|
||||||
|
dns_search: example.com
|
||||||
|
dns_search:
|
||||||
|
- dc1.example.com
|
||||||
|
- dc2.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### working\_dir, entrypoint, user, hostname, domainname, mem\_limit, privileged, restart, stdin\_open, tty, cpu\_shares
|
||||||
|
|
||||||
|
Each of these is a single value, analogous to its
|
||||||
|
[docker run](https://docs.docker.com/reference/run/) counterpart.
|
||||||
|
|
||||||
|
```
|
||||||
|
cpu_shares: 73
|
||||||
|
|
||||||
|
working_dir: /code
|
||||||
|
entrypoint: /code/entrypoint.sh
|
||||||
|
user: postgresql
|
||||||
|
|
||||||
|
hostname: foo
|
||||||
|
domainname: foo.com
|
||||||
|
|
||||||
|
mem_limit: 1000000000
|
||||||
|
privileged: true
|
||||||
|
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compose documentation
|
||||||
|
|
||||||
|
- [Installing Compose](install.md)
|
||||||
|
- [User guide](index.md)
|
||||||
|
- [Command line reference](cli.md)
|
||||||
|
- [Compose environment variables](env.md)
|
||||||
|
- [Compose command line completion](completion.md)
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
# Experimental: Compose, Swarm and Multi-Host Networking
|
|
||||||
|
|
||||||
Compose now supports multi-host networking as standard. Read more here:
|
|
||||||
|
|
||||||
https://docs.docker.com/compose/networking
|
|
||||||
BIN
logo.png
BIN
logo.png
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB |
|
|
@ -1,35 +0,0 @@
|
||||||
Triaging of issues
|
|
||||||
------------------
|
|
||||||
|
|
||||||
The docker-compose issue triage process follows
|
|
||||||
https://github.com/docker/docker/blob/master/project/ISSUE-TRIAGE.md
|
|
||||||
with the following additions or exceptions.
|
|
||||||
|
|
||||||
|
|
||||||
### Classify the Issue
|
|
||||||
|
|
||||||
The following labels are provided in additional to the standard labels:
|
|
||||||
|
|
||||||
| Kind | Description |
|
|
||||||
|--------------|-------------------------------------------------------------------|
|
|
||||||
| kind/cleanup | A refactor or improvement that is related to quality not function |
|
|
||||||
| kind/parity | A request for feature parity with docker cli |
|
|
||||||
|
|
||||||
|
|
||||||
### Functional areas
|
|
||||||
|
|
||||||
Most issues should fit into one of the following functional areas:
|
|
||||||
|
|
||||||
| Area |
|
|
||||||
|-----------------|
|
|
||||||
| area/build |
|
|
||||||
| area/cli |
|
|
||||||
| area/config |
|
|
||||||
| area/logs |
|
|
||||||
| area/networking |
|
|
||||||
| area/packaging |
|
|
||||||
| area/run |
|
|
||||||
| area/scale |
|
|
||||||
| area/tests |
|
|
||||||
| area/up |
|
|
||||||
| area/volumes |
|
|
||||||
|
|
@ -1,139 +0,0 @@
|
||||||
Building a Compose release
|
|
||||||
==========================
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
The release scripts require the following tools installed on the host:
|
|
||||||
|
|
||||||
* https://hub.github.com/
|
|
||||||
* https://stedolan.github.io/jq/
|
|
||||||
* http://pandoc.org/
|
|
||||||
|
|
||||||
## To get started with a new release
|
|
||||||
|
|
||||||
Create a branch, update version, and add release notes by running `make-branch`
|
|
||||||
|
|
||||||
./script/release/make-branch $VERSION [$BASE_VERSION]
|
|
||||||
|
|
||||||
`$BASE_VERSION` will default to master. Use the last version tag for a bug fix
|
|
||||||
release.
|
|
||||||
|
|
||||||
As part of this script you'll be asked to:
|
|
||||||
|
|
||||||
1. Update the version in `compose/__init__.py` and `script/run/run.sh`.
|
|
||||||
|
|
||||||
If the next release will be an RC, append `-rcN`, e.g. `1.4.0-rc1`.
|
|
||||||
|
|
||||||
2. Write release notes in `CHANGES.md`.
|
|
||||||
|
|
||||||
Almost every feature enhancement should be mentioned, with the most
|
|
||||||
visible/exciting ones first. Use descriptive sentences and give context
|
|
||||||
where appropriate.
|
|
||||||
|
|
||||||
Bug fixes are worth mentioning if it's likely that they've affected lots
|
|
||||||
of people, or if they were regressions in the previous version.
|
|
||||||
|
|
||||||
Improvements to the code are not worth mentioning.
|
|
||||||
|
|
||||||
3. Create a new repository on [bintray](https://bintray.com/docker-compose).
|
|
||||||
The name has to match the name of the branch (e.g. `bump-1.9.0`) and the
|
|
||||||
type should be "Generic". Other fields can be left blank.
|
|
||||||
|
|
||||||
4. Check that the `vnext-compose` branch on
|
|
||||||
[the docs repo](https://github.com/docker/docker.github.io/) has
|
|
||||||
documentation for all the new additions in the upcoming release, and create
|
|
||||||
a PR there for what needs to be amended.
|
|
||||||
|
|
||||||
|
|
||||||
## When a PR is merged into master that we want in the release
|
|
||||||
|
|
||||||
1. Check out the bump branch and run the cherry pick script
|
|
||||||
|
|
||||||
git checkout bump-$VERSION
|
|
||||||
./script/release/cherry-pick-pr $PR_NUMBER
|
|
||||||
|
|
||||||
2. When you are done cherry-picking branches move the bump version commit to HEAD
|
|
||||||
|
|
||||||
./script/release/rebase-bump-commit
|
|
||||||
git push --force $USERNAME bump-$VERSION
|
|
||||||
|
|
||||||
|
|
||||||
## To release a version (whether RC or stable)
|
|
||||||
|
|
||||||
Check out the bump branch and run the `build-binaries` script
|
|
||||||
|
|
||||||
git checkout bump-$VERSION
|
|
||||||
./script/release/build-binaries
|
|
||||||
|
|
||||||
When prompted build the non-linux binaries and test them.
|
|
||||||
|
|
||||||
1. Download the osx binary from Bintray. Make sure that the latest Travis
|
|
||||||
build has finished, otherwise you'll be downloading an old binary.
|
|
||||||
|
|
||||||
https://dl.bintray.com/docker-compose/$BRANCH_NAME/
|
|
||||||
|
|
||||||
2. Download the windows binary from AppVeyor
|
|
||||||
|
|
||||||
https://ci.appveyor.com/project/docker/compose
|
|
||||||
|
|
||||||
3. Draft a release from the tag on GitHub (the script will open the window for
|
|
||||||
you)
|
|
||||||
|
|
||||||
The tag will only be present on Github when you run the `push-release`
|
|
||||||
script in step 7, but you can pre-fill it at that point.
|
|
||||||
|
|
||||||
4. Paste in installation instructions and release notes. Here's an example -
|
|
||||||
change the Compose version and Docker version as appropriate:
|
|
||||||
|
|
||||||
If you're a Mac or Windows user, the best way to install Compose and keep it up-to-date is **[Docker for Mac and Windows](https://www.docker.com/products/docker)**.
|
|
||||||
|
|
||||||
Note that Compose 1.9.0 requires Docker Engine 1.10.0 or later for version 2 of the Compose File format, and Docker Engine 1.9.1 or later for version 1. Docker for Mac and Windows will automatically install the latest version of Docker Engine for you.
|
|
||||||
|
|
||||||
Alternatively, you can use the usual commands to install or upgrade Compose:
|
|
||||||
|
|
||||||
```
|
|
||||||
curl -L https://github.com/docker/compose/releases/download/1.9.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
|
|
||||||
chmod +x /usr/local/bin/docker-compose
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [install docs](https://docs.docker.com/compose/install/) for more install options and instructions.
|
|
||||||
|
|
||||||
Here's what's new:
|
|
||||||
|
|
||||||
...release notes go here...
|
|
||||||
|
|
||||||
5. Attach the binaries and `script/run/run.sh`
|
|
||||||
|
|
||||||
6. Add "Thanks" with a list of contributors. The contributor list can be generated
|
|
||||||
by running `./script/release/contributors`.
|
|
||||||
|
|
||||||
7. If everything looks good, it's time to push the release.
|
|
||||||
|
|
||||||
|
|
||||||
./script/release/push-release
|
|
||||||
|
|
||||||
|
|
||||||
8. Merge the bump PR.
|
|
||||||
|
|
||||||
8. Publish the release on GitHub.
|
|
||||||
|
|
||||||
9. Check that all the binaries download (following the install instructions) and run.
|
|
||||||
|
|
||||||
10. Email maintainers@dockerproject.org and engineering@docker.com about the new release.
|
|
||||||
|
|
||||||
## If it’s a stable release (not an RC)
|
|
||||||
|
|
||||||
1. Close the release’s milestone.
|
|
||||||
|
|
||||||
## If it’s a minor release (1.x.0), rather than a patch release (1.x.y)
|
|
||||||
|
|
||||||
1. Open a PR against `master` to:
|
|
||||||
|
|
||||||
- update `CHANGELOG.md` to bring it in line with `release`
|
|
||||||
- bump the version in `compose/__init__.py` to the *next* minor version number with `dev` appended. For example, if you just released `1.4.0`, update it to `1.5.0dev`.
|
|
||||||
|
|
||||||
2. Get the PR merged.
|
|
||||||
|
|
||||||
## Finally
|
|
||||||
|
|
||||||
1. Celebrate, however you’d like.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
pyinstaller==3.2.1
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
coverage==3.7.1
|
|
||||||
mock >= 1.0.1
|
mock >= 1.0.1
|
||||||
pytest==2.7.2
|
nose==1.3.4
|
||||||
pytest-cov==2.1.0
|
git+https://github.com/pyinstaller/pyinstaller.git@12e40471c77f588ea5be352f7219c873ddaae056#egg=pyinstaller
|
||||||
|
unittest2==0.8.0
|
||||||
|
flake8==2.3.0
|
||||||
|
pep8==1.6.1
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,8 @@
|
||||||
PyYAML==3.11
|
PyYAML==3.10
|
||||||
backports.ssl-match-hostname==3.5.0.1; python_version < '3'
|
docker-py==1.0.0
|
||||||
cached-property==1.2.0
|
dockerpty==0.3.2
|
||||||
colorama==0.3.7
|
|
||||||
docker==2.0.2
|
|
||||||
dockerpty==0.4.1
|
|
||||||
docopt==0.6.1
|
docopt==0.6.1
|
||||||
enum34==1.0.4; python_version < '3.4'
|
requests==2.2.1
|
||||||
functools32==3.2.3.post2; python_version < '3.2'
|
six==1.7.3
|
||||||
ipaddress==1.0.16
|
texttable==0.8.2
|
||||||
jsonschema==2.5.1
|
websocket-client==0.11.0
|
||||||
pypiwin32==219; sys_platform == 'win32'
|
|
||||||
requests==2.11.1
|
|
||||||
six==1.10.0
|
|
||||||
texttable==0.8.4
|
|
||||||
websocket-client==0.32.0
|
|
||||||
|
|
|
||||||
33
script/.validate
Normal file
33
script/.validate
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ -z "$VALIDATE_UPSTREAM" ]; then
|
||||||
|
# this is kind of an expensive check, so let's not do this twice if we
|
||||||
|
# are running more than one validate bundlescript
|
||||||
|
|
||||||
|
VALIDATE_REPO='https://github.com/docker/fig.git'
|
||||||
|
VALIDATE_BRANCH='master'
|
||||||
|
|
||||||
|
if [ "$TRAVIS" = 'true' -a "$TRAVIS_PULL_REQUEST" != 'false' ]; then
|
||||||
|
VALIDATE_REPO="https://github.com/${TRAVIS_REPO_SLUG}.git"
|
||||||
|
VALIDATE_BRANCH="${TRAVIS_BRANCH}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
VALIDATE_HEAD="$(git rev-parse --verify HEAD)"
|
||||||
|
|
||||||
|
git fetch -q "$VALIDATE_REPO" "refs/heads/$VALIDATE_BRANCH"
|
||||||
|
VALIDATE_UPSTREAM="$(git rev-parse --verify FETCH_HEAD)"
|
||||||
|
|
||||||
|
VALIDATE_COMMIT_LOG="$VALIDATE_UPSTREAM..$VALIDATE_HEAD"
|
||||||
|
VALIDATE_COMMIT_DIFF="$VALIDATE_UPSTREAM...$VALIDATE_HEAD"
|
||||||
|
|
||||||
|
validate_diff() {
|
||||||
|
if [ "$VALIDATE_UPSTREAM" != "$VALIDATE_HEAD" ]; then
|
||||||
|
git diff "$VALIDATE_COMMIT_DIFF" "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
validate_log() {
|
||||||
|
if [ "$VALIDATE_UPSTREAM" != "$VALIDATE_HEAD" ]; then
|
||||||
|
git log "$VALIDATE_COMMIT_LOG" "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
fi
|
||||||
12
script/build-linux
Executable file
12
script/build-linux
Executable file
|
|
@ -0,0 +1,12 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
TAG="docker-compose"
|
||||||
|
docker build -t "$TAG" .
|
||||||
|
docker run \
|
||||||
|
--rm \
|
||||||
|
--user=user \
|
||||||
|
--volume="$(pwd):/code" \
|
||||||
|
--entrypoint="script/build-linux-inner" \
|
||||||
|
"$TAG"
|
||||||
10
script/build-linux-inner
Executable file
10
script/build-linux-inner
Executable file
|
|
@ -0,0 +1,10 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
mkdir -p `pwd`/dist
|
||||||
|
chmod 777 `pwd`/dist
|
||||||
|
|
||||||
|
pyinstaller -F bin/docker-compose
|
||||||
|
mv dist/docker-compose dist/docker-compose-Linux-x86_64
|
||||||
|
dist/docker-compose-Linux-x86_64 --version
|
||||||
10
script/build-osx
Executable file
10
script/build-osx
Executable file
|
|
@ -0,0 +1,10 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -ex
|
||||||
|
rm -rf venv
|
||||||
|
virtualenv venv
|
||||||
|
venv/bin/pip install -r requirements.txt
|
||||||
|
venv/bin/pip install -r requirements-dev.txt
|
||||||
|
venv/bin/pip install .
|
||||||
|
venv/bin/pyinstaller -F bin/docker-compose
|
||||||
|
mv dist/docker-compose dist/docker-compose-Darwin-x86_64
|
||||||
|
dist/docker-compose-Darwin-x86_64 --version
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
if [ -z "$1" ]; then
|
|
||||||
>&2 echo "First argument must be image tag."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
TAG=$1
|
|
||||||
VERSION="$(python setup.py --version)"
|
|
||||||
|
|
||||||
./script/build/write-git-sha
|
|
||||||
python setup.py sdist bdist_wheel
|
|
||||||
docker build --build-arg version=$VERSION -t docker/compose:$TAG -f Dockerfile.run .
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -ex
|
|
||||||
|
|
||||||
./script/clean
|
|
||||||
|
|
||||||
TAG="docker-compose"
|
|
||||||
docker build -t "$TAG" . | tail -n 200
|
|
||||||
docker run \
|
|
||||||
--rm --entrypoint="script/build/linux-entrypoint" \
|
|
||||||
-v $(pwd)/dist:/code/dist \
|
|
||||||
-v $(pwd)/.git:/code/.git \
|
|
||||||
"$TAG"
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -ex
|
|
||||||
|
|
||||||
TARGET=dist/docker-compose-$(uname -s)-$(uname -m)
|
|
||||||
VENV=/code/.tox/py27
|
|
||||||
|
|
||||||
mkdir -p `pwd`/dist
|
|
||||||
chmod 777 `pwd`/dist
|
|
||||||
|
|
||||||
$VENV/bin/pip install -q -r requirements-build.txt
|
|
||||||
./script/build/write-git-sha
|
|
||||||
su -c "$VENV/bin/pyinstaller docker-compose.spec" user
|
|
||||||
mv dist/docker-compose $TARGET
|
|
||||||
$TARGET version
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -ex
|
|
||||||
|
|
||||||
PATH="/usr/local/bin:$PATH"
|
|
||||||
|
|
||||||
rm -rf venv
|
|
||||||
|
|
||||||
virtualenv -p /usr/local/bin/python venv
|
|
||||||
venv/bin/pip install -r requirements.txt
|
|
||||||
venv/bin/pip install -r requirements-build.txt
|
|
||||||
venv/bin/pip install --no-deps .
|
|
||||||
./script/build/write-git-sha
|
|
||||||
venv/bin/pyinstaller docker-compose.spec
|
|
||||||
mv dist/docker-compose dist/docker-compose-Darwin-x86_64
|
|
||||||
dist/docker-compose-Darwin-x86_64 version
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
# Builds the Windows binary.
|
|
||||||
#
|
|
||||||
# From a fresh 64-bit Windows 10 install, prepare the system as follows:
|
|
||||||
#
|
|
||||||
# 1. Install Git:
|
|
||||||
#
|
|
||||||
# http://git-scm.com/download/win
|
|
||||||
#
|
|
||||||
# 2. Install Python 2.7.10:
|
|
||||||
#
|
|
||||||
# https://www.python.org/downloads/
|
|
||||||
#
|
|
||||||
# 3. Append ";C:\Python27;C:\Python27\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
|
|
||||||
# $ Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
|
|
||||||
#
|
|
||||||
# 5. Clone the repository:
|
|
||||||
#
|
|
||||||
# $ git clone https://github.com/docker/compose.git
|
|
||||||
# $ cd compose
|
|
||||||
#
|
|
||||||
# 6. Build the binary:
|
|
||||||
#
|
|
||||||
# .\script\build\windows.ps1
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
|
|
||||||
# Remove virtualenv
|
|
||||||
if (Test-Path venv) {
|
|
||||||
Remove-Item -Recurse -Force .\venv
|
|
||||||
}
|
|
||||||
|
|
||||||
# Remove .pyc files
|
|
||||||
Get-ChildItem -Recurse -Include *.pyc | foreach ($_) { Remove-Item $_.FullName }
|
|
||||||
|
|
||||||
# Create virtualenv
|
|
||||||
virtualenv .\venv
|
|
||||||
|
|
||||||
# pip and pyinstaller generate lots of warnings, so we need to ignore them
|
|
||||||
$ErrorActionPreference = "Continue"
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
.\venv\Scripts\pip install pypiwin32==219
|
|
||||||
.\venv\Scripts\pip install -r requirements.txt
|
|
||||||
.\venv\Scripts\pip install --no-deps .
|
|
||||||
.\venv\Scripts\pip install --allow-external pyinstaller -r requirements-build.txt
|
|
||||||
|
|
||||||
git rev-parse --short HEAD | out-file -encoding ASCII compose\GITSHA
|
|
||||||
|
|
||||||
# Build binary
|
|
||||||
.\venv\Scripts\pyinstaller .\docker-compose.spec
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
|
|
||||||
Move-Item -Force .\dist\docker-compose.exe .\dist\docker-compose-Windows-x86_64.exe
|
|
||||||
.\dist\docker-compose-Windows-x86_64.exe --version
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
#
|
|
||||||
# Write the current commit sha to the file GITSHA. This file is included in
|
|
||||||
# packaging so that `docker-compose version` can include the git sha.
|
|
||||||
#
|
|
||||||
set -e
|
|
||||||
git rev-parse --short HEAD > compose/GITSHA
|
|
||||||
20
script/ci
20
script/ci
|
|
@ -1,8 +1,18 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
# This should be run inside a container built from the Dockerfile
|
||||||
|
# at the root of the repo:
|
||||||
#
|
#
|
||||||
# Backwards compatiblity for jenkins
|
# $ TAG="docker-compose:$(git rev-parse --short HEAD)"
|
||||||
#
|
# $ docker build -t "$TAG" .
|
||||||
# TODO: remove this script after all current PRs and jenkins are updated with
|
# $ docker run --rm --volume="/var/run/docker.sock:/var/run/docker.sock" --volume="$(pwd)/.git:/code/.git" -e "TAG=$TAG" --entrypoint="script/ci" "$TAG"
|
||||||
# the new script/test/ci change
|
|
||||||
set -e
|
set -e
|
||||||
exec script/test/ci
|
|
||||||
|
>&2 echo "Validating DCO"
|
||||||
|
script/validate-dco
|
||||||
|
|
||||||
|
export DOCKER_VERSIONS=all
|
||||||
|
. script/test-versions
|
||||||
|
|
||||||
|
>&2 echo "Building Linux binary"
|
||||||
|
su -c script/build-linux-inner user
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,3 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
|
||||||
|
|
||||||
find . -type f -name '*.pyc' -delete
|
find . -type f -name '*.pyc' -delete
|
||||||
find . -name .coverage.* -delete
|
|
||||||
find . -name __pycache__ -delete
|
|
||||||
rm -rf docs/_site build dist docker-compose.egg-info
|
rm -rf docs/_site build dist docker-compose.egg-info
|
||||||
|
|
|
||||||
21
script/dev
Executable file
21
script/dev
Executable file
|
|
@ -0,0 +1,21 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# This is a script for running Compose inside a Docker container. It's handy for
|
||||||
|
# development.
|
||||||
|
#
|
||||||
|
# $ ln -s `pwd`/script/dev /usr/local/bin/docker-compose
|
||||||
|
# $ cd /a/compose/project
|
||||||
|
# $ docker-compose up
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Follow symbolic links
|
||||||
|
if [ -h "$0" ]; then
|
||||||
|
DIR=$(readlink "$0")
|
||||||
|
else
|
||||||
|
DIR=$0
|
||||||
|
fi
|
||||||
|
DIR="$(dirname "$DIR")"/..
|
||||||
|
|
||||||
|
docker build -t docker-compose $DIR
|
||||||
|
exec docker run -i -t -v /var/run/docker.sock:/var/run/docker.sock -v `pwd`:`pwd` -w `pwd` docker-compose $@
|
||||||
88
script/dind
Executable file
88
script/dind
Executable file
|
|
@ -0,0 +1,88 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# DinD: a wrapper script which allows docker to be run inside a docker container.
|
||||||
|
# Original version by Jerome Petazzoni <jerome@docker.com>
|
||||||
|
# See the blog post: http://blog.docker.com/2013/09/docker-can-now-run-within-docker/
|
||||||
|
#
|
||||||
|
# This script should be executed inside a docker container in privilieged mode
|
||||||
|
# ('docker run --privileged', introduced in docker 0.6).
|
||||||
|
|
||||||
|
# Usage: dind CMD [ARG...]
|
||||||
|
|
||||||
|
# apparmor sucks and Docker needs to know that it's in a container (c) @tianon
|
||||||
|
export container=docker
|
||||||
|
|
||||||
|
# First, make sure that cgroups are mounted correctly.
|
||||||
|
CGROUP=/cgroup
|
||||||
|
|
||||||
|
mkdir -p "$CGROUP"
|
||||||
|
|
||||||
|
if ! mountpoint -q "$CGROUP"; then
|
||||||
|
mount -n -t tmpfs -o uid=0,gid=0,mode=0755 cgroup $CGROUP || {
|
||||||
|
echo >&2 'Could not make a tmpfs mount. Did you use --privileged?'
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d /sys/kernel/security ] && ! mountpoint -q /sys/kernel/security; then
|
||||||
|
mount -t securityfs none /sys/kernel/security || {
|
||||||
|
echo >&2 'Could not mount /sys/kernel/security.'
|
||||||
|
echo >&2 'AppArmor detection and -privileged mode might break.'
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Mount the cgroup hierarchies exactly as they are in the parent system.
|
||||||
|
for SUBSYS in $(cut -d: -f2 /proc/1/cgroup); do
|
||||||
|
mkdir -p "$CGROUP/$SUBSYS"
|
||||||
|
if ! mountpoint -q $CGROUP/$SUBSYS; then
|
||||||
|
mount -n -t cgroup -o "$SUBSYS" cgroup "$CGROUP/$SUBSYS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The two following sections address a bug which manifests itself
|
||||||
|
# by a cryptic "lxc-start: no ns_cgroup option specified" when
|
||||||
|
# trying to start containers withina container.
|
||||||
|
# The bug seems to appear when the cgroup hierarchies are not
|
||||||
|
# mounted on the exact same directories in the host, and in the
|
||||||
|
# container.
|
||||||
|
|
||||||
|
# Named, control-less cgroups are mounted with "-o name=foo"
|
||||||
|
# (and appear as such under /proc/<pid>/cgroup) but are usually
|
||||||
|
# mounted on a directory named "foo" (without the "name=" prefix).
|
||||||
|
# Systemd and OpenRC (and possibly others) both create such a
|
||||||
|
# cgroup. To avoid the aforementioned bug, we symlink "foo" to
|
||||||
|
# "name=foo". This shouldn't have any adverse effect.
|
||||||
|
name="${SUBSYS#name=}"
|
||||||
|
if [ "$name" != "$SUBSYS" ]; then
|
||||||
|
ln -s "$SUBSYS" "$CGROUP/$name"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Likewise, on at least one system, it has been reported that
|
||||||
|
# systemd would mount the CPU and CPU accounting controllers
|
||||||
|
# (respectively "cpu" and "cpuacct") with "-o cpuacct,cpu"
|
||||||
|
# but on a directory called "cpu,cpuacct" (note the inversion
|
||||||
|
# in the order of the groups). This tries to work around it.
|
||||||
|
if [ "$SUBSYS" = 'cpuacct,cpu' ]; then
|
||||||
|
ln -s "$SUBSYS" "$CGROUP/cpu,cpuacct"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Note: as I write those lines, the LXC userland tools cannot setup
|
||||||
|
# a "sub-container" properly if the "devices" cgroup is not in its
|
||||||
|
# own hierarchy. Let's detect this and issue a warning.
|
||||||
|
if ! grep -q :devices: /proc/1/cgroup; then
|
||||||
|
echo >&2 'WARNING: the "devices" cgroup should be in its own hierarchy.'
|
||||||
|
fi
|
||||||
|
if ! grep -qw devices /proc/1/cgroup; then
|
||||||
|
echo >&2 'WARNING: it looks like the "devices" cgroup is not mounted.'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Mount /tmp
|
||||||
|
mount -t tmpfs none /tmp
|
||||||
|
|
||||||
|
if [ $# -gt 0 ]; then
|
||||||
|
exec "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo >&2 'ERROR: No command specified.'
|
||||||
|
echo >&2 'You probably want to run hack/make.sh, or maybe a shell?'
|
||||||
11
script/docs
Executable file
11
script/docs
Executable file
|
|
@ -0,0 +1,11 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
# import the existing docs build cmds from docker/docker
|
||||||
|
DOCSPORT=8000
|
||||||
|
GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
|
||||||
|
DOCKER_DOCS_IMAGE="compose-docs$GIT_BRANCH"
|
||||||
|
DOCKER_RUN_DOCS="docker run --rm -it -e NOCACHE"
|
||||||
|
|
||||||
|
docker build -t "$DOCKER_DOCS_IMAGE" -f docs/Dockerfile .
|
||||||
|
$DOCKER_RUN_DOCS -p $DOCSPORT:8000 "$DOCKER_DOCS_IMAGE" mkdocs serve
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
#
|
|
||||||
# Build the release binaries
|
|
||||||
#
|
|
||||||
|
|
||||||
. "$(dirname "${BASH_SOURCE[0]}")/utils.sh"
|
|
||||||
|
|
||||||
function usage() {
|
|
||||||
>&2 cat << EOM
|
|
||||||
Build binaries for the release.
|
|
||||||
|
|
||||||
This script requires that 'git config branch.${BRANCH}.release' is set to the
|
|
||||||
release version for the release branch.
|
|
||||||
|
|
||||||
EOM
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
|
||||||
VERSION="$(git config "branch.${BRANCH}.release")" || usage
|
|
||||||
REPO=docker/compose
|
|
||||||
|
|
||||||
# Build the binaries
|
|
||||||
script/clean
|
|
||||||
script/build/linux
|
|
||||||
|
|
||||||
echo "Building the container distribution"
|
|
||||||
script/build/image $VERSION
|
|
||||||
|
|
||||||
echo "Create a github release"
|
|
||||||
# TODO: script more of this https://developer.github.com/v3/repos/releases/
|
|
||||||
browser https://github.com/$REPO/releases/new
|
|
||||||
|
|
||||||
echo "Don't forget to download the osx and windows binaries from appveyor/bintray\!"
|
|
||||||
echo "https://dl.bintray.com/docker-compose/$BRANCH/"
|
|
||||||
echo "https://ci.appveyor.com/project/docker/compose"
|
|
||||||
echo
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
#
|
|
||||||
# Cherry-pick a PR into the release branch
|
|
||||||
#
|
|
||||||
|
|
||||||
set -e
|
|
||||||
set -o pipefail
|
|
||||||
|
|
||||||
|
|
||||||
function usage() {
|
|
||||||
>&2 cat << EOM
|
|
||||||
Cherry-pick commits from a github pull request.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
|
|
||||||
$0 <github PR number>
|
|
||||||
EOM
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
[ -n "$1" ] || usage
|
|
||||||
|
|
||||||
if [ -z "$(command -v hub 2> /dev/null)" ]; then
|
|
||||||
>&2 echo "$0 requires https://hub.github.com/."
|
|
||||||
>&2 echo "Please install it and make sure it is available on your \$PATH."
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
REPO=docker/compose
|
|
||||||
GITHUB=https://github.com/$REPO/pull
|
|
||||||
PR=$1
|
|
||||||
url="$GITHUB/$PR"
|
|
||||||
hub am -3 $url
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue