mirror of
https://github.com/docker/compose.git
synced 2026-02-10 10:39:23 +08:00
Compare commits
316 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03535a6158 | ||
|
|
4ac02bfca6 | ||
|
|
72003de737 | ||
|
|
a0db5bee3e | ||
|
|
59c45e3398 | ||
|
|
ea8364fd11 | ||
|
|
cf6b09e94b | ||
|
|
66555aa69b | ||
|
|
bb943d5cb5 | ||
|
|
178c50d46f | ||
|
|
32cdabefc2 | ||
|
|
5b07c581e0 | ||
|
|
17bbb9d357 | ||
|
|
ec2966222a | ||
|
|
8076c7d7fe | ||
|
|
b2425c1f1e | ||
|
|
8d8dd37d2c | ||
|
|
b1739e703b | ||
|
|
a42f2007ea | ||
|
|
1dd5ef4133 | ||
|
|
c3215a1764 | ||
|
|
bd320b19fe | ||
|
|
0fa0131372 | ||
|
|
a7fe67e691 | ||
|
|
f78dfa7958 | ||
|
|
d95de03de9 | ||
|
|
e3eccd1047 | ||
|
|
d32994c250 | ||
|
|
a516d61b49 | ||
|
|
6eb1c8896f | ||
|
|
160e2dc7b9 | ||
|
|
5e8bcd2d29 | ||
|
|
2eb20d89af | ||
|
|
ab65c829ea | ||
|
|
83110b8e6b | ||
|
|
0a8f9abfae | ||
|
|
3146fe5e4b | ||
|
|
4a33686787 | ||
|
|
64239f1408 | ||
|
|
c39a0b0a2d | ||
|
|
8a7b3fb0eb | ||
|
|
4251f2b732 | ||
|
|
9f4775c554 | ||
|
|
7ff607fb7a | ||
|
|
b25ed59b1c | ||
|
|
eabca3d7b7 | ||
|
|
45b8d526ba | ||
|
|
75247e5a54 | ||
|
|
2cf1fa6c9d | ||
|
|
3b7ea5c055 | ||
|
|
461f1ad5d5 | ||
|
|
2307adc4b2 | ||
|
|
d8d0fd6dc9 | ||
|
|
e4d6a2c240 | ||
|
|
deb2de3c07 | ||
|
|
bc1f6c97d8 | ||
|
|
7c087f1c07 | ||
|
|
fd30920aac | ||
|
|
9bc7604e0e | ||
|
|
de07e0471e | ||
|
|
dfc6206d0d | ||
|
|
6c45b6ccdb | ||
|
|
d79dc85fa5 | ||
|
|
22aca1a8a5 | ||
|
|
74302560f5 | ||
|
|
bc2f6044fd | ||
|
|
1476027410 | ||
|
|
28fa49e569 | ||
|
|
27e4f982fa | ||
|
|
0bc4a28dcc | ||
|
|
bd535c76d0 | ||
|
|
ef027599f7 | ||
|
|
f1e4fb7736 | ||
|
|
df3221df61 | ||
|
|
43fdae8bc6 | ||
|
|
4869fed97f | ||
|
|
16d6018419 | ||
|
|
8297f55f2c | ||
|
|
724be54f09 | ||
|
|
e3c4a662d9 | ||
|
|
c981ce929a | ||
|
|
620e29b63f | ||
|
|
2af7693e64 | ||
|
|
7be8b4c06d | ||
|
|
cbd3ca07c4 | ||
|
|
edb6b24b8f | ||
|
|
608f29c7cb | ||
|
|
37ed743ee8 | ||
|
|
17a8a7be4b | ||
|
|
f57db078ba | ||
|
|
2dd1cc80ca | ||
|
|
55095ef488 | ||
|
|
7eb476e61d | ||
|
|
72095f54b2 | ||
|
|
26f45efea2 | ||
|
|
4827e60641 | ||
|
|
74a0c47389 | ||
|
|
aa0c43df96 | ||
|
|
4ef2e21cca | ||
|
|
3ee8437eaa | ||
|
|
b903217a4a | ||
|
|
2406a3936a | ||
|
|
69db596b5d | ||
|
|
9a90a27376 | ||
|
|
a2f3c0b5da | ||
|
|
3b638f0c43 | ||
|
|
b252300e94 | ||
|
|
be553e393a | ||
|
|
91c90a722a | ||
|
|
d399d386ae | ||
|
|
4cb47e498b | ||
|
|
3056ae4be3 | ||
|
|
5b777ee5f1 | ||
|
|
c885aaa5f8 | ||
|
|
4257707244 | ||
|
|
70c3676084 | ||
|
|
e89826fe43 | ||
|
|
0e172228d1 | ||
|
|
cb1a5e0a24 | ||
|
|
3c105c6db2 | ||
|
|
5182bd0968 | ||
|
|
200c44cff3 | ||
|
|
05544ce241 | ||
|
|
cc834aa564 | ||
|
|
a5505e7711 | ||
|
|
8f8e322de2 | ||
|
|
64762c9dea | ||
|
|
8ebec9a67f | ||
|
|
45b2712032 | ||
|
|
3c0f297ba6 | ||
|
|
c0123c7477 | ||
|
|
2a0782c660 | ||
|
|
481a8cb7ab | ||
|
|
788741025e | ||
|
|
c12d1d73f0 | ||
|
|
e794e79209 | ||
|
|
98b6d7be78 | ||
|
|
a12cf826cd | ||
|
|
d18cfa1c98 | ||
|
|
9a04ae0ddf | ||
|
|
ab77cef7ab | ||
|
|
5c58180538 | ||
|
|
429a3feabc | ||
|
|
1033439e63 | ||
|
|
e9d946b038 | ||
|
|
4e8337c168 | ||
|
|
e34a62956e | ||
|
|
0150b38b8f | ||
|
|
bb85e238e0 | ||
|
|
65ae22e79a | ||
|
|
04da6b035e | ||
|
|
d3e94f2caf | ||
|
|
4f6d02867b | ||
|
|
f98323b79e | ||
|
|
3f4b16181d | ||
|
|
5dde2a2498 | ||
|
|
06a1b32c12 | ||
|
|
46433c70b6 | ||
|
|
9260603149 | ||
|
|
9abdd337b5 | ||
|
|
8773f51583 | ||
|
|
38a3ee8d63 | ||
|
|
cb275b8633 | ||
|
|
604b370bb4 | ||
|
|
782a46fd60 | ||
|
|
1bd3d0dd77 | ||
|
|
b64ea85916 | ||
|
|
b47ab2b0f6 | ||
|
|
11280e4f30 | ||
|
|
a83876da09 | ||
|
|
e66c0452d5 | ||
|
|
899670fc6c | ||
|
|
ea45715a50 | ||
|
|
28f9c8d047 | ||
|
|
292fe6640e | ||
|
|
b759b9854a | ||
|
|
40943d5c81 | ||
|
|
93a0195dc8 | ||
|
|
7f0745d146 | ||
|
|
9813a8d5be | ||
|
|
491181ec31 | ||
|
|
e1ad5b1b99 | ||
|
|
67d4b7c587 | ||
|
|
98f663bab2 | ||
|
|
75b3dcf5fd | ||
|
|
deeb6c2236 | ||
|
|
c838f7da18 | ||
|
|
e1e2b75691 | ||
|
|
392c118bbc | ||
|
|
dbd1e56dd3 | ||
|
|
7544580b4b | ||
|
|
2efb4f5be0 | ||
|
|
5e8dc6c972 | ||
|
|
23c8ec8930 | ||
|
|
bd8affb7aa | ||
|
|
7a943739cb | ||
|
|
6580c5609c | ||
|
|
a07c83659d | ||
|
|
0f58b9f6b4 | ||
|
|
fed391a23e | ||
|
|
4d1b2f1547 | ||
|
|
60411e9f05 | ||
|
|
b318585f3c | ||
|
|
1820306d0a | ||
|
|
bf1c1b4c17 | ||
|
|
352062c2dc | ||
|
|
d3f88cace5 | ||
|
|
92249364b6 | ||
|
|
872a1b5a5c | ||
|
|
b969988ccb | ||
|
|
a5aac7d59e | ||
|
|
c16f4d4041 | ||
|
|
d91c458d52 | ||
|
|
475a7e19a9 | ||
|
|
f43bfaadaa | ||
|
|
2680756dd6 | ||
|
|
837f368361 | ||
|
|
9df5481066 | ||
|
|
59c7528b4e | ||
|
|
d2385e3c2c | ||
|
|
6b600faf0b | ||
|
|
431fdaa0f1 | ||
|
|
ed12f2539c | ||
|
|
0dc19cb885 | ||
|
|
62b9c64311 | ||
|
|
3408e0d463 | ||
|
|
f4b599551a | ||
|
|
267be12bb2 | ||
|
|
ec5c864cc7 | ||
|
|
cabe47a379 | ||
|
|
b4fbab4b56 | ||
|
|
6797a322b5 | ||
|
|
e04c5cb52c | ||
|
|
a3f70a9f64 | ||
|
|
f407504679 | ||
|
|
253b245a1c | ||
|
|
fac49b62b6 | ||
|
|
92ae5af019 | ||
|
|
c270e9d622 | ||
|
|
1c5194e2ec | ||
|
|
537d435a28 | ||
|
|
5d76d183b4 | ||
|
|
d4b7ed94e1 | ||
|
|
d978787fcc | ||
|
|
3535270ef0 | ||
|
|
b9eb55a225 | ||
|
|
35b217a0a4 | ||
|
|
c37dc558fb | ||
|
|
7a8f5e10fd | ||
|
|
192fce9153 | ||
|
|
fc4c35e977 | ||
|
|
648c89768b | ||
|
|
e0b0801e87 | ||
|
|
71e7103662 | ||
|
|
dbd723659b | ||
|
|
6b221d5687 | ||
|
|
ce8ef23c09 | ||
|
|
b0159e5100 | ||
|
|
ee6bb9a252 | ||
|
|
866050937a | ||
|
|
41ee65b664 | ||
|
|
7fd37c89b9 | ||
|
|
8d3c9dccc5 | ||
|
|
f5d43b6452 | ||
|
|
c48ee5caef | ||
|
|
2827786886 | ||
|
|
7ad91f3f00 | ||
|
|
24044fa704 | ||
|
|
07fa169fd2 | ||
|
|
8157f0887d | ||
|
|
6dab8c1b89 | ||
|
|
63bd05d40e | ||
|
|
e51851c884 | ||
|
|
e224c4caa4 | ||
|
|
dc857a7ad5 | ||
|
|
aca0e42178 | ||
|
|
15037ce0e5 | ||
|
|
9d55e01e2a | ||
|
|
8eee2bf913 | ||
|
|
3965db9dff | ||
|
|
294453433d | ||
|
|
22f897ed09 | ||
|
|
df7c2cc43f | ||
|
|
f2bf7f9e0d | ||
|
|
69c241ba12 | ||
|
|
59e31ff544 | ||
|
|
62a4d214e8 | ||
|
|
73bd4aca74 | ||
|
|
342ed948ec | ||
|
|
3fc7ad3291 | ||
|
|
a39460d7b2 | ||
|
|
6ab084a338 | ||
|
|
406425a6b9 | ||
|
|
b690b0d20e | ||
|
|
796df302dd | ||
|
|
1346805bef | ||
|
|
16744bc78b | ||
|
|
fc3c12ad90 | ||
|
|
bbcbe9df9f | ||
|
|
1a240f50ae | ||
|
|
47970761c5 | ||
|
|
df7bc8cbb8 | ||
|
|
ba9d744293 | ||
|
|
d5854bb625 | ||
|
|
e5f7690137 | ||
|
|
b0f398caaa | ||
|
|
0a15e7fe9c | ||
|
|
255b9419dd | ||
|
|
c63947b5e2 | ||
|
|
b5d1979d58 | ||
|
|
94aa097bc3 | ||
|
|
72ce7ce374 | ||
|
|
6d64f20ad6 | ||
|
|
506f54e9c3 | ||
|
|
90f5eda930 | ||
|
|
c0450f7df0 |
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
||||
.git
|
||||
venv
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,4 +5,4 @@
|
||||
/dist
|
||||
/docs/_site
|
||||
/venv
|
||||
fig.spec
|
||||
docker-compose.spec
|
||||
|
||||
30
.travis.yml
30
.travis.yml
@@ -1,30 +0,0 @@
|
||||
language: python
|
||||
python:
|
||||
- '2.6'
|
||||
- '2.7'
|
||||
env:
|
||||
global:
|
||||
- secure: exbot0LTV/0Wic6ElKCrOZmh2ZrieuGwEqfYKf5rVuwu1sLngYRihh+lBL/hTwc79NSu829pbwiWfsQZrXbk/yvaS7avGR0CLDoipyPxlYa2/rfs/o4OdTZqXv0LcFmmd54j5QBMpWU1S+CYOwNkwas57trrvIpPbzWjMtfYzOU=
|
||||
install:
|
||||
- pip install .
|
||||
- pip install -r requirements.txt
|
||||
- pip install -r requirements-dev.txt
|
||||
- sudo curl -L -o /usr/local/bin/orchard https://github.com/orchardup/go-orchard/releases/download/2.0.5/linux
|
||||
- sudo chmod +x /usr/local/bin/orchard
|
||||
before_script:
|
||||
- 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then orchard hosts rm -f $TRAVIS_JOB_ID || true; fi'
|
||||
- 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then orchard hosts create $TRAVIS_JOB_ID; fi'
|
||||
script:
|
||||
- nosetests tests/unit
|
||||
- flake8 fig
|
||||
- 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then script/travis-integration; fi'
|
||||
after_script:
|
||||
- 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then orchard hosts rm -f $TRAVIS_JOB_ID; fi'
|
||||
deploy:
|
||||
provider: pypi
|
||||
user: orchard
|
||||
password:
|
||||
secure: M8UMupCLSsB1hV00Zn6ra8Vg81SCFBpbcRsa0nUw9kgXn9hOCESWYVHTqQ1ksWZOa8z6WMaqYtoosPKXGJQNf0wF/kEVDsMUeaZWOF/PqDkx1EwQ1diVfwlbN4/k0iX+Se7SrZfiWnJiAqiIPqToQipvLlJohqf8WwfPcVvILVE=
|
||||
on:
|
||||
tags: true
|
||||
repo: orchardup/fig
|
||||
84
CHANGES.md
84
CHANGES.md
@@ -1,6 +1,90 @@
|
||||
Change log
|
||||
==========
|
||||
|
||||
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)
|
||||
------------------
|
||||
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
# Contributing to Fig
|
||||
# Contributing to Compose
|
||||
|
||||
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.
|
||||
|
||||
## TL;DR
|
||||
|
||||
Pull requests will need:
|
||||
|
||||
- Tests
|
||||
- Documentation
|
||||
- [To be signed off](https://github.com/docker/docker/blob/master/CONTRIBUTING.md#sign-your-work)
|
||||
- A logical series of [well written commits](https://github.com/alphagov/styleguides/blob/master/git.md)
|
||||
|
||||
## Development environment
|
||||
|
||||
If you're looking contribute to [Fig](http://www.fig.sh/)
|
||||
If you're looking contribute to Compose
|
||||
but you're new to the project or maybe even to Python, here are the steps
|
||||
that should get you started.
|
||||
|
||||
1. Fork [https://github.com/orchardup/fig](https://github.com/orchardup/fig) to your username. kvz in this example.
|
||||
1. Clone your forked repository locally `git clone git@github.com:kvz/fig.git`.
|
||||
1. Enter the local directory `cd fig`.
|
||||
1. Set up a development environment `python setup.py develop`. That will install the dependencies and set up a symlink from your `fig` executable to the checkout of the repo. So from any of your fig projects, `fig` now refers to your development project. Time to start hacking : )
|
||||
1. Works for you? Run the test suite via `./script/test` to verify it won't break other usecases.
|
||||
1. All good? Commit and push to GitHub, and submit a pull request.
|
||||
1. Fork [https://github.com/docker/compose](https://github.com/docker/compose) to your username.
|
||||
1. Clone your forked repository locally `git clone git@github.com:yourusername/compose.git`.
|
||||
1. Enter the local directory `cd compose`.
|
||||
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.
|
||||
|
||||
## Running the test suite
|
||||
|
||||
@@ -29,65 +38,26 @@ OS X:
|
||||
|
||||
Note that this only works on Mountain Lion, not Mavericks, due to a [bug in PyInstaller](http://www.pyinstaller.org/ticket/807).
|
||||
|
||||
## Sign your work
|
||||
|
||||
The sign-off is a simple line at the end of the explanation for the
|
||||
patch, which certifies that you wrote it or otherwise have the right to
|
||||
pass it on as an open-source patch. The rules are pretty simple: if you
|
||||
can certify the below (from [developercertificate.org](http://developercertificate.org/)):
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
|
||||
then you just add a line saying
|
||||
|
||||
Signed-off-by: Random J Developer <random@developer.example.org>
|
||||
|
||||
using your real name (sorry, no pseudonyms or anonymous contributions.)
|
||||
|
||||
The easiest way to do this is to use the `--signoff` flag when committing. E.g.:
|
||||
|
||||
|
||||
$ git commit --signoff
|
||||
|
||||
|
||||
## Release process
|
||||
|
||||
1. Open pull request that:
|
||||
|
||||
- Updates version in `fig/__init__.py`
|
||||
- Updates version in `docs/install.md`
|
||||
- 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
|
||||
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
|
||||
|
||||
16
Dockerfile
16
Dockerfile
@@ -1,11 +1,17 @@
|
||||
FROM orchardup/python:2.7
|
||||
ADD requirements.txt /code/
|
||||
FROM debian:wheezy
|
||||
RUN apt-get update -qq && apt-get install -qy python python-pip python-dev git && apt-get clean
|
||||
RUN useradd -d /home/user -m -s /bin/bash user
|
||||
WORKDIR /code/
|
||||
|
||||
ADD requirements.txt /code/
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
ADD requirements-dev.txt /code/
|
||||
RUN pip install -r requirements-dev.txt
|
||||
|
||||
ADD . /code/
|
||||
RUN python setup.py develop
|
||||
RUN useradd -d /home/user -m -s /bin/bash user
|
||||
RUN python setup.py install
|
||||
|
||||
RUN chown -R user /code/
|
||||
USER user
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/docker-compose"]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Aanand Prasad <aanand.prasad@gmail.com> (@aanand)
|
||||
Ben Firshman <ben@firshman.co.uk> (@bfirsh)
|
||||
Chris Corbyn <chris@w3style.co.uk> (@d11wtq)
|
||||
Nathan LeClaire <nathan.leclaire@gmail.com> (@nathanleclaire)
|
||||
Daniel Nephin <dnephin@gmail.com> (@dnephin)
|
||||
|
||||
@@ -4,7 +4,7 @@ include requirements.txt
|
||||
include requirements-dev.txt
|
||||
include tox.ini
|
||||
include *.md
|
||||
recursive-exclude tests *
|
||||
recursive-include tests *
|
||||
global-exclude *.pyc
|
||||
global-exclude *.pyo
|
||||
global-exclude *.un~
|
||||
|
||||
59
README.md
59
README.md
@@ -1,20 +1,34 @@
|
||||
Fig
|
||||
===
|
||||
Docker Compose
|
||||
==============
|
||||
|
||||
[](https://travis-ci.org/orchardup/fig)
|
||||
[](http://badge.fury.io/py/fig)
|
||||
[](https://app.wercker.com/project/bykey/d5dbac3907301c3d5ce735e2d5e95a5b)
|
||||
|
||||
Fast, isolated development environments using Docker.
|
||||
*(Previously known as Fig)*
|
||||
|
||||
Define your app's environment with Docker so it can be reproduced anywhere:
|
||||
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.
|
||||
|
||||
FROM orchardup/python:2.7
|
||||
ADD . /code
|
||||
WORKDIR /code
|
||||
RUN pip install -r requirements.txt
|
||||
CMD python app.py
|
||||
Compose is great for development environments, staging servers, and CI. We don't
|
||||
recommend that you use it in production yet.
|
||||
|
||||
Define the services that make up your app so they can be run together in an isolated environment:
|
||||
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:
|
||||
@@ -23,25 +37,18 @@ web:
|
||||
- db
|
||||
ports:
|
||||
- "8000:8000"
|
||||
- "49100:22"
|
||||
db:
|
||||
image: orchardup/postgresql
|
||||
image: postgres
|
||||
```
|
||||
|
||||
(No more installing Postgres on your laptop!)
|
||||
Lastly, run `docker-compose up` and Compose will start and run your entire app.
|
||||
|
||||
Then type `fig up`, and Fig will start and run your entire app:
|
||||
Compose has commands for managing the whole lifecycle of your application:
|
||||
|
||||

|
||||
|
||||
There are commands to:
|
||||
|
||||
- start, stop and rebuild services
|
||||
- view the status of running services
|
||||
- tail running services' log output
|
||||
- run a one-off command on a service
|
||||
|
||||
Fig is a project from [Orchard](https://orchardup.com), a Docker hosting service. [Follow us on Twitter](https://twitter.com/orchardup) to keep up to date with Fig and other Docker news.
|
||||
* 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
|
||||
|
||||
Installation and documentation
|
||||
------------------------------
|
||||
|
||||
28
ROADMAP.md
Normal file
28
ROADMAP.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Roadmap
|
||||
|
||||
## More than just development environments
|
||||
|
||||
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:
|
||||
|
||||
- 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 should recommend a technique for zero-downtime deploys.
|
||||
|
||||
## Integration with Swarm
|
||||
|
||||
Compose should integrate really well with Swarm so you can take an application you've developed on your laptop and run it on a Swarm cluster.
|
||||
|
||||
The current state of integration is documented in [SWARM.md](SWARM.md).
|
||||
|
||||
## Applications spanning multiple teams
|
||||
|
||||
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).
|
||||
|
||||
## 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)
|
||||
51
SWARM.md
Normal file
51
SWARM.md
Normal file
@@ -0,0 +1,51 @@
|
||||
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.
|
||||
3
bin/docker-compose
Executable file
3
bin/docker-compose
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env python
|
||||
from compose.cli.main import main
|
||||
main()
|
||||
@@ -1,4 +1,4 @@
|
||||
from __future__ import unicode_literals
|
||||
from .service import Service # noqa:flake8
|
||||
|
||||
__version__ = '0.5.2'
|
||||
__version__ = '1.1.0'
|
||||
140
compose/cli/command.py
Normal file
140
compose/cli/command.py
Normal file
@@ -0,0 +1,140 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from requests.exceptions import ConnectionError, SSLError
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import yaml
|
||||
import six
|
||||
|
||||
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 . import verbose_proxy
|
||||
from . import errors
|
||||
from .. import __version__
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(DocoptCommand):
|
||||
base_dir = '.'
|
||||
|
||||
def dispatch(self, *args, **kwargs):
|
||||
try:
|
||||
super(Command, self).dispatch(*args, **kwargs)
|
||||
except SSLError, e:
|
||||
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'),
|
||||
verbose=options.get('--verbose'))
|
||||
|
||||
handler(project, command_options)
|
||||
|
||||
def get_client(self, verbose=False):
|
||||
client = docker_client()
|
||||
if verbose:
|
||||
version_info = six.iteritems(client.version())
|
||||
log.info("Compose version %s", __version__)
|
||||
log.info("Docker base_url: %s", client.base_url)
|
||||
log.info("Docker version: %s",
|
||||
", ".join("%s=%s" % item for item in version_info))
|
||||
return verbose_proxy.VerboseProxy('docker', client)
|
||||
return client
|
||||
|
||||
def get_config(self, config_path):
|
||||
try:
|
||||
with open(config_path, 'r') as fh:
|
||||
return yaml.safe_load(fh)
|
||||
except IOError as e:
|
||||
raise errors.UserError(six.text_type(e))
|
||||
|
||||
def get_project(self, config_path, project_name=None, verbose=False):
|
||||
try:
|
||||
return Project.from_config(
|
||||
self.get_project_name(config_path, project_name),
|
||||
self.get_config(config_path),
|
||||
self.get_client(verbose=verbose))
|
||||
except ConfigError as e:
|
||||
raise errors.UserError(six.text_type(e))
|
||||
|
||||
def get_project_name(self, config_path, project_name=None):
|
||||
def normalize_name(name):
|
||||
return re.sub(r'[^a-z0-9]', '', name.lower())
|
||||
|
||||
if 'FIG_PROJECT_NAME' in os.environ:
|
||||
log.warn('The FIG_PROJECT_NAME environment variable is deprecated.')
|
||||
log.warn('Please use COMPOSE_PROJECT_NAME instead.')
|
||||
|
||||
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)
|
||||
|
||||
project = os.path.basename(os.path.dirname(os.path.abspath(config_path)))
|
||||
if project:
|
||||
return normalize_name(project)
|
||||
|
||||
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)
|
||||
35
compose/cli/docker_client.py
Normal file
35
compose/cli/docker_client.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from docker import Client
|
||||
from docker import tls
|
||||
import ssl
|
||||
import os
|
||||
|
||||
|
||||
def docker_client():
|
||||
"""
|
||||
Returns a docker-py client configured using environment variables
|
||||
according to the same logic as the official Docker client.
|
||||
"""
|
||||
cert_path = os.environ.get('DOCKER_CERT_PATH', '')
|
||||
if cert_path == '':
|
||||
cert_path = os.path.join(os.environ.get('HOME', ''), '.docker')
|
||||
|
||||
base_url = os.environ.get('DOCKER_HOST')
|
||||
tls_config = None
|
||||
|
||||
if os.environ.get('DOCKER_TLS_VERIFY', '') != '':
|
||||
parts = base_url.split('://', 1)
|
||||
base_url = '%s://%s' % ('https', parts[1])
|
||||
|
||||
client_cert = (os.path.join(cert_path, 'cert.pem'), os.path.join(cert_path, 'key.pem'))
|
||||
ca_cert = os.path.join(cert_path, 'ca.pem')
|
||||
|
||||
tls_config = tls.TLSConfig(
|
||||
ssl_version=ssl.PROTOCOL_TLSv1,
|
||||
verify=True,
|
||||
assert_hostname=False,
|
||||
client_cert=client_cert,
|
||||
ca_cert=ca_cert,
|
||||
)
|
||||
|
||||
timeout = int(os.environ.get('DOCKER_CLIENT_TIMEOUT', 60))
|
||||
return Client(base_url=base_url, tls=tls_config, version='1.14', timeout=timeout)
|
||||
@@ -23,7 +23,7 @@ class DocoptCommand(object):
|
||||
def dispatch(self, argv, global_options):
|
||||
self.perform_command(*self.parse(argv, global_options))
|
||||
|
||||
def perform_command(self, options, command, handler, command_options):
|
||||
def perform_command(self, options, handler, command_options):
|
||||
handler(command_options)
|
||||
|
||||
def parse(self, argv, global_options):
|
||||
@@ -43,7 +43,7 @@ class DocoptCommand(object):
|
||||
raise NoSuchCommand(command, self)
|
||||
|
||||
command_options = docopt_full_help(docstring, options['ARGS'], options_first=True)
|
||||
return (options, command, handler, command_options)
|
||||
return options, handler, command_options
|
||||
|
||||
|
||||
class NoSuchCommand(Exception):
|
||||
@@ -9,6 +9,8 @@ class UserError(Exception):
|
||||
def __unicode__(self):
|
||||
return self.msg
|
||||
|
||||
__str__ = __unicode__
|
||||
|
||||
|
||||
class DockerNotFoundMac(UserError):
|
||||
def __init__(self):
|
||||
@@ -37,10 +39,10 @@ class DockerNotFoundGeneric(UserError):
|
||||
""")
|
||||
|
||||
|
||||
class ConnectionErrorDockerOSX(UserError):
|
||||
class ConnectionErrorBoot2Docker(UserError):
|
||||
def __init__(self):
|
||||
super(ConnectionErrorDockerOSX, self).__init__("""
|
||||
Couldn't connect to Docker daemon - you might need to run `docker-osx shell`.
|
||||
super(ConnectionErrorBoot2Docker, self).__init__("""
|
||||
Couldn't connect to Docker daemon - you might need to run `boot2docker up`.
|
||||
""")
|
||||
|
||||
|
||||
@@ -53,8 +55,10 @@ class ConnectionErrorGeneric(UserError):
|
||||
""" % url)
|
||||
|
||||
|
||||
class FigFileNotFound(UserError):
|
||||
def __init__(self, filename):
|
||||
super(FigFileNotFound, self).__init__("""
|
||||
Can't find %s. Are you in the right directory?
|
||||
""" % filename)
|
||||
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))
|
||||
@@ -4,11 +4,17 @@ import os
|
||||
import texttable
|
||||
|
||||
|
||||
def get_tty_width():
|
||||
tty_size = os.popen('stty size', 'r').read().split()
|
||||
if len(tty_size) != 2:
|
||||
return 80
|
||||
_, width = tty_size
|
||||
return int(width)
|
||||
|
||||
|
||||
class Formatter(object):
|
||||
def table(self, headers, rows):
|
||||
height, width = os.popen('stty size', 'r').read().split()
|
||||
|
||||
table = texttable.Texttable(max_width=width)
|
||||
table = texttable.Texttable(max_width=get_tty_width())
|
||||
table.set_cols_dtype(['t' for h in headers])
|
||||
table.add_rows([headers] + rows)
|
||||
table.set_deco(table.HEADER)
|
||||
@@ -10,11 +10,11 @@ from .utils import split_buffer
|
||||
|
||||
|
||||
class LogPrinter(object):
|
||||
def __init__(self, containers, attach_params=None, output=sys.stdout):
|
||||
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()
|
||||
self.generators = self._make_log_generators(monochrome)
|
||||
self.output = output
|
||||
|
||||
def run(self):
|
||||
@@ -35,12 +35,18 @@ class LogPrinter(object):
|
||||
prefix_width = max(prefix_width, len(container.name_without_project))
|
||||
return prefix_width
|
||||
|
||||
def _make_log_generators(self):
|
||||
def _make_log_generators(self, monochrome):
|
||||
color_fns = cycle(colors.rainbow())
|
||||
generators = []
|
||||
|
||||
def no_color(text):
|
||||
return text
|
||||
|
||||
for container in self.containers:
|
||||
color_fn = color_fns.next()
|
||||
if monochrome:
|
||||
color_fn = no_color
|
||||
else:
|
||||
color_fn = color_fns.next()
|
||||
generators.append(self._make_log_generator(container, color_fn))
|
||||
|
||||
return generators
|
||||
@@ -4,6 +4,7 @@ import logging
|
||||
import sys
|
||||
import re
|
||||
import signal
|
||||
from operator import attrgetter
|
||||
|
||||
from inspect import getdoc
|
||||
import dockerpty
|
||||
@@ -16,7 +17,7 @@ from .formatter import Formatter
|
||||
from .log_printer import LogPrinter
|
||||
from .utils import yesno
|
||||
|
||||
from ..packages.docker.errors import APIError
|
||||
from docker.errors import APIError
|
||||
from .errors import UserError
|
||||
from .docopt_command import NoSuchCommand
|
||||
|
||||
@@ -67,16 +68,16 @@ def parse_doc_section(name, source):
|
||||
|
||||
|
||||
class TopLevelCommand(Command):
|
||||
"""Punctual, lightweight development environments using Docker.
|
||||
"""Fast, isolated development environments using Docker.
|
||||
|
||||
Usage:
|
||||
fig [options] [COMMAND] [ARGS...]
|
||||
fig -h|--help
|
||||
docker-compose [options] [COMMAND] [ARGS...]
|
||||
docker-compose -h|--help
|
||||
|
||||
Options:
|
||||
--verbose Show more output
|
||||
--version Print version and exit
|
||||
-f, --file FILE Specify an alternate fig file (default: fig.yml)
|
||||
-f, --file FILE Specify an alternate compose file (default: docker-compose.yml)
|
||||
-p, --project-name NAME Specify an alternate project name (default: directory name)
|
||||
|
||||
Commands:
|
||||
@@ -84,27 +85,30 @@ class TopLevelCommand(Command):
|
||||
help Get help on a command
|
||||
kill Kill containers
|
||||
logs View output from containers
|
||||
port Print the public port for a port binding
|
||||
ps List containers
|
||||
pull Pulls service images
|
||||
rm Remove stopped containers
|
||||
run Run a one-off command
|
||||
scale Set number of containers for a service
|
||||
start Start services
|
||||
stop Stop services
|
||||
restart Restart services
|
||||
up Create and start containers
|
||||
|
||||
"""
|
||||
def docopt_options(self):
|
||||
options = super(TopLevelCommand, self).docopt_options()
|
||||
options['version'] = "fig %s" % __version__
|
||||
options['version'] = "docker-compose %s" % __version__
|
||||
return options
|
||||
|
||||
def build(self, options):
|
||||
def build(self, project, options):
|
||||
"""
|
||||
Build or rebuild services.
|
||||
|
||||
Services are built once and then tagged as `project_service`,
|
||||
e.g. `figtest_db`. If you change a service's `Dockerfile` or the
|
||||
contents of its build directory, you can run `fig build` to rebuild it.
|
||||
e.g. `composetest_db`. If you change a service's `Dockerfile` or the
|
||||
contents of its build directory, you can run `compose build` to rebuild it.
|
||||
|
||||
Usage: build [options] [SERVICE...]
|
||||
|
||||
@@ -112,9 +116,9 @@ class TopLevelCommand(Command):
|
||||
--no-cache Do not use cache when building the image.
|
||||
"""
|
||||
no_cache = bool(options.get('--no-cache', False))
|
||||
self.project.build(service_names=options['SERVICE'], no_cache=no_cache)
|
||||
project.build(service_names=options['SERVICE'], no_cache=no_cache)
|
||||
|
||||
def help(self, options):
|
||||
def help(self, project, options):
|
||||
"""
|
||||
Get help on a command.
|
||||
|
||||
@@ -125,25 +129,56 @@ class TopLevelCommand(Command):
|
||||
raise NoSuchCommand(command, self)
|
||||
raise SystemExit(getdoc(getattr(self, command)))
|
||||
|
||||
def kill(self, options):
|
||||
def kill(self, project, options):
|
||||
"""
|
||||
Force stop service containers.
|
||||
|
||||
Usage: kill [SERVICE...]
|
||||
"""
|
||||
self.project.kill(service_names=options['SERVICE'])
|
||||
Usage: kill [options] [SERVICE...]
|
||||
|
||||
def logs(self, options):
|
||||
Options:
|
||||
-s SIGNAL SIGNAL to send to the container.
|
||||
Default signal is SIGKILL.
|
||||
"""
|
||||
signal = options.get('-s', 'SIGKILL')
|
||||
|
||||
project.kill(service_names=options['SERVICE'], signal=signal)
|
||||
|
||||
def logs(self, project, options):
|
||||
"""
|
||||
View output from containers.
|
||||
|
||||
Usage: logs [SERVICE...]
|
||||
"""
|
||||
containers = self.project.containers(service_names=options['SERVICE'], stopped=True)
|
||||
print("Attaching to", list_containers(containers))
|
||||
LogPrinter(containers, attach_params={'logs': True}).run()
|
||||
Usage: logs [options] [SERVICE...]
|
||||
|
||||
def ps(self, options):
|
||||
Options:
|
||||
--no-color Produce monochrome output.
|
||||
"""
|
||||
containers = project.containers(service_names=options['SERVICE'], stopped=True)
|
||||
|
||||
monochrome = options['--no-color']
|
||||
print("Attaching to", list_containers(containers))
|
||||
LogPrinter(containers, attach_params={'logs': True}, monochrome=monochrome).run()
|
||||
|
||||
def port(self, project, options):
|
||||
"""
|
||||
Print the public port for a port binding.
|
||||
|
||||
Usage: port [options] SERVICE PRIVATE_PORT
|
||||
|
||||
Options:
|
||||
--protocol=proto tcp or udp (defaults to tcp)
|
||||
--index=index index of the container if there are multiple
|
||||
instances of a service (defaults to 1)
|
||||
"""
|
||||
service = project.get_service(options['SERVICE'])
|
||||
try:
|
||||
container = service.get_container(number=options.get('--index') or 1)
|
||||
except ValueError as e:
|
||||
raise UserError(str(e))
|
||||
print(container.get_local_port(
|
||||
options['PRIVATE_PORT'],
|
||||
protocol=options.get('--protocol') or 'tcp') or '')
|
||||
|
||||
def ps(self, project, options):
|
||||
"""
|
||||
List containers.
|
||||
|
||||
@@ -152,7 +187,10 @@ class TopLevelCommand(Command):
|
||||
Options:
|
||||
-q Only display IDs
|
||||
"""
|
||||
containers = self.project.containers(service_names=options['SERVICE'], stopped=True) + self.project.containers(service_names=options['SERVICE'], one_off=True)
|
||||
containers = sorted(
|
||||
project.containers(service_names=options['SERVICE'], stopped=True) +
|
||||
project.containers(service_names=options['SERVICE'], one_off=True),
|
||||
key=attrgetter('name'))
|
||||
|
||||
if options['-q']:
|
||||
for container in containers:
|
||||
@@ -177,7 +215,23 @@ class TopLevelCommand(Command):
|
||||
])
|
||||
print(Formatter().table(headers, rows))
|
||||
|
||||
def rm(self, options):
|
||||
def pull(self, project, options):
|
||||
"""
|
||||
Pulls images for services.
|
||||
|
||||
Usage: pull [options] [SERVICE...]
|
||||
|
||||
Options:
|
||||
--allow-insecure-ssl Allow insecure connections to the docker
|
||||
registry
|
||||
"""
|
||||
insecure_registry = options['--allow-insecure-ssl']
|
||||
project.pull(
|
||||
service_names=options['SERVICE'],
|
||||
insecure_registry=insecure_registry
|
||||
)
|
||||
|
||||
def rm(self, project, options):
|
||||
"""
|
||||
Remove stopped service containers.
|
||||
|
||||
@@ -187,53 +241,62 @@ class TopLevelCommand(Command):
|
||||
--force Don't ask to confirm removal
|
||||
-v Remove volumes associated with containers
|
||||
"""
|
||||
all_containers = self.project.containers(service_names=options['SERVICE'], stopped=True)
|
||||
all_containers = project.containers(service_names=options['SERVICE'], stopped=True)
|
||||
stopped_containers = [c for c in all_containers if not c.is_running]
|
||||
|
||||
if len(stopped_containers) > 0:
|
||||
print("Going to remove", list_containers(stopped_containers))
|
||||
if options.get('--force') \
|
||||
or yesno("Are you sure? [yN] ", default=False):
|
||||
self.project.remove_stopped(
|
||||
project.remove_stopped(
|
||||
service_names=options['SERVICE'],
|
||||
v=options.get('-v', False)
|
||||
)
|
||||
else:
|
||||
print("No stopped containers")
|
||||
|
||||
def run(self, options):
|
||||
def run(self, project, options):
|
||||
"""
|
||||
Run a one-off command on a service.
|
||||
|
||||
For example:
|
||||
|
||||
$ fig run web python manage.py shell
|
||||
$ docker-compose run web python manage.py shell
|
||||
|
||||
By default, linked services will be started, unless they are already
|
||||
running. If you do not want to start linked services, use
|
||||
`fig run --no-deps SERVICE COMMAND [ARGS...]`.
|
||||
`docker-compose run --no-deps SERVICE COMMAND [ARGS...]`.
|
||||
|
||||
Usage: run [options] SERVICE [COMMAND] [ARGS...]
|
||||
Usage: run [options] [-e KEY=VAL...] SERVICE [COMMAND] [ARGS...]
|
||||
|
||||
Options:
|
||||
-d Detached mode: Run container in the background, print
|
||||
new container name.
|
||||
-T Disable pseudo-tty allocation. By default `fig run`
|
||||
allocates a TTY.
|
||||
--rm Remove container after run. Ignored in detached mode.
|
||||
--no-deps Don't start linked services.
|
||||
--allow-insecure-ssl Allow insecure connections to the docker
|
||||
registry
|
||||
-d Detached mode: Run container in the background, print
|
||||
new container name.
|
||||
--entrypoint CMD Override the entrypoint of the image.
|
||||
-e KEY=VAL Set an environment variable (can be used multiple times)
|
||||
--no-deps Don't start linked services.
|
||||
--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.
|
||||
"""
|
||||
service = project.get_service(options['SERVICE'])
|
||||
|
||||
service = self.project.get_service(options['SERVICE'])
|
||||
insecure_registry = options['--allow-insecure-ssl']
|
||||
|
||||
if not options['--no-deps']:
|
||||
deps = service.get_linked_names()
|
||||
|
||||
if len(deps) > 0:
|
||||
self.project.up(
|
||||
project.up(
|
||||
service_names=deps,
|
||||
start_links=True,
|
||||
recreate=False,
|
||||
insecure_registry=insecure_registry,
|
||||
detach=options['-d']
|
||||
)
|
||||
|
||||
tty = True
|
||||
@@ -249,28 +312,47 @@ class TopLevelCommand(Command):
|
||||
'command': command,
|
||||
'tty': tty,
|
||||
'stdin_open': not options['-d'],
|
||||
'detach': options['-d'],
|
||||
}
|
||||
container = service.create_container(one_off=True, **container_options)
|
||||
|
||||
if options['-e']:
|
||||
for option in options['-e']:
|
||||
if 'environment' not in service.options:
|
||||
service.options['environment'] = {}
|
||||
k, v = option.split('=', 1)
|
||||
service.options['environment'][k] = v
|
||||
|
||||
if options['--entrypoint']:
|
||||
container_options['entrypoint'] = options.get('--entrypoint')
|
||||
container = service.create_container(
|
||||
one_off=True,
|
||||
insecure_registry=insecure_registry,
|
||||
**container_options
|
||||
)
|
||||
|
||||
service_ports = None
|
||||
if options['--service-ports']:
|
||||
service_ports = service.options['ports']
|
||||
if options['-d']:
|
||||
service.start_container(container, ports=None, one_off=True)
|
||||
service.start_container(container, ports=service_ports, one_off=True)
|
||||
print(container.name)
|
||||
else:
|
||||
service.start_container(container, ports=None, one_off=True)
|
||||
dockerpty.start(self.client, container.id)
|
||||
service.start_container(container, ports=service_ports, one_off=True)
|
||||
dockerpty.start(project.client, container.id, interactive=not options['-T'])
|
||||
exit_code = container.wait()
|
||||
if options['--rm']:
|
||||
log.info("Removing %s..." % container.name)
|
||||
self.client.remove_container(container.id)
|
||||
project.client.remove_container(container.id)
|
||||
sys.exit(exit_code)
|
||||
|
||||
def scale(self, options):
|
||||
def scale(self, project, options):
|
||||
"""
|
||||
Set number of containers to run for a service.
|
||||
|
||||
Numbers are specified in the form `service=num` as arguments.
|
||||
For example:
|
||||
|
||||
$ fig scale web=2 worker=3
|
||||
$ docker-compose scale web=2 worker=3
|
||||
|
||||
Usage: scale [SERVICE=NUM...]
|
||||
"""
|
||||
@@ -284,78 +366,101 @@ class TopLevelCommand(Command):
|
||||
raise UserError('Number of containers for service "%s" is not a '
|
||||
'number' % service_name)
|
||||
try:
|
||||
self.project.get_service(service_name).scale(num)
|
||||
project.get_service(service_name).scale(num)
|
||||
except CannotBeScaledError:
|
||||
raise UserError('Service "%s" cannot be scaled because it specifies a port on the host. If multiple containers for this service were created, the port would clash.\n\nRemove the ":" from the port definition in fig.yml so Docker can choose a random port for each container.' % service_name)
|
||||
raise UserError(
|
||||
'Service "%s" cannot be scaled because it specifies a port '
|
||||
'on the host. If multiple containers for this service were '
|
||||
'created, the port would clash.\n\nRemove the ":" from the '
|
||||
'port definition in docker-compose.yml so Docker can choose a random '
|
||||
'port for each container.' % service_name)
|
||||
|
||||
def start(self, options):
|
||||
def start(self, project, options):
|
||||
"""
|
||||
Start existing containers.
|
||||
|
||||
Usage: start [SERVICE...]
|
||||
"""
|
||||
self.project.start(service_names=options['SERVICE'])
|
||||
project.start(service_names=options['SERVICE'])
|
||||
|
||||
def stop(self, options):
|
||||
def stop(self, project, options):
|
||||
"""
|
||||
Stop running containers without removing them.
|
||||
|
||||
They can be started again with `fig start`.
|
||||
They can be started again with `docker-compose start`.
|
||||
|
||||
Usage: stop [SERVICE...]
|
||||
"""
|
||||
self.project.stop(service_names=options['SERVICE'])
|
||||
project.stop(service_names=options['SERVICE'])
|
||||
|
||||
def up(self, options):
|
||||
def restart(self, project, options):
|
||||
"""
|
||||
Restart running containers.
|
||||
|
||||
Usage: restart [SERVICE...]
|
||||
"""
|
||||
project.restart(service_names=options['SERVICE'])
|
||||
|
||||
def up(self, project, options):
|
||||
"""
|
||||
Build, (re)create, start and attach to containers for a service.
|
||||
|
||||
By default, `fig up` will aggregate the output of each container, and
|
||||
when it exits, all containers will be stopped. If you run `fig up -d`,
|
||||
By default, `docker-compose up` will aggregate the output of each container, and
|
||||
when it exits, all containers will be stopped. If you run `docker-compose up -d`,
|
||||
it'll start the containers in the background and leave them running.
|
||||
|
||||
If there are existing containers for a service, `fig up` will stop
|
||||
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 `fig.yml` are picked up. If you do not want existing
|
||||
containers to be recreated, `fig up --no-recreate` will re-use existing
|
||||
so that changes in `docker-compose.yml` are picked up. If you do not want existing
|
||||
containers to be recreated, `docker-compose up --no-recreate` will re-use existing
|
||||
containers.
|
||||
|
||||
Usage: up [options] [SERVICE...]
|
||||
|
||||
Options:
|
||||
-d Detached mode: Run containers in the background,
|
||||
print new container names.
|
||||
--no-deps Don't start linked services.
|
||||
--no-recreate If containers already exist, don't recreate them.
|
||||
--allow-insecure-ssl Allow insecure connections to the docker
|
||||
registry
|
||||
-d Detached mode: Run containers in the background,
|
||||
print new container names.
|
||||
--no-color Produce monochrome output.
|
||||
--no-deps Don't start linked services.
|
||||
--no-recreate If containers already exist, don't recreate them.
|
||||
--no-build Don't build an image, even if it's missing
|
||||
"""
|
||||
insecure_registry = options['--allow-insecure-ssl']
|
||||
detached = options['-d']
|
||||
|
||||
monochrome = options['--no-color']
|
||||
|
||||
start_links = not options['--no-deps']
|
||||
recreate = not options['--no-recreate']
|
||||
service_names = options['SERVICE']
|
||||
|
||||
self.project.up(
|
||||
project.up(
|
||||
service_names=service_names,
|
||||
start_links=start_links,
|
||||
recreate=recreate
|
||||
recreate=recreate,
|
||||
insecure_registry=insecure_registry,
|
||||
detach=options['-d'],
|
||||
do_build=not options['--no-build'],
|
||||
)
|
||||
|
||||
to_attach = [c for s in self.project.get_services(service_names) for c in s.containers()]
|
||||
to_attach = [c for s in project.get_services(service_names) for c in s.containers()]
|
||||
|
||||
if not detached:
|
||||
print("Attaching to", list_containers(to_attach))
|
||||
log_printer = LogPrinter(to_attach, attach_params={"logs": True})
|
||||
log_printer = LogPrinter(to_attach, attach_params={"logs": True}, monochrome=monochrome)
|
||||
|
||||
try:
|
||||
log_printer.run()
|
||||
finally:
|
||||
def handler(signal, frame):
|
||||
self.project.kill(service_names=service_names)
|
||||
project.kill(service_names=service_names)
|
||||
sys.exit(0)
|
||||
signal.signal(signal.SIGINT, handler)
|
||||
|
||||
print("Gracefully stopping... (press Ctrl+C again to force)")
|
||||
self.project.stop(service_names=service_names)
|
||||
project.stop(service_names=service_names)
|
||||
|
||||
|
||||
def list_containers(containers):
|
||||
@@ -7,25 +7,6 @@ import subprocess
|
||||
import platform
|
||||
|
||||
|
||||
def cached_property(f):
|
||||
"""
|
||||
returns a cached property that is calculated by function f
|
||||
http://code.activestate.com/recipes/576563-cached-property/
|
||||
"""
|
||||
def get(self):
|
||||
try:
|
||||
return self._property_cache[f]
|
||||
except AttributeError:
|
||||
self._property_cache = {}
|
||||
x = self._property_cache[f] = f(self)
|
||||
return x
|
||||
except KeyError:
|
||||
x = self._property_cache[f] = f(self)
|
||||
return x
|
||||
|
||||
return property(get)
|
||||
|
||||
|
||||
def yesno(prompt, default=None):
|
||||
"""
|
||||
Prompt the user for a yes or no.
|
||||
@@ -81,10 +62,6 @@ def mkdir(path, permissions=0o700):
|
||||
return path
|
||||
|
||||
|
||||
def docker_url():
|
||||
return os.environ.get('DOCKER_HOST')
|
||||
|
||||
|
||||
def split_buffer(reader, separator):
|
||||
"""
|
||||
Given a generator which yields strings and a separator string,
|
||||
58
compose/cli/verbose_proxy.py
Normal file
58
compose/cli/verbose_proxy.py
Normal file
@@ -0,0 +1,58 @@
|
||||
|
||||
import functools
|
||||
from itertools import chain
|
||||
import logging
|
||||
import pprint
|
||||
|
||||
import six
|
||||
|
||||
|
||||
def format_call(args, kwargs):
|
||||
args = (repr(a) for a in args)
|
||||
kwargs = ("{0!s}={1!r}".format(*item) for item in six.iteritems(kwargs))
|
||||
return "({0})".format(", ".join(chain(args, kwargs)))
|
||||
|
||||
|
||||
def format_return(result, max_lines):
|
||||
if isinstance(result, (list, tuple, set)):
|
||||
return "({0} with {1} items)".format(type(result).__name__, len(result))
|
||||
|
||||
if result:
|
||||
lines = pprint.pformat(result).split('\n')
|
||||
extra = '\n...' if len(lines) > max_lines else ''
|
||||
return '\n'.join(lines[:max_lines]) + extra
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class VerboseProxy(object):
|
||||
"""Proxy all function calls to another class and log method name, arguments
|
||||
and return values for each call.
|
||||
"""
|
||||
|
||||
def __init__(self, obj_name, obj, log_name=None, max_lines=10):
|
||||
self.obj_name = obj_name
|
||||
self.obj = obj
|
||||
self.max_lines = max_lines
|
||||
self.log = logging.getLogger(log_name or __name__)
|
||||
|
||||
def __getattr__(self, name):
|
||||
attr = getattr(self.obj, name)
|
||||
|
||||
if not six.callable(attr):
|
||||
return attr
|
||||
|
||||
return functools.partial(self.proxy_callable, name)
|
||||
|
||||
def proxy_callable(self, call_name, *args, **kwargs):
|
||||
self.log.info("%s %s <- %s",
|
||||
self.obj_name,
|
||||
call_name,
|
||||
format_call(args, kwargs))
|
||||
|
||||
result = getattr(self.obj, call_name)(*args, **kwargs)
|
||||
self.log.info("%s %s -> %s",
|
||||
self.obj_name,
|
||||
call_name,
|
||||
format_return(result, self.max_lines))
|
||||
return result
|
||||
@@ -1,6 +1,8 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
import six
|
||||
|
||||
|
||||
class Container(object):
|
||||
"""
|
||||
@@ -20,10 +22,8 @@ class Container(object):
|
||||
new_dictionary = {
|
||||
'Id': dictionary['Id'],
|
||||
'Image': dictionary['Image'],
|
||||
'Name': '/' + get_container_name(dictionary),
|
||||
}
|
||||
for name in dictionary.get('Names', []):
|
||||
if len(name.split('/')) == 2:
|
||||
new_dictionary['Name'] = name
|
||||
return cls(client, new_dictionary, **kwargs)
|
||||
|
||||
@classmethod
|
||||
@@ -63,50 +63,58 @@ class Container(object):
|
||||
return None
|
||||
|
||||
@property
|
||||
def human_readable_ports(self):
|
||||
def ports(self):
|
||||
self.inspect_if_not_inspected()
|
||||
if not self.dictionary['NetworkSettings']['Ports']:
|
||||
return ''
|
||||
ports = []
|
||||
for private, public in list(self.dictionary['NetworkSettings']['Ports'].items()):
|
||||
if public:
|
||||
ports.append('%s->%s' % (public[0]['HostPort'], private))
|
||||
else:
|
||||
ports.append(private)
|
||||
return ', '.join(ports)
|
||||
return self.get('NetworkSettings.Ports') or {}
|
||||
|
||||
@property
|
||||
def human_readable_ports(self):
|
||||
def format_port(private, public):
|
||||
if not public:
|
||||
return private
|
||||
return '{HostIp}:{HostPort}->{private}'.format(
|
||||
private=private, **public[0])
|
||||
|
||||
return ', '.join(format_port(*item)
|
||||
for item in sorted(six.iteritems(self.ports)))
|
||||
|
||||
@property
|
||||
def human_readable_state(self):
|
||||
self.inspect_if_not_inspected()
|
||||
if self.dictionary['State']['Running']:
|
||||
if self.dictionary['State'].get('Ghost'):
|
||||
return 'Ghost'
|
||||
else:
|
||||
return 'Up'
|
||||
if self.is_running:
|
||||
return 'Ghost' if self.get('State.Ghost') else 'Up'
|
||||
else:
|
||||
return 'Exit %s' % self.dictionary['State']['ExitCode']
|
||||
return 'Exit %s' % self.get('State.ExitCode')
|
||||
|
||||
@property
|
||||
def human_readable_command(self):
|
||||
self.inspect_if_not_inspected()
|
||||
if self.dictionary['Config']['Cmd']:
|
||||
return ' '.join(self.dictionary['Config']['Cmd'])
|
||||
else:
|
||||
return ''
|
||||
entrypoint = self.get('Config.Entrypoint') or []
|
||||
cmd = self.get('Config.Cmd') or []
|
||||
return ' '.join(entrypoint + cmd)
|
||||
|
||||
@property
|
||||
def environment(self):
|
||||
self.inspect_if_not_inspected()
|
||||
out = {}
|
||||
for var in self.dictionary.get('Config', {}).get('Env', []):
|
||||
k, v = var.split('=', 1)
|
||||
out[k] = v
|
||||
return out
|
||||
return dict(var.split("=", 1) for var in self.get('Config.Env') or [])
|
||||
|
||||
@property
|
||||
def is_running(self):
|
||||
return self.get('State.Running')
|
||||
|
||||
def get(self, key):
|
||||
"""Return a value from the container or None if the value is not set.
|
||||
|
||||
:param key: a string using dotted notation for nested dictionary
|
||||
lookups
|
||||
"""
|
||||
self.inspect_if_not_inspected()
|
||||
return self.dictionary['State']['Running']
|
||||
|
||||
def get_value(dictionary, key):
|
||||
return (dictionary or {}).get(key)
|
||||
|
||||
return reduce(get_value, key.split('.'), self.dictionary)
|
||||
|
||||
def get_local_port(self, port, protocol='tcp'):
|
||||
port = self.ports.get("%s/%s" % (port, protocol))
|
||||
return "{HostIp}:{HostPort}".format(**port[0]) if port else None
|
||||
|
||||
def start(self, **options):
|
||||
return self.client.start(self.id, **options)
|
||||
@@ -114,8 +122,11 @@ class Container(object):
|
||||
def stop(self, **options):
|
||||
return self.client.stop(self.id, **options)
|
||||
|
||||
def kill(self):
|
||||
return self.client.kill(self.id)
|
||||
def kill(self, **options):
|
||||
return self.client.kill(self.id, **options)
|
||||
|
||||
def restart(self):
|
||||
return self.client.restart(self.id)
|
||||
|
||||
def remove(self, **options):
|
||||
return self.client.remove_container(self.id, **options)
|
||||
@@ -132,6 +143,7 @@ class Container(object):
|
||||
|
||||
def inspect(self):
|
||||
self.dictionary = self.client.inspect_container(self.id)
|
||||
self.has_been_inspected = True
|
||||
return self.dictionary
|
||||
|
||||
def links(self):
|
||||
@@ -156,3 +168,14 @@ class Container(object):
|
||||
if type(self) != type(other):
|
||||
return False
|
||||
return self.id == other.id
|
||||
|
||||
|
||||
def get_container_name(container):
|
||||
if not container.get('Name') and not container.get('Names'):
|
||||
return None
|
||||
# inspect
|
||||
if 'Name' in container:
|
||||
return container['Name']
|
||||
# ps
|
||||
shortest_name = min(container['Names'], key=lambda n: len(n.split('/')))
|
||||
return shortest_name.split('/')[-1]
|
||||
@@ -19,7 +19,9 @@ def stream_output(output, stream):
|
||||
all_events.append(event)
|
||||
|
||||
if 'progress' in event or 'progressDetail' in event:
|
||||
image_id = event['id']
|
||||
image_id = event.get('id')
|
||||
if not image_id:
|
||||
continue
|
||||
|
||||
if image_id in lines:
|
||||
diff = len(lines) - lines[image_id]
|
||||
@@ -1,9 +1,10 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
import logging
|
||||
|
||||
from .service import Service
|
||||
from .container import Container
|
||||
from .packages.docker.errors import APIError
|
||||
from docker.errors import APIError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -14,7 +15,8 @@ def sort_service_dicts(services):
|
||||
temporary_marked = set()
|
||||
sorted_services = []
|
||||
|
||||
get_service_names = lambda links: [link.split(':')[0] for link in links]
|
||||
def get_service_names(links):
|
||||
return [link.split(':')[0] for link in links]
|
||||
|
||||
def visit(n):
|
||||
if n['name'] in temporary_marked:
|
||||
@@ -66,7 +68,7 @@ class Project(object):
|
||||
dicts = []
|
||||
for service_name, service in list(config.items()):
|
||||
if not isinstance(service, dict):
|
||||
raise ConfigurationError('Service "%s" doesn\'t have any configuration options. All top level keys in your fig.yml must map to a dictionary of configuration options.')
|
||||
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)
|
||||
service['name'] = service_name
|
||||
dicts.append(service)
|
||||
return cls.from_dicts(name, dicts, client)
|
||||
@@ -155,6 +157,10 @@ class Project(object):
|
||||
for service in reversed(self.get_services(service_names)):
|
||||
service.kill(**options)
|
||||
|
||||
def restart(self, service_names=None, **options):
|
||||
for service in self.get_services(service_names):
|
||||
service.restart(**options)
|
||||
|
||||
def build(self, service_names=None, no_cache=False):
|
||||
for service in self.get_services(service_names):
|
||||
if service.can_be_built():
|
||||
@@ -162,29 +168,43 @@ class Project(object):
|
||||
else:
|
||||
log.info('%s uses an image, skipping' % service.name)
|
||||
|
||||
def up(self, service_names=None, start_links=True, recreate=True):
|
||||
def up(self,
|
||||
service_names=None,
|
||||
start_links=True,
|
||||
recreate=True,
|
||||
insecure_registry=False,
|
||||
detach=False,
|
||||
do_build=True):
|
||||
running_containers = []
|
||||
|
||||
for service in self.get_services(service_names, include_links=start_links):
|
||||
if recreate:
|
||||
for (_, container) in service.recreate_containers():
|
||||
for (_, container) in service.recreate_containers(
|
||||
insecure_registry=insecure_registry,
|
||||
detach=detach,
|
||||
do_build=do_build):
|
||||
running_containers.append(container)
|
||||
else:
|
||||
for container in service.start_or_create_containers():
|
||||
for container in service.start_or_create_containers(
|
||||
insecure_registry=insecure_registry,
|
||||
detach=detach,
|
||||
do_build=do_build):
|
||||
running_containers.append(container)
|
||||
|
||||
return running_containers
|
||||
|
||||
def pull(self, service_names=None, insecure_registry=False):
|
||||
for service in self.get_services(service_names, include_links=True):
|
||||
service.pull(insecure_registry=insecure_registry)
|
||||
|
||||
def remove_stopped(self, service_names=None, **options):
|
||||
for service in self.get_services(service_names):
|
||||
service.remove_stopped(**options)
|
||||
|
||||
def containers(self, service_names=None, *args, **kwargs):
|
||||
l = []
|
||||
for service in self.get_services(service_names):
|
||||
for container in service.containers(*args, **kwargs):
|
||||
l.append(container)
|
||||
return l
|
||||
def containers(self, service_names=None, stopped=False, one_off=False):
|
||||
return [Container.from_ps(self.client, container)
|
||||
for container in self.client.containers(all=stopped)
|
||||
for service in self.get_services(service_names)
|
||||
if service.has_container(container, one_off=one_off)]
|
||||
|
||||
def _inject_links(self, acc, service):
|
||||
linked_names = service.get_linked_names()
|
||||
673
compose/service.py
Normal file
673
compose/service.py
Normal file
@@ -0,0 +1,673 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
import re
|
||||
import os
|
||||
from operator import attrgetter
|
||||
import sys
|
||||
|
||||
from docker.errors import APIError
|
||||
|
||||
from .container import Container, get_container_name
|
||||
from .progress_stream import stream_output, StreamOutputError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
DOCKER_CONFIG_KEYS = [
|
||||
'cap_add',
|
||||
'cap_drop',
|
||||
'cpu_shares',
|
||||
'command',
|
||||
'detach',
|
||||
'dns',
|
||||
'dns_search',
|
||||
'domainname',
|
||||
'entrypoint',
|
||||
'env_file',
|
||||
'environment',
|
||||
'hostname',
|
||||
'image',
|
||||
'mem_limit',
|
||||
'net',
|
||||
'ports',
|
||||
'privileged',
|
||||
'restart',
|
||||
'stdin_open',
|
||||
'tty',
|
||||
'user',
|
||||
'volumes',
|
||||
'volumes_from',
|
||||
'working_dir',
|
||||
]
|
||||
DOCKER_CONFIG_HINTS = {
|
||||
'cpu_share' : 'cpu_shares',
|
||||
'link' : 'links',
|
||||
'port' : 'ports',
|
||||
'privilege' : 'privileged',
|
||||
'priviliged': 'privileged',
|
||||
'privilige' : 'privileged',
|
||||
'volume' : 'volumes',
|
||||
'workdir' : 'working_dir',
|
||||
}
|
||||
|
||||
DOCKER_START_KEYS = [
|
||||
'cap_add',
|
||||
'cap_drop',
|
||||
'dns',
|
||||
'dns_search',
|
||||
'env_file',
|
||||
'net',
|
||||
'privileged',
|
||||
'restart',
|
||||
]
|
||||
|
||||
VALID_NAME_CHARS = '[a-zA-Z0-9]'
|
||||
|
||||
|
||||
class BuildError(Exception):
|
||||
def __init__(self, service, reason):
|
||||
self.service = service
|
||||
self.reason = reason
|
||||
|
||||
|
||||
class CannotBeScaledError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ConfigError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
VolumeSpec = namedtuple('VolumeSpec', 'external internal mode')
|
||||
|
||||
|
||||
ServiceName = namedtuple('ServiceName', 'project service number')
|
||||
|
||||
|
||||
class Service(object):
|
||||
def __init__(self, name, client=None, project='default', links=None, external_links=None, volumes_from=None, **options):
|
||||
if not re.match('^%s+$' % VALID_NAME_CHARS, name):
|
||||
raise ConfigError('Invalid service name "%s" - only %s are allowed' % (name, VALID_NAME_CHARS))
|
||||
if not re.match('^%s+$' % VALID_NAME_CHARS, project):
|
||||
raise ConfigError('Invalid project name "%s" - only %s are allowed' % (project, VALID_NAME_CHARS))
|
||||
if 'image' in options and 'build' in options:
|
||||
raise ConfigError('Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both.' % name)
|
||||
|
||||
for filename in get_env_files(options):
|
||||
if not os.path.exists(filename):
|
||||
raise ConfigError("Couldn't find env file for service %s: %s" % (name, filename))
|
||||
|
||||
supported_options = DOCKER_CONFIG_KEYS + ['build', 'expose',
|
||||
'external_links']
|
||||
|
||||
for k in options:
|
||||
if k not in supported_options:
|
||||
msg = "Unsupported config option for %s service: '%s'" % (name, k)
|
||||
if k in DOCKER_CONFIG_HINTS:
|
||||
msg += " (did you mean '%s'?)" % DOCKER_CONFIG_HINTS[k]
|
||||
raise ConfigError(msg)
|
||||
|
||||
self.name = name
|
||||
self.client = client
|
||||
self.project = project
|
||||
self.links = links or []
|
||||
self.external_links = external_links or []
|
||||
self.volumes_from = volumes_from or []
|
||||
self.options = options
|
||||
|
||||
def containers(self, stopped=False, one_off=False):
|
||||
return [Container.from_ps(self.client, container)
|
||||
for container in self.client.containers(all=stopped)
|
||||
if self.has_container(container, one_off=one_off)]
|
||||
|
||||
def has_container(self, container, one_off=False):
|
||||
"""Return True if `container` was created to fulfill this service."""
|
||||
name = get_container_name(container)
|
||||
if not name or not is_valid_name(name, one_off):
|
||||
return False
|
||||
project, name, _number = parse_name(name)
|
||||
return project == self.project and name == self.name
|
||||
|
||||
def get_container(self, number=1):
|
||||
"""Return a :class:`compose.container.Container` for this service. The
|
||||
container must be active, and match `number`.
|
||||
"""
|
||||
for container in self.client.containers():
|
||||
if not self.has_container(container):
|
||||
continue
|
||||
_, _, container_number = parse_name(get_container_name(container))
|
||||
if container_number == number:
|
||||
return Container.from_ps(self.client, container)
|
||||
|
||||
raise ValueError("No container found for %s_%s" % (self.name, number))
|
||||
|
||||
def start(self, **options):
|
||||
for c in self.containers(stopped=True):
|
||||
self.start_container_if_stopped(c, **options)
|
||||
|
||||
def stop(self, **options):
|
||||
for c in self.containers():
|
||||
log.info("Stopping %s..." % c.name)
|
||||
c.stop(**options)
|
||||
|
||||
def kill(self, **options):
|
||||
for c in self.containers():
|
||||
log.info("Killing %s..." % c.name)
|
||||
c.kill(**options)
|
||||
|
||||
def restart(self, **options):
|
||||
for c in self.containers():
|
||||
log.info("Restarting %s..." % c.name)
|
||||
c.restart(**options)
|
||||
|
||||
def scale(self, desired_num):
|
||||
"""
|
||||
Adjusts the number of containers to the specified number and ensures
|
||||
they are running.
|
||||
|
||||
- creates containers until there are at least `desired_num`
|
||||
- stops containers until there are at most `desired_num` running
|
||||
- starts containers until there are at least `desired_num` running
|
||||
- removes all stopped containers
|
||||
"""
|
||||
if not self.can_be_scaled():
|
||||
raise CannotBeScaledError()
|
||||
|
||||
# Create enough containers
|
||||
containers = self.containers(stopped=True)
|
||||
while len(containers) < desired_num:
|
||||
log.info("Creating %s..." % self._next_container_name(containers))
|
||||
containers.append(self.create_container(detach=True))
|
||||
|
||||
running_containers = []
|
||||
stopped_containers = []
|
||||
for c in containers:
|
||||
if c.is_running:
|
||||
running_containers.append(c)
|
||||
else:
|
||||
stopped_containers.append(c)
|
||||
running_containers.sort(key=lambda c: c.number)
|
||||
stopped_containers.sort(key=lambda c: c.number)
|
||||
|
||||
# Stop containers
|
||||
while len(running_containers) > desired_num:
|
||||
c = running_containers.pop()
|
||||
log.info("Stopping %s..." % c.name)
|
||||
c.stop(timeout=1)
|
||||
stopped_containers.append(c)
|
||||
|
||||
# Start containers
|
||||
while len(running_containers) < desired_num:
|
||||
c = stopped_containers.pop(0)
|
||||
log.info("Starting %s..." % c.name)
|
||||
self.start_container(c)
|
||||
running_containers.append(c)
|
||||
|
||||
self.remove_stopped()
|
||||
|
||||
def remove_stopped(self, **options):
|
||||
for c in self.containers(stopped=True):
|
||||
if not c.is_running:
|
||||
log.info("Removing %s..." % c.name)
|
||||
c.remove(**options)
|
||||
|
||||
def create_container(self,
|
||||
one_off=False,
|
||||
insecure_registry=False,
|
||||
do_build=True,
|
||||
**override_options):
|
||||
"""
|
||||
Create a container for this service. If the image doesn't exist, attempt to pull
|
||||
it.
|
||||
"""
|
||||
container_options = self._get_container_create_options(
|
||||
override_options,
|
||||
one_off=one_off)
|
||||
|
||||
if (do_build and
|
||||
self.can_be_built() and
|
||||
not self.client.images(name=self.full_name)):
|
||||
self.build()
|
||||
|
||||
try:
|
||||
return Container.create(self.client, **container_options)
|
||||
except APIError as e:
|
||||
if e.response.status_code == 404 and e.explanation and 'No such image' in str(e.explanation):
|
||||
log.info('Pulling image %s...' % container_options['image'])
|
||||
output = self.client.pull(
|
||||
container_options['image'],
|
||||
stream=True,
|
||||
insecure_registry=insecure_registry
|
||||
)
|
||||
stream_output(output, sys.stdout)
|
||||
return Container.create(self.client, **container_options)
|
||||
raise
|
||||
|
||||
def recreate_containers(self, insecure_registry=False, do_build=True, **override_options):
|
||||
"""
|
||||
If a container for this service doesn't exist, create and start one. If there are
|
||||
any, stop them, create+start new ones, and remove the old containers.
|
||||
"""
|
||||
containers = self.containers(stopped=True)
|
||||
if not containers:
|
||||
log.info("Creating %s..." % self._next_container_name(containers))
|
||||
container = self.create_container(
|
||||
insecure_registry=insecure_registry,
|
||||
do_build=do_build,
|
||||
**override_options)
|
||||
self.start_container(container)
|
||||
return [(None, container)]
|
||||
else:
|
||||
tuples = []
|
||||
|
||||
for c in containers:
|
||||
log.info("Recreating %s..." % c.name)
|
||||
tuples.append(self.recreate_container(c, insecure_registry=insecure_registry, **override_options))
|
||||
|
||||
return tuples
|
||||
|
||||
def recreate_container(self, container, **override_options):
|
||||
"""Recreate a container. An intermediate container is created so that
|
||||
the new container has the same name, while still supporting
|
||||
`volumes-from` the original container.
|
||||
"""
|
||||
try:
|
||||
container.stop()
|
||||
except APIError as e:
|
||||
if (e.response.status_code == 500
|
||||
and e.explanation
|
||||
and 'no such process' in str(e.explanation)):
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
|
||||
intermediate_container = Container.create(
|
||||
self.client,
|
||||
image=container.image,
|
||||
entrypoint=['/bin/echo'],
|
||||
command=[],
|
||||
detach=True,
|
||||
)
|
||||
intermediate_container.start(volumes_from=container.id)
|
||||
intermediate_container.wait()
|
||||
container.remove()
|
||||
|
||||
options = dict(override_options)
|
||||
new_container = self.create_container(do_build=False, **options)
|
||||
self.start_container(new_container, intermediate_container=intermediate_container)
|
||||
|
||||
intermediate_container.remove()
|
||||
|
||||
return (intermediate_container, new_container)
|
||||
|
||||
def start_container_if_stopped(self, container, **options):
|
||||
if container.is_running:
|
||||
return container
|
||||
else:
|
||||
log.info("Starting %s..." % container.name)
|
||||
return self.start_container(container, **options)
|
||||
|
||||
def start_container(self, container, intermediate_container=None, **override_options):
|
||||
options = dict(self.options, **override_options)
|
||||
port_bindings = build_port_bindings(options.get('ports') or [])
|
||||
|
||||
volume_bindings = dict(
|
||||
build_volume_binding(parse_volume_spec(volume))
|
||||
for volume in options.get('volumes') or []
|
||||
if ':' in volume)
|
||||
|
||||
privileged = options.get('privileged', False)
|
||||
net = options.get('net', 'bridge')
|
||||
dns = options.get('dns', None)
|
||||
dns_search = options.get('dns_search', None)
|
||||
cap_add = options.get('cap_add', None)
|
||||
cap_drop = options.get('cap_drop', None)
|
||||
|
||||
restart = parse_restart_spec(options.get('restart', None))
|
||||
|
||||
container.start(
|
||||
links=self._get_links(link_to_self=options.get('one_off', False)),
|
||||
port_bindings=port_bindings,
|
||||
binds=volume_bindings,
|
||||
volumes_from=self._get_volumes_from(intermediate_container),
|
||||
privileged=privileged,
|
||||
network_mode=net,
|
||||
dns=dns,
|
||||
dns_search=dns_search,
|
||||
restart_policy=restart,
|
||||
cap_add=cap_add,
|
||||
cap_drop=cap_drop,
|
||||
)
|
||||
return container
|
||||
|
||||
def start_or_create_containers(
|
||||
self,
|
||||
insecure_registry=False,
|
||||
detach=False,
|
||||
do_build=True):
|
||||
containers = self.containers(stopped=True)
|
||||
|
||||
if not containers:
|
||||
log.info("Creating %s..." % self._next_container_name(containers))
|
||||
new_container = self.create_container(
|
||||
insecure_registry=insecure_registry,
|
||||
detach=detach,
|
||||
do_build=do_build,
|
||||
)
|
||||
return [self.start_container(new_container)]
|
||||
else:
|
||||
return [self.start_container_if_stopped(c) for c in containers]
|
||||
|
||||
def get_linked_names(self):
|
||||
return [s.name for (s, _) in self.links]
|
||||
|
||||
def _next_container_name(self, all_containers, one_off=False):
|
||||
bits = [self.project, self.name]
|
||||
if one_off:
|
||||
bits.append('run')
|
||||
return '_'.join(bits + [str(self._next_container_number(all_containers))])
|
||||
|
||||
def _next_container_number(self, all_containers):
|
||||
numbers = [parse_name(c.name).number for c in all_containers]
|
||||
return 1 if not numbers else max(numbers) + 1
|
||||
|
||||
def _get_links(self, link_to_self):
|
||||
links = []
|
||||
for service, link_name in self.links:
|
||||
for container in service.containers():
|
||||
links.append((container.name, link_name or service.name))
|
||||
links.append((container.name, container.name))
|
||||
links.append((container.name, container.name_without_project))
|
||||
if link_to_self:
|
||||
for container in self.containers():
|
||||
links.append((container.name, self.name))
|
||||
links.append((container.name, container.name))
|
||||
links.append((container.name, container.name_without_project))
|
||||
for external_link in self.external_links:
|
||||
if ':' not in external_link:
|
||||
link_name = external_link
|
||||
else:
|
||||
external_link, link_name = external_link.split(':')
|
||||
links.append((external_link, link_name))
|
||||
return links
|
||||
|
||||
def _get_volumes_from(self, intermediate_container=None):
|
||||
volumes_from = []
|
||||
for volume_source in self.volumes_from:
|
||||
if isinstance(volume_source, Service):
|
||||
containers = volume_source.containers(stopped=True)
|
||||
|
||||
if not containers:
|
||||
volumes_from.append(volume_source.create_container().id)
|
||||
else:
|
||||
volumes_from.extend(map(attrgetter('id'), containers))
|
||||
|
||||
elif isinstance(volume_source, Container):
|
||||
volumes_from.append(volume_source.id)
|
||||
|
||||
if intermediate_container:
|
||||
volumes_from.append(intermediate_container.id)
|
||||
|
||||
return volumes_from
|
||||
|
||||
def _get_container_create_options(self, override_options, one_off=False):
|
||||
container_options = dict(
|
||||
(k, self.options[k])
|
||||
for k in DOCKER_CONFIG_KEYS if k in self.options)
|
||||
container_options.update(override_options)
|
||||
|
||||
container_options['name'] = self._next_container_name(
|
||||
self.containers(stopped=True, one_off=one_off),
|
||||
one_off)
|
||||
|
||||
# If a qualified hostname was given, split it into an
|
||||
# unqualified hostname and a domainname unless domainname
|
||||
# was also given explicitly. This matches the behavior of
|
||||
# the official Docker CLI in that scenario.
|
||||
if ('hostname' in container_options
|
||||
and 'domainname' not in container_options
|
||||
and '.' in container_options['hostname']):
|
||||
parts = container_options['hostname'].partition('.')
|
||||
container_options['hostname'] = parts[0]
|
||||
container_options['domainname'] = parts[2]
|
||||
|
||||
if 'ports' in container_options or 'expose' in self.options:
|
||||
ports = []
|
||||
all_ports = container_options.get('ports', []) + self.options.get('expose', [])
|
||||
for port in all_ports:
|
||||
port = str(port)
|
||||
if ':' in port:
|
||||
port = port.split(':')[-1]
|
||||
if '/' in port:
|
||||
port = tuple(port.split('/'))
|
||||
ports.append(port)
|
||||
container_options['ports'] = ports
|
||||
|
||||
if 'volumes' in container_options:
|
||||
container_options['volumes'] = dict(
|
||||
(parse_volume_spec(v).internal, {})
|
||||
for v in container_options['volumes'])
|
||||
|
||||
container_options['environment'] = merge_environment(container_options)
|
||||
|
||||
if self.can_be_built():
|
||||
container_options['image'] = self.full_name
|
||||
else:
|
||||
container_options['image'] = self._get_image_name(container_options['image'])
|
||||
|
||||
# Delete options which are only used when starting
|
||||
for key in DOCKER_START_KEYS:
|
||||
container_options.pop(key, None)
|
||||
|
||||
return container_options
|
||||
|
||||
def _get_image_name(self, image):
|
||||
repo, tag = parse_repository_tag(image)
|
||||
if tag == "":
|
||||
tag = "latest"
|
||||
return '%s:%s' % (repo, tag)
|
||||
|
||||
def build(self, no_cache=False):
|
||||
log.info('Building %s...' % self.name)
|
||||
|
||||
build_output = self.client.build(
|
||||
self.options['build'],
|
||||
tag=self.full_name,
|
||||
stream=True,
|
||||
rm=True,
|
||||
nocache=no_cache,
|
||||
)
|
||||
|
||||
try:
|
||||
all_events = stream_output(build_output, sys.stdout)
|
||||
except StreamOutputError, e:
|
||||
raise BuildError(self, unicode(e))
|
||||
|
||||
image_id = None
|
||||
|
||||
for event in all_events:
|
||||
if 'stream' in event:
|
||||
match = re.search(r'Successfully built ([0-9a-f]+)', event.get('stream', ''))
|
||||
if match:
|
||||
image_id = match.group(1)
|
||||
|
||||
if image_id is None:
|
||||
raise BuildError(self, event if all_events else 'Unknown')
|
||||
|
||||
return image_id
|
||||
|
||||
def can_be_built(self):
|
||||
return 'build' in self.options
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
"""
|
||||
The tag to give to images built for this service.
|
||||
"""
|
||||
return '%s_%s' % (self.project, self.name)
|
||||
|
||||
def can_be_scaled(self):
|
||||
for port in self.options.get('ports', []):
|
||||
if ':' in str(port):
|
||||
return False
|
||||
return True
|
||||
|
||||
def pull(self, insecure_registry=False):
|
||||
if 'image' in self.options:
|
||||
image_name = self._get_image_name(self.options['image'])
|
||||
log.info('Pulling %s (%s)...' % (self.name, image_name))
|
||||
self.client.pull(
|
||||
image_name,
|
||||
insecure_registry=insecure_registry
|
||||
)
|
||||
|
||||
|
||||
NAME_RE = re.compile(r'^([^_]+)_([^_]+)_(run_)?(\d+)$')
|
||||
|
||||
|
||||
def is_valid_name(name, one_off=False):
|
||||
match = NAME_RE.match(name)
|
||||
if match is None:
|
||||
return False
|
||||
if one_off:
|
||||
return match.group(3) == 'run_'
|
||||
else:
|
||||
return match.group(3) is None
|
||||
|
||||
|
||||
def parse_name(name):
|
||||
match = NAME_RE.match(name)
|
||||
(project, service_name, _, suffix) = match.groups()
|
||||
return ServiceName(project, service_name, int(suffix))
|
||||
|
||||
|
||||
def parse_restart_spec(restart_config):
|
||||
if not restart_config:
|
||||
return None
|
||||
parts = restart_config.split(':')
|
||||
if len(parts) > 2:
|
||||
raise ConfigError("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 parse_volume_spec(volume_config):
|
||||
parts = volume_config.split(':')
|
||||
if len(parts) > 3:
|
||||
raise ConfigError("Volume %s has incorrect format, should be "
|
||||
"external:internal[:mode]" % volume_config)
|
||||
|
||||
if len(parts) == 1:
|
||||
return VolumeSpec(None, parts[0], 'rw')
|
||||
|
||||
if len(parts) == 2:
|
||||
parts.append('rw')
|
||||
|
||||
external, internal, mode = parts
|
||||
if mode not in ('rw', 'ro'):
|
||||
raise ConfigError("Volume %s has invalid mode (%s), should be "
|
||||
"one of: rw, ro." % (volume_config, mode))
|
||||
|
||||
return VolumeSpec(external, internal, mode)
|
||||
|
||||
|
||||
def parse_repository_tag(s):
|
||||
if ":" not in s:
|
||||
return s, ""
|
||||
repo, tag = s.rsplit(":", 1)
|
||||
if "/" in tag:
|
||||
return s, ""
|
||||
return repo, tag
|
||||
|
||||
|
||||
def build_volume_binding(volume_spec):
|
||||
internal = {'bind': volume_spec.internal, 'ro': volume_spec.mode == 'ro'}
|
||||
external = os.path.expanduser(volume_spec.external)
|
||||
return os.path.abspath(os.path.expandvars(external)), internal
|
||||
|
||||
|
||||
def build_port_bindings(ports):
|
||||
port_bindings = {}
|
||||
for port in ports:
|
||||
internal_port, external = split_port(port)
|
||||
if internal_port in port_bindings:
|
||||
port_bindings[internal_port].append(external)
|
||||
else:
|
||||
port_bindings[internal_port] = [external]
|
||||
return port_bindings
|
||||
|
||||
|
||||
def split_port(port):
|
||||
parts = str(port).split(':')
|
||||
if not 1 <= len(parts) <= 3:
|
||||
raise ConfigError('Invalid port "%s", should be '
|
||||
'[[remote_ip:]remote_port:]port[/protocol]' % port)
|
||||
|
||||
if len(parts) == 1:
|
||||
internal_port, = parts
|
||||
return internal_port, None
|
||||
if len(parts) == 2:
|
||||
external_port, internal_port = parts
|
||||
return internal_port, external_port
|
||||
|
||||
external_ip, external_port, internal_port = parts
|
||||
return internal_port, (external_ip, external_port or None)
|
||||
|
||||
|
||||
def get_env_files(options):
|
||||
env_files = options.get('env_file', [])
|
||||
if not isinstance(env_files, list):
|
||||
env_files = [env_files]
|
||||
return env_files
|
||||
|
||||
|
||||
def merge_environment(options):
|
||||
env = {}
|
||||
|
||||
for f in get_env_files(options):
|
||||
env.update(env_vars_from_file(f))
|
||||
|
||||
if 'environment' in options:
|
||||
if isinstance(options['environment'], list):
|
||||
env.update(dict(split_env(e) for e in options['environment']))
|
||||
else:
|
||||
env.update(options['environment'])
|
||||
|
||||
return dict(resolve_env(k, v) for k, v in env.iteritems())
|
||||
|
||||
|
||||
def split_env(env):
|
||||
if '=' in env:
|
||||
return env.split('=', 1)
|
||||
else:
|
||||
return env, None
|
||||
|
||||
|
||||
def resolve_env(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.
|
||||
"""
|
||||
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
|
||||
326
contrib/completion/bash/docker-compose
Normal file
326
contrib/completion/bash/docker-compose
Normal file
@@ -0,0 +1,326 @@
|
||||
#!bash
|
||||
#
|
||||
# bash completion for docker-compose
|
||||
#
|
||||
# This work is based on the completion for the docker command.
|
||||
#
|
||||
# This script provides completion of:
|
||||
# - commands and their options
|
||||
# - service names
|
||||
# - filepaths
|
||||
#
|
||||
# To enable the completions either:
|
||||
# - place this file in /etc/bash_completion.d
|
||||
# or
|
||||
# - copy this file to e.g. ~/.docker-compose-completion.sh and add the line
|
||||
# below to your .bashrc after bash completion features are loaded
|
||||
# . ~/.docker-compose-completion.sh
|
||||
|
||||
|
||||
# For compatibility reasons, Compose and therefore its completion supports several
|
||||
# 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
|
||||
echo docker-compose.yml
|
||||
}
|
||||
|
||||
# Extracts all service names from the compose file.
|
||||
___docker-compose_all_services_in_compose_file() {
|
||||
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
|
||||
__docker-compose_services_all() {
|
||||
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
|
||||
___docker-compose_services_with_key() {
|
||||
# flatten sections to one line, then filter lines containing the key and return section name.
|
||||
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}'
|
||||
}
|
||||
|
||||
# All services that are defined by a Dockerfile reference
|
||||
__docker-compose_services_from_build() {
|
||||
COMPREPLY=( $(compgen -W "$(___docker-compose_services_with_key build)" -- "$cur") )
|
||||
}
|
||||
|
||||
# All services that are defined by an image
|
||||
__docker-compose_services_from_image() {
|
||||
COMPREPLY=( $(compgen -W "$(___docker-compose_services_with_key image)" -- "$cur") )
|
||||
}
|
||||
|
||||
# The services for which containers have been created, optionally filtered
|
||||
# by a boolean expression passed in as argument.
|
||||
__docker-compose_services_with() {
|
||||
local containers names
|
||||
containers="$(docker-compose 2>/dev/null ${compose_file:+-f $compose_file} ${compose_project:+-p $compose_project} ps -q)"
|
||||
names=( $(docker 2>/dev/null inspect --format "{{if ${1:-true}}} {{ .Name }} {{end}}" $containers) )
|
||||
names=( ${names[@]%_*} ) # strip trailing numbers
|
||||
names=( ${names[@]#*_} ) # strip project name
|
||||
COMPREPLY=( $(compgen -W "${names[*]}" -- "$cur") )
|
||||
}
|
||||
|
||||
# The services for which at least one running container exists
|
||||
__docker-compose_services_running() {
|
||||
__docker-compose_services_with '.State.Running'
|
||||
}
|
||||
|
||||
# The services for which at least one stopped container exists
|
||||
__docker-compose_services_stopped() {
|
||||
__docker-compose_services_with 'not .State.Running'
|
||||
}
|
||||
|
||||
|
||||
_docker-compose_build() {
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=( $( compgen -W "--no-cache" -- "$cur" ) )
|
||||
;;
|
||||
*)
|
||||
__docker-compose_services_from_build
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
_docker-compose_docker-compose() {
|
||||
case "$prev" in
|
||||
--file|-f)
|
||||
_filedir
|
||||
return
|
||||
;;
|
||||
--project-name|-p)
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=( $( compgen -W "--help -h --verbose --version --file -f --project-name -p" -- "$cur" ) )
|
||||
;;
|
||||
*)
|
||||
COMPREPLY=( $( compgen -W "${commands[*]}" -- "$cur" ) )
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
_docker-compose_help() {
|
||||
COMPREPLY=( $( compgen -W "${commands[*]}" -- "$cur" ) )
|
||||
}
|
||||
|
||||
|
||||
_docker-compose_kill() {
|
||||
case "$prev" in
|
||||
-s)
|
||||
COMPREPLY=( $( compgen -W "SIGHUP SIGINT SIGKILL SIGUSR1 SIGUSR2" -- "$(echo $cur | tr '[:lower:]' '[:upper:]')" ) )
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=( $( compgen -W "-s" -- "$cur" ) )
|
||||
;;
|
||||
*)
|
||||
__docker-compose_services_running
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
_docker-compose_logs() {
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=( $( compgen -W "--no-color" -- "$cur" ) )
|
||||
;;
|
||||
*)
|
||||
__docker-compose_services_all
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
_docker-compose_port() {
|
||||
case "$prev" in
|
||||
--protocol)
|
||||
COMPREPLY=( $( compgen -W "tcp udp" -- "$cur" ) )
|
||||
return;
|
||||
;;
|
||||
--index)
|
||||
return;
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=( $( compgen -W "--protocol --index" -- "$cur" ) )
|
||||
;;
|
||||
*)
|
||||
__docker-compose_services_all
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
_docker-compose_ps() {
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=( $( compgen -W "-q" -- "$cur" ) )
|
||||
;;
|
||||
*)
|
||||
__docker-compose_services_all
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
_docker-compose_pull() {
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=( $( compgen -W "--allow-insecure-ssl" -- "$cur" ) )
|
||||
;;
|
||||
*)
|
||||
__docker-compose_services_from_image
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
_docker-compose_restart() {
|
||||
__docker-compose_services_running
|
||||
}
|
||||
|
||||
|
||||
_docker-compose_rm() {
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=( $( compgen -W "--force -v" -- "$cur" ) )
|
||||
;;
|
||||
*)
|
||||
__docker-compose_services_stopped
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
_docker-compose_run() {
|
||||
case "$prev" in
|
||||
-e)
|
||||
COMPREPLY=( $( compgen -e -- "$cur" ) )
|
||||
compopt -o nospace
|
||||
return
|
||||
;;
|
||||
--entrypoint)
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=( $( compgen -W "--allow-insecure-ssl -d --entrypoint -e --no-deps --rm --service-ports -T" -- "$cur" ) )
|
||||
;;
|
||||
*)
|
||||
__docker-compose_services_all
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
_docker-compose_scale() {
|
||||
case "$prev" in
|
||||
=)
|
||||
COMPREPLY=("$cur")
|
||||
;;
|
||||
*)
|
||||
COMPREPLY=( $(compgen -S "=" -W "$(___docker-compose_all_services_in_compose_file)" -- "$cur") )
|
||||
compopt -o nospace
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
_docker-compose_start() {
|
||||
__docker-compose_services_stopped
|
||||
}
|
||||
|
||||
|
||||
_docker-compose_stop() {
|
||||
__docker-compose_services_running
|
||||
}
|
||||
|
||||
|
||||
_docker-compose_up() {
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=( $( compgen -W "--allow-insecure-ssl -d --no-build --no-color --no-deps --no-recreate" -- "$cur" ) )
|
||||
;;
|
||||
*)
|
||||
__docker-compose_services_all
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
_docker-compose() {
|
||||
local commands=(
|
||||
build
|
||||
help
|
||||
kill
|
||||
logs
|
||||
port
|
||||
ps
|
||||
pull
|
||||
restart
|
||||
rm
|
||||
run
|
||||
scale
|
||||
start
|
||||
stop
|
||||
up
|
||||
)
|
||||
|
||||
COMPREPLY=()
|
||||
local cur prev words cword
|
||||
_get_comp_words_by_ref -n : cur prev words cword
|
||||
|
||||
# search subcommand and invoke its handler.
|
||||
# special treatment of some top-level options
|
||||
local command='docker-compose'
|
||||
local counter=1
|
||||
local compose_file compose_project
|
||||
while [ $counter -lt $cword ]; do
|
||||
case "${words[$counter]}" in
|
||||
-f|--file)
|
||||
(( counter++ ))
|
||||
compose_file="${words[$counter]}"
|
||||
;;
|
||||
-p|--project-name)
|
||||
(( counter++ ))
|
||||
compose_project="${words[$counter]}"
|
||||
;;
|
||||
-*)
|
||||
;;
|
||||
*)
|
||||
command="${words[$counter]}"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
(( counter++ ))
|
||||
done
|
||||
|
||||
local completions_func=_docker-compose_${command}
|
||||
declare -F $completions_func >/dev/null && $completions_func
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
complete -F _docker-compose docker-compose
|
||||
@@ -1 +0,0 @@
|
||||
/_site
|
||||
@@ -1 +0,0 @@
|
||||
www.fig.sh
|
||||
@@ -1,10 +1,15 @@
|
||||
FROM ubuntu:13.10
|
||||
RUN apt-get -qq update && apt-get install -y ruby1.8 bundler python
|
||||
RUN locale-gen en_US.UTF-8
|
||||
ADD Gemfile /code/
|
||||
ADD Gemfile.lock /code/
|
||||
WORKDIR /code
|
||||
RUN bundle install
|
||||
ADD . /code
|
||||
EXPOSE 4000
|
||||
CMD bundle exec jekyll build
|
||||
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,3 +0,0 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem 'github-pages'
|
||||
@@ -1,62 +0,0 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
RedCloth (4.2.9)
|
||||
blankslate (2.1.2.4)
|
||||
classifier (1.3.3)
|
||||
fast-stemmer (>= 1.0.0)
|
||||
colorator (0.1)
|
||||
commander (4.1.5)
|
||||
highline (~> 1.6.11)
|
||||
fast-stemmer (1.0.2)
|
||||
ffi (1.9.3)
|
||||
github-pages (12)
|
||||
RedCloth (= 4.2.9)
|
||||
jekyll (= 1.4.2)
|
||||
kramdown (= 1.2.0)
|
||||
liquid (= 2.5.4)
|
||||
maruku (= 0.7.0)
|
||||
rdiscount (= 2.1.7)
|
||||
redcarpet (= 2.3.0)
|
||||
highline (1.6.20)
|
||||
jekyll (1.4.2)
|
||||
classifier (~> 1.3)
|
||||
colorator (~> 0.1)
|
||||
commander (~> 4.1.3)
|
||||
liquid (~> 2.5.2)
|
||||
listen (~> 1.3)
|
||||
maruku (~> 0.7.0)
|
||||
pygments.rb (~> 0.5.0)
|
||||
redcarpet (~> 2.3.0)
|
||||
safe_yaml (~> 0.9.7)
|
||||
toml (~> 0.1.0)
|
||||
kramdown (1.2.0)
|
||||
liquid (2.5.4)
|
||||
listen (1.3.1)
|
||||
rb-fsevent (>= 0.9.3)
|
||||
rb-inotify (>= 0.9)
|
||||
rb-kqueue (>= 0.2)
|
||||
maruku (0.7.0)
|
||||
parslet (1.5.0)
|
||||
blankslate (~> 2.0)
|
||||
posix-spawn (0.3.8)
|
||||
pygments.rb (0.5.4)
|
||||
posix-spawn (~> 0.3.6)
|
||||
yajl-ruby (~> 1.1.0)
|
||||
rb-fsevent (0.9.4)
|
||||
rb-inotify (0.9.3)
|
||||
ffi (>= 0.5.0)
|
||||
rb-kqueue (0.2.0)
|
||||
ffi (>= 0.5.0)
|
||||
rdiscount (2.1.7)
|
||||
redcarpet (2.3.0)
|
||||
safe_yaml (0.9.7)
|
||||
toml (0.1.0)
|
||||
parslet (~> 1.5.0)
|
||||
yajl-ruby (1.1.0)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
github-pages
|
||||
@@ -1,3 +0,0 @@
|
||||
markdown: redcarpet
|
||||
encoding: utf-8
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-gb">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{{ page.title }}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href='http://fonts.googleapis.com/css?family=Lilita+One|Lato:300,400,700' rel='stylesheet' type='text/css'>
|
||||
<link rel="stylesheet" type="text/css" href="css/bootstrap.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="css/fig.css?{{ site.time | date:'%Y%m%d%U%H%N%S' }}">
|
||||
<link rel="canonical" href="http://www.fig.sh{% if page.url =="/index.html" %}/{% else %}{{ page.url }}{% endif %}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="logo mobile-logo">
|
||||
<a href="index.html">
|
||||
<img src="img/logo.png">
|
||||
Fig
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="content">{{ content }}</div>
|
||||
|
||||
<div class="sidebar">
|
||||
<h1 class="logo">
|
||||
<a href="index.html">
|
||||
<img src="img/logo.png">
|
||||
Fig
|
||||
</a>
|
||||
</h1>
|
||||
|
||||
<ul class="nav">
|
||||
<li><a href="index.html">Home</a></li>
|
||||
<li><a href="install.html">Install</a></li>
|
||||
<li><a href="rails.html">Get started with Rails</a></li>
|
||||
<li><a href="django.html">Get started with Django</a></li>
|
||||
<li><a href="wordpress.html">Get started with Wordpress</a></li>
|
||||
</ul>
|
||||
<ul class="nav">
|
||||
<li>Reference:</li>
|
||||
<ul>
|
||||
<li><a href="yml.html">fig.yml</a></li>
|
||||
<li><a href="cli.html">Commands</a></li>
|
||||
<li><a href="env.html">Environment variables</a></li>
|
||||
</ul>
|
||||
</ul>
|
||||
<ul class="nav">
|
||||
<li><a href="https://github.com/orchardup/fig">Fig on GitHub</a></li>
|
||||
<li><a href="http://webchat.freenode.net/?channels=%23orchardup&uio=d4">#orchardup on Freenode</a></li>
|
||||
</ul>
|
||||
|
||||
<p>Fig is a project from <a href="https://www.orchardup.com">Orchard</a>, a Docker hosting service.</p>
|
||||
<p><a href="https://twitter.com/orchardup">Follow us on Twitter</a> to keep up to date with Fig and other Docker news.</p>
|
||||
|
||||
<div class="badges">
|
||||
<iframe src="http://ghbtns.com/github-btn.html?user=orchardup&repo=fig&type=watch&count=true" allowtransparency="true" frameborder="0" scrolling="0" width="100" height="20"></iframe>
|
||||
<a href="https://twitter.com/share" class="twitter-share-button" data-url="http://orchardup.github.io/fig/">Tweet</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');</script>
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', 'UA-43996733-3', 'orchardup.github.io');
|
||||
ga('send', 'pageview');
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
214
docs/cli.md
214
docs/cli.md
@@ -1,87 +1,181 @@
|
||||
---
|
||||
layout: default
|
||||
title: Fig CLI reference
|
||||
---
|
||||
|
||||
CLI reference
|
||||
=============
|
||||
|
||||
Most commands are run against one or more services. If the service is omitted, it will apply to all services.
|
||||
|
||||
Run `fig [COMMAND] --help` for full usage.
|
||||
|
||||
## build
|
||||
|
||||
Build or rebuild services.
|
||||
|
||||
Services are built once and then tagged as `project_service`, e.g. `figtest_db`. If you change a service's `Dockerfile` or the contents of its build directory, you can run `fig build` to rebuild it.
|
||||
|
||||
## help
|
||||
|
||||
Get help on a command.
|
||||
|
||||
## kill
|
||||
|
||||
Force stop service containers.
|
||||
|
||||
## logs
|
||||
|
||||
View output from services.
|
||||
|
||||
## ps
|
||||
|
||||
List containers.
|
||||
|
||||
## rm
|
||||
|
||||
Remove stopped service containers.
|
||||
page_title: Compose CLI reference
|
||||
page_description: Compose CLI reference
|
||||
page_keywords: fig, composition, compose, docker, orchestration, cli, reference
|
||||
|
||||
|
||||
## run
|
||||
# CLI reference
|
||||
|
||||
Run a one-off command on a service.
|
||||
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 example:
|
||||
For full usage information, run `docker-compose [COMMAND] --help`.
|
||||
|
||||
$ fig run web python manage.py shell
|
||||
## Commands
|
||||
|
||||
By default, linked services will be started, unless they are already running.
|
||||
### build
|
||||
|
||||
One-off commands are started in new containers with the same config as a normal container for that service, so volumes, links, etc will all be created as expected. The only thing different to a normal container is the command will be overridden with the one specified and no ports will be created in case they collide.
|
||||
Builds or rebuilds services.
|
||||
|
||||
Links are also created between one-off commands and the other containers for that service so you can do stuff like this:
|
||||
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.
|
||||
|
||||
$ fig run db /bin/sh -c "psql -h \$DB_1_PORT_5432_TCP_ADDR -U docker"
|
||||
### help
|
||||
|
||||
If you do not want linked containers to be started when running the one-off command, specify the `--no-deps` flag:
|
||||
Displays help and usage instructions for a command.
|
||||
|
||||
$ fig run --no-deps web python manage.py shell
|
||||
### kill
|
||||
|
||||
## scale
|
||||
Forces running containers to stop by sending a `SIGKILL` signal. Optionally the
|
||||
signal can be passed, for example:
|
||||
|
||||
Set number of containers to run for a service.
|
||||
$ docker-compose kill -s SIGINT
|
||||
|
||||
Numbers are specified in the form `service=num` as arguments.
|
||||
For example:
|
||||
### logs
|
||||
|
||||
$ fig scale web=2 worker=3
|
||||
Displays log output from services.
|
||||
|
||||
## start
|
||||
### port
|
||||
|
||||
Start existing containers for a service.
|
||||
Prints the public port for a port binding
|
||||
|
||||
## stop
|
||||
### ps
|
||||
|
||||
Stop running containers without removing them. They can be started again with `fig start`.
|
||||
Lists containers.
|
||||
|
||||
## up
|
||||
### pull
|
||||
|
||||
Build, (re)create, start and attach to containers for a service.
|
||||
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, `fig up` will aggregate the output of each container, and when it exits, all containers will be stopped. If you run `fig up -d`, it'll start the containers in the background and leave them 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, `fig up` will stop and recreate them (preserving mounted volumes with [volumes-from]), so that changes in `fig.yml` are picked up. If you do no want containers to be stopped and recreated, use `fig up --no-recreate`. This will still start any stopped containers, if needed.
|
||||
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.1.0/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)
|
||||
7
docs/css/bootstrap.min.css
vendored
7
docs/css/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
187
docs/css/fig.css
187
docs/css/fig.css
@@ -1,187 +0,0 @@
|
||||
body {
|
||||
padding-top: 20px;
|
||||
padding-bottom: 60px;
|
||||
font-family: 'Lato', sans-serif;
|
||||
font-weight: 300;
|
||||
font-size: 18px;
|
||||
color: #362;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: 'Lato', sans-serif;
|
||||
font-weight: 400;
|
||||
color: #25594D;
|
||||
}
|
||||
|
||||
h2, h3, h4, h5, h6 {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
a, a:hover, a:visited {
|
||||
color: #4D9900;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
pre, code {
|
||||
border: none;
|
||||
background: #D5E1B4;
|
||||
}
|
||||
|
||||
code, pre code {
|
||||
color: #484F40;
|
||||
}
|
||||
|
||||
pre {
|
||||
border-bottom: 2px solid #bec9a1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.84em;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: none;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-family: 'Lilita One', sans-serif;
|
||||
font-size: 64px;
|
||||
margin: 20px 0 40px 0;
|
||||
}
|
||||
|
||||
.logo a {
|
||||
color: #a41211;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
width: 60px;
|
||||
vertical-align: -8px;
|
||||
}
|
||||
|
||||
.mobile-logo {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
font-size: 15px;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.sidebar a {
|
||||
color: #a41211;
|
||||
}
|
||||
|
||||
.sidebar p {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.sidebar {
|
||||
text-align: center;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.sidebar .logo {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.mobile-logo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.content h1 {
|
||||
margin: 60px 0 55px 0;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 280px;
|
||||
overflow-y: auto;
|
||||
padding-left: 40px;
|
||||
padding-right: 10px;
|
||||
border-right: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-left: 320px;
|
||||
max-width: 650px;
|
||||
}
|
||||
}
|
||||
|
||||
.nav {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.nav li a {
|
||||
display: block;
|
||||
padding: 5px 0;
|
||||
line-height: 1.2;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav li a:hover, .nav li a:focus {
|
||||
text-decoration: underline;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.nav ul {
|
||||
padding-left: 20px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.badges {
|
||||
margin: 40px 0;
|
||||
}
|
||||
|
||||
a.btn {
|
||||
background: #25594D;
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.btn:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.strapline {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.strapline {
|
||||
font-size: 40px;
|
||||
display: block;
|
||||
line-height: 1.2;
|
||||
margin-top: 25px;
|
||||
margin-bottom: 35px;
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -1,18 +1,17 @@
|
||||
---
|
||||
layout: default
|
||||
title: Getting started with Fig and Django
|
||||
title: Getting started with Compose and Django
|
||||
---
|
||||
|
||||
Getting started with Fig and Django
|
||||
Getting started with Compose and Django
|
||||
===================================
|
||||
|
||||
Let's use Fig to set up and run a Django/PostgreSQL app. Before starting, you'll need to have [Fig installed](install.html).
|
||||
Let's use Compose to set up and run a Django/PostgreSQL app. Before starting, you'll need to have [Compose installed](install.md).
|
||||
|
||||
Let's set up the three files that'll get us started. First, our app is going to be running inside a Docker container which contains all of its dependencies. We can define what goes inside that Docker container using a file called `Dockerfile`. It'll contain this to start with:
|
||||
|
||||
FROM orchardup/python:2.7
|
||||
FROM python:2.7
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
RUN apt-get update -qq && apt-get install -y python-psycopg2
|
||||
RUN mkdir /code
|
||||
WORKDIR /code
|
||||
ADD requirements.txt /code/
|
||||
@@ -24,11 +23,12 @@ That'll install our application inside an image with Python installed alongside
|
||||
Second, we define our Python dependencies in a file called `requirements.txt`:
|
||||
|
||||
Django
|
||||
psycopg2
|
||||
|
||||
Simple enough. Finally, this is all tied together with a file called `fig.yml`. It describes the services that our app comprises of (a web server and database), what Docker images they use, how they link together, what volumes will be mounted inside the containers and what ports they expose.
|
||||
Simple enough. Finally, this is all tied together with a file called `docker-compose.yml`. It describes the services that our app comprises of (a web server and database), what Docker images they use, how they link together, what volumes will be mounted inside the containers and what ports they expose.
|
||||
|
||||
db:
|
||||
image: orchardup/postgresql
|
||||
image: postgres
|
||||
web:
|
||||
build: .
|
||||
command: python manage.py runserver 0.0.0.0:8000
|
||||
@@ -39,35 +39,34 @@ Simple enough. Finally, this is all tied together with a file called `fig.yml`.
|
||||
links:
|
||||
- db
|
||||
|
||||
See the [`fig.yml` reference](yml.html) for more information on how it works.
|
||||
See the [`docker-compose.yml` reference](yml.html) for more information on how it works.
|
||||
|
||||
We can now start a Django project using `fig run`:
|
||||
We can now start a Django project using `docker-compose run`:
|
||||
|
||||
$ fig run web django-admin.py startproject figexample .
|
||||
$ docker-compose run web django-admin.py startproject composeexample .
|
||||
|
||||
First, Fig will build an image for the `web` service using the `Dockerfile`. It will then run `django-admin.py startproject figexample .` inside a container using that image.
|
||||
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 using that image.
|
||||
|
||||
This will generate a Django app inside the current directory:
|
||||
|
||||
$ ls
|
||||
Dockerfile fig.yml figexample manage.py requirements.txt
|
||||
Dockerfile docker-compose.yml composeexample manage.py requirements.txt
|
||||
|
||||
First thing we need to do is set up the database connection. Replace the `DATABASES = ...` definition in `figexample/settings.py` to read:
|
||||
First thing we need to do is set up the database connection. Replace the `DATABASES = ...` definition in `composeexample/settings.py` to read:
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||
'NAME': 'docker',
|
||||
'USER': 'docker',
|
||||
'PASSWORD': 'docker',
|
||||
'HOST': os.environ.get('DB_1_PORT_5432_TCP_ADDR'),
|
||||
'PORT': os.environ.get('DB_1_PORT_5432_TCP_PORT'),
|
||||
'NAME': 'postgres',
|
||||
'USER': 'postgres',
|
||||
'HOST': 'db',
|
||||
'PORT': 5432,
|
||||
}
|
||||
}
|
||||
|
||||
These settings are determined by the [orchardup/postgresql](https://github.com/orchardup/docker-postgresql) Docker image we are using.
|
||||
These settings are determined by the [postgres](https://registry.hub.docker.com/_/postgres/) Docker image we are using.
|
||||
|
||||
Then, run `fig up`:
|
||||
Then, run `docker-compose up`:
|
||||
|
||||
Recreating myapp_db_1...
|
||||
Recreating myapp_web_1...
|
||||
@@ -80,13 +79,21 @@ Then, run `fig up`:
|
||||
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 'figexample.settings'
|
||||
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.
|
||||
|
||||
And your Django app should be running at [localhost:8000](http://localhost:8000) (or [localdocker:8000](http://localdocker:8000) if you're using docker-osx).
|
||||
And your Django app should 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 `fig up` and in another terminal run:
|
||||
You can also run management commands with Docker. To set up your database, for example, run `docker-compose up` and in another terminal run:
|
||||
|
||||
$ fig run web python manage.py syncdb
|
||||
$ docker-compose run web python manage.py syncdb
|
||||
|
||||
## 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)
|
||||
|
||||
28
docs/env.md
28
docs/env.md
@@ -1,31 +1,41 @@
|
||||
---
|
||||
layout: default
|
||||
title: Fig environment variables reference
|
||||
title: Compose environment variables reference
|
||||
---
|
||||
|
||||
Environment variables reference
|
||||
===============================
|
||||
|
||||
Fig 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.
|
||||
**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.
|
||||
|
||||
To see what environment variables are available to a service, run `fig run SERVICE env`.
|
||||
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_1_PORT=tcp://172.17.0.5:5432`
|
||||
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_1_PORT_5432_TCP=tcp://172.17.0.5:5432`
|
||||
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_1_PORT_5432_TCP_ADDR=172.17.0.5`
|
||||
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_1_PORT_5432_TCP_PORT=5432`
|
||||
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_1_PORT_5432_TCP_PROTO=tcp`
|
||||
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.io/en/latest/use/port_redirection/#linking-a-container
|
||||
[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)
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
jekyll:
|
||||
build: .
|
||||
ports:
|
||||
- "4000:4000"
|
||||
volumes:
|
||||
- .:/code
|
||||
environment:
|
||||
- LANG=en_US.UTF-8
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 130 KiB |
175
docs/index.md
175
docs/index.md
@@ -1,68 +1,88 @@
|
||||
---
|
||||
layout: default
|
||||
title: Fig | Fast, isolated development environments using Docker
|
||||
---
|
||||
page_title: Compose: Multi-container orchestration for Docker
|
||||
page_description: Introduction and Overview of Compose
|
||||
page_keywords: documentation, docs, docker, compose, orchestration, containers
|
||||
|
||||
<strong class="strapline">Fast, isolated development environments using Docker.</strong>
|
||||
|
||||
Define your app's environment with Docker so it can be reproduced anywhere:
|
||||
# Docker Compose
|
||||
|
||||
FROM orchardup/python:2.7
|
||||
ADD . /code
|
||||
WORKDIR /code
|
||||
RUN pip install -r requirements.txt
|
||||
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.
|
||||
|
||||
Define the services that make up your app so they can be run together in an isolated environment:
|
||||
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: .
|
||||
command: python app.py
|
||||
links:
|
||||
- db
|
||||
ports:
|
||||
- "8000:8000"
|
||||
db:
|
||||
image: orchardup/postgresql
|
||||
image: postgres
|
||||
```
|
||||
|
||||
(No more installing Postgres on your laptop!)
|
||||
Lastly, run `docker-compose up` and Compose will start and run your entire app.
|
||||
|
||||
Then type `fig up`, and Fig 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
|
||||
|
||||
There are commands to:
|
||||
## Compose documentation
|
||||
|
||||
- start, stop and rebuild services
|
||||
- view the status of running services
|
||||
- tail running services' log output
|
||||
- run a one-off command on a service
|
||||
- [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
|
||||
|
||||
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.
|
||||
|
||||
Let's get a basic Python web app running on Fig. It assumes a little knowledge of Python, but the concepts should be clear if you're not familiar with it.
|
||||
### Installation and set-up
|
||||
|
||||
First, [install Docker and Fig](install.html).
|
||||
First, [install Docker and Compose](install.md).
|
||||
|
||||
You'll want to make a directory for the project:
|
||||
Next, you'll want to make a directory for the project:
|
||||
|
||||
$ mkdir figtest
|
||||
$ cd figtest
|
||||
$ 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:
|
||||
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=os.environ.get('REDIS_1_PORT_6379_TCP_ADDR'),
|
||||
port=int(os.environ.get('REDIS_1_PORT_6379_TCP_PORT'))
|
||||
)
|
||||
redis = Redis(host='redis', port=6379)
|
||||
|
||||
@app.route('/')
|
||||
def hello():
|
||||
@@ -73,21 +93,32 @@ if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", debug=True)
|
||||
```
|
||||
|
||||
We define our Python dependencies in a file called `requirements.txt`:
|
||||
Next, define the Python dependencies in a file called `requirements.txt`:
|
||||
|
||||
flask
|
||||
redis
|
||||
|
||||
Next, we want to create a Docker image containing all of our app's dependencies. We specify how to build one using a file called `Dockerfile`:
|
||||
### Create a Docker image
|
||||
|
||||
FROM orchardup/python:2.7
|
||||
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 install Python, our code and our Python dependencies inside 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/).
|
||||
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/).
|
||||
|
||||
We then define a set of services using `fig.yml`:
|
||||
### Define services
|
||||
|
||||
Next, define a set of services using `docker-compose.yml`:
|
||||
|
||||
web:
|
||||
build: .
|
||||
@@ -99,45 +130,61 @@ We then define a set of services using `fig.yml`:
|
||||
links:
|
||||
- redis
|
||||
redis:
|
||||
image: orchardup/redis
|
||||
image: redis
|
||||
|
||||
This defines two services:
|
||||
|
||||
- `web`, which is built from `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 [orchardup/redis](https://index.docker.io/u/orchardup/redis/).
|
||||
- `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.
|
||||
|
||||
Now if we run `fig up`, it'll pull a Redis image, build an image for our own code, and start everything up:
|
||||
### Build and run your app with Compose
|
||||
|
||||
$ fig up
|
||||
Pulling image orchardup/redis...
|
||||
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 figtest_redis_1...
|
||||
Starting figtest_web_1...
|
||||
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/
|
||||
|
||||
Open up [http://localhost:5000](http://localhost:5000) in your browser (or [http://localdocker:5000](http://localdocker:5000) if you're using [docker-osx](https://github.com/noplay/docker-osx)) and you should see it running!
|
||||
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 to `fig up` and use `fig ps` to see what is currently running:
|
||||
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:
|
||||
|
||||
$ fig up -d
|
||||
Starting figtest_redis_1...
|
||||
Starting figtest_web_1...
|
||||
$ fig ps
|
||||
Name Command State Ports
|
||||
$ docker-compose up -d
|
||||
Starting composetest_redis_1...
|
||||
Starting composetest_web_1...
|
||||
$ docker-compose ps
|
||||
Name Command State Ports
|
||||
-------------------------------------------------------------------
|
||||
figtest_redis_1 /usr/local/bin/run Up
|
||||
figtest_web_1 /bin/sh -c python app.py Up 5000->5000/tcp
|
||||
composetest_redis_1 /usr/local/bin/run Up
|
||||
composetest_web_1 /bin/sh -c python app.py Up 5000->5000/tcp
|
||||
|
||||
`fig run` allows you to run one-off commands for your services. For example, to see what environment variables are available to the `web` service:
|
||||
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:
|
||||
|
||||
$ fig run web env
|
||||
$ 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.
|
||||
|
||||
|
||||
See `fig --help` other commands that are available.
|
||||
|
||||
If you started Fig with `fig up -d`, you'll probably want to stop your services once you've finished with them:
|
||||
|
||||
$ fig stop
|
||||
|
||||
That's more-or-less how Fig works. See the reference section below for full details on the commands, configuration file and environment variables. If you have any thoughts or suggestions, [open an issue on GitHub](https://github.com/orchardup/fig) or [email us](mailto:hello@orchardup.com).
|
||||
|
||||
@@ -1,31 +1,52 @@
|
||||
---
|
||||
layout: default
|
||||
title: Installing Fig
|
||||
---
|
||||
page_title: Installing Compose
|
||||
page_description: How to intall Docker Compose
|
||||
page_keywords: compose, orchestration, install, installation, docker, documentation
|
||||
|
||||
Installing Fig
|
||||
==============
|
||||
|
||||
First, install Docker version 1.0 or greater. If you're on OS X, you can use [docker-osx](https://github.com/noplay/docker-osx):
|
||||
## Installing Compose
|
||||
|
||||
$ curl https://raw.githubusercontent.com/noplay/docker-osx/1.1.1/docker-osx > /usr/local/bin/docker-osx
|
||||
$ chmod +x /usr/local/bin/docker-osx
|
||||
$ docker-osx shell
|
||||
To install Compose, you'll need to install Docker first. You'll then install
|
||||
Compose with a `curl` command.
|
||||
|
||||
Docker has guides for [Ubuntu](http://docs.docker.io/en/latest/installation/ubuntulinux/) and [other platforms](http://docs.docker.io/en/latest/installation/) in their documentation.
|
||||
### Install Docker
|
||||
|
||||
Next, install Fig. On OS X:
|
||||
First, you'll need to install Docker version 1.3 or greater.
|
||||
|
||||
$ curl -L https://github.com/orchardup/fig/releases/download/0.5.2/darwin > /usr/local/bin/fig
|
||||
$ chmod +x /usr/local/bin/fig
|
||||
If you're on OS X, you can use the
|
||||
[OS X installer](https://docs.docker.com/installation/mac/) to install both
|
||||
Docker and the OSX helper app, boot2docker. Once boot2docker is running, set the
|
||||
environment variables that'll configure Docker and Compose to talk to it:
|
||||
|
||||
On 64-bit Linux:
|
||||
$(boot2docker shellinit)
|
||||
|
||||
$ curl -L https://github.com/orchardup/fig/releases/download/0.5.2/linux > /usr/local/bin/fig
|
||||
$ chmod +x /usr/local/bin/fig
|
||||
To persist the environment variables across shell sessions, add the above line
|
||||
to your `~/.bashrc` file.
|
||||
|
||||
Fig is also available as a Python package if you're on another platform (or if you prefer that sort of thing):
|
||||
For complete instructions, or if you are on another platform, consult Docker's
|
||||
[installation instructions](https://docs.docker.com/installation/).
|
||||
|
||||
$ sudo pip install -U fig
|
||||
### Install Compose
|
||||
|
||||
That should be all you need! Run `fig --version` to see if it worked.
|
||||
To install Compose, run the following commands:
|
||||
|
||||
curl -L https://github.com/docker/compose/releases/download/1.1.0/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)
|
||||
|
||||
7
docs/mkdocs.yml
Normal file
7
docs/mkdocs.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
- ['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']
|
||||
@@ -1,16 +1,16 @@
|
||||
---
|
||||
layout: default
|
||||
title: Getting started with Fig and Rails
|
||||
title: Getting started with Compose and Rails
|
||||
---
|
||||
|
||||
Getting started with Fig and Rails
|
||||
Getting started with Compose and Rails
|
||||
==================================
|
||||
|
||||
We're going to use Fig to set up and run a Rails/PostgreSQL app. Before starting, you'll need to have [Fig installed](install.html).
|
||||
We're going to use Compose to set up and run a Rails/PostgreSQL app. Before starting, you'll need to have [Compose installed](install.md).
|
||||
|
||||
Let's set up the three files that'll get us started. First, our app is going to be running inside a Docker container which contains all of its dependencies. We can define what goes inside that Docker container using a file called `Dockerfile`. It'll contain this to start with:
|
||||
|
||||
FROM binaryphile/ruby:2.0.0-p247
|
||||
FROM ruby:2.2.0
|
||||
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev
|
||||
RUN mkdir /myapp
|
||||
WORKDIR /myapp
|
||||
@@ -23,17 +23,17 @@ That'll put our application code inside an image with Ruby, Bundler and all our
|
||||
Next, we have a bootstrap `Gemfile` which just loads Rails. It'll be overwritten in a moment by `rails new`.
|
||||
|
||||
source 'https://rubygems.org'
|
||||
gem 'rails', '4.0.2'
|
||||
gem 'rails', '4.2.0'
|
||||
|
||||
Finally, `fig.yml` is where the magic happens. It describes what services our app comprises (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 we need to link them together and expose the web app's port.
|
||||
Finally, `docker-compose.yml` is where the magic happens. It describes what services our app comprises (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 we need to link them together and expose the web app's port.
|
||||
|
||||
db:
|
||||
image: orchardup/postgresql
|
||||
image: postgres
|
||||
ports:
|
||||
- "5432"
|
||||
web:
|
||||
build: .
|
||||
command: bundle exec rackup -p 3000
|
||||
command: bundle exec rails s -p 3000 -b '0.0.0.0'
|
||||
volumes:
|
||||
- .:/myapp
|
||||
ports:
|
||||
@@ -41,14 +41,14 @@ Finally, `fig.yml` is where the magic happens. It describes what services our ap
|
||||
links:
|
||||
- db
|
||||
|
||||
With those files in place, we can now generate the Rails skeleton app using `fig run`:
|
||||
With those files in place, we can now generate the Rails skeleton app using `docker-compose run`:
|
||||
|
||||
$ fig run web rails new . --force --database=postgresql --skip-bundle
|
||||
$ docker-compose run web rails new . --force --database=postgresql --skip-bundle
|
||||
|
||||
First, Fig 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 a fresh app generated:
|
||||
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 a fresh app generated:
|
||||
|
||||
$ ls
|
||||
Dockerfile app fig.yml tmp
|
||||
Dockerfile app docker-compose.yml tmp
|
||||
Gemfile bin lib vendor
|
||||
Gemfile.lock config log
|
||||
README.rdoc config.ru public
|
||||
@@ -60,21 +60,20 @@ Uncomment the line in your new `Gemfile` which loads `therubyracer`, so we've go
|
||||
|
||||
Now that we've got a new `Gemfile`, we need to build the image again. (This, and changes to the Dockerfile itself, should be the only times you'll need to rebuild).
|
||||
|
||||
$ fig build
|
||||
$ docker-compose build
|
||||
|
||||
The app is now bootable, but we're not quite there yet. By default, Rails expects a database to be running on `localhost` - we need to point it at the `db` container instead. We also need to change the username and password to align with the defaults set by `orchardup/postgresql`.
|
||||
The app is now bootable, but we're not quite there yet. By default, Rails expects a database to be running on `localhost` - we need to point it at the `db` container instead. We 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`. Replace its contents with the following:
|
||||
|
||||
development: &default
|
||||
adapter: postgresql
|
||||
encoding: unicode
|
||||
database: myapp_development
|
||||
database: postgres
|
||||
pool: 5
|
||||
username: docker
|
||||
password: docker
|
||||
host: <%= ENV.fetch('DB_1_PORT_5432_TCP_ADDR', 'localhost') %>
|
||||
port: <%= ENV.fetch('DB_1_PORT_5432_TCP_PORT', '5432') %>
|
||||
username: postgres
|
||||
password:
|
||||
host: db
|
||||
|
||||
test:
|
||||
<<: *default
|
||||
@@ -82,18 +81,25 @@ Open up your newly-generated `database.yml`. Replace its contents with the follo
|
||||
|
||||
We can now boot the app.
|
||||
|
||||
$ fig up
|
||||
$ 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.0.0 (2013-11-22) [x86_64-linux-gnu]
|
||||
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, we just need to create the database. In another terminal, run:
|
||||
|
||||
$ fig run web rake db:create
|
||||
$ docker-compose run web rake db:create
|
||||
|
||||
And we're rolling—see for yourself at [localhost:3000](http://localhost:3000) (or [localdocker:3000](http://localdocker:3000) if you're using docker-osx).
|
||||
And we're rolling—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).
|
||||
|
||||

|
||||
## 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)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
---
|
||||
layout: default
|
||||
title: Getting started with Fig and Wordpress
|
||||
title: Getting started with Compose and Wordpress
|
||||
---
|
||||
|
||||
Getting started with Fig and Wordpress
|
||||
Getting started with Compose and Wordpress
|
||||
======================================
|
||||
|
||||
Fig makes it nice and easy to run Wordpress in an isolated environment. [Install Fig](install.html), then download Wordpress into the current directory:
|
||||
Compose makes it nice and easy to run Wordpress in an isolated environment. [Install Compose](install.md), then download Wordpress into the current directory:
|
||||
|
||||
$ curl http://wordpress.org/wordpress-3.8.1.tar.gz | tar -xvzf -
|
||||
$ curl https://wordpress.org/latest.tar.gz | tar -xvzf -
|
||||
|
||||
This will create a directory called `wordpress`, which you can rename to the name of your project if you wish. Inside that directory, we create `Dockerfile`, a file that defines what environment your app is going to run in:
|
||||
|
||||
@@ -19,7 +19,7 @@ ADD . /code
|
||||
|
||||
This instructs Docker on how to build an image that contains PHP and Wordpress. 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 up, `fig.yml` starts our web service and a separate MySQL instance:
|
||||
Next up, `docker-compose.yml` starts our web service and a separate MySQL instance:
|
||||
|
||||
```
|
||||
web:
|
||||
@@ -37,14 +37,14 @@ db:
|
||||
MYSQL_DATABASE: wordpress
|
||||
```
|
||||
|
||||
Two supporting files are needed to get this working - first up, `wp-config.php` is the standard Wordpress config file with a single change to make it read the MySQL host and port from the environment variables passed in by Fig:
|
||||
Two supporting files are needed to get this working - first up, `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', getenv("DB_1_PORT_3306_TCP_ADDR") . ":" . getenv("DB_1_PORT_3306_TCP_PORT"));
|
||||
define('DB_HOST', "db:3306");
|
||||
define('DB_CHARSET', 'utf8');
|
||||
define('DB_COLLATE', '');
|
||||
|
||||
@@ -88,4 +88,13 @@ if(file_exists($root.$path))
|
||||
}else include_once 'index.php';
|
||||
```
|
||||
|
||||
With those four files in place, run `fig up` inside your Wordpress directory and it'll pull and build the images we need, and then start the web and database containers. You'll then be able to visit Wordpress and set it up by visiting [localhost:8000](http://localhost:8000) - or [localdocker:8000](http://localdocker:8000) if you're using docker-osx.
|
||||
With those four files in place, run `docker-compose up` inside your Wordpress directory and it'll pull and build the images we need, 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).
|
||||
|
||||
## 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)
|
||||
|
||||
138
docs/yml.md
138
docs/yml.md
@@ -1,18 +1,25 @@
|
||||
---
|
||||
layout: default
|
||||
title: fig.yml reference
|
||||
title: docker-compose.yml reference
|
||||
page_title: docker-compose.yml reference
|
||||
page_description: docker-compose.yml reference
|
||||
page_keywords: fig, composition, compose, docker
|
||||
---
|
||||
|
||||
fig.yml reference
|
||||
=================
|
||||
# docker-compose.yml reference
|
||||
|
||||
Each service defined in `fig.yml` must specify exactly one of `image` or `build`. Other keys are optional, and are analogous to their `docker run` command-line counterparts.
|
||||
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 `fig.yml`.
|
||||
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
|
||||
### image
|
||||
|
||||
Tag or partial image ID. Can be local or remote - Fig will attempt to pull if it doesn't exist locally.
|
||||
Tag or partial image ID. Can be local or remote - Compose will attempt to
|
||||
pull if it doesn't exist locally.
|
||||
|
||||
```
|
||||
image: ubuntu
|
||||
@@ -22,7 +29,10 @@ image: a4bc65fd
|
||||
|
||||
### build
|
||||
|
||||
Path to a directory containing a Dockerfile. Fig will build and tag it with a generated name, and use that image thereafter.
|
||||
Path to a directory containing a Dockerfile. 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
|
||||
@@ -36,10 +46,12 @@ Override the default command.
|
||||
command: bundle exec thin -p 3000
|
||||
```
|
||||
|
||||
<a name="links"></a>
|
||||
### links
|
||||
|
||||
|
||||
Link to containers in another service. Optionally specify an alternate name for the link, which will determine how environment variables are prefixed, e.g. `db` -> `DB_1_PORT`, `db:database` -> `DATABASE_1_PORT`
|
||||
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:
|
||||
@@ -48,11 +60,41 @@ links:
|
||||
- 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).
|
||||
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.
|
||||
> **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:
|
||||
@@ -64,7 +106,8 @@ ports:
|
||||
|
||||
### 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 ports without publishing them to the host machine - they'll only be
|
||||
accessible to linked services. Only the internal port can be specified.
|
||||
|
||||
```
|
||||
expose:
|
||||
@@ -74,14 +117,14 @@ expose:
|
||||
|
||||
### volumes
|
||||
|
||||
Mount paths as volumes, optionally specifying a path on the host machine (`HOST:CONTAINER`).
|
||||
|
||||
Note: Mapping local volumes is currently unsupported on boot2docker. We recommend you use [docker-osx](https://github.com/noplay/docker-osx) if want to map local 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
|
||||
@@ -98,7 +141,8 @@ volumes_from:
|
||||
|
||||
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 Fig is running on, which can be helpful for secret or host-specific values.
|
||||
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:
|
||||
@@ -110,6 +154,21 @@ environment:
|
||||
- SESSION_SECRET
|
||||
```
|
||||
|
||||
### env_file
|
||||
|
||||
Add environment variables from a file. Can be a single value or a list.
|
||||
|
||||
Environment variables specified in `environment` override these values.
|
||||
|
||||
```
|
||||
env_file:
|
||||
- .env
|
||||
```
|
||||
|
||||
```
|
||||
RACK_ENV: development
|
||||
```
|
||||
|
||||
### net
|
||||
|
||||
Networking mode. Use the same values as the docker client `--net` parameter.
|
||||
@@ -132,11 +191,39 @@ dns:
|
||||
- 9.9.9.9
|
||||
```
|
||||
|
||||
### working\_dir, entrypoint, user, hostname, domainname, mem\_limit, privileged
|
||||
### cap_add, cap_drop
|
||||
|
||||
Each of these is a single value, analogous to its [docker run](https://docs.docker.com/reference/run/) counterpart.
|
||||
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
|
||||
@@ -146,4 +233,17 @@ 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,99 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from ..packages.docker import Client
|
||||
from requests.exceptions import ConnectionError
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import yaml
|
||||
from ..packages import six
|
||||
|
||||
from ..project import Project
|
||||
from ..service import ConfigError
|
||||
from .docopt_command import DocoptCommand
|
||||
from .formatter import Formatter
|
||||
from .utils import cached_property, docker_url, call_silently, is_mac, is_ubuntu
|
||||
from . import errors
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(DocoptCommand):
|
||||
base_dir = '.'
|
||||
|
||||
def __init__(self):
|
||||
self._yaml_path = os.environ.get('FIG_FILE', None)
|
||||
self.explicit_project_name = None
|
||||
|
||||
def dispatch(self, *args, **kwargs):
|
||||
try:
|
||||
super(Command, self).dispatch(*args, **kwargs)
|
||||
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', 'docker-osx']) == 0:
|
||||
raise errors.ConnectionErrorDockerOSX()
|
||||
else:
|
||||
raise errors.ConnectionErrorGeneric(self.client.base_url)
|
||||
|
||||
def perform_command(self, options, *args, **kwargs):
|
||||
if options['--file'] is not None:
|
||||
self.yaml_path = os.path.join(self.base_dir, options['--file'])
|
||||
if options['--project-name'] is not None:
|
||||
self.explicit_project_name = options['--project-name']
|
||||
return super(Command, self).perform_command(options, *args, **kwargs)
|
||||
|
||||
@cached_property
|
||||
def client(self):
|
||||
return Client(docker_url())
|
||||
|
||||
@cached_property
|
||||
def project(self):
|
||||
try:
|
||||
config = yaml.safe_load(open(self.yaml_path))
|
||||
except IOError as e:
|
||||
if e.errno == errno.ENOENT:
|
||||
raise errors.FigFileNotFound(os.path.basename(e.filename))
|
||||
raise errors.UserError(six.text_type(e))
|
||||
|
||||
try:
|
||||
return Project.from_config(self.project_name, config, self.client)
|
||||
except ConfigError as e:
|
||||
raise errors.UserError(six.text_type(e))
|
||||
|
||||
@cached_property
|
||||
def project_name(self):
|
||||
project = os.path.basename(os.path.dirname(os.path.abspath(self.yaml_path)))
|
||||
if self.explicit_project_name is not None:
|
||||
project = self.explicit_project_name
|
||||
project = re.sub(r'[^a-zA-Z0-9]', '', project)
|
||||
if not project:
|
||||
project = 'default'
|
||||
return project
|
||||
|
||||
@cached_property
|
||||
def formatter(self):
|
||||
return Formatter()
|
||||
|
||||
@cached_property
|
||||
def yaml_path(self):
|
||||
if self._yaml_path is not None:
|
||||
return self._yaml_path
|
||||
elif os.path.exists(os.path.join(self.base_dir, 'fig.yaml')):
|
||||
|
||||
log.warning("Fig just read the file 'fig.yaml' on startup, rather than 'fig.yml'")
|
||||
log.warning("Please be aware that fig.yml the expected extension in most cases, and using .yaml can cause compatibility issues in future")
|
||||
|
||||
return os.path.join(self.base_dir, 'fig.yaml')
|
||||
else:
|
||||
return os.path.join(self.base_dir, 'fig.yml')
|
||||
|
||||
@yaml_path.setter
|
||||
def yaml_path(self, value):
|
||||
self._yaml_path = value
|
||||
@@ -1,20 +0,0 @@
|
||||
# Copyright 2013 dotCloud inc.
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .version import version
|
||||
|
||||
__version__ = version
|
||||
__title__ = 'docker-py'
|
||||
|
||||
from .client import Client # flake8: noqa
|
||||
@@ -1,7 +0,0 @@
|
||||
from .auth import (
|
||||
INDEX_URL,
|
||||
encode_header,
|
||||
load_config,
|
||||
resolve_authconfig,
|
||||
resolve_repository_name
|
||||
) # flake8: noqa
|
||||
@@ -1,167 +0,0 @@
|
||||
# Copyright 2013 dotCloud inc.
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import base64
|
||||
import fileinput
|
||||
import json
|
||||
import os
|
||||
|
||||
from fig.packages import six
|
||||
|
||||
from ..utils import utils
|
||||
from .. import errors
|
||||
|
||||
INDEX_URL = 'https://index.docker.io/v1/'
|
||||
DOCKER_CONFIG_FILENAME = '.dockercfg'
|
||||
|
||||
|
||||
def swap_protocol(url):
|
||||
if url.startswith('http://'):
|
||||
return url.replace('http://', 'https://', 1)
|
||||
if url.startswith('https://'):
|
||||
return url.replace('https://', 'http://', 1)
|
||||
return url
|
||||
|
||||
|
||||
def expand_registry_url(hostname):
|
||||
if hostname.startswith('http:') or hostname.startswith('https:'):
|
||||
if '/' not in hostname[9:]:
|
||||
hostname = hostname + '/v1/'
|
||||
return hostname
|
||||
if utils.ping('https://' + hostname + '/v1/_ping'):
|
||||
return 'https://' + hostname + '/v1/'
|
||||
return 'http://' + hostname + '/v1/'
|
||||
|
||||
|
||||
def resolve_repository_name(repo_name):
|
||||
if '://' in repo_name:
|
||||
raise errors.InvalidRepository(
|
||||
'Repository name cannot contain a scheme ({0})'.format(repo_name))
|
||||
parts = repo_name.split('/', 1)
|
||||
if '.' not in parts[0] and ':' not in parts[0] and parts[0] != 'localhost':
|
||||
# This is a docker index repo (ex: foo/bar or ubuntu)
|
||||
return INDEX_URL, repo_name
|
||||
if len(parts) < 2:
|
||||
raise errors.InvalidRepository(
|
||||
'Invalid repository name ({0})'.format(repo_name))
|
||||
|
||||
if 'index.docker.io' in parts[0]:
|
||||
raise errors.InvalidRepository(
|
||||
'Invalid repository name, try "{0}" instead'.format(parts[1]))
|
||||
|
||||
return expand_registry_url(parts[0]), parts[1]
|
||||
|
||||
|
||||
def resolve_authconfig(authconfig, registry=None):
|
||||
"""Return the authentication data from the given auth configuration for a
|
||||
specific registry. We'll do our best to infer the correct URL for the
|
||||
registry, trying both http and https schemes. Returns an empty dictionnary
|
||||
if no data exists."""
|
||||
# Default to the public index server
|
||||
registry = registry or INDEX_URL
|
||||
|
||||
# Ff its not the index server there are three cases:
|
||||
#
|
||||
# 1. this is a full config url -> it should be used as is
|
||||
# 2. it could be a full url, but with the wrong protocol
|
||||
# 3. it can be the hostname optionally with a port
|
||||
#
|
||||
# as there is only one auth entry which is fully qualified we need to start
|
||||
# parsing and matching
|
||||
if '/' not in registry:
|
||||
registry = registry + '/v1/'
|
||||
if not registry.startswith('http:') and not registry.startswith('https:'):
|
||||
registry = 'https://' + registry
|
||||
|
||||
if registry in authconfig:
|
||||
return authconfig[registry]
|
||||
return authconfig.get(swap_protocol(registry), None)
|
||||
|
||||
|
||||
def encode_auth(auth_info):
|
||||
return base64.b64encode(auth_info.get('username', '') + b':' +
|
||||
auth_info.get('password', ''))
|
||||
|
||||
|
||||
def decode_auth(auth):
|
||||
if isinstance(auth, six.string_types):
|
||||
auth = auth.encode('ascii')
|
||||
s = base64.b64decode(auth)
|
||||
login, pwd = s.split(b':')
|
||||
return login.decode('ascii'), pwd.decode('ascii')
|
||||
|
||||
|
||||
def encode_header(auth):
|
||||
auth_json = json.dumps(auth).encode('ascii')
|
||||
return base64.b64encode(auth_json)
|
||||
|
||||
|
||||
def encode_full_header(auth):
|
||||
""" Returns the given auth block encoded for the X-Registry-Config header.
|
||||
"""
|
||||
return encode_header({'configs': auth})
|
||||
|
||||
|
||||
def load_config(root=None):
|
||||
"""Loads authentication data from a Docker configuration file in the given
|
||||
root directory."""
|
||||
conf = {}
|
||||
data = None
|
||||
|
||||
config_file = os.path.join(root or os.environ.get('HOME', '.'),
|
||||
DOCKER_CONFIG_FILENAME)
|
||||
|
||||
# First try as JSON
|
||||
try:
|
||||
with open(config_file) as f:
|
||||
conf = {}
|
||||
for registry, entry in six.iteritems(json.load(f)):
|
||||
username, password = decode_auth(entry['auth'])
|
||||
conf[registry] = {
|
||||
'username': username,
|
||||
'password': password,
|
||||
'email': entry['email'],
|
||||
'serveraddress': registry,
|
||||
}
|
||||
return conf
|
||||
except:
|
||||
pass
|
||||
|
||||
# If that fails, we assume the configuration file contains a single
|
||||
# authentication token for the public registry in the following format:
|
||||
#
|
||||
# auth = AUTH_TOKEN
|
||||
# email = email@domain.com
|
||||
try:
|
||||
data = []
|
||||
for line in fileinput.input(config_file):
|
||||
data.append(line.strip().split(' = ')[1])
|
||||
if len(data) < 2:
|
||||
# Not enough data
|
||||
raise errors.InvalidConfigFile(
|
||||
'Invalid or empty configuration file!')
|
||||
|
||||
username, password = decode_auth(data[0])
|
||||
conf[INDEX_URL] = {
|
||||
'username': username,
|
||||
'password': password,
|
||||
'email': data[1],
|
||||
'serveraddress': INDEX_URL,
|
||||
}
|
||||
return conf
|
||||
except:
|
||||
pass
|
||||
|
||||
# If all fails, return an empty config
|
||||
return {}
|
||||
@@ -1,860 +0,0 @@
|
||||
# Copyright 2013 dotCloud inc.
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import json
|
||||
import re
|
||||
import shlex
|
||||
import struct
|
||||
import warnings
|
||||
|
||||
import requests
|
||||
import requests.exceptions
|
||||
from fig.packages import six
|
||||
|
||||
from .auth import auth
|
||||
from .unixconn import unixconn
|
||||
from .utils import utils
|
||||
from . import errors
|
||||
|
||||
if not six.PY3:
|
||||
import websocket
|
||||
|
||||
DEFAULT_DOCKER_API_VERSION = '1.12'
|
||||
DEFAULT_TIMEOUT_SECONDS = 60
|
||||
STREAM_HEADER_SIZE_BYTES = 8
|
||||
|
||||
|
||||
class Client(requests.Session):
|
||||
def __init__(self, base_url=None, version=DEFAULT_DOCKER_API_VERSION,
|
||||
timeout=DEFAULT_TIMEOUT_SECONDS):
|
||||
super(Client, self).__init__()
|
||||
if base_url is None:
|
||||
base_url = "http+unix://var/run/docker.sock"
|
||||
if 'unix:///' in base_url:
|
||||
base_url = base_url.replace('unix:/', 'unix:')
|
||||
if base_url.startswith('unix:'):
|
||||
base_url = "http+" + base_url
|
||||
if base_url.startswith('tcp:'):
|
||||
base_url = base_url.replace('tcp:', 'http:')
|
||||
if base_url.endswith('/'):
|
||||
base_url = base_url[:-1]
|
||||
self.base_url = base_url
|
||||
self._version = version
|
||||
self._timeout = timeout
|
||||
self._auth_configs = auth.load_config()
|
||||
|
||||
self.mount('http+unix://', unixconn.UnixAdapter(base_url, timeout))
|
||||
|
||||
def _set_request_timeout(self, kwargs):
|
||||
"""Prepare the kwargs for an HTTP request by inserting the timeout
|
||||
parameter, if not already present."""
|
||||
kwargs.setdefault('timeout', self._timeout)
|
||||
return kwargs
|
||||
|
||||
def _post(self, url, **kwargs):
|
||||
return self.post(url, **self._set_request_timeout(kwargs))
|
||||
|
||||
def _get(self, url, **kwargs):
|
||||
return self.get(url, **self._set_request_timeout(kwargs))
|
||||
|
||||
def _delete(self, url, **kwargs):
|
||||
return self.delete(url, **self._set_request_timeout(kwargs))
|
||||
|
||||
def _url(self, path):
|
||||
return '{0}/v{1}{2}'.format(self.base_url, self._version, path)
|
||||
|
||||
def _raise_for_status(self, response, explanation=None):
|
||||
"""Raises stored :class:`APIError`, if one occurred."""
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
raise errors.APIError(e, response, explanation=explanation)
|
||||
|
||||
def _result(self, response, json=False, binary=False):
|
||||
assert not (json and binary)
|
||||
self._raise_for_status(response)
|
||||
|
||||
if json:
|
||||
return response.json()
|
||||
if binary:
|
||||
return response.content
|
||||
return response.text
|
||||
|
||||
def _container_config(self, image, command, hostname=None, user=None,
|
||||
detach=False, stdin_open=False, tty=False,
|
||||
mem_limit=0, ports=None, environment=None, dns=None,
|
||||
volumes=None, volumes_from=None,
|
||||
network_disabled=False, entrypoint=None,
|
||||
cpu_shares=None, working_dir=None, domainname=None,
|
||||
memswap_limit=0):
|
||||
if isinstance(command, six.string_types):
|
||||
command = shlex.split(str(command))
|
||||
if isinstance(environment, dict):
|
||||
environment = [
|
||||
'{0}={1}'.format(k, v) for k, v in environment.items()
|
||||
]
|
||||
|
||||
if isinstance(ports, list):
|
||||
exposed_ports = {}
|
||||
for port_definition in ports:
|
||||
port = port_definition
|
||||
proto = 'tcp'
|
||||
if isinstance(port_definition, tuple):
|
||||
if len(port_definition) == 2:
|
||||
proto = port_definition[1]
|
||||
port = port_definition[0]
|
||||
exposed_ports['{0}/{1}'.format(port, proto)] = {}
|
||||
ports = exposed_ports
|
||||
|
||||
if isinstance(volumes, list):
|
||||
volumes_dict = {}
|
||||
for vol in volumes:
|
||||
volumes_dict[vol] = {}
|
||||
volumes = volumes_dict
|
||||
|
||||
if volumes_from:
|
||||
if not isinstance(volumes_from, six.string_types):
|
||||
volumes_from = ','.join(volumes_from)
|
||||
else:
|
||||
# Force None, an empty list or dict causes client.start to fail
|
||||
volumes_from = None
|
||||
|
||||
attach_stdin = False
|
||||
attach_stdout = False
|
||||
attach_stderr = False
|
||||
stdin_once = False
|
||||
|
||||
if not detach:
|
||||
attach_stdout = True
|
||||
attach_stderr = True
|
||||
|
||||
if stdin_open:
|
||||
attach_stdin = True
|
||||
stdin_once = True
|
||||
|
||||
if utils.compare_version('1.10', self._version) >= 0:
|
||||
message = ('{0!r} parameter has no effect on create_container().'
|
||||
' It has been moved to start()')
|
||||
if dns is not None:
|
||||
raise errors.DockerException(message.format('dns'))
|
||||
if volumes_from is not None:
|
||||
raise errors.DockerException(message.format('volumes_from'))
|
||||
|
||||
return {
|
||||
'Hostname': hostname,
|
||||
'Domainname': domainname,
|
||||
'ExposedPorts': ports,
|
||||
'User': user,
|
||||
'Tty': tty,
|
||||
'OpenStdin': stdin_open,
|
||||
'StdinOnce': stdin_once,
|
||||
'Memory': mem_limit,
|
||||
'AttachStdin': attach_stdin,
|
||||
'AttachStdout': attach_stdout,
|
||||
'AttachStderr': attach_stderr,
|
||||
'Env': environment,
|
||||
'Cmd': command,
|
||||
'Dns': dns,
|
||||
'Image': image,
|
||||
'Volumes': volumes,
|
||||
'VolumesFrom': volumes_from,
|
||||
'NetworkDisabled': network_disabled,
|
||||
'Entrypoint': entrypoint,
|
||||
'CpuShares': cpu_shares,
|
||||
'WorkingDir': working_dir,
|
||||
'MemorySwap': memswap_limit
|
||||
}
|
||||
|
||||
def _post_json(self, url, data, **kwargs):
|
||||
# Go <1.1 can't unserialize null to a string
|
||||
# so we do this disgusting thing here.
|
||||
data2 = {}
|
||||
if data is not None:
|
||||
for k, v in six.iteritems(data):
|
||||
if v is not None:
|
||||
data2[k] = v
|
||||
|
||||
if 'headers' not in kwargs:
|
||||
kwargs['headers'] = {}
|
||||
kwargs['headers']['Content-Type'] = 'application/json'
|
||||
return self._post(url, data=json.dumps(data2), **kwargs)
|
||||
|
||||
def _attach_params(self, override=None):
|
||||
return override or {
|
||||
'stdout': 1,
|
||||
'stderr': 1,
|
||||
'stream': 1
|
||||
}
|
||||
|
||||
def _attach_websocket(self, container, params=None):
|
||||
if six.PY3:
|
||||
raise NotImplementedError("This method is not currently supported "
|
||||
"under python 3")
|
||||
url = self._url("/containers/{0}/attach/ws".format(container))
|
||||
req = requests.Request("POST", url, params=self._attach_params(params))
|
||||
full_url = req.prepare().url
|
||||
full_url = full_url.replace("http://", "ws://", 1)
|
||||
full_url = full_url.replace("https://", "wss://", 1)
|
||||
return self._create_websocket_connection(full_url)
|
||||
|
||||
def _create_websocket_connection(self, url):
|
||||
return websocket.create_connection(url)
|
||||
|
||||
def _get_raw_response_socket(self, response):
|
||||
self._raise_for_status(response)
|
||||
if six.PY3:
|
||||
return response.raw._fp.fp.raw._sock
|
||||
else:
|
||||
return response.raw._fp.fp._sock
|
||||
|
||||
def _stream_helper(self, response):
|
||||
"""Generator for data coming from a chunked-encoded HTTP response."""
|
||||
socket_fp = self._get_raw_response_socket(response)
|
||||
socket_fp.setblocking(1)
|
||||
socket = socket_fp.makefile()
|
||||
while True:
|
||||
# Because Docker introduced newlines at the end of chunks in v0.9,
|
||||
# and only on some API endpoints, we have to cater for both cases.
|
||||
size_line = socket.readline()
|
||||
if size_line == '\r\n':
|
||||
size_line = socket.readline()
|
||||
|
||||
size = int(size_line, 16)
|
||||
if size <= 0:
|
||||
break
|
||||
data = socket.readline()
|
||||
if not data:
|
||||
break
|
||||
yield data
|
||||
|
||||
def _multiplexed_buffer_helper(self, response):
|
||||
"""A generator of multiplexed data blocks read from a buffered
|
||||
response."""
|
||||
buf = self._result(response, binary=True)
|
||||
walker = 0
|
||||
while True:
|
||||
if len(buf[walker:]) < 8:
|
||||
break
|
||||
_, length = struct.unpack_from('>BxxxL', buf[walker:])
|
||||
start = walker + STREAM_HEADER_SIZE_BYTES
|
||||
end = start + length
|
||||
walker = end
|
||||
yield buf[start:end]
|
||||
|
||||
def _multiplexed_socket_stream_helper(self, response):
|
||||
"""A generator of multiplexed data blocks coming from a response
|
||||
socket."""
|
||||
socket = self._get_raw_response_socket(response)
|
||||
|
||||
def recvall(socket, size):
|
||||
blocks = []
|
||||
while size > 0:
|
||||
block = socket.recv(size)
|
||||
if not block:
|
||||
return None
|
||||
|
||||
blocks.append(block)
|
||||
size -= len(block)
|
||||
|
||||
sep = bytes() if six.PY3 else str()
|
||||
data = sep.join(blocks)
|
||||
return data
|
||||
|
||||
while True:
|
||||
socket.settimeout(None)
|
||||
header = recvall(socket, STREAM_HEADER_SIZE_BYTES)
|
||||
if not header:
|
||||
break
|
||||
_, length = struct.unpack('>BxxxL', header)
|
||||
if not length:
|
||||
break
|
||||
data = recvall(socket, length)
|
||||
if not data:
|
||||
break
|
||||
yield data
|
||||
|
||||
def attach(self, container, stdout=True, stderr=True,
|
||||
stream=False, logs=False):
|
||||
if isinstance(container, dict):
|
||||
container = container.get('Id')
|
||||
params = {
|
||||
'logs': logs and 1 or 0,
|
||||
'stdout': stdout and 1 or 0,
|
||||
'stderr': stderr and 1 or 0,
|
||||
'stream': stream and 1 or 0,
|
||||
}
|
||||
u = self._url("/containers/{0}/attach".format(container))
|
||||
response = self._post(u, params=params, stream=stream)
|
||||
|
||||
# Stream multi-plexing was only introduced in API v1.6. Anything before
|
||||
# that needs old-style streaming.
|
||||
if utils.compare_version('1.6', self._version) < 0:
|
||||
def stream_result():
|
||||
self._raise_for_status(response)
|
||||
for line in response.iter_lines(chunk_size=1,
|
||||
decode_unicode=True):
|
||||
# filter out keep-alive new lines
|
||||
if line:
|
||||
yield line
|
||||
|
||||
return stream_result() if stream else \
|
||||
self._result(response, binary=True)
|
||||
|
||||
sep = bytes() if six.PY3 else str()
|
||||
|
||||
return stream and self._multiplexed_socket_stream_helper(response) or \
|
||||
sep.join([x for x in self._multiplexed_buffer_helper(response)])
|
||||
|
||||
def attach_socket(self, container, params=None, ws=False):
|
||||
if params is None:
|
||||
params = {
|
||||
'stdout': 1,
|
||||
'stderr': 1,
|
||||
'stream': 1
|
||||
}
|
||||
|
||||
if ws:
|
||||
return self._attach_websocket(container, params)
|
||||
|
||||
if isinstance(container, dict):
|
||||
container = container.get('Id')
|
||||
|
||||
u = self._url("/containers/{0}/attach".format(container))
|
||||
return self._get_raw_response_socket(self.post(
|
||||
u, None, params=self._attach_params(params), stream=True))
|
||||
|
||||
def build(self, path=None, tag=None, quiet=False, fileobj=None,
|
||||
nocache=False, rm=False, stream=False, timeout=None,
|
||||
custom_context=False, encoding=None):
|
||||
remote = context = headers = None
|
||||
if path is None and fileobj is None:
|
||||
raise TypeError("Either path or fileobj needs to be provided.")
|
||||
|
||||
if custom_context:
|
||||
if not fileobj:
|
||||
raise TypeError("You must specify fileobj with custom_context")
|
||||
context = fileobj
|
||||
elif fileobj is not None:
|
||||
context = utils.mkbuildcontext(fileobj)
|
||||
elif path.startswith(('http://', 'https://',
|
||||
'git://', 'github.com/')):
|
||||
remote = path
|
||||
else:
|
||||
context = utils.tar(path)
|
||||
|
||||
if utils.compare_version('1.8', self._version) >= 0:
|
||||
stream = True
|
||||
|
||||
u = self._url('/build')
|
||||
params = {
|
||||
't': tag,
|
||||
'remote': remote,
|
||||
'q': quiet,
|
||||
'nocache': nocache,
|
||||
'rm': rm
|
||||
}
|
||||
|
||||
if context is not None:
|
||||
headers = {'Content-Type': 'application/tar'}
|
||||
if encoding:
|
||||
headers['Content-Encoding'] = encoding
|
||||
|
||||
if utils.compare_version('1.9', self._version) >= 0:
|
||||
# If we don't have any auth data so far, try reloading the config
|
||||
# file one more time in case anything showed up in there.
|
||||
if not self._auth_configs:
|
||||
self._auth_configs = auth.load_config()
|
||||
|
||||
# Send the full auth configuration (if any exists), since the build
|
||||
# could use any (or all) of the registries.
|
||||
if self._auth_configs:
|
||||
headers['X-Registry-Config'] = auth.encode_full_header(
|
||||
self._auth_configs
|
||||
)
|
||||
|
||||
response = self._post(
|
||||
u,
|
||||
data=context,
|
||||
params=params,
|
||||
headers=headers,
|
||||
stream=stream,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
if context is not None:
|
||||
context.close()
|
||||
|
||||
if stream:
|
||||
return self._stream_helper(response)
|
||||
else:
|
||||
output = self._result(response)
|
||||
srch = r'Successfully built ([0-9a-f]+)'
|
||||
match = re.search(srch, output)
|
||||
if not match:
|
||||
return None, output
|
||||
return match.group(1), output
|
||||
|
||||
def commit(self, container, repository=None, tag=None, message=None,
|
||||
author=None, conf=None):
|
||||
params = {
|
||||
'container': container,
|
||||
'repo': repository,
|
||||
'tag': tag,
|
||||
'comment': message,
|
||||
'author': author
|
||||
}
|
||||
u = self._url("/commit")
|
||||
return self._result(self._post_json(u, data=conf, params=params),
|
||||
json=True)
|
||||
|
||||
def containers(self, quiet=False, all=False, trunc=True, latest=False,
|
||||
since=None, before=None, limit=-1, size=False):
|
||||
params = {
|
||||
'limit': 1 if latest else limit,
|
||||
'all': 1 if all else 0,
|
||||
'size': 1 if size else 0,
|
||||
'trunc_cmd': 1 if trunc else 0,
|
||||
'since': since,
|
||||
'before': before
|
||||
}
|
||||
u = self._url("/containers/json")
|
||||
res = self._result(self._get(u, params=params), True)
|
||||
|
||||
if quiet:
|
||||
return [{'Id': x['Id']} for x in res]
|
||||
return res
|
||||
|
||||
def copy(self, container, resource):
|
||||
if isinstance(container, dict):
|
||||
container = container.get('Id')
|
||||
res = self._post_json(
|
||||
self._url("/containers/{0}/copy".format(container)),
|
||||
data={"Resource": resource},
|
||||
stream=True
|
||||
)
|
||||
self._raise_for_status(res)
|
||||
return res.raw
|
||||
|
||||
def create_container(self, image, command=None, hostname=None, user=None,
|
||||
detach=False, stdin_open=False, tty=False,
|
||||
mem_limit=0, ports=None, environment=None, dns=None,
|
||||
volumes=None, volumes_from=None,
|
||||
network_disabled=False, name=None, entrypoint=None,
|
||||
cpu_shares=None, working_dir=None, domainname=None,
|
||||
memswap_limit=0):
|
||||
|
||||
config = self._container_config(
|
||||
image, command, hostname, user, detach, stdin_open, tty, mem_limit,
|
||||
ports, environment, dns, volumes, volumes_from, network_disabled,
|
||||
entrypoint, cpu_shares, working_dir, domainname, memswap_limit
|
||||
)
|
||||
return self.create_container_from_config(config, name)
|
||||
|
||||
def create_container_from_config(self, config, name=None):
|
||||
u = self._url("/containers/create")
|
||||
params = {
|
||||
'name': name
|
||||
}
|
||||
res = self._post_json(u, data=config, params=params)
|
||||
return self._result(res, True)
|
||||
|
||||
def diff(self, container):
|
||||
if isinstance(container, dict):
|
||||
container = container.get('Id')
|
||||
return self._result(self._get(self._url("/containers/{0}/changes".
|
||||
format(container))), True)
|
||||
|
||||
def events(self):
|
||||
return self._stream_helper(self.get(self._url('/events'), stream=True))
|
||||
|
||||
def export(self, container):
|
||||
if isinstance(container, dict):
|
||||
container = container.get('Id')
|
||||
res = self._get(self._url("/containers/{0}/export".format(container)),
|
||||
stream=True)
|
||||
self._raise_for_status(res)
|
||||
return res.raw
|
||||
|
||||
def get_image(self, image):
|
||||
res = self._get(self._url("/images/{0}/get".format(image)),
|
||||
stream=True)
|
||||
self._raise_for_status(res)
|
||||
return res.raw
|
||||
|
||||
def history(self, image):
|
||||
res = self._get(self._url("/images/{0}/history".format(image)))
|
||||
self._raise_for_status(res)
|
||||
return self._result(res)
|
||||
|
||||
def images(self, name=None, quiet=False, all=False, viz=False):
|
||||
if viz:
|
||||
if utils.compare_version('1.7', self._version) >= 0:
|
||||
raise Exception('Viz output is not supported in API >= 1.7!')
|
||||
return self._result(self._get(self._url("images/viz")))
|
||||
params = {
|
||||
'filter': name,
|
||||
'only_ids': 1 if quiet else 0,
|
||||
'all': 1 if all else 0,
|
||||
}
|
||||
res = self._result(self._get(self._url("/images/json"), params=params),
|
||||
True)
|
||||
if quiet:
|
||||
return [x['Id'] for x in res]
|
||||
return res
|
||||
|
||||
def import_image(self, src=None, repository=None, tag=None, image=None):
|
||||
u = self._url("/images/create")
|
||||
params = {
|
||||
'repo': repository,
|
||||
'tag': tag
|
||||
}
|
||||
|
||||
if src:
|
||||
try:
|
||||
# XXX: this is ways not optimal but the only way
|
||||
# for now to import tarballs through the API
|
||||
fic = open(src)
|
||||
data = fic.read()
|
||||
fic.close()
|
||||
src = "-"
|
||||
except IOError:
|
||||
# file does not exists or not a file (URL)
|
||||
data = None
|
||||
if isinstance(src, six.string_types):
|
||||
params['fromSrc'] = src
|
||||
return self._result(self._post(u, data=data, params=params))
|
||||
return self._result(self._post(u, data=src, params=params))
|
||||
|
||||
if image:
|
||||
params['fromImage'] = image
|
||||
return self._result(self._post(u, data=None, params=params))
|
||||
|
||||
raise Exception("Must specify a src or image")
|
||||
|
||||
def info(self):
|
||||
return self._result(self._get(self._url("/info")),
|
||||
True)
|
||||
|
||||
def insert(self, image, url, path):
|
||||
if utils.compare_version('1.12', self._version) >= 0:
|
||||
raise errors.DeprecatedMethod(
|
||||
'insert is not available for API version >=1.12'
|
||||
)
|
||||
api_url = self._url("/images/" + image + "/insert")
|
||||
params = {
|
||||
'url': url,
|
||||
'path': path
|
||||
}
|
||||
return self._result(self._post(api_url, params=params))
|
||||
|
||||
def inspect_container(self, container):
|
||||
if isinstance(container, dict):
|
||||
container = container.get('Id')
|
||||
return self._result(
|
||||
self._get(self._url("/containers/{0}/json".format(container))),
|
||||
True)
|
||||
|
||||
def inspect_image(self, image_id):
|
||||
return self._result(
|
||||
self._get(self._url("/images/{0}/json".format(image_id))),
|
||||
True
|
||||
)
|
||||
|
||||
def kill(self, container, signal=None):
|
||||
if isinstance(container, dict):
|
||||
container = container.get('Id')
|
||||
url = self._url("/containers/{0}/kill".format(container))
|
||||
params = {}
|
||||
if signal is not None:
|
||||
params['signal'] = signal
|
||||
res = self._post(url, params=params)
|
||||
|
||||
self._raise_for_status(res)
|
||||
|
||||
def load_image(self, data):
|
||||
res = self._post(self._url("/images/load"), data=data)
|
||||
self._raise_for_status(res)
|
||||
|
||||
def login(self, username, password=None, email=None, registry=None,
|
||||
reauth=False):
|
||||
# If we don't have any auth data so far, try reloading the config file
|
||||
# one more time in case anything showed up in there.
|
||||
if not self._auth_configs:
|
||||
self._auth_configs = auth.load_config()
|
||||
|
||||
registry = registry or auth.INDEX_URL
|
||||
|
||||
authcfg = auth.resolve_authconfig(self._auth_configs, registry)
|
||||
# If we found an existing auth config for this registry and username
|
||||
# combination, we can return it immediately unless reauth is requested.
|
||||
if authcfg and authcfg.get('username', None) == username \
|
||||
and not reauth:
|
||||
return authcfg
|
||||
|
||||
req_data = {
|
||||
'username': username,
|
||||
'password': password,
|
||||
'email': email,
|
||||
'serveraddress': registry,
|
||||
}
|
||||
|
||||
response = self._post_json(self._url('/auth'), data=req_data)
|
||||
if response.status_code == 200:
|
||||
self._auth_configs[registry] = req_data
|
||||
return self._result(response, json=True)
|
||||
|
||||
def logs(self, container, stdout=True, stderr=True, stream=False,
|
||||
timestamps=False):
|
||||
if isinstance(container, dict):
|
||||
container = container.get('Id')
|
||||
if utils.compare_version('1.11', self._version) >= 0:
|
||||
params = {'stderr': stderr and 1 or 0,
|
||||
'stdout': stdout and 1 or 0,
|
||||
'timestamps': timestamps and 1 or 0,
|
||||
'follow': stream and 1 or 0}
|
||||
url = self._url("/containers/{0}/logs".format(container))
|
||||
res = self._get(url, params=params, stream=stream)
|
||||
if stream:
|
||||
return self._multiplexed_socket_stream_helper(res)
|
||||
elif six.PY3:
|
||||
return bytes().join(
|
||||
[x for x in self._multiplexed_buffer_helper(res)]
|
||||
)
|
||||
else:
|
||||
return str().join(
|
||||
[x for x in self._multiplexed_buffer_helper(res)]
|
||||
)
|
||||
return self.attach(
|
||||
container,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
stream=stream,
|
||||
logs=True
|
||||
)
|
||||
|
||||
def ping(self):
|
||||
return self._result(self._get(self._url('/_ping')))
|
||||
|
||||
def port(self, container, private_port):
|
||||
if isinstance(container, dict):
|
||||
container = container.get('Id')
|
||||
res = self._get(self._url("/containers/{0}/json".format(container)))
|
||||
self._raise_for_status(res)
|
||||
json_ = res.json()
|
||||
s_port = str(private_port)
|
||||
h_ports = None
|
||||
|
||||
h_ports = json_['NetworkSettings']['Ports'].get(s_port + '/udp')
|
||||
if h_ports is None:
|
||||
h_ports = json_['NetworkSettings']['Ports'].get(s_port + '/tcp')
|
||||
|
||||
return h_ports
|
||||
|
||||
def pull(self, repository, tag=None, stream=False):
|
||||
if not tag:
|
||||
repository, tag = utils.parse_repository_tag(repository)
|
||||
registry, repo_name = auth.resolve_repository_name(repository)
|
||||
if repo_name.count(":") == 1:
|
||||
repository, tag = repository.rsplit(":", 1)
|
||||
|
||||
params = {
|
||||
'tag': tag,
|
||||
'fromImage': repository
|
||||
}
|
||||
headers = {}
|
||||
|
||||
if utils.compare_version('1.5', self._version) >= 0:
|
||||
# If we don't have any auth data so far, try reloading the config
|
||||
# file one more time in case anything showed up in there.
|
||||
if not self._auth_configs:
|
||||
self._auth_configs = auth.load_config()
|
||||
authcfg = auth.resolve_authconfig(self._auth_configs, registry)
|
||||
|
||||
# Do not fail here if no authentication exists for this specific
|
||||
# registry as we can have a readonly pull. Just put the header if
|
||||
# we can.
|
||||
if authcfg:
|
||||
headers['X-Registry-Auth'] = auth.encode_header(authcfg)
|
||||
|
||||
response = self._post(self._url('/images/create'), params=params,
|
||||
headers=headers, stream=stream, timeout=None)
|
||||
|
||||
if stream:
|
||||
return self._stream_helper(response)
|
||||
else:
|
||||
return self._result(response)
|
||||
|
||||
def push(self, repository, stream=False):
|
||||
registry, repo_name = auth.resolve_repository_name(repository)
|
||||
u = self._url("/images/{0}/push".format(repository))
|
||||
headers = {}
|
||||
|
||||
if utils.compare_version('1.5', self._version) >= 0:
|
||||
# If we don't have any auth data so far, try reloading the config
|
||||
# file one more time in case anything showed up in there.
|
||||
if not self._auth_configs:
|
||||
self._auth_configs = auth.load_config()
|
||||
authcfg = auth.resolve_authconfig(self._auth_configs, registry)
|
||||
|
||||
# Do not fail here if no authentication exists for this specific
|
||||
# registry as we can have a readonly pull. Just put the header if
|
||||
# we can.
|
||||
if authcfg:
|
||||
headers['X-Registry-Auth'] = auth.encode_header(authcfg)
|
||||
|
||||
response = self._post_json(u, None, headers=headers, stream=stream)
|
||||
else:
|
||||
response = self._post_json(u, None, stream=stream)
|
||||
|
||||
return stream and self._stream_helper(response) \
|
||||
or self._result(response)
|
||||
|
||||
def remove_container(self, container, v=False, link=False, force=False):
|
||||
if isinstance(container, dict):
|
||||
container = container.get('Id')
|
||||
params = {'v': v, 'link': link, 'force': force}
|
||||
res = self._delete(self._url("/containers/" + container),
|
||||
params=params)
|
||||
self._raise_for_status(res)
|
||||
|
||||
def remove_image(self, image, force=False, noprune=False):
|
||||
params = {'force': force, 'noprune': noprune}
|
||||
res = self._delete(self._url("/images/" + image), params=params)
|
||||
self._raise_for_status(res)
|
||||
|
||||
def restart(self, container, timeout=10):
|
||||
if isinstance(container, dict):
|
||||
container = container.get('Id')
|
||||
params = {'t': timeout}
|
||||
url = self._url("/containers/{0}/restart".format(container))
|
||||
res = self._post(url, params=params)
|
||||
self._raise_for_status(res)
|
||||
|
||||
def search(self, term):
|
||||
return self._result(self._get(self._url("/images/search"),
|
||||
params={'term': term}),
|
||||
True)
|
||||
|
||||
def start(self, container, binds=None, port_bindings=None, lxc_conf=None,
|
||||
publish_all_ports=False, links=None, privileged=False,
|
||||
dns=None, dns_search=None, volumes_from=None, network_mode=None):
|
||||
if isinstance(container, dict):
|
||||
container = container.get('Id')
|
||||
|
||||
if isinstance(lxc_conf, dict):
|
||||
formatted = []
|
||||
for k, v in six.iteritems(lxc_conf):
|
||||
formatted.append({'Key': k, 'Value': str(v)})
|
||||
lxc_conf = formatted
|
||||
|
||||
start_config = {
|
||||
'LxcConf': lxc_conf
|
||||
}
|
||||
if binds:
|
||||
start_config['Binds'] = utils.convert_volume_binds(binds)
|
||||
|
||||
if port_bindings:
|
||||
start_config['PortBindings'] = utils.convert_port_bindings(
|
||||
port_bindings
|
||||
)
|
||||
|
||||
start_config['PublishAllPorts'] = publish_all_ports
|
||||
|
||||
if links:
|
||||
if isinstance(links, dict):
|
||||
links = six.iteritems(links)
|
||||
|
||||
formatted_links = [
|
||||
'{0}:{1}'.format(k, v) for k, v in sorted(links)
|
||||
]
|
||||
|
||||
start_config['Links'] = formatted_links
|
||||
|
||||
start_config['Privileged'] = privileged
|
||||
|
||||
if utils.compare_version('1.10', self._version) >= 0:
|
||||
if dns is not None:
|
||||
start_config['Dns'] = dns
|
||||
if volumes_from is not None:
|
||||
if isinstance(volumes_from, six.string_types):
|
||||
volumes_from = volumes_from.split(',')
|
||||
start_config['VolumesFrom'] = volumes_from
|
||||
else:
|
||||
warning_message = ('{0!r} parameter is discarded. It is only'
|
||||
' available for API version greater or equal'
|
||||
' than 1.10')
|
||||
|
||||
if dns is not None:
|
||||
warnings.warn(warning_message.format('dns'),
|
||||
DeprecationWarning)
|
||||
if volumes_from is not None:
|
||||
warnings.warn(warning_message.format('volumes_from'),
|
||||
DeprecationWarning)
|
||||
|
||||
if dns_search:
|
||||
start_config['DnsSearch'] = dns_search
|
||||
|
||||
if network_mode:
|
||||
start_config['NetworkMode'] = network_mode
|
||||
|
||||
url = self._url("/containers/{0}/start".format(container))
|
||||
res = self._post_json(url, data=start_config)
|
||||
self._raise_for_status(res)
|
||||
|
||||
def resize(self, container, height, width):
|
||||
if isinstance(container, dict):
|
||||
container = container.get('Id')
|
||||
|
||||
params = {'h': height, 'w': width}
|
||||
url = self._url("/containers/{0}/resize".format(container))
|
||||
res = self._post(url, params=params)
|
||||
self._raise_for_status(res)
|
||||
|
||||
def stop(self, container, timeout=10):
|
||||
if isinstance(container, dict):
|
||||
container = container.get('Id')
|
||||
params = {'t': timeout}
|
||||
url = self._url("/containers/{0}/stop".format(container))
|
||||
res = self._post(url, params=params,
|
||||
timeout=max(timeout, self._timeout))
|
||||
self._raise_for_status(res)
|
||||
|
||||
def tag(self, image, repository, tag=None, force=False):
|
||||
params = {
|
||||
'tag': tag,
|
||||
'repo': repository,
|
||||
'force': 1 if force else 0
|
||||
}
|
||||
url = self._url("/images/{0}/tag".format(image))
|
||||
res = self._post(url, params=params)
|
||||
self._raise_for_status(res)
|
||||
return res.status_code == 201
|
||||
|
||||
def top(self, container):
|
||||
u = self._url("/containers/{0}/top".format(container))
|
||||
return self._result(self._get(u), True)
|
||||
|
||||
def version(self):
|
||||
return self._result(self._get(self._url("/version")), True)
|
||||
|
||||
def wait(self, container):
|
||||
if isinstance(container, dict):
|
||||
container = container.get('Id')
|
||||
url = self._url("/containers/{0}/wait".format(container))
|
||||
res = self._post(url, timeout=None)
|
||||
self._raise_for_status(res)
|
||||
json_ = res.json()
|
||||
if 'StatusCode' in json_:
|
||||
return json_['StatusCode']
|
||||
return -1
|
||||
@@ -1,65 +0,0 @@
|
||||
# Copyright 2014 dotCloud inc.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
class APIError(requests.exceptions.HTTPError):
|
||||
def __init__(self, message, response, explanation=None):
|
||||
# requests 1.2 supports response as a keyword argument, but
|
||||
# requests 1.1 doesn't
|
||||
super(APIError, self).__init__(message)
|
||||
self.response = response
|
||||
|
||||
self.explanation = explanation
|
||||
|
||||
if self.explanation is None and response.content:
|
||||
self.explanation = response.content.strip()
|
||||
|
||||
def __str__(self):
|
||||
message = super(APIError, self).__str__()
|
||||
|
||||
if self.is_client_error():
|
||||
message = '%s Client Error: %s' % (
|
||||
self.response.status_code, self.response.reason)
|
||||
|
||||
elif self.is_server_error():
|
||||
message = '%s Server Error: %s' % (
|
||||
self.response.status_code, self.response.reason)
|
||||
|
||||
if self.explanation:
|
||||
message = '%s ("%s")' % (message, self.explanation)
|
||||
|
||||
return message
|
||||
|
||||
def is_client_error(self):
|
||||
return 400 <= self.response.status_code < 500
|
||||
|
||||
def is_server_error(self):
|
||||
return 500 <= self.response.status_code < 600
|
||||
|
||||
|
||||
class DockerException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidRepository(DockerException):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidConfigFile(DockerException):
|
||||
pass
|
||||
|
||||
|
||||
class DeprecatedMethod(DockerException):
|
||||
pass
|
||||
@@ -1 +0,0 @@
|
||||
from .unixconn import UnixAdapter # flake8: noqa
|
||||
@@ -1,71 +0,0 @@
|
||||
# Copyright 2013 dotCloud inc.
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from fig.packages import six
|
||||
|
||||
if six.PY3:
|
||||
import http.client as httplib
|
||||
else:
|
||||
import httplib
|
||||
import requests.adapters
|
||||
import socket
|
||||
|
||||
try:
|
||||
import requests.packages.urllib3.connectionpool as connectionpool
|
||||
except ImportError:
|
||||
import urllib3.connectionpool as connectionpool
|
||||
|
||||
|
||||
class UnixHTTPConnection(httplib.HTTPConnection, object):
|
||||
def __init__(self, base_url, unix_socket, timeout=60):
|
||||
httplib.HTTPConnection.__init__(self, 'localhost', timeout=timeout)
|
||||
self.base_url = base_url
|
||||
self.unix_socket = unix_socket
|
||||
self.timeout = timeout
|
||||
|
||||
def connect(self):
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.settimeout(self.timeout)
|
||||
sock.connect(self.base_url.replace("http+unix:/", ""))
|
||||
self.sock = sock
|
||||
|
||||
def _extract_path(self, url):
|
||||
# remove the base_url entirely..
|
||||
return url.replace(self.base_url, "")
|
||||
|
||||
def request(self, method, url, **kwargs):
|
||||
url = self._extract_path(self.unix_socket)
|
||||
super(UnixHTTPConnection, self).request(method, url, **kwargs)
|
||||
|
||||
|
||||
class UnixHTTPConnectionPool(connectionpool.HTTPConnectionPool):
|
||||
def __init__(self, base_url, socket_path, timeout=60):
|
||||
connectionpool.HTTPConnectionPool.__init__(self, 'localhost',
|
||||
timeout=timeout)
|
||||
self.base_url = base_url
|
||||
self.socket_path = socket_path
|
||||
self.timeout = timeout
|
||||
|
||||
def _new_conn(self):
|
||||
return UnixHTTPConnection(self.base_url, self.socket_path,
|
||||
self.timeout)
|
||||
|
||||
|
||||
class UnixAdapter(requests.adapters.HTTPAdapter):
|
||||
def __init__(self, base_url, timeout=60):
|
||||
self.base_url = base_url
|
||||
self.timeout = timeout
|
||||
super(UnixAdapter, self).__init__()
|
||||
|
||||
def get_connection(self, socket_path, proxies=None):
|
||||
return UnixHTTPConnectionPool(self.base_url, socket_path, self.timeout)
|
||||
@@ -1,4 +0,0 @@
|
||||
from .utils import (
|
||||
compare_version, convert_port_bindings, convert_volume_binds,
|
||||
mkbuildcontext, ping, tar, parse_repository_tag
|
||||
) # flake8: noqa
|
||||
@@ -1,147 +0,0 @@
|
||||
# Copyright 2013 dotCloud inc.
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import io
|
||||
import tarfile
|
||||
import tempfile
|
||||
from distutils.version import StrictVersion
|
||||
|
||||
import requests
|
||||
from fig.packages import six
|
||||
|
||||
|
||||
def mkbuildcontext(dockerfile):
|
||||
f = tempfile.NamedTemporaryFile()
|
||||
t = tarfile.open(mode='w', fileobj=f)
|
||||
if isinstance(dockerfile, io.StringIO):
|
||||
dfinfo = tarfile.TarInfo('Dockerfile')
|
||||
if six.PY3:
|
||||
raise TypeError('Please use io.BytesIO to create in-memory '
|
||||
'Dockerfiles with Python 3')
|
||||
else:
|
||||
dfinfo.size = len(dockerfile.getvalue())
|
||||
elif isinstance(dockerfile, io.BytesIO):
|
||||
dfinfo = tarfile.TarInfo('Dockerfile')
|
||||
dfinfo.size = len(dockerfile.getvalue())
|
||||
else:
|
||||
dfinfo = t.gettarinfo(fileobj=dockerfile, arcname='Dockerfile')
|
||||
t.addfile(dfinfo, dockerfile)
|
||||
t.close()
|
||||
f.seek(0)
|
||||
return f
|
||||
|
||||
|
||||
def tar(path):
|
||||
f = tempfile.NamedTemporaryFile()
|
||||
t = tarfile.open(mode='w', fileobj=f)
|
||||
t.add(path, arcname='.')
|
||||
t.close()
|
||||
f.seek(0)
|
||||
return f
|
||||
|
||||
|
||||
def compare_version(v1, v2):
|
||||
"""Compare docker versions
|
||||
|
||||
>>> v1 = '1.9'
|
||||
>>> v2 = '1.10'
|
||||
>>> compare_version(v1, v2)
|
||||
1
|
||||
>>> compare_version(v2, v1)
|
||||
-1
|
||||
>>> compare_version(v2, v2)
|
||||
0
|
||||
"""
|
||||
s1 = StrictVersion(v1)
|
||||
s2 = StrictVersion(v2)
|
||||
if s1 == s2:
|
||||
return 0
|
||||
elif s1 > s2:
|
||||
return -1
|
||||
else:
|
||||
return 1
|
||||
|
||||
|
||||
def ping(url):
|
||||
try:
|
||||
res = requests.get(url)
|
||||
except Exception:
|
||||
return False
|
||||
else:
|
||||
return res.status_code < 400
|
||||
|
||||
|
||||
def _convert_port_binding(binding):
|
||||
result = {'HostIp': '', 'HostPort': ''}
|
||||
if isinstance(binding, tuple):
|
||||
if len(binding) == 2:
|
||||
result['HostPort'] = binding[1]
|
||||
result['HostIp'] = binding[0]
|
||||
elif isinstance(binding[0], six.string_types):
|
||||
result['HostIp'] = binding[0]
|
||||
else:
|
||||
result['HostPort'] = binding[0]
|
||||
elif isinstance(binding, dict):
|
||||
if 'HostPort' in binding:
|
||||
result['HostPort'] = binding['HostPort']
|
||||
if 'HostIp' in binding:
|
||||
result['HostIp'] = binding['HostIp']
|
||||
else:
|
||||
raise ValueError(binding)
|
||||
else:
|
||||
result['HostPort'] = binding
|
||||
|
||||
if result['HostPort'] is None:
|
||||
result['HostPort'] = ''
|
||||
else:
|
||||
result['HostPort'] = str(result['HostPort'])
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def convert_port_bindings(port_bindings):
|
||||
result = {}
|
||||
for k, v in six.iteritems(port_bindings):
|
||||
key = str(k)
|
||||
if '/' not in key:
|
||||
key = key + '/tcp'
|
||||
if isinstance(v, list):
|
||||
result[key] = [_convert_port_binding(binding) for binding in v]
|
||||
else:
|
||||
result[key] = [_convert_port_binding(v)]
|
||||
return result
|
||||
|
||||
|
||||
def convert_volume_binds(binds):
|
||||
result = []
|
||||
for k, v in binds.items():
|
||||
if isinstance(v, dict):
|
||||
result.append('%s:%s:%s' % (
|
||||
k, v['bind'], 'ro' if v.get('ro', False) else 'rw'
|
||||
))
|
||||
else:
|
||||
result.append('%s:%s:rw' % (k, v))
|
||||
return result
|
||||
|
||||
|
||||
def parse_repository_tag(repo):
|
||||
column_index = repo.rfind(':')
|
||||
if column_index < 0:
|
||||
return repo, None
|
||||
tag = repo[column_index+1:]
|
||||
slash_index = tag.find('/')
|
||||
if slash_index < 0:
|
||||
return repo[:column_index], tag
|
||||
|
||||
return repo, None
|
||||
@@ -1 +0,0 @@
|
||||
version = "0.3.2"
|
||||
@@ -1,404 +0,0 @@
|
||||
"""Utilities for writing code that runs on Python 2 and 3"""
|
||||
|
||||
# Copyright (c) 2010-2013 Benjamin Peterson
|
||||
#
|
||||
# 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.
|
||||
|
||||
import operator
|
||||
import sys
|
||||
import types
|
||||
|
||||
__author__ = "Benjamin Peterson <benjamin@python.org>"
|
||||
__version__ = "1.3.0"
|
||||
|
||||
|
||||
# True if we are running on Python 3.
|
||||
PY3 = sys.version_info[0] == 3
|
||||
|
||||
if PY3:
|
||||
string_types = str,
|
||||
integer_types = int,
|
||||
class_types = type,
|
||||
text_type = str
|
||||
binary_type = bytes
|
||||
|
||||
MAXSIZE = sys.maxsize
|
||||
else:
|
||||
string_types = basestring,
|
||||
integer_types = (int, long)
|
||||
class_types = (type, types.ClassType)
|
||||
text_type = unicode
|
||||
binary_type = str
|
||||
|
||||
if sys.platform.startswith("java"):
|
||||
# Jython always uses 32 bits.
|
||||
MAXSIZE = int((1 << 31) - 1)
|
||||
else:
|
||||
# It's possible to have sizeof(long) != sizeof(Py_ssize_t).
|
||||
class X(object):
|
||||
def __len__(self):
|
||||
return 1 << 31
|
||||
try:
|
||||
len(X())
|
||||
except OverflowError:
|
||||
# 32-bit
|
||||
MAXSIZE = int((1 << 31) - 1)
|
||||
else:
|
||||
# 64-bit
|
||||
MAXSIZE = int((1 << 63) - 1)
|
||||
del X
|
||||
|
||||
|
||||
def _add_doc(func, doc):
|
||||
"""Add documentation to a function."""
|
||||
func.__doc__ = doc
|
||||
|
||||
|
||||
def _import_module(name):
|
||||
"""Import module, returning the module after the last dot."""
|
||||
__import__(name)
|
||||
return sys.modules[name]
|
||||
|
||||
|
||||
class _LazyDescr(object):
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __get__(self, obj, tp):
|
||||
result = self._resolve()
|
||||
setattr(obj, self.name, result)
|
||||
# This is a bit ugly, but it avoids running this again.
|
||||
delattr(tp, self.name)
|
||||
return result
|
||||
|
||||
|
||||
class MovedModule(_LazyDescr):
|
||||
|
||||
def __init__(self, name, old, new=None):
|
||||
super(MovedModule, self).__init__(name)
|
||||
if PY3:
|
||||
if new is None:
|
||||
new = name
|
||||
self.mod = new
|
||||
else:
|
||||
self.mod = old
|
||||
|
||||
def _resolve(self):
|
||||
return _import_module(self.mod)
|
||||
|
||||
|
||||
class MovedAttribute(_LazyDescr):
|
||||
|
||||
def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None):
|
||||
super(MovedAttribute, self).__init__(name)
|
||||
if PY3:
|
||||
if new_mod is None:
|
||||
new_mod = name
|
||||
self.mod = new_mod
|
||||
if new_attr is None:
|
||||
if old_attr is None:
|
||||
new_attr = name
|
||||
else:
|
||||
new_attr = old_attr
|
||||
self.attr = new_attr
|
||||
else:
|
||||
self.mod = old_mod
|
||||
if old_attr is None:
|
||||
old_attr = name
|
||||
self.attr = old_attr
|
||||
|
||||
def _resolve(self):
|
||||
module = _import_module(self.mod)
|
||||
return getattr(module, self.attr)
|
||||
|
||||
|
||||
|
||||
class _MovedItems(types.ModuleType):
|
||||
"""Lazy loading of moved objects"""
|
||||
|
||||
|
||||
_moved_attributes = [
|
||||
MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"),
|
||||
MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"),
|
||||
MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"),
|
||||
MovedAttribute("map", "itertools", "builtins", "imap", "map"),
|
||||
MovedAttribute("reload_module", "__builtin__", "imp", "reload"),
|
||||
MovedAttribute("reduce", "__builtin__", "functools"),
|
||||
MovedAttribute("StringIO", "StringIO", "io"),
|
||||
MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"),
|
||||
MovedAttribute("zip", "itertools", "builtins", "izip", "zip"),
|
||||
|
||||
MovedModule("builtins", "__builtin__"),
|
||||
MovedModule("configparser", "ConfigParser"),
|
||||
MovedModule("copyreg", "copy_reg"),
|
||||
MovedModule("http_cookiejar", "cookielib", "http.cookiejar"),
|
||||
MovedModule("http_cookies", "Cookie", "http.cookies"),
|
||||
MovedModule("html_entities", "htmlentitydefs", "html.entities"),
|
||||
MovedModule("html_parser", "HTMLParser", "html.parser"),
|
||||
MovedModule("http_client", "httplib", "http.client"),
|
||||
MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"),
|
||||
MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"),
|
||||
MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"),
|
||||
MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"),
|
||||
MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"),
|
||||
MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"),
|
||||
MovedModule("cPickle", "cPickle", "pickle"),
|
||||
MovedModule("queue", "Queue"),
|
||||
MovedModule("reprlib", "repr"),
|
||||
MovedModule("socketserver", "SocketServer"),
|
||||
MovedModule("tkinter", "Tkinter"),
|
||||
MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"),
|
||||
MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"),
|
||||
MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"),
|
||||
MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"),
|
||||
MovedModule("tkinter_tix", "Tix", "tkinter.tix"),
|
||||
MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"),
|
||||
MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"),
|
||||
MovedModule("tkinter_colorchooser", "tkColorChooser",
|
||||
"tkinter.colorchooser"),
|
||||
MovedModule("tkinter_commondialog", "tkCommonDialog",
|
||||
"tkinter.commondialog"),
|
||||
MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"),
|
||||
MovedModule("tkinter_font", "tkFont", "tkinter.font"),
|
||||
MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"),
|
||||
MovedModule("tkinter_tksimpledialog", "tkSimpleDialog",
|
||||
"tkinter.simpledialog"),
|
||||
MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"),
|
||||
MovedModule("winreg", "_winreg"),
|
||||
]
|
||||
for attr in _moved_attributes:
|
||||
setattr(_MovedItems, attr.name, attr)
|
||||
del attr
|
||||
|
||||
moves = sys.modules[__name__ + ".moves"] = _MovedItems("moves")
|
||||
|
||||
|
||||
def add_move(move):
|
||||
"""Add an item to six.moves."""
|
||||
setattr(_MovedItems, move.name, move)
|
||||
|
||||
|
||||
def remove_move(name):
|
||||
"""Remove item from six.moves."""
|
||||
try:
|
||||
delattr(_MovedItems, name)
|
||||
except AttributeError:
|
||||
try:
|
||||
del moves.__dict__[name]
|
||||
except KeyError:
|
||||
raise AttributeError("no such move, %r" % (name,))
|
||||
|
||||
|
||||
if PY3:
|
||||
_meth_func = "__func__"
|
||||
_meth_self = "__self__"
|
||||
|
||||
_func_closure = "__closure__"
|
||||
_func_code = "__code__"
|
||||
_func_defaults = "__defaults__"
|
||||
_func_globals = "__globals__"
|
||||
|
||||
_iterkeys = "keys"
|
||||
_itervalues = "values"
|
||||
_iteritems = "items"
|
||||
_iterlists = "lists"
|
||||
else:
|
||||
_meth_func = "im_func"
|
||||
_meth_self = "im_self"
|
||||
|
||||
_func_closure = "func_closure"
|
||||
_func_code = "func_code"
|
||||
_func_defaults = "func_defaults"
|
||||
_func_globals = "func_globals"
|
||||
|
||||
_iterkeys = "iterkeys"
|
||||
_itervalues = "itervalues"
|
||||
_iteritems = "iteritems"
|
||||
_iterlists = "iterlists"
|
||||
|
||||
|
||||
try:
|
||||
advance_iterator = next
|
||||
except NameError:
|
||||
def advance_iterator(it):
|
||||
return it.next()
|
||||
next = advance_iterator
|
||||
|
||||
|
||||
try:
|
||||
callable = callable
|
||||
except NameError:
|
||||
def callable(obj):
|
||||
return any("__call__" in klass.__dict__ for klass in type(obj).__mro__)
|
||||
|
||||
|
||||
if PY3:
|
||||
def get_unbound_function(unbound):
|
||||
return unbound
|
||||
|
||||
Iterator = object
|
||||
else:
|
||||
def get_unbound_function(unbound):
|
||||
return unbound.im_func
|
||||
|
||||
class Iterator(object):
|
||||
|
||||
def next(self):
|
||||
return type(self).__next__(self)
|
||||
|
||||
callable = callable
|
||||
_add_doc(get_unbound_function,
|
||||
"""Get the function out of a possibly unbound function""")
|
||||
|
||||
|
||||
get_method_function = operator.attrgetter(_meth_func)
|
||||
get_method_self = operator.attrgetter(_meth_self)
|
||||
get_function_closure = operator.attrgetter(_func_closure)
|
||||
get_function_code = operator.attrgetter(_func_code)
|
||||
get_function_defaults = operator.attrgetter(_func_defaults)
|
||||
get_function_globals = operator.attrgetter(_func_globals)
|
||||
|
||||
|
||||
def iterkeys(d, **kw):
|
||||
"""Return an iterator over the keys of a dictionary."""
|
||||
return iter(getattr(d, _iterkeys)(**kw))
|
||||
|
||||
def itervalues(d, **kw):
|
||||
"""Return an iterator over the values of a dictionary."""
|
||||
return iter(getattr(d, _itervalues)(**kw))
|
||||
|
||||
def iteritems(d, **kw):
|
||||
"""Return an iterator over the (key, value) pairs of a dictionary."""
|
||||
return iter(getattr(d, _iteritems)(**kw))
|
||||
|
||||
def iterlists(d, **kw):
|
||||
"""Return an iterator over the (key, [values]) pairs of a dictionary."""
|
||||
return iter(getattr(d, _iterlists)(**kw))
|
||||
|
||||
|
||||
if PY3:
|
||||
def b(s):
|
||||
return s.encode("latin-1")
|
||||
def u(s):
|
||||
return s
|
||||
if sys.version_info[1] <= 1:
|
||||
def int2byte(i):
|
||||
return bytes((i,))
|
||||
else:
|
||||
# This is about 2x faster than the implementation above on 3.2+
|
||||
int2byte = operator.methodcaller("to_bytes", 1, "big")
|
||||
import io
|
||||
StringIO = io.StringIO
|
||||
BytesIO = io.BytesIO
|
||||
else:
|
||||
def b(s):
|
||||
return s
|
||||
def u(s):
|
||||
return unicode(s, "unicode_escape")
|
||||
int2byte = chr
|
||||
import StringIO
|
||||
StringIO = BytesIO = StringIO.StringIO
|
||||
_add_doc(b, """Byte literal""")
|
||||
_add_doc(u, """Text literal""")
|
||||
|
||||
|
||||
if PY3:
|
||||
import builtins
|
||||
exec_ = getattr(builtins, "exec")
|
||||
|
||||
|
||||
def reraise(tp, value, tb=None):
|
||||
if value.__traceback__ is not tb:
|
||||
raise value.with_traceback(tb)
|
||||
raise value
|
||||
|
||||
|
||||
print_ = getattr(builtins, "print")
|
||||
del builtins
|
||||
|
||||
else:
|
||||
def exec_(_code_, _globs_=None, _locs_=None):
|
||||
"""Execute code in a namespace."""
|
||||
if _globs_ is None:
|
||||
frame = sys._getframe(1)
|
||||
_globs_ = frame.f_globals
|
||||
if _locs_ is None:
|
||||
_locs_ = frame.f_locals
|
||||
del frame
|
||||
elif _locs_ is None:
|
||||
_locs_ = _globs_
|
||||
exec("""exec _code_ in _globs_, _locs_""")
|
||||
|
||||
|
||||
exec_("""def reraise(tp, value, tb=None):
|
||||
raise tp, value, tb
|
||||
""")
|
||||
|
||||
|
||||
def print_(*args, **kwargs):
|
||||
"""The new-style print function."""
|
||||
fp = kwargs.pop("file", sys.stdout)
|
||||
if fp is None:
|
||||
return
|
||||
def write(data):
|
||||
if not isinstance(data, basestring):
|
||||
data = str(data)
|
||||
fp.write(data)
|
||||
want_unicode = False
|
||||
sep = kwargs.pop("sep", None)
|
||||
if sep is not None:
|
||||
if isinstance(sep, unicode):
|
||||
want_unicode = True
|
||||
elif not isinstance(sep, str):
|
||||
raise TypeError("sep must be None or a string")
|
||||
end = kwargs.pop("end", None)
|
||||
if end is not None:
|
||||
if isinstance(end, unicode):
|
||||
want_unicode = True
|
||||
elif not isinstance(end, str):
|
||||
raise TypeError("end must be None or a string")
|
||||
if kwargs:
|
||||
raise TypeError("invalid keyword arguments to print()")
|
||||
if not want_unicode:
|
||||
for arg in args:
|
||||
if isinstance(arg, unicode):
|
||||
want_unicode = True
|
||||
break
|
||||
if want_unicode:
|
||||
newline = unicode("\n")
|
||||
space = unicode(" ")
|
||||
else:
|
||||
newline = "\n"
|
||||
space = " "
|
||||
if sep is None:
|
||||
sep = space
|
||||
if end is None:
|
||||
end = newline
|
||||
for i, arg in enumerate(args):
|
||||
if i:
|
||||
write(sep)
|
||||
write(arg)
|
||||
write(end)
|
||||
|
||||
_add_doc(reraise, """Reraise an exception.""")
|
||||
|
||||
|
||||
def with_metaclass(meta, base=object):
|
||||
"""Create a base class with a metaclass."""
|
||||
return meta("NewBase", (base,), {})
|
||||
475
fig/service.py
475
fig/service.py
@@ -1,475 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from .packages.docker.errors import APIError
|
||||
import logging
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
from .container import Container
|
||||
from .progress_stream import stream_output, StreamOutputError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
DOCKER_CONFIG_KEYS = ['image', 'command', 'hostname', 'domainname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'entrypoint', 'privileged', 'volumes_from', 'net', 'working_dir']
|
||||
DOCKER_CONFIG_HINTS = {
|
||||
'link' : 'links',
|
||||
'port' : 'ports',
|
||||
'privilege' : 'privileged',
|
||||
'priviliged': 'privileged',
|
||||
'privilige' : 'privileged',
|
||||
'volume' : 'volumes',
|
||||
'workdir' : 'working_dir',
|
||||
}
|
||||
|
||||
VALID_NAME_CHARS = '[a-zA-Z0-9]'
|
||||
|
||||
|
||||
class BuildError(Exception):
|
||||
def __init__(self, service, reason):
|
||||
self.service = service
|
||||
self.reason = reason
|
||||
|
||||
|
||||
class CannotBeScaledError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ConfigError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class Service(object):
|
||||
def __init__(self, name, client=None, project='default', links=None, volumes_from=None, **options):
|
||||
if not re.match('^%s+$' % VALID_NAME_CHARS, name):
|
||||
raise ConfigError('Invalid service name "%s" - only %s are allowed' % (name, VALID_NAME_CHARS))
|
||||
if not re.match('^%s+$' % VALID_NAME_CHARS, project):
|
||||
raise ConfigError('Invalid project name "%s" - only %s are allowed' % (project, VALID_NAME_CHARS))
|
||||
if 'image' in options and 'build' in options:
|
||||
raise ConfigError('Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both.' % name)
|
||||
|
||||
supported_options = DOCKER_CONFIG_KEYS + ['build', 'expose']
|
||||
|
||||
for k in options:
|
||||
if k not in supported_options:
|
||||
msg = "Unsupported config option for %s service: '%s'" % (name, k)
|
||||
if k in DOCKER_CONFIG_HINTS:
|
||||
msg += " (did you mean '%s'?)" % DOCKER_CONFIG_HINTS[k]
|
||||
raise ConfigError(msg)
|
||||
|
||||
self.name = name
|
||||
self.client = client
|
||||
self.project = project
|
||||
self.links = links or []
|
||||
self.volumes_from = volumes_from or []
|
||||
self.options = options
|
||||
|
||||
def containers(self, stopped=False, one_off=False):
|
||||
l = []
|
||||
for container in self.client.containers(all=stopped):
|
||||
name = get_container_name(container)
|
||||
if not name or not is_valid_name(name, one_off):
|
||||
continue
|
||||
project, name, number = parse_name(name)
|
||||
if project == self.project and name == self.name:
|
||||
l.append(Container.from_ps(self.client, container))
|
||||
return l
|
||||
|
||||
def start(self, **options):
|
||||
for c in self.containers(stopped=True):
|
||||
self.start_container_if_stopped(c, **options)
|
||||
|
||||
def stop(self, **options):
|
||||
for c in self.containers():
|
||||
log.info("Stopping %s..." % c.name)
|
||||
c.stop(**options)
|
||||
|
||||
def kill(self, **options):
|
||||
for c in self.containers():
|
||||
log.info("Killing %s..." % c.name)
|
||||
c.kill(**options)
|
||||
|
||||
def scale(self, desired_num):
|
||||
"""
|
||||
Adjusts the number of containers to the specified number and ensures they are running.
|
||||
|
||||
- creates containers until there are at least `desired_num`
|
||||
- stops containers until there are at most `desired_num` running
|
||||
- starts containers until there are at least `desired_num` running
|
||||
- removes all stopped containers
|
||||
"""
|
||||
if not self.can_be_scaled():
|
||||
raise CannotBeScaledError()
|
||||
|
||||
# Create enough containers
|
||||
containers = self.containers(stopped=True)
|
||||
while len(containers) < desired_num:
|
||||
containers.append(self.create_container())
|
||||
|
||||
running_containers = []
|
||||
stopped_containers = []
|
||||
for c in containers:
|
||||
if c.is_running:
|
||||
running_containers.append(c)
|
||||
else:
|
||||
stopped_containers.append(c)
|
||||
running_containers.sort(key=lambda c: c.number)
|
||||
stopped_containers.sort(key=lambda c: c.number)
|
||||
|
||||
# Stop containers
|
||||
while len(running_containers) > desired_num:
|
||||
c = running_containers.pop()
|
||||
log.info("Stopping %s..." % c.name)
|
||||
c.stop(timeout=1)
|
||||
stopped_containers.append(c)
|
||||
|
||||
# Start containers
|
||||
while len(running_containers) < desired_num:
|
||||
c = stopped_containers.pop(0)
|
||||
log.info("Starting %s..." % c.name)
|
||||
self.start_container(c)
|
||||
running_containers.append(c)
|
||||
|
||||
self.remove_stopped()
|
||||
|
||||
def remove_stopped(self, **options):
|
||||
for c in self.containers(stopped=True):
|
||||
if not c.is_running:
|
||||
log.info("Removing %s..." % c.name)
|
||||
c.remove(**options)
|
||||
|
||||
def create_container(self, one_off=False, **override_options):
|
||||
"""
|
||||
Create a container for this service. If the image doesn't exist, attempt to pull
|
||||
it.
|
||||
"""
|
||||
container_options = self._get_container_create_options(override_options, one_off=one_off)
|
||||
try:
|
||||
return Container.create(self.client, **container_options)
|
||||
except APIError as e:
|
||||
if e.response.status_code == 404 and e.explanation and 'No such image' in str(e.explanation):
|
||||
log.info('Pulling image %s...' % container_options['image'])
|
||||
output = self.client.pull(container_options['image'], stream=True)
|
||||
stream_output(output, sys.stdout)
|
||||
return Container.create(self.client, **container_options)
|
||||
raise
|
||||
|
||||
def recreate_containers(self, **override_options):
|
||||
"""
|
||||
If a container for this service doesn't exist, create and start one. If there are
|
||||
any, stop them, create+start new ones, and remove the old containers.
|
||||
"""
|
||||
containers = self.containers(stopped=True)
|
||||
|
||||
if len(containers) == 0:
|
||||
log.info("Creating %s..." % self.next_container_name())
|
||||
container = self.create_container(**override_options)
|
||||
self.start_container(container)
|
||||
return [(None, container)]
|
||||
else:
|
||||
tuples = []
|
||||
|
||||
for c in containers:
|
||||
log.info("Recreating %s..." % c.name)
|
||||
tuples.append(self.recreate_container(c, **override_options))
|
||||
|
||||
return tuples
|
||||
|
||||
def recreate_container(self, container, **override_options):
|
||||
try:
|
||||
container.stop()
|
||||
except APIError as e:
|
||||
if (e.response.status_code == 500
|
||||
and e.explanation
|
||||
and 'no such process' in str(e.explanation)):
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
|
||||
intermediate_container = Container.create(
|
||||
self.client,
|
||||
image=container.image,
|
||||
entrypoint=['echo'],
|
||||
command=[],
|
||||
)
|
||||
intermediate_container.start(volumes_from=container.id)
|
||||
intermediate_container.wait()
|
||||
container.remove()
|
||||
|
||||
options = dict(override_options)
|
||||
new_container = self.create_container(**options)
|
||||
self.start_container(new_container, intermediate_container=intermediate_container)
|
||||
|
||||
intermediate_container.remove()
|
||||
|
||||
return (intermediate_container, new_container)
|
||||
|
||||
def start_container_if_stopped(self, container, **options):
|
||||
if container.is_running:
|
||||
return container
|
||||
else:
|
||||
log.info("Starting %s..." % container.name)
|
||||
return self.start_container(container, **options)
|
||||
|
||||
def start_container(self, container=None, intermediate_container=None, **override_options):
|
||||
if container is None:
|
||||
container = self.create_container(**override_options)
|
||||
|
||||
options = self.options.copy()
|
||||
options.update(override_options)
|
||||
|
||||
port_bindings = {}
|
||||
|
||||
if options.get('ports', None) is not None:
|
||||
for port in options['ports']:
|
||||
internal_port, external_port = split_port(port)
|
||||
port_bindings[internal_port] = external_port
|
||||
|
||||
volume_bindings = {}
|
||||
|
||||
if options.get('volumes', None) is not None:
|
||||
for volume in options['volumes']:
|
||||
if ':' in volume:
|
||||
external_dir, internal_dir = volume.split(':')
|
||||
volume_bindings[os.path.abspath(external_dir)] = {
|
||||
'bind': internal_dir,
|
||||
'ro': False,
|
||||
}
|
||||
|
||||
privileged = options.get('privileged', False)
|
||||
net = options.get('net', 'bridge')
|
||||
dns = options.get('dns', None)
|
||||
|
||||
container.start(
|
||||
links=self._get_links(link_to_self=override_options.get('one_off', False)),
|
||||
port_bindings=port_bindings,
|
||||
binds=volume_bindings,
|
||||
volumes_from=self._get_volumes_from(intermediate_container),
|
||||
privileged=privileged,
|
||||
network_mode=net,
|
||||
dns=dns,
|
||||
)
|
||||
return container
|
||||
|
||||
def start_or_create_containers(self):
|
||||
containers = self.containers(stopped=True)
|
||||
|
||||
if len(containers) == 0:
|
||||
log.info("Creating %s..." % self.next_container_name())
|
||||
new_container = self.create_container()
|
||||
return [self.start_container(new_container)]
|
||||
else:
|
||||
return [self.start_container_if_stopped(c) for c in containers]
|
||||
|
||||
def get_linked_names(self):
|
||||
return [s.name for (s, _) in self.links]
|
||||
|
||||
def next_container_name(self, one_off=False):
|
||||
bits = [self.project, self.name]
|
||||
if one_off:
|
||||
bits.append('run')
|
||||
return '_'.join(bits + [str(self.next_container_number(one_off=one_off))])
|
||||
|
||||
def next_container_number(self, one_off=False):
|
||||
numbers = [parse_name(c.name)[2] for c in self.containers(stopped=True, one_off=one_off)]
|
||||
|
||||
if len(numbers) == 0:
|
||||
return 1
|
||||
else:
|
||||
return max(numbers) + 1
|
||||
|
||||
def _get_links(self, link_to_self):
|
||||
links = []
|
||||
for service, link_name in self.links:
|
||||
for container in service.containers():
|
||||
if link_name:
|
||||
links.append((container.name, link_name))
|
||||
links.append((container.name, container.name))
|
||||
links.append((container.name, container.name_without_project))
|
||||
if link_to_self:
|
||||
for container in self.containers():
|
||||
links.append((container.name, container.name))
|
||||
links.append((container.name, container.name_without_project))
|
||||
return links
|
||||
|
||||
def _get_volumes_from(self, intermediate_container=None):
|
||||
volumes_from = []
|
||||
for v in self.volumes_from:
|
||||
if isinstance(v, Service):
|
||||
for container in v.containers(stopped=True):
|
||||
volumes_from.append(container.id)
|
||||
elif isinstance(v, Container):
|
||||
volumes_from.append(v.id)
|
||||
|
||||
if intermediate_container:
|
||||
volumes_from.append(intermediate_container.id)
|
||||
|
||||
return volumes_from
|
||||
|
||||
def _get_container_create_options(self, override_options, one_off=False):
|
||||
container_options = dict((k, self.options[k]) for k in DOCKER_CONFIG_KEYS if k in self.options)
|
||||
container_options.update(override_options)
|
||||
|
||||
container_options['name'] = self.next_container_name(one_off)
|
||||
|
||||
# If a qualified hostname was given, split it into an
|
||||
# unqualified hostname and a domainname unless domainname
|
||||
# was also given explicitly. This matches the behavior of
|
||||
# the official Docker CLI in that scenario.
|
||||
if ('hostname' in container_options
|
||||
and 'domainname' not in container_options
|
||||
and '.' in container_options['hostname']):
|
||||
parts = container_options['hostname'].partition('.')
|
||||
container_options['hostname'] = parts[0]
|
||||
container_options['domainname'] = parts[2]
|
||||
|
||||
if 'ports' in container_options or 'expose' in self.options:
|
||||
ports = []
|
||||
all_ports = container_options.get('ports', []) + self.options.get('expose', [])
|
||||
for port in all_ports:
|
||||
port = str(port)
|
||||
if ':' in port:
|
||||
port = port.split(':')[-1]
|
||||
if '/' in port:
|
||||
port = tuple(port.split('/'))
|
||||
ports.append(port)
|
||||
container_options['ports'] = ports
|
||||
|
||||
if 'volumes' in container_options:
|
||||
container_options['volumes'] = dict((split_volume(v)[1], {}) for v in container_options['volumes'])
|
||||
|
||||
if 'environment' in container_options:
|
||||
if isinstance(container_options['environment'], list):
|
||||
container_options['environment'] = dict(split_env(e) for e in container_options['environment'])
|
||||
container_options['environment'] = dict(resolve_env(k, v) for k, v in container_options['environment'].iteritems())
|
||||
|
||||
if self.can_be_built():
|
||||
if len(self.client.images(name=self._build_tag_name())) == 0:
|
||||
self.build()
|
||||
container_options['image'] = self._build_tag_name()
|
||||
|
||||
# Delete options which are only used when starting
|
||||
for key in ['privileged', 'net', 'dns']:
|
||||
if key in container_options:
|
||||
del container_options[key]
|
||||
|
||||
return container_options
|
||||
|
||||
def build(self, no_cache=False):
|
||||
log.info('Building %s...' % self.name)
|
||||
|
||||
build_output = self.client.build(
|
||||
self.options['build'],
|
||||
tag=self._build_tag_name(),
|
||||
stream=True,
|
||||
rm=True,
|
||||
nocache=no_cache,
|
||||
)
|
||||
|
||||
try:
|
||||
all_events = stream_output(build_output, sys.stdout)
|
||||
except StreamOutputError, e:
|
||||
raise BuildError(self, unicode(e))
|
||||
|
||||
image_id = None
|
||||
|
||||
for event in all_events:
|
||||
if 'stream' in event:
|
||||
match = re.search(r'Successfully built ([0-9a-f]+)', event.get('stream', ''))
|
||||
if match:
|
||||
image_id = match.group(1)
|
||||
|
||||
if image_id is None:
|
||||
raise BuildError(self)
|
||||
|
||||
return image_id
|
||||
|
||||
def can_be_built(self):
|
||||
return 'build' in self.options
|
||||
|
||||
def _build_tag_name(self):
|
||||
"""
|
||||
The tag to give to images built for this service.
|
||||
"""
|
||||
return '%s_%s' % (self.project, self.name)
|
||||
|
||||
def can_be_scaled(self):
|
||||
for port in self.options.get('ports', []):
|
||||
if ':' in str(port):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
NAME_RE = re.compile(r'^([^_]+)_([^_]+)_(run_)?(\d+)$')
|
||||
|
||||
|
||||
def is_valid_name(name, one_off=False):
|
||||
match = NAME_RE.match(name)
|
||||
if match is None:
|
||||
return False
|
||||
if one_off:
|
||||
return match.group(3) == 'run_'
|
||||
else:
|
||||
return match.group(3) is None
|
||||
|
||||
|
||||
def parse_name(name, one_off=False):
|
||||
match = NAME_RE.match(name)
|
||||
(project, service_name, _, suffix) = match.groups()
|
||||
return (project, service_name, int(suffix))
|
||||
|
||||
|
||||
def get_container_name(container):
|
||||
if not container.get('Name') and not container.get('Names'):
|
||||
return None
|
||||
# inspect
|
||||
if 'Name' in container:
|
||||
return container['Name']
|
||||
# ps
|
||||
for name in container['Names']:
|
||||
if len(name.split('/')) == 2:
|
||||
return name[1:]
|
||||
|
||||
|
||||
def split_volume(v):
|
||||
"""
|
||||
If v is of the format EXTERNAL:INTERNAL, returns (EXTERNAL, INTERNAL).
|
||||
If v is of the format INTERNAL, returns (None, INTERNAL).
|
||||
"""
|
||||
if ':' in v:
|
||||
return v.split(':', 1)
|
||||
else:
|
||||
return (None, v)
|
||||
|
||||
|
||||
def split_port(port):
|
||||
port = str(port)
|
||||
external_ip = None
|
||||
if ':' in port:
|
||||
external_port, internal_port = port.rsplit(':', 1)
|
||||
if ':' in external_port:
|
||||
external_ip, external_port = external_port.split(':', 1)
|
||||
else:
|
||||
external_port, internal_port = (None, port)
|
||||
if external_ip:
|
||||
if external_port:
|
||||
external_port = (external_ip, external_port)
|
||||
else:
|
||||
external_port = (external_ip,)
|
||||
return internal_port, external_port
|
||||
|
||||
|
||||
def split_env(env):
|
||||
if '=' in env:
|
||||
return env.split('=', 1)
|
||||
else:
|
||||
return env, None
|
||||
|
||||
|
||||
def resolve_env(key, val):
|
||||
if val is not None:
|
||||
return key, val
|
||||
elif key in os.environ:
|
||||
return key, os.environ[key]
|
||||
else:
|
||||
return key, ''
|
||||
@@ -1,5 +1,6 @@
|
||||
mock==1.0.1
|
||||
nose==1.3.0
|
||||
pyinstaller==2.1
|
||||
unittest2
|
||||
flake8
|
||||
mock >= 1.0.1
|
||||
nose==1.3.4
|
||||
git+https://github.com/pyinstaller/pyinstaller.git@12e40471c77f588ea5be352f7219c873ddaae056#egg=pyinstaller
|
||||
unittest2==0.8.0
|
||||
flake8==2.3.0
|
||||
pep8==1.6.1
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
docopt==0.6.1
|
||||
PyYAML==3.10
|
||||
docker-py==0.7.1
|
||||
dockerpty==0.3.2
|
||||
docopt==0.6.1
|
||||
requests==2.2.1
|
||||
six==1.7.3
|
||||
texttable==0.8.1
|
||||
websocket-client==0.11.0
|
||||
dockerpty==0.2.3
|
||||
|
||||
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
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -ex
|
||||
pushd docs
|
||||
fig run jekyll jekyll build
|
||||
popd
|
||||
@@ -2,6 +2,7 @@
|
||||
set -ex
|
||||
mkdir -p `pwd`/dist
|
||||
chmod 777 `pwd`/dist
|
||||
docker build -t fig .
|
||||
docker run -v `pwd`/dist:/code/dist fig pyinstaller -F bin/fig
|
||||
docker run -v `pwd`/dist:/code/dist fig dist/fig --version
|
||||
docker build -t docker-compose .
|
||||
docker run -u user -v `pwd`/dist:/code/dist --rm --entrypoint pyinstaller docker-compose -F bin/docker-compose
|
||||
mv dist/docker-compose dist/docker-compose-Linux-x86_64
|
||||
docker run -u user -v `pwd`/dist:/code/dist --rm --entrypoint dist/docker-compose-Linux-x86_64 docker-compose --version
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
set -ex
|
||||
rm -rf venv
|
||||
virtualenv venv
|
||||
venv/bin/pip install pyinstaller==2.1
|
||||
venv/bin/pip install -r requirements.txt
|
||||
venv/bin/pip install -r requirements-dev.txt
|
||||
venv/bin/pip install .
|
||||
venv/bin/pyinstaller -F bin/fig
|
||||
dist/fig --version
|
||||
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,3 +1,3 @@
|
||||
#!/bin/sh
|
||||
find . -type f -name '*.pyc' -delete
|
||||
rm -rf docs/_site build dist fig.egg-info
|
||||
rm -rf docs/_site build dist docker-compose.egg-info
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -ex
|
||||
|
||||
script/build-docs
|
||||
|
||||
pushd docs/_site
|
||||
|
||||
export GIT_DIR=.git-gh-pages
|
||||
export GIT_WORK_TREE=.
|
||||
|
||||
if [ ! -d "$GIT_DIR" ]; then
|
||||
git init
|
||||
fi
|
||||
|
||||
if !(git remote | grep origin); then
|
||||
git remote add origin git@github.com:orchardup/fig.git
|
||||
fi
|
||||
|
||||
git fetch origin
|
||||
git reset --soft origin/gh-pages
|
||||
|
||||
echo ".git-gh-pages" > .gitignore
|
||||
|
||||
git add -A .
|
||||
|
||||
git commit -m "update" || echo "didn't commit"
|
||||
git push origin master:gh-pages
|
||||
|
||||
popd
|
||||
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 $@
|
||||
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,2 +0,0 @@
|
||||
#!/bin/bash
|
||||
open file://`pwd`/docs/_site/index.html
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
flake8 fig
|
||||
PYTHONIOENCODING=ascii nosetests $@
|
||||
set -ex
|
||||
docker build -t docker-compose .
|
||||
docker run -v /var/run/docker.sock:/var/run/docker.sock --rm --entrypoint flake8 docker-compose compose
|
||||
docker run -v /var/run/docker.sock:/var/run/docker.sock --rm --entrypoint nosetests docker-compose $@
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -ex
|
||||
|
||||
# Kill background processes on exit
|
||||
trap 'kill -9 $(jobs -p)' SIGINT SIGTERM EXIT
|
||||
|
||||
export DOCKER_HOST=tcp://localhost:4243
|
||||
orchard proxy -H $TRAVIS_JOB_ID $DOCKER_HOST &
|
||||
sleep 2
|
||||
nosetests -v
|
||||
56
script/validate-dco
Executable file
56
script/validate-dco
Executable file
@@ -0,0 +1,56 @@
|
||||
#!/bin/bash
|
||||
|
||||
source "$(dirname "$BASH_SOURCE")/.validate"
|
||||
|
||||
adds=$(validate_diff --numstat | awk '{ s += $1 } END { print s }')
|
||||
dels=$(validate_diff --numstat | awk '{ s += $2 } END { print s }')
|
||||
notDocs="$(validate_diff --numstat | awk '$3 !~ /^docs\// { print $3 }')"
|
||||
|
||||
: ${adds:=0}
|
||||
: ${dels:=0}
|
||||
|
||||
# "Username may only contain alphanumeric characters or dashes and cannot begin with a dash"
|
||||
githubUsernameRegex='[a-zA-Z0-9][a-zA-Z0-9-]+'
|
||||
|
||||
# https://github.com/docker/docker/blob/master/CONTRIBUTING.md#sign-your-work
|
||||
dcoPrefix='Signed-off-by:'
|
||||
dcoRegex="^(Docker-DCO-1.1-)?$dcoPrefix ([^<]+) <([^<>@]+@[^<>]+)>( \\(github: ($githubUsernameRegex)\\))?$"
|
||||
|
||||
check_dco() {
|
||||
grep -qE "$dcoRegex"
|
||||
}
|
||||
|
||||
if [ $adds -eq 0 -a $dels -eq 0 ]; then
|
||||
echo '0 adds, 0 deletions; nothing to validate! :)'
|
||||
elif [ -z "$notDocs" -a $adds -le 1 -a $dels -le 1 ]; then
|
||||
echo 'Congratulations! DCO small-patch-exception material!'
|
||||
else
|
||||
commits=( $(validate_log --format='format:%H%n') )
|
||||
badCommits=()
|
||||
for commit in "${commits[@]}"; do
|
||||
if [ -z "$(git log -1 --format='format:' --name-status "$commit")" ]; then
|
||||
# no content (ie, Merge commit, etc)
|
||||
continue
|
||||
fi
|
||||
if ! git log -1 --format='format:%B' "$commit" | check_dco; then
|
||||
badCommits+=( "$commit" )
|
||||
fi
|
||||
done
|
||||
if [ ${#badCommits[@]} -eq 0 ]; then
|
||||
echo "Congratulations! All commits are properly signed with the DCO!"
|
||||
else
|
||||
{
|
||||
echo "These commits do not have a proper '$dcoPrefix' marker:"
|
||||
for commit in "${badCommits[@]}"; do
|
||||
echo " - $commit"
|
||||
done
|
||||
echo
|
||||
echo 'Please amend each commit to include a properly formatted DCO marker.'
|
||||
echo
|
||||
echo 'Visit the following URL for information about the Docker DCO:'
|
||||
echo ' https://github.com/docker/docker/blob/master/CONTRIBUTING.md#sign-your-work'
|
||||
echo
|
||||
} >&2
|
||||
false
|
||||
fi
|
||||
fi
|
||||
43
setup.py
43
setup.py
@@ -3,9 +3,10 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from setuptools import setup, find_packages
|
||||
import re
|
||||
import os
|
||||
import codecs
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
def read(*parts):
|
||||
@@ -22,26 +23,44 @@ def find_version(*file_paths):
|
||||
return version_match.group(1)
|
||||
raise RuntimeError("Unable to find version string.")
|
||||
|
||||
with open('requirements.txt') as f:
|
||||
install_requires = f.read().splitlines()
|
||||
|
||||
with open('requirements-dev.txt') as f:
|
||||
tests_require = f.read().splitlines()
|
||||
install_requires = [
|
||||
'docopt >= 0.6.1, < 0.7',
|
||||
'PyYAML >= 3.10, < 4',
|
||||
'requests >= 2.2.1, < 2.5.0',
|
||||
'texttable >= 0.8.1, < 0.9',
|
||||
'websocket-client >= 0.11.0, < 1.0',
|
||||
'docker-py >= 0.6.0, < 0.8',
|
||||
'dockerpty >= 0.3.2, < 0.4',
|
||||
'six >= 1.3.0, < 2',
|
||||
]
|
||||
|
||||
tests_require = [
|
||||
'mock >= 1.0.1',
|
||||
'nose',
|
||||
'pyinstaller',
|
||||
'flake8',
|
||||
]
|
||||
|
||||
|
||||
if sys.version_info < (2, 7):
|
||||
tests_require.append('unittest2')
|
||||
|
||||
|
||||
setup(
|
||||
name='fig',
|
||||
version=find_version("fig", "__init__.py"),
|
||||
description='Punctual, lightweight development environments using Docker',
|
||||
url='http://www.fig.sh/',
|
||||
name='docker-compose',
|
||||
version=find_version("compose", "__init__.py"),
|
||||
description='Multi-container orchestration for Docker',
|
||||
url='https://www.docker.com/',
|
||||
author='Docker, Inc.',
|
||||
license='Apache License 2.0',
|
||||
packages=find_packages(),
|
||||
packages=find_packages(exclude=[ 'tests.*', 'tests' ]),
|
||||
include_package_data=True,
|
||||
test_suite='nose.collector',
|
||||
install_requires=install_requires,
|
||||
tests_require=tests_require,
|
||||
entry_points="""
|
||||
[console_scripts]
|
||||
fig=fig.cli.main:main
|
||||
docker-compose=compose.cli.main:main
|
||||
""",
|
||||
)
|
||||
|
||||
5
tests/fixtures/commands-composefile/docker-compose.yml
vendored
Normal file
5
tests/fixtures/commands-composefile/docker-compose.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
implicit:
|
||||
image: composetest_test
|
||||
explicit:
|
||||
image: composetest_test
|
||||
command: [ "/bin/true" ]
|
||||
5
tests/fixtures/commands-figfile/fig.yml
vendored
5
tests/fixtures/commands-figfile/fig.yml
vendored
@@ -1,5 +0,0 @@
|
||||
implicit:
|
||||
image: figtest_test
|
||||
explicit:
|
||||
image: figtest_test
|
||||
command: [ "/bin/true" ]
|
||||
3
tests/fixtures/dockerfile-with-volume/Dockerfile
vendored
Normal file
3
tests/fixtures/dockerfile-with-volume/Dockerfile
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
FROM busybox
|
||||
VOLUME /data
|
||||
CMD sleep 3000
|
||||
2
tests/fixtures/dockerfile_with_entrypoint/Dockerfile
vendored
Normal file
2
tests/fixtures/dockerfile_with_entrypoint/Dockerfile
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
FROM busybox:latest
|
||||
ENTRYPOINT echo "From prebuilt entrypoint"
|
||||
2
tests/fixtures/dockerfile_with_entrypoint/docker-compose.yml
vendored
Normal file
2
tests/fixtures/dockerfile_with_entrypoint/docker-compose.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
service:
|
||||
build: tests/fixtures/dockerfile_with_entrypoint
|
||||
11
tests/fixtures/env/one.env
vendored
Normal file
11
tests/fixtures/env/one.env
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# Keep the blank lines and comments in this file, please
|
||||
|
||||
ONE=2
|
||||
TWO=1
|
||||
|
||||
# (thanks)
|
||||
|
||||
THREE=3
|
||||
|
||||
FOO=bar
|
||||
# FOO=somethingelse
|
||||
4
tests/fixtures/env/resolve.env
vendored
Normal file
4
tests/fixtures/env/resolve.env
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
FILE_DEF=F1
|
||||
FILE_DEF_EMPTY=
|
||||
ENV_DEF
|
||||
NO_DEF
|
||||
2
tests/fixtures/env/two.env
vendored
Normal file
2
tests/fixtures/env/two.env
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
FOO=baz
|
||||
DOO=dah
|
||||
7
tests/fixtures/environment-composefile/docker-compose.yml
vendored
Normal file
7
tests/fixtures/environment-composefile/docker-compose.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
service:
|
||||
image: busybox:latest
|
||||
command: sleep 5
|
||||
|
||||
environment:
|
||||
foo: bar
|
||||
hello: world
|
||||
7
tests/fixtures/ports-composefile/docker-compose.yml
vendored
Normal file
7
tests/fixtures/ports-composefile/docker-compose.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
simple:
|
||||
image: busybox:latest
|
||||
command: /bin/sleep 300
|
||||
ports:
|
||||
- '3000'
|
||||
- '9999:3001'
|
||||
6
tests/fixtures/simple-composefile/docker-compose.yml
vendored
Normal file
6
tests/fixtures/simple-composefile/docker-compose.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
simple:
|
||||
image: busybox:latest
|
||||
command: /bin/sleep 300
|
||||
another:
|
||||
image: busybox:latest
|
||||
command: /bin/sleep 300
|
||||
@@ -1,50 +1,74 @@
|
||||
from __future__ import absolute_import
|
||||
from .testcases import DockerClientTestCase
|
||||
from mock import patch
|
||||
from fig.cli.main import TopLevelCommand
|
||||
from fig.packages.six import StringIO
|
||||
import sys
|
||||
|
||||
from six import StringIO
|
||||
from mock import patch
|
||||
|
||||
from .testcases import DockerClientTestCase
|
||||
from compose.cli.main import TopLevelCommand
|
||||
|
||||
|
||||
class CLITestCase(DockerClientTestCase):
|
||||
def setUp(self):
|
||||
super(CLITestCase, self).setUp()
|
||||
self.old_sys_exit = sys.exit
|
||||
sys.exit = lambda code=0: None
|
||||
self.command = TopLevelCommand()
|
||||
self.command.base_dir = 'tests/fixtures/simple-figfile'
|
||||
self.command.base_dir = 'tests/fixtures/simple-composefile'
|
||||
|
||||
def tearDown(self):
|
||||
sys.exit = self.old_sys_exit
|
||||
self.command.project.kill()
|
||||
self.command.project.remove_stopped()
|
||||
self.project.kill()
|
||||
self.project.remove_stopped()
|
||||
|
||||
@property
|
||||
def project(self):
|
||||
return self.command.get_project(self.command.get_config_path())
|
||||
|
||||
def test_help(self):
|
||||
old_base_dir = self.command.base_dir
|
||||
self.command.base_dir = 'tests/fixtures/no-composefile'
|
||||
with self.assertRaises(SystemExit) as exc_context:
|
||||
self.command.dispatch(['help', 'up'], None)
|
||||
self.assertIn('Usage: up [options] [SERVICE...]', str(exc_context.exception))
|
||||
# self.project.kill() fails during teardown
|
||||
# unless there is a composefile.
|
||||
self.command.base_dir = old_base_dir
|
||||
|
||||
# TODO: address the "Inappropriate ioctl for device" warnings in test output
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
def test_ps(self, mock_stdout):
|
||||
self.command.project.get_service('simple').create_container()
|
||||
self.project.get_service('simple').create_container()
|
||||
self.command.dispatch(['ps'], None)
|
||||
self.assertIn('simplefigfile_simple_1', mock_stdout.getvalue())
|
||||
self.assertIn('simplecomposefile_simple_1', mock_stdout.getvalue())
|
||||
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
def test_ps_default_figfile(self, mock_stdout):
|
||||
self.command.base_dir = 'tests/fixtures/multiple-figfiles'
|
||||
def test_ps_default_composefile(self, mock_stdout):
|
||||
self.command.base_dir = 'tests/fixtures/multiple-composefiles'
|
||||
self.command.dispatch(['up', '-d'], None)
|
||||
self.command.dispatch(['ps'], None)
|
||||
|
||||
output = mock_stdout.getvalue()
|
||||
self.assertIn('multiplefigfiles_simple_1', output)
|
||||
self.assertIn('multiplefigfiles_another_1', output)
|
||||
self.assertNotIn('multiplefigfiles_yetanother_1', output)
|
||||
self.assertIn('multiplecomposefiles_simple_1', output)
|
||||
self.assertIn('multiplecomposefiles_another_1', output)
|
||||
self.assertNotIn('multiplecomposefiles_yetanother_1', output)
|
||||
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
def test_ps_alternate_figfile(self, mock_stdout):
|
||||
self.command.base_dir = 'tests/fixtures/multiple-figfiles'
|
||||
self.command.dispatch(['-f', 'fig2.yml', 'up', '-d'], None)
|
||||
self.command.dispatch(['-f', 'fig2.yml', 'ps'], None)
|
||||
def test_ps_alternate_composefile(self, mock_stdout):
|
||||
self.command.base_dir = 'tests/fixtures/multiple-composefiles'
|
||||
self.command.dispatch(['-f', 'compose2.yml', 'up', '-d'], None)
|
||||
self.command.dispatch(['-f', 'compose2.yml', 'ps'], None)
|
||||
|
||||
output = mock_stdout.getvalue()
|
||||
self.assertNotIn('multiplefigfiles_simple_1', output)
|
||||
self.assertNotIn('multiplefigfiles_another_1', output)
|
||||
self.assertIn('multiplefigfiles_yetanother_1', output)
|
||||
self.assertNotIn('multiplecomposefiles_simple_1', output)
|
||||
self.assertNotIn('multiplecomposefiles_another_1', output)
|
||||
self.assertIn('multiplecomposefiles_yetanother_1', output)
|
||||
|
||||
@patch('compose.service.log')
|
||||
def test_pull(self, mock_logging):
|
||||
self.command.dispatch(['pull'], None)
|
||||
mock_logging.info.assert_any_call('Pulling simple (busybox:latest)...')
|
||||
mock_logging.info.assert_any_call('Pulling another (busybox:latest)...')
|
||||
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
def test_build_no_cache(self, mock_stdout):
|
||||
@@ -64,34 +88,40 @@ class CLITestCase(DockerClientTestCase):
|
||||
|
||||
def test_up(self):
|
||||
self.command.dispatch(['up', '-d'], None)
|
||||
service = self.command.project.get_service('simple')
|
||||
another = self.command.project.get_service('another')
|
||||
service = self.project.get_service('simple')
|
||||
another = self.project.get_service('another')
|
||||
self.assertEqual(len(service.containers()), 1)
|
||||
self.assertEqual(len(another.containers()), 1)
|
||||
|
||||
# Ensure containers don't have stdin and stdout connected in -d mode
|
||||
config = service.containers()[0].inspect()['Config']
|
||||
self.assertFalse(config['AttachStderr'])
|
||||
self.assertFalse(config['AttachStdout'])
|
||||
self.assertFalse(config['AttachStdin'])
|
||||
|
||||
def test_up_with_links(self):
|
||||
self.command.base_dir = 'tests/fixtures/links-figfile'
|
||||
self.command.base_dir = 'tests/fixtures/links-composefile'
|
||||
self.command.dispatch(['up', '-d', 'web'], None)
|
||||
web = self.command.project.get_service('web')
|
||||
db = self.command.project.get_service('db')
|
||||
console = self.command.project.get_service('console')
|
||||
web = self.project.get_service('web')
|
||||
db = self.project.get_service('db')
|
||||
console = self.project.get_service('console')
|
||||
self.assertEqual(len(web.containers()), 1)
|
||||
self.assertEqual(len(db.containers()), 1)
|
||||
self.assertEqual(len(console.containers()), 0)
|
||||
|
||||
def test_up_with_no_deps(self):
|
||||
self.command.base_dir = 'tests/fixtures/links-figfile'
|
||||
self.command.base_dir = 'tests/fixtures/links-composefile'
|
||||
self.command.dispatch(['up', '-d', '--no-deps', 'web'], None)
|
||||
web = self.command.project.get_service('web')
|
||||
db = self.command.project.get_service('db')
|
||||
console = self.command.project.get_service('console')
|
||||
web = self.project.get_service('web')
|
||||
db = self.project.get_service('db')
|
||||
console = self.project.get_service('console')
|
||||
self.assertEqual(len(web.containers()), 1)
|
||||
self.assertEqual(len(db.containers()), 0)
|
||||
self.assertEqual(len(console.containers()), 0)
|
||||
|
||||
def test_up_with_recreate(self):
|
||||
self.command.dispatch(['up', '-d'], None)
|
||||
service = self.command.project.get_service('simple')
|
||||
service = self.project.get_service('simple')
|
||||
self.assertEqual(len(service.containers()), 1)
|
||||
|
||||
old_ids = [c.id for c in service.containers()]
|
||||
@@ -105,7 +135,7 @@ class CLITestCase(DockerClientTestCase):
|
||||
|
||||
def test_up_with_keep_old(self):
|
||||
self.command.dispatch(['up', '-d'], None)
|
||||
service = self.command.project.get_service('simple')
|
||||
service = self.project.get_service('simple')
|
||||
self.assertEqual(len(service.containers()), 1)
|
||||
|
||||
old_ids = [c.id for c in service.containers()]
|
||||
@@ -117,34 +147,40 @@ class CLITestCase(DockerClientTestCase):
|
||||
|
||||
self.assertEqual(old_ids, new_ids)
|
||||
|
||||
|
||||
@patch('dockerpty.start')
|
||||
def test_run_service_without_links(self, mock_stdout):
|
||||
self.command.base_dir = 'tests/fixtures/links-figfile'
|
||||
self.command.base_dir = 'tests/fixtures/links-composefile'
|
||||
self.command.dispatch(['run', 'console', '/bin/true'], None)
|
||||
self.assertEqual(len(self.command.project.containers()), 0)
|
||||
self.assertEqual(len(self.project.containers()), 0)
|
||||
|
||||
# Ensure stdin/out was open
|
||||
container = self.project.containers(stopped=True, one_off=True)[0]
|
||||
config = container.inspect()['Config']
|
||||
self.assertTrue(config['AttachStderr'])
|
||||
self.assertTrue(config['AttachStdout'])
|
||||
self.assertTrue(config['AttachStdin'])
|
||||
|
||||
@patch('dockerpty.start')
|
||||
def test_run_service_with_links(self, __):
|
||||
self.command.base_dir = 'tests/fixtures/links-figfile'
|
||||
self.command.base_dir = 'tests/fixtures/links-composefile'
|
||||
self.command.dispatch(['run', 'web', '/bin/true'], None)
|
||||
db = self.command.project.get_service('db')
|
||||
console = self.command.project.get_service('console')
|
||||
db = self.project.get_service('db')
|
||||
console = self.project.get_service('console')
|
||||
self.assertEqual(len(db.containers()), 1)
|
||||
self.assertEqual(len(console.containers()), 0)
|
||||
|
||||
@patch('dockerpty.start')
|
||||
def test_run_with_no_deps(self, __):
|
||||
self.command.base_dir = 'tests/fixtures/links-figfile'
|
||||
self.command.base_dir = 'tests/fixtures/links-composefile'
|
||||
self.command.dispatch(['run', '--no-deps', 'web', '/bin/true'], None)
|
||||
db = self.command.project.get_service('db')
|
||||
db = self.project.get_service('db')
|
||||
self.assertEqual(len(db.containers()), 0)
|
||||
|
||||
@patch('dockerpty.start')
|
||||
def test_run_does_not_recreate_linked_containers(self, __):
|
||||
self.command.base_dir = 'tests/fixtures/links-figfile'
|
||||
self.command.base_dir = 'tests/fixtures/links-composefile'
|
||||
self.command.dispatch(['up', '-d', 'db'], None)
|
||||
db = self.command.project.get_service('db')
|
||||
db = self.project.get_service('db')
|
||||
self.assertEqual(len(db.containers()), 1)
|
||||
|
||||
old_ids = [c.id for c in db.containers()]
|
||||
@@ -158,14 +194,14 @@ class CLITestCase(DockerClientTestCase):
|
||||
|
||||
@patch('dockerpty.start')
|
||||
def test_run_without_command(self, __):
|
||||
self.command.base_dir = 'tests/fixtures/commands-figfile'
|
||||
self.client.build('tests/fixtures/simple-dockerfile', tag='figtest_test')
|
||||
self.command.base_dir = 'tests/fixtures/commands-composefile'
|
||||
self.check_build('tests/fixtures/simple-dockerfile', tag='composetest_test')
|
||||
|
||||
for c in self.command.project.containers(stopped=True, one_off=True):
|
||||
for c in self.project.containers(stopped=True, one_off=True):
|
||||
c.remove()
|
||||
|
||||
self.command.dispatch(['run', 'implicit'], None)
|
||||
service = self.command.project.get_service('implicit')
|
||||
service = self.project.get_service('implicit')
|
||||
containers = service.containers(stopped=True, one_off=True)
|
||||
self.assertEqual(
|
||||
[c.human_readable_command for c in containers],
|
||||
@@ -173,40 +209,175 @@ class CLITestCase(DockerClientTestCase):
|
||||
)
|
||||
|
||||
self.command.dispatch(['run', 'explicit'], None)
|
||||
service = self.command.project.get_service('explicit')
|
||||
service = self.project.get_service('explicit')
|
||||
containers = service.containers(stopped=True, one_off=True)
|
||||
self.assertEqual(
|
||||
[c.human_readable_command for c in containers],
|
||||
[u'/bin/true'],
|
||||
)
|
||||
|
||||
@patch('dockerpty.start')
|
||||
def test_run_service_with_entrypoint_overridden(self, _):
|
||||
self.command.base_dir = 'tests/fixtures/dockerfile_with_entrypoint'
|
||||
name = 'service'
|
||||
self.command.dispatch(
|
||||
['run', '--entrypoint', '/bin/echo', name, 'helloworld'],
|
||||
None
|
||||
)
|
||||
service = self.project.get_service(name)
|
||||
container = service.containers(stopped=True, one_off=True)[0]
|
||||
self.assertEqual(
|
||||
container.human_readable_command,
|
||||
u'/bin/echo helloworld'
|
||||
)
|
||||
|
||||
@patch('dockerpty.start')
|
||||
def test_run_service_with_environement_overridden(self, _):
|
||||
name = 'service'
|
||||
self.command.base_dir = 'tests/fixtures/environment-composefile'
|
||||
self.command.dispatch(
|
||||
['run', '-e', 'foo=notbar', '-e', 'allo=moto=bobo',
|
||||
'-e', 'alpha=beta', name],
|
||||
None
|
||||
)
|
||||
service = self.project.get_service(name)
|
||||
container = service.containers(stopped=True, one_off=True)[0]
|
||||
# env overriden
|
||||
self.assertEqual('notbar', container.environment['foo'])
|
||||
# keep environement from yaml
|
||||
self.assertEqual('world', container.environment['hello'])
|
||||
# added option from command line
|
||||
self.assertEqual('beta', container.environment['alpha'])
|
||||
# make sure a value with a = don't crash out
|
||||
self.assertEqual('moto=bobo', container.environment['allo'])
|
||||
|
||||
@patch('dockerpty.start')
|
||||
def test_run_service_without_map_ports(self, __):
|
||||
# create one off container
|
||||
self.command.base_dir = 'tests/fixtures/ports-composefile'
|
||||
self.command.dispatch(['run', '-d', 'simple'], None)
|
||||
container = self.project.get_service('simple').containers(one_off=True)[0]
|
||||
|
||||
# get port information
|
||||
port_random = container.get_local_port(3000)
|
||||
port_assigned = container.get_local_port(3001)
|
||||
|
||||
# close all one off containers we just created
|
||||
container.stop()
|
||||
|
||||
# check the ports
|
||||
self.assertEqual(port_random, None)
|
||||
self.assertEqual(port_assigned, None)
|
||||
|
||||
@patch('dockerpty.start')
|
||||
def test_run_service_with_map_ports(self, __):
|
||||
# create one off container
|
||||
self.command.base_dir = 'tests/fixtures/ports-composefile'
|
||||
self.command.dispatch(['run', '-d', '--service-ports', 'simple'], None)
|
||||
container = self.project.get_service('simple').containers(one_off=True)[0]
|
||||
|
||||
# get port information
|
||||
port_random = container.get_local_port(3000)
|
||||
port_assigned = container.get_local_port(3001)
|
||||
|
||||
# close all one off containers we just created
|
||||
container.stop()
|
||||
|
||||
# check the ports
|
||||
self.assertNotEqual(port_random, None)
|
||||
self.assertIn("0.0.0.0", port_random)
|
||||
self.assertEqual(port_assigned, "0.0.0.0:9999")
|
||||
|
||||
def test_rm(self):
|
||||
service = self.command.project.get_service('simple')
|
||||
service = self.project.get_service('simple')
|
||||
service.create_container()
|
||||
service.kill()
|
||||
self.assertEqual(len(service.containers(stopped=True)), 1)
|
||||
self.command.dispatch(['rm', '--force'], None)
|
||||
self.assertEqual(len(service.containers(stopped=True)), 0)
|
||||
|
||||
def test_scale(self):
|
||||
project = self.command.project
|
||||
def test_kill(self):
|
||||
self.command.dispatch(['up', '-d'], None)
|
||||
service = self.project.get_service('simple')
|
||||
self.assertEqual(len(service.containers()), 1)
|
||||
self.assertTrue(service.containers()[0].is_running)
|
||||
|
||||
self.command.scale({'SERVICE=NUM': ['simple=1']})
|
||||
self.command.dispatch(['kill'], None)
|
||||
|
||||
self.assertEqual(len(service.containers(stopped=True)), 1)
|
||||
self.assertFalse(service.containers(stopped=True)[0].is_running)
|
||||
|
||||
def test_kill_signal_sigint(self):
|
||||
self.command.dispatch(['up', '-d'], None)
|
||||
service = self.project.get_service('simple')
|
||||
self.assertEqual(len(service.containers()), 1)
|
||||
self.assertTrue(service.containers()[0].is_running)
|
||||
|
||||
self.command.dispatch(['kill', '-s', 'SIGINT'], None)
|
||||
|
||||
self.assertEqual(len(service.containers()), 1)
|
||||
# The container is still running. It has been only interrupted
|
||||
self.assertTrue(service.containers()[0].is_running)
|
||||
|
||||
def test_kill_interrupted_service(self):
|
||||
self.command.dispatch(['up', '-d'], None)
|
||||
service = self.project.get_service('simple')
|
||||
self.command.dispatch(['kill', '-s', 'SIGINT'], None)
|
||||
self.assertTrue(service.containers()[0].is_running)
|
||||
|
||||
self.command.dispatch(['kill', '-s', 'SIGKILL'], None)
|
||||
|
||||
self.assertEqual(len(service.containers(stopped=True)), 1)
|
||||
self.assertFalse(service.containers(stopped=True)[0].is_running)
|
||||
|
||||
def test_restart(self):
|
||||
service = self.project.get_service('simple')
|
||||
container = service.create_container()
|
||||
service.start_container(container)
|
||||
started_at = container.dictionary['State']['StartedAt']
|
||||
self.command.dispatch(['restart'], None)
|
||||
container.inspect()
|
||||
self.assertNotEqual(
|
||||
container.dictionary['State']['FinishedAt'],
|
||||
'0001-01-01T00:00:00Z',
|
||||
)
|
||||
self.assertNotEqual(
|
||||
container.dictionary['State']['StartedAt'],
|
||||
started_at,
|
||||
)
|
||||
|
||||
def test_scale(self):
|
||||
project = self.project
|
||||
|
||||
self.command.scale(project, {'SERVICE=NUM': ['simple=1']})
|
||||
self.assertEqual(len(project.get_service('simple').containers()), 1)
|
||||
|
||||
self.command.scale({'SERVICE=NUM': ['simple=3', 'another=2']})
|
||||
self.command.scale(project, {'SERVICE=NUM': ['simple=3', 'another=2']})
|
||||
self.assertEqual(len(project.get_service('simple').containers()), 3)
|
||||
self.assertEqual(len(project.get_service('another').containers()), 2)
|
||||
|
||||
self.command.scale({'SERVICE=NUM': ['simple=1', 'another=1']})
|
||||
self.command.scale(project, {'SERVICE=NUM': ['simple=1', 'another=1']})
|
||||
self.assertEqual(len(project.get_service('simple').containers()), 1)
|
||||
self.assertEqual(len(project.get_service('another').containers()), 1)
|
||||
|
||||
self.command.scale({'SERVICE=NUM': ['simple=1', 'another=1']})
|
||||
self.command.scale(project, {'SERVICE=NUM': ['simple=1', 'another=1']})
|
||||
self.assertEqual(len(project.get_service('simple').containers()), 1)
|
||||
self.assertEqual(len(project.get_service('another').containers()), 1)
|
||||
|
||||
self.command.scale({'SERVICE=NUM': ['simple=0', 'another=0']})
|
||||
self.command.scale(project, {'SERVICE=NUM': ['simple=0', 'another=0']})
|
||||
self.assertEqual(len(project.get_service('simple').containers()), 0)
|
||||
self.assertEqual(len(project.get_service('another').containers()), 0)
|
||||
|
||||
def test_port(self):
|
||||
self.command.base_dir = 'tests/fixtures/ports-composefile'
|
||||
self.command.dispatch(['up', '-d'], None)
|
||||
container = self.project.get_service('simple').get_container()
|
||||
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
def get_port(number, mock_stdout):
|
||||
self.command.dispatch(['port', 'simple', str(number)], None)
|
||||
return mock_stdout.getvalue().rstrip()
|
||||
|
||||
self.assertEqual(get_port(3000), container.get_local_port(3000))
|
||||
self.assertEqual(get_port(3001), "0.0.0.0:9999")
|
||||
self.assertEqual(get_port(3002), "")
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from __future__ import unicode_literals
|
||||
from fig.project import Project, ConfigurationError
|
||||
from fig.container import Container
|
||||
from compose.project import Project, ConfigurationError
|
||||
from compose.container import Container
|
||||
from .testcases import DockerClientTestCase
|
||||
|
||||
|
||||
class ProjectTest(DockerClientTestCase):
|
||||
def test_volumes_from_service(self):
|
||||
project = Project.from_config(
|
||||
name='figtest',
|
||||
name='composetest',
|
||||
config={
|
||||
'data': {
|
||||
'image': 'busybox:latest',
|
||||
@@ -29,14 +29,14 @@ class ProjectTest(DockerClientTestCase):
|
||||
self.client,
|
||||
image='busybox:latest',
|
||||
volumes=['/var/data'],
|
||||
name='figtest_data_container',
|
||||
name='composetest_data_container',
|
||||
)
|
||||
project = Project.from_config(
|
||||
name='figtest',
|
||||
name='composetest',
|
||||
config={
|
||||
'db': {
|
||||
'image': 'busybox:latest',
|
||||
'volumes_from': ['figtest_data_container'],
|
||||
'volumes_from': ['composetest_data_container'],
|
||||
},
|
||||
},
|
||||
client=self.client,
|
||||
@@ -47,7 +47,7 @@ class ProjectTest(DockerClientTestCase):
|
||||
def test_start_stop_kill_remove(self):
|
||||
web = self.create_service('web')
|
||||
db = self.create_service('db')
|
||||
project = Project('figtest', [web, db], self.client)
|
||||
project = Project('composetest', [web, db], self.client)
|
||||
|
||||
project.start()
|
||||
|
||||
@@ -80,7 +80,7 @@ class ProjectTest(DockerClientTestCase):
|
||||
def test_project_up(self):
|
||||
web = self.create_service('web')
|
||||
db = self.create_service('db', volumes=['/var/db'])
|
||||
project = Project('figtest', [web, db], self.client)
|
||||
project = Project('composetest', [web, db], self.client)
|
||||
project.start()
|
||||
self.assertEqual(len(project.containers()), 0)
|
||||
|
||||
@@ -94,22 +94,22 @@ class ProjectTest(DockerClientTestCase):
|
||||
|
||||
def test_project_up_recreates_containers(self):
|
||||
web = self.create_service('web')
|
||||
db = self.create_service('db', volumes=['/var/db'])
|
||||
project = Project('figtest', [web, db], self.client)
|
||||
db = self.create_service('db', volumes=['/etc'])
|
||||
project = Project('composetest', [web, db], self.client)
|
||||
project.start()
|
||||
self.assertEqual(len(project.containers()), 0)
|
||||
|
||||
project.up(['db'])
|
||||
self.assertEqual(len(project.containers()), 1)
|
||||
old_db_id = project.containers()[0].id
|
||||
db_volume_path = project.containers()[0].inspect()['Volumes']['/var/db']
|
||||
db_volume_path = project.containers()[0].get('Volumes./etc')
|
||||
|
||||
project.up()
|
||||
self.assertEqual(len(project.containers()), 2)
|
||||
|
||||
db_container = [c for c in project.containers() if 'db' in c.name][0]
|
||||
self.assertNotEqual(c.id, old_db_id)
|
||||
self.assertEqual(c.inspect()['Volumes']['/var/db'], db_volume_path)
|
||||
self.assertNotEqual(db_container.id, old_db_id)
|
||||
self.assertEqual(db_container.get('Volumes./etc'), db_volume_path)
|
||||
|
||||
project.kill()
|
||||
project.remove_stopped()
|
||||
@@ -117,7 +117,7 @@ class ProjectTest(DockerClientTestCase):
|
||||
def test_project_up_with_no_recreate_running(self):
|
||||
web = self.create_service('web')
|
||||
db = self.create_service('db', volumes=['/var/db'])
|
||||
project = Project('figtest', [web, db], self.client)
|
||||
project = Project('composetest', [web, db], self.client)
|
||||
project.start()
|
||||
self.assertEqual(len(project.containers()), 0)
|
||||
|
||||
@@ -130,8 +130,9 @@ class ProjectTest(DockerClientTestCase):
|
||||
self.assertEqual(len(project.containers()), 2)
|
||||
|
||||
db_container = [c for c in project.containers() if 'db' in c.name][0]
|
||||
self.assertEqual(c.id, old_db_id)
|
||||
self.assertEqual(c.inspect()['Volumes']['/var/db'], db_volume_path)
|
||||
self.assertEqual(db_container.id, old_db_id)
|
||||
self.assertEqual(db_container.inspect()['Volumes']['/var/db'],
|
||||
db_volume_path)
|
||||
|
||||
project.kill()
|
||||
project.remove_stopped()
|
||||
@@ -139,7 +140,7 @@ class ProjectTest(DockerClientTestCase):
|
||||
def test_project_up_with_no_recreate_stopped(self):
|
||||
web = self.create_service('web')
|
||||
db = self.create_service('db', volumes=['/var/db'])
|
||||
project = Project('figtest', [web, db], self.client)
|
||||
project = Project('composetest', [web, db], self.client)
|
||||
project.start()
|
||||
self.assertEqual(len(project.containers()), 0)
|
||||
|
||||
@@ -158,8 +159,9 @@ class ProjectTest(DockerClientTestCase):
|
||||
self.assertEqual(len(new_containers), 2)
|
||||
|
||||
db_container = [c for c in new_containers if 'db' in c.name][0]
|
||||
self.assertEqual(c.id, old_db_id)
|
||||
self.assertEqual(c.inspect()['Volumes']['/var/db'], db_volume_path)
|
||||
self.assertEqual(db_container.id, old_db_id)
|
||||
self.assertEqual(db_container.inspect()['Volumes']['/var/db'],
|
||||
db_volume_path)
|
||||
|
||||
project.kill()
|
||||
project.remove_stopped()
|
||||
@@ -167,7 +169,7 @@ class ProjectTest(DockerClientTestCase):
|
||||
def test_project_up_without_all_services(self):
|
||||
console = self.create_service('console')
|
||||
db = self.create_service('db')
|
||||
project = Project('figtest', [console, db], self.client)
|
||||
project = Project('composetest', [console, db], self.client)
|
||||
project.start()
|
||||
self.assertEqual(len(project.containers()), 0)
|
||||
|
||||
@@ -184,7 +186,7 @@ class ProjectTest(DockerClientTestCase):
|
||||
db = self.create_service('db', volumes=['/var/db'])
|
||||
web = self.create_service('web', links=[(db, 'db')])
|
||||
|
||||
project = Project('figtest', [web, db, console], self.client)
|
||||
project = Project('composetest', [web, db, console], self.client)
|
||||
project.start()
|
||||
self.assertEqual(len(project.containers()), 0)
|
||||
|
||||
@@ -202,7 +204,7 @@ class ProjectTest(DockerClientTestCase):
|
||||
db = self.create_service('db', volumes=['/var/db'])
|
||||
web = self.create_service('web', links=[(db, 'db')])
|
||||
|
||||
project = Project('figtest', [web, db, console], self.client)
|
||||
project = Project('composetest', [web, db, console], self.client)
|
||||
project.start()
|
||||
self.assertEqual(len(project.containers()), 0)
|
||||
|
||||
@@ -217,7 +219,7 @@ class ProjectTest(DockerClientTestCase):
|
||||
|
||||
def test_unscale_after_restart(self):
|
||||
web = self.create_service('web')
|
||||
project = Project('figtest', [web], self.client)
|
||||
project = Project('composetest', [web], self.client)
|
||||
|
||||
project.start()
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user