mirror of
https://github.com/docker/compose.git
synced 2026-02-10 10:39:23 +08:00
Compare commits
473 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d4ef4e0b3 | ||
|
|
882ef2ccd8 | ||
|
|
d6cd76c3c1 | ||
|
|
bd0be2cdc7 | ||
|
|
a8d7ebd987 | ||
|
|
00f61196a4 | ||
|
|
c21d6706b6 | ||
|
|
c3c5d91c47 | ||
|
|
7fa4cd1214 | ||
|
|
f353d9fbc0 | ||
|
|
09018855ce | ||
|
|
719954b02f | ||
|
|
67bc3fabe4 | ||
|
|
e724a346c7 | ||
|
|
87b4545b44 | ||
|
|
58a7844129 | ||
|
|
4353f7b9f9 | ||
|
|
8f8693e13e | ||
|
|
363a6563c7 | ||
|
|
59d6af73fa | ||
|
|
cd7f67018e | ||
|
|
b7e8770c4f | ||
|
|
ad4cc5d6df | ||
|
|
ca14ed68f7 | ||
|
|
71514cb380 | ||
|
|
8212f1bd45 | ||
|
|
dca3bbdea3 | ||
|
|
8ed7dfef6f | ||
|
|
631f5be02f | ||
|
|
4f4ea2a402 | ||
|
|
5a5bffebd1 | ||
|
|
8749bc0844 | ||
|
|
f3d0c63db2 | ||
|
|
93a846db31 | ||
|
|
686c25d50f | ||
|
|
ef6555f084 | ||
|
|
1344099e29 | ||
|
|
48f3d41947 | ||
|
|
7da8e6be3b | ||
|
|
4795fd874f | ||
|
|
276fee105b | ||
|
|
8af4ae7935 | ||
|
|
91ceb33d5a | ||
|
|
0b4d9401ee | ||
|
|
889d3636f4 | ||
|
|
b0f945d2da | ||
|
|
93c529182e | ||
|
|
412034a023 | ||
|
|
30c9e7323a | ||
|
|
051f56a1e6 | ||
|
|
b5ce23885b | ||
|
|
0fdb8bf814 | ||
|
|
e538923545 | ||
|
|
c0f65a9f4c | ||
|
|
b0cb31c186 | ||
|
|
3080244c0b | ||
|
|
b183a66db1 | ||
|
|
022f81711e | ||
|
|
4f40d0c168 | ||
|
|
f5ac1fa073 | ||
|
|
f79eb7b9ad | ||
|
|
b0b6ed31c4 | ||
|
|
ea7ee301c0 | ||
|
|
41315b32cb | ||
|
|
80eaf4cc9f | ||
|
|
ef4eb66723 | ||
|
|
82bc7cd5ba | ||
|
|
3304c68891 | ||
|
|
1e6d912fbc | ||
|
|
4ef3bbcdf2 | ||
|
|
62059d55e6 | ||
|
|
ed50a0a3a0 | ||
|
|
28d2aff8b8 | ||
|
|
862971cffa | ||
|
|
c8022457eb | ||
|
|
9bbf1a33d1 | ||
|
|
0ac8c3cb03 | ||
|
|
d5c9626040 | ||
|
|
ad9c5ad938 | ||
|
|
70d2e64dfe | ||
|
|
1dccd58209 | ||
|
|
e0103ac0d4 | ||
|
|
4d745ab87a | ||
|
|
417d9c2d51 | ||
|
|
4997facbb4 | ||
|
|
df87bd91c8 | ||
|
|
1748b0f81a | ||
|
|
6829efd4d3 | ||
|
|
99f2a3a583 | ||
|
|
0f2f9db6d8 | ||
|
|
d6223371d6 | ||
|
|
4817d5944c | ||
|
|
f626fc5ce8 | ||
|
|
1579a125a3 | ||
|
|
7fb9ec29c4 | ||
|
|
f78e89f265 | ||
|
|
b06294399a | ||
|
|
b8e0aed21c | ||
|
|
4bce388b51 | ||
|
|
6c95eed781 | ||
|
|
4f366d8355 | ||
|
|
878d90febf | ||
|
|
1a77feea3f | ||
|
|
7e0ab0714f | ||
|
|
2e6bc078fb | ||
|
|
3dd860f0ba | ||
|
|
de800dea0f | ||
|
|
fed4377ef6 | ||
|
|
021bf46557 | ||
|
|
b7e5116267 | ||
|
|
9532e5a4f2 | ||
|
|
e5a118e3ce | ||
|
|
a631c1eddb | ||
|
|
855855a0e6 | ||
|
|
b808674132 | ||
|
|
7e574fca71 | ||
|
|
7d617d60bc | ||
|
|
da71e01d30 | ||
|
|
a89bc304f6 | ||
|
|
240495f07f | ||
|
|
2e19887bf1 | ||
|
|
a982e516fc | ||
|
|
3af56e1602 | ||
|
|
16f8106149 | ||
|
|
86a08c00f2 | ||
|
|
0ca9fa8b2b | ||
|
|
688f82c1cf | ||
|
|
9a44708081 | ||
|
|
89789c54ad | ||
|
|
d17c4d27fa | ||
|
|
25ee3f0033 | ||
|
|
8098b65576 | ||
|
|
fb81c37ca6 | ||
|
|
e6ec76161d | ||
|
|
b317071cf3 | ||
|
|
bb922d63f5 | ||
|
|
2291fa2d45 | ||
|
|
3c6652c101 | ||
|
|
43af1684c1 | ||
|
|
2cdde099fa | ||
|
|
310c7623f9 | ||
|
|
6e64802545 | ||
|
|
8b5015c10f | ||
|
|
ed549155b3 | ||
|
|
39ae91c81c | ||
|
|
b6acb3cd8c | ||
|
|
24a6c240fc | ||
|
|
15b763acdb | ||
|
|
3cd116b99d | ||
|
|
b559653c8c | ||
|
|
5f17423d3e | ||
|
|
2a442ec6d9 | ||
|
|
ceff5cb9ca | ||
|
|
a467a8a094 | ||
|
|
4926f8aef6 | ||
|
|
927115c3d4 | ||
|
|
1d7247b67e | ||
|
|
a1cd00e3f0 | ||
|
|
fd568b389d | ||
|
|
4f95e81c6d | ||
|
|
619e783a05 | ||
|
|
f3f7f000fe | ||
|
|
219751abc7 | ||
|
|
0b48e137e8 | ||
|
|
947742852e | ||
|
|
94277a3eb0 | ||
|
|
11a2100d53 | ||
|
|
530d7af5cf | ||
|
|
502d58abe6 | ||
|
|
eb073c53f4 | ||
|
|
d866415b9a | ||
|
|
dd40658f87 | ||
|
|
b3382ffd4f | ||
|
|
15a0fac939 | ||
|
|
78227c3c06 | ||
|
|
e4e802d1f8 | ||
|
|
b24a60ba9f | ||
|
|
461b600068 | ||
|
|
e3cff5d17d | ||
|
|
0f70b8638f | ||
|
|
8584525e8d | ||
|
|
e3e2247159 | ||
|
|
0650c4485a | ||
|
|
e708f4f59d | ||
|
|
907918b492 | ||
|
|
6dbe321a45 | ||
|
|
2a415ede08 | ||
|
|
43369cda9c | ||
|
|
a2557a3354 | ||
|
|
1a14449fe6 | ||
|
|
0b89ae6f20 | ||
|
|
cec6dc28bb | ||
|
|
853ce255ea | ||
|
|
db852e14e4 | ||
|
|
98dd0cd1f8 | ||
|
|
c441ac90d6 | ||
|
|
9d7b54d8fd | ||
|
|
59f04c6e29 | ||
|
|
367ae0c848 | ||
|
|
4e0f555c58 | ||
|
|
baf18decae | ||
|
|
826b8ca4d3 | ||
|
|
fa2fb6bd38 | ||
|
|
f9ea5ecf40 | ||
|
|
99f7eba930 | ||
|
|
e1b27acd02 | ||
|
|
9aab7bc242 | ||
|
|
83dcceacaf | ||
|
|
1f06070f12 | ||
|
|
35c6e0314c | ||
|
|
276e43ca6b | ||
|
|
b7046777d1 | ||
|
|
1c14fc06da | ||
|
|
f4ef2c09d6 | ||
|
|
16495c577b | ||
|
|
965426e39b | ||
|
|
02f119e4b7 | ||
|
|
721327110d | ||
|
|
d209ded13c | ||
|
|
49c2080bcd | ||
|
|
37efdb1f8b | ||
|
|
4c582e4352 | ||
|
|
5dca6c232e | ||
|
|
eef4bc3917 | ||
|
|
9931cd2c4c | ||
|
|
0226675766 | ||
|
|
ae9c965823 | ||
|
|
d7a90092c2 | ||
|
|
85fb8956f3 | ||
|
|
1e5b9dc2eb | ||
|
|
d21b8f1ba3 | ||
|
|
4c5a80f253 | ||
|
|
198598c936 | ||
|
|
8b5a882459 | ||
|
|
dae451019b | ||
|
|
dfc729b8f2 | ||
|
|
eb7ea76d6c | ||
|
|
3c8ef6a94c | ||
|
|
081afd9bbf | ||
|
|
528bed9ef6 | ||
|
|
4ecf5e01ff | ||
|
|
81a32a266f | ||
|
|
25c70c2af4 | ||
|
|
7b1f01bb52 | ||
|
|
c23189a5fa | ||
|
|
86b723e227 | ||
|
|
2534a0964f | ||
|
|
42e6296b0e | ||
|
|
74440b2f92 | ||
|
|
95f4e2c7c3 | ||
|
|
9e9a66f0f8 | ||
|
|
300234429c | ||
|
|
3384bc5fcf | ||
|
|
08f936b2e7 | ||
|
|
df925bc759 | ||
|
|
33c7b3f752 | ||
|
|
ac7a97f420 | ||
|
|
d5fcb0de67 | ||
|
|
f47431d591 | ||
|
|
8610adcaf3 | ||
|
|
8f38b28816 | ||
|
|
db3b4731cd | ||
|
|
30b7c22c71 | ||
|
|
f89f9e3190 | ||
|
|
f1fc1d7a16 | ||
|
|
0e30d0085b | ||
|
|
98f32a21e7 | ||
|
|
fa195bc829 | ||
|
|
882dc673ce | ||
|
|
44f5a3ea46 | ||
|
|
ac4e800763 | ||
|
|
2e57575a61 | ||
|
|
03535a6158 | ||
|
|
c41342501b | ||
|
|
4ac02bfca6 | ||
|
|
35d5d1a5b1 | ||
|
|
72003de737 | ||
|
|
a0db5bee3e | ||
|
|
59c45e3398 | ||
|
|
ea8364fd11 | ||
|
|
cf6b09e94b | ||
|
|
66555aa69b | ||
|
|
bb943d5cb5 | ||
|
|
178c50d46f | ||
|
|
32cdabefc2 | ||
|
|
5b07c581e0 | ||
|
|
17bbb9d357 | ||
|
|
ec2966222a | ||
|
|
8076c7d7fe | ||
|
|
b2425c1f1e | ||
|
|
34c6920b37 | ||
|
|
8d8dd37d2c | ||
|
|
b1739e703b | ||
|
|
a42f2007ea | ||
|
|
1dd5ef4133 | ||
|
|
c3215a1764 | ||
|
|
bd320b19fe | ||
|
|
0fa0131372 | ||
|
|
a7fe67e691 | ||
|
|
f78dfa7958 | ||
|
|
d95de03de9 | ||
|
|
e3eccd1047 | ||
|
|
d32994c250 | ||
|
|
a516d61b49 | ||
|
|
6eb1c8896f | ||
|
|
160e2dc7b9 | ||
|
|
5e8bcd2d29 | ||
|
|
2eb20d89af | ||
|
|
ab65c829ea | ||
|
|
83110b8e6b | ||
|
|
3a8a25b9b3 | ||
|
|
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 | ||
|
|
d3f88cace5 |
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
.git
|
||||
build
|
||||
dist
|
||||
venv
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,4 +5,4 @@
|
||||
/dist
|
||||
/docs/_site
|
||||
/venv
|
||||
fig.spec
|
||||
docker-compose.spec
|
||||
|
||||
121
CHANGES.md
121
CHANGES.md
@@ -1,7 +1,124 @@
|
||||
Change log
|
||||
==========
|
||||
|
||||
1.0.0 (2014-10-07)
|
||||
1.3.1 (2015-06-21)
|
||||
------------------
|
||||
|
||||
The following bugs have been fixed:
|
||||
|
||||
- `docker-compose build` would always attempt to pull the base image before building.
|
||||
- `docker-compose help migrate-to-labels` failed with an error.
|
||||
- If no network mode was specified, Compose would set it to "bridge", rather than allowing the Docker daemon to use its configured default network mode.
|
||||
|
||||
1.3.0 (2015-06-18)
|
||||
------------------
|
||||
|
||||
Firstly, two important notes:
|
||||
|
||||
- **This release contains breaking changes, and you will need to either remove or migrate your existing containers before running your app** - see the [upgrading section of the install docs](https://github.com/docker/compose/blob/1.3.0rc1/docs/install.md#upgrading) for details.
|
||||
|
||||
- Compose now requires Docker 1.6.0 or later.
|
||||
|
||||
We've done a lot of work in this release to remove hacks and make Compose more stable:
|
||||
|
||||
- Compose now uses container labels, rather than names, to keep track of containers. This makes Compose both faster and easier to integrate with your own tools.
|
||||
|
||||
- Compose no longer uses "intermediate containers" when recreating containers for a service. This makes `docker-compose up` less complex and more resilient to failure.
|
||||
|
||||
There are some new features:
|
||||
|
||||
- `docker-compose up` has an **experimental** new behaviour: it will only recreate containers for services whose configuration has changed in `docker-compose.yml`. This will eventually become the default, but for now you can take it for a spin:
|
||||
|
||||
$ docker-compose up --x-smart-recreate
|
||||
|
||||
- When invoked in a subdirectory of a project, `docker-compose` will now climb up through parent directories until it finds a `docker-compose.yml`.
|
||||
|
||||
Several new configuration keys have been added to `docker-compose.yml`:
|
||||
|
||||
- `dockerfile`, like `docker build --file`, lets you specify an alternate Dockerfile to use with `build`.
|
||||
- `labels`, like `docker run --labels`, lets you add custom metadata to containers.
|
||||
- `extra_hosts`, like `docker run --add-host`, lets you add entries to a container's `/etc/hosts` file.
|
||||
- `pid: host`, like `docker run --pid=host`, lets you reuse the same PID namespace as the host machine.
|
||||
- `cpuset`, like `docker run --cpuset-cpus`, lets you specify which CPUs to allow execution in.
|
||||
- `read_only`, like `docker run --read-only`, lets you mount a container's filesystem as read-only.
|
||||
- `security_opt`, like `docker run --security-opt`, lets you specify [security options](https://docs.docker.com/reference/run/#security-configuration).
|
||||
- `log_driver`, like `docker run --log-driver`, lets you specify a [log driver](https://docs.docker.com/reference/run/#logging-drivers-log-driver).
|
||||
|
||||
Many bugs have been fixed, including the following:
|
||||
|
||||
- The output of `docker-compose run` was sometimes truncated, especially when running under Jenkins.
|
||||
- A service's volumes would sometimes not update after volume configuration was changed in `docker-compose.yml`.
|
||||
- Authenticating against third-party registries would sometimes fail.
|
||||
- `docker-compose run --rm` would fail to remove the container if the service had a `restart` policy in place.
|
||||
- `docker-compose scale` would refuse to scale a service beyond 1 container if it exposed a specific port number on the host.
|
||||
- Compose would refuse to create multiple volume entries with the same host path.
|
||||
|
||||
Thanks @ahromis, @albers, @aleksandr-vin, @antoineco, @ccverak, @chernjie, @dnephin, @edmorley, @fordhurley, @josephpage, @KyleJamesWalker, @lsowen, @mchasal, @noironetworks, @sdake, @sdurrheimer, @sherter, @stephenlawrence, @thaJeztah, @thieman, @turtlemonvh, @twhiteman, @vdemeester, @xuxinkun and @zwily!
|
||||
|
||||
1.2.0 (2015-04-16)
|
||||
------------------
|
||||
|
||||
- `docker-compose.yml` now supports an `extends` option, which enables a service to inherit configuration from another service in another configuration file. This is really good for sharing common configuration between apps, or for configuring the same app for different environments. Here's the [documentation](https://github.com/docker/compose/blob/master/docs/yml.md#extends).
|
||||
|
||||
- When using Compose with a Swarm cluster, containers that depend on one another will be co-scheduled on the same node. This means that most Compose apps will now work out of the box, as long as they don't use `build`.
|
||||
|
||||
- Repeated invocations of `docker-compose up` when using Compose with a Swarm cluster now work reliably.
|
||||
|
||||
- Directories passed to `build`, filenames passed to `env_file` and volume host paths passed to `volumes` are now treated as relative to the *directory of the configuration file*, not the directory that `docker-compose` is being run in. In the majority of cases, those are the same, but if you use the `-f|--file` argument to specify a configuration file in another directory, **this is a breaking change**.
|
||||
|
||||
- A service can now share another service's network namespace with `net: container:<service>`.
|
||||
|
||||
- `volumes_from` and `net: container:<service>` entries are taken into account when resolving dependencies, so `docker-compose up <service>` will correctly start all dependencies of `<service>`.
|
||||
|
||||
- `docker-compose run` now accepts a `--user` argument to specify a user to run the command as, just like `docker run`.
|
||||
|
||||
- The `up`, `stop` and `restart` commands now accept a `--timeout` (or `-t`) argument to specify how long to wait when attempting to gracefully stop containers, just like `docker stop`.
|
||||
|
||||
- `docker-compose rm` now accepts `-f` as a shorthand for `--force`, just like `docker rm`.
|
||||
|
||||
Thanks, @abesto, @albers, @alunduil, @dnephin, @funkyfuture, @gilclark, @IanVS, @KingsleyKelly, @knutwalker, @thaJeztah and @vmalloc!
|
||||
|
||||
1.1.0 (2015-02-25)
|
||||
------------------
|
||||
|
||||
Fig has been renamed to Docker Compose, or just Compose for short. This has several implications for you:
|
||||
|
||||
- The command you type is now `docker-compose`, not `fig`.
|
||||
- You should rename your fig.yml to docker-compose.yml.
|
||||
- If you’re installing via PyPi, the package is now `docker-compose`, so install it with `pip install docker-compose`.
|
||||
|
||||
Besides that, there’s a lot of new stuff in this release:
|
||||
|
||||
- We’ve made a few small changes to ensure that Compose will work with Swarm, Docker’s new clustering tool (https://github.com/docker/swarm). Eventually you'll be able to point Compose at a Swarm cluster instead of a standalone Docker host and it’ll run your containers on the cluster with no extra work from you. As Swarm is still developing, integration is rough and lots of Compose features don't work yet.
|
||||
|
||||
- `docker-compose run` now has a `--service-ports` flag for exposing ports on the given service. This is useful for e.g. running your webapp with an interactive debugger.
|
||||
|
||||
- You can now link to containers outside your app with the `external_links` option in docker-compose.yml.
|
||||
|
||||
- You can now prevent `docker-compose up` from automatically building images with the `--no-build` option. This will make fewer API calls and run faster.
|
||||
|
||||
- If you don’t specify a tag when using the `image` key, Compose will default to the `latest` tag, rather than pulling all tags.
|
||||
|
||||
- `docker-compose kill` now supports the `-s` flag, allowing you to specify the exact signal you want to send to a service’s containers.
|
||||
|
||||
- docker-compose.yml now has an `env_file` key, analogous to `docker run --env-file`, letting you specify multiple environment variables in a separate file. This is great if you have a lot of them, or if you want to keep sensitive information out of version control.
|
||||
|
||||
- docker-compose.yml now supports the `dns_search`, `cap_add`, `cap_drop`, `cpu_shares` and `restart` options, analogous to `docker run`’s `--dns-search`, `--cap-add`, `--cap-drop`, `--cpu-shares` and `--restart` options.
|
||||
|
||||
- Compose now ships with Bash tab completion - see the installation and usage docs at https://github.com/docker/compose/blob/1.1.0/docs/completion.md
|
||||
|
||||
- A number of bugs have been fixed - see the milestone for details: https://github.com/docker/compose/issues?q=milestone%3A1.1.0+
|
||||
|
||||
Thanks @dnephin, @squebe, @jbalonso, @raulcd, @benlangfield, @albers, @ggtools, @bersace, @dtenenba, @petercv, @drewkett, @TFenby, @paulRbr, @Aigeruth and @salehe!
|
||||
|
||||
1.0.1 (2014-11-04)
|
||||
------------------
|
||||
|
||||
- Added an `--allow-insecure-ssl` option to allow `fig up`, `fig run` and `fig pull` to pull from insecure registries.
|
||||
- Fixed `fig run` not showing output in Jenkins.
|
||||
- Fixed a bug where Fig couldn't build Dockerfiles with ADD statements pointing at URLs.
|
||||
|
||||
1.0.0 (2014-10-16)
|
||||
------------------
|
||||
|
||||
The highlights:
|
||||
@@ -43,6 +160,8 @@ Other things:
|
||||
- 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)
|
||||
------------------
|
||||
|
||||
|
||||
125
CONTRIBUTING.md
125
CONTRIBUTING.md
@@ -1,98 +1,87 @@
|
||||
# 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/docker/fig](https://github.com/docker/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.
|
||||
2. Clone your forked repository locally `git clone git@github.com:yourusername/compose.git`.
|
||||
3. Enter the local directory `cd compose`.
|
||||
4. 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
|
||||
|
||||
Use the test script to run linting checks and then the full test suite against
|
||||
different Python interpreters:
|
||||
|
||||
$ script/test
|
||||
|
||||
Tests are run against a Docker daemon inside a container, so that we can test
|
||||
against multiple Docker versions. By default they'll run against only the latest
|
||||
Docker version - set the `DOCKER_VERSIONS` environment variable to "all" to run
|
||||
against all supported versions:
|
||||
|
||||
$ DOCKER_VERSIONS=all script/test
|
||||
|
||||
Arguments to `script/test` are passed through to the `nosetests` executable, so
|
||||
you can specify a test directory, file, module, class or method:
|
||||
|
||||
$ script/test tests/unit
|
||||
$ script/test tests/unit/cli_test.py
|
||||
$ script/test tests.integration.service_test
|
||||
$ script/test tests.integration.service_test:ServiceTest.test_containers
|
||||
|
||||
## Building binaries
|
||||
|
||||
Linux:
|
||||
`script/build-linux` will build the Linux binary inside a Docker container:
|
||||
|
||||
$ script/build-linux
|
||||
|
||||
OS X:
|
||||
`script/build-osx` will build the Mac OS X binary inside a virtualenv:
|
||||
|
||||
$ script/build-osx
|
||||
|
||||
Note that this only works on Mountain Lion, not Mavericks, due to a [bug in PyInstaller](http://www.pyinstaller.org/ticket/807).
|
||||
|
||||
## 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
|
||||
For official releases, you should build inside a Mountain Lion VM for proper
|
||||
compatibility. Run the this script first to prepare the environment before
|
||||
building - it will use Homebrew to make sure Python is installed and
|
||||
up-to-date.
|
||||
|
||||
$ script/prepare-osx
|
||||
|
||||
## 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`
|
||||
- Adds release notes to `CHANGES.md`
|
||||
|
||||
2. Create unpublished GitHub release with release notes
|
||||
|
||||
3. Build Linux version on any Docker host with `script/build-linux` and attach to release
|
||||
|
||||
4. Build OS X version on Mountain Lion with `script/build-osx` and attach to release as `fig-Darwin-x86_64` and `fig-Linux-x86_64`.
|
||||
|
||||
3. Build Linux version on any Docker host with `script/build-linux` and attach
|
||||
to release
|
||||
4. Build OS X version on Mountain Lion with `script/build-osx` and attach to
|
||||
release as `docker-compose-Darwin-x86_64` and `docker-compose-Linux-x86_64`.
|
||||
5. Publish GitHub release, creating tag
|
||||
|
||||
6. Update website with `script/deploy-docs`
|
||||
|
||||
7. Upload PyPi package
|
||||
|
||||
$ git checkout $VERSION
|
||||
$ python setup.py sdist upload
|
||||
$ git checkout $VERSION
|
||||
$ python setup.py sdist upload
|
||||
|
||||
63
Dockerfile
63
Dockerfile
@@ -1,5 +1,64 @@
|
||||
FROM debian:wheezy
|
||||
RUN apt-get update -qq && apt-get install -qy python python-pip python-dev git && apt-get clean
|
||||
|
||||
RUN set -ex; \
|
||||
apt-get update -qq; \
|
||||
apt-get install -y \
|
||||
gcc \
|
||||
make \
|
||||
zlib1g \
|
||||
zlib1g-dev \
|
||||
libssl-dev \
|
||||
git \
|
||||
apt-transport-https \
|
||||
ca-certificates \
|
||||
curl \
|
||||
lxc \
|
||||
iptables \
|
||||
; \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Build Python 2.7.9 from source
|
||||
RUN set -ex; \
|
||||
curl -LO https://www.python.org/ftp/python/2.7.9/Python-2.7.9.tgz; \
|
||||
tar -xzf Python-2.7.9.tgz; \
|
||||
cd Python-2.7.9; \
|
||||
./configure --enable-shared; \
|
||||
make; \
|
||||
make install; \
|
||||
cd ..; \
|
||||
rm -rf /Python-2.7.9; \
|
||||
rm Python-2.7.9.tgz
|
||||
|
||||
# Make libpython findable
|
||||
ENV LD_LIBRARY_PATH /usr/local/lib
|
||||
|
||||
# Install setuptools
|
||||
RUN set -ex; \
|
||||
curl -LO https://bootstrap.pypa.io/ez_setup.py; \
|
||||
python ez_setup.py; \
|
||||
rm ez_setup.py
|
||||
|
||||
# Install pip
|
||||
RUN set -ex; \
|
||||
curl -LO https://pypi.python.org/packages/source/p/pip/pip-7.0.1.tar.gz; \
|
||||
tar -xzf pip-7.0.1.tar.gz; \
|
||||
cd pip-7.0.1; \
|
||||
python setup.py install; \
|
||||
cd ..; \
|
||||
rm -rf pip-7.0.1; \
|
||||
rm pip-7.0.1.tar.gz
|
||||
|
||||
ENV ALL_DOCKER_VERSIONS 1.6.0 1.7.0
|
||||
|
||||
RUN set -ex; \
|
||||
curl https://get.docker.com/builds/Linux/x86_64/docker-1.6.0 -o /usr/local/bin/docker-1.6.0; \
|
||||
chmod +x /usr/local/bin/docker-1.6.0; \
|
||||
curl https://test.docker.com/builds/Linux/x86_64/docker-1.7.0 -o /usr/local/bin/docker-1.7.0; \
|
||||
chmod +x /usr/local/bin/docker-1.7.0
|
||||
|
||||
# Set the default Docker to be run
|
||||
RUN ln -s /usr/local/bin/docker-1.6.0 /usr/local/bin/docker
|
||||
|
||||
RUN useradd -d /home/user -m -s /bin/bash user
|
||||
WORKDIR /code/
|
||||
|
||||
@@ -13,3 +72,5 @@ ADD . /code/
|
||||
RUN python setup.py install
|
||||
|
||||
RUN chown -R user /code/
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/docker-compose"]
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
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,8 @@ include requirements.txt
|
||||
include requirements-dev.txt
|
||||
include tox.ini
|
||||
include *.md
|
||||
recursive-exclude tests *
|
||||
include contrib/completion/bash/docker-compose
|
||||
recursive-include tests *
|
||||
global-exclude *.pyc
|
||||
global-exclude *.pyo
|
||||
global-exclude *.un~
|
||||
|
||||
74
README.md
74
README.md
@@ -1,46 +1,52 @@
|
||||
Fig
|
||||
===
|
||||
Docker Compose
|
||||
==============
|
||||
*(Previously known as Fig)*
|
||||
|
||||
[](https://app.wercker.com/project/bykey/d5dbac3907301c3d5ce735e2d5e95a5b)
|
||||
Compose is a tool for defining and running multi-container 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.
|
||||
|
||||
Fast, isolated development environments using Docker.
|
||||
Compose is great for development environments, staging servers, and CI. We don't
|
||||
recommend that you use it in production yet.
|
||||
|
||||
Define your app's environment with Docker so it can be reproduced anywhere:
|
||||
Using Compose is basically a three-step process.
|
||||
|
||||
FROM python:2.7
|
||||
ADD . /code
|
||||
WORKDIR /code
|
||||
RUN pip install -r requirements.txt
|
||||
CMD python app.py
|
||||
1. Define your app's environment with a `Dockerfile` so it can be
|
||||
reproduced anywhere.
|
||||
2. Define the services that make up your app in `docker-compose.yml` so
|
||||
they can be run together in an isolated environment:
|
||||
3. Lastly, run `docker-compose up` and Compose will start and run your entire app.
|
||||
|
||||
Define the services that make up your app so they can be run together in an isolated environment:
|
||||
A `docker-compose.yml` looks like this:
|
||||
|
||||
```yaml
|
||||
web:
|
||||
build: .
|
||||
links:
|
||||
- db
|
||||
ports:
|
||||
- "8000:8000"
|
||||
- "49100:22"
|
||||
db:
|
||||
image: postgres
|
||||
```
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- "5000:5000"
|
||||
volumes:
|
||||
- .:/code
|
||||
links:
|
||||
- redis
|
||||
redis:
|
||||
image: redis
|
||||
|
||||
(No more installing Postgres on your laptop!)
|
||||
Compose has commands for managing the whole lifecycle of your application:
|
||||
|
||||
Then type `fig up`, and Fig will start and run your entire app:
|
||||
|
||||

|
||||
|
||||
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
|
||||
* 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
|
||||
------------------------------
|
||||
|
||||
Full documentation is available on [Fig's website](http://www.fig.sh/).
|
||||
- Full documentation is available on [Docker's website](http://docs.docker.com/compose/).
|
||||
- If you have any questions, you can talk in real-time with other developers in the #docker-compose IRC channel on Freenode. [Click here to join using IRCCloud.](https://www.irccloud.com/invite?hostname=irc.freenode.net&channel=%23docker-compose)
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
[](http://jenkins.dockerproject.org/job/Compose%20Master/)
|
||||
|
||||
Want to help build Compose? Check out our [contributing documentation](https://github.com/docker/compose/blob/master/CONTRIBUTING.md).
|
||||
|
||||
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)
|
||||
32
SWARM.md
Normal file
32
SWARM.md
Normal file
@@ -0,0 +1,32 @@
|
||||
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.
|
||||
|
||||
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, **linked containers are automatically scheduled on the same host**.
|
||||
|
||||
Building
|
||||
--------
|
||||
|
||||
`docker build` against a Swarm cluster is not implemented, so for now the `build` option will not work - you will need to manually build your service's image, push it somewhere and use `image` to instruct Compose to pull it. Here's an example using the Docker Hub:
|
||||
|
||||
$ docker build -t myusername/web .
|
||||
$ docker push myusername/web
|
||||
$ cat docker-compose.yml
|
||||
web:
|
||||
image: myusername/web
|
||||
links: ["db"]
|
||||
db:
|
||||
image: postgres
|
||||
$ docker-compose up -d
|
||||
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()
|
||||
3
compose/__init__.py
Normal file
3
compose/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
__version__ = '1.3.1'
|
||||
@@ -1,17 +1,16 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from requests.exceptions import ConnectionError, SSLError
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import yaml
|
||||
import six
|
||||
|
||||
from .. import config
|
||||
from ..project import Project
|
||||
from ..service import ConfigError
|
||||
from .docopt_command import DocoptCommand
|
||||
from .utils import call_silently, is_mac, is_ubuntu
|
||||
from .utils import call_silently, is_mac, is_ubuntu, find_candidates_in_parent_dirs
|
||||
from .docker_client import docker_client
|
||||
from . import verbose_proxy
|
||||
from . import errors
|
||||
@@ -19,6 +18,13 @@ from .. import __version__
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
SUPPORTED_FILENAMES = [
|
||||
'docker-compose.yml',
|
||||
'docker-compose.yaml',
|
||||
'fig.yml',
|
||||
'fig.yaml',
|
||||
]
|
||||
|
||||
|
||||
class Command(DocoptCommand):
|
||||
base_dir = '.'
|
||||
@@ -26,7 +32,7 @@ class Command(DocoptCommand):
|
||||
def dispatch(self, *args, **kwargs):
|
||||
try:
|
||||
super(Command, self).dispatch(*args, **kwargs)
|
||||
except SSLError, e:
|
||||
except SSLError as e:
|
||||
raise errors.UserError('SSL error: %s' % e)
|
||||
except ConnectionError:
|
||||
if call_silently(['which', 'docker']) != 0:
|
||||
@@ -42,7 +48,16 @@ class Command(DocoptCommand):
|
||||
raise errors.ConnectionErrorGeneric(self.get_client().base_url)
|
||||
|
||||
def perform_command(self, options, handler, command_options):
|
||||
explicit_config_path = options.get('--file') or os.environ.get('FIG_FILE')
|
||||
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'),
|
||||
@@ -54,36 +69,31 @@ class Command(DocoptCommand):
|
||||
client = docker_client()
|
||||
if verbose:
|
||||
version_info = six.iteritems(client.version())
|
||||
log.info("Fig version %s", __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:
|
||||
if e.errno == errno.ENOENT:
|
||||
raise errors.FigFileNotFound(os.path.basename(e.filename))
|
||||
raise errors.UserError(six.text_type(e))
|
||||
|
||||
def get_project(self, config_path, project_name=None, verbose=False):
|
||||
try:
|
||||
return Project.from_config(
|
||||
return Project.from_dicts(
|
||||
self.get_project_name(config_path, project_name),
|
||||
self.get_config(config_path),
|
||||
config.load(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-zA-Z0-9]', '', name)
|
||||
return re.sub(r'[^a-z0-9]', '', name.lower())
|
||||
|
||||
project_name = project_name or os.environ.get('FIG_PROJECT_NAME')
|
||||
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)
|
||||
|
||||
@@ -97,13 +107,24 @@ class Command(DocoptCommand):
|
||||
if file_path:
|
||||
return os.path.join(self.base_dir, file_path)
|
||||
|
||||
if 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 "
|
||||
(candidates, path) = find_candidates_in_parent_dirs(SUPPORTED_FILENAMES, self.base_dir)
|
||||
|
||||
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")
|
||||
"issues in future.\n")
|
||||
|
||||
return os.path.join(self.base_dir, 'fig.yaml')
|
||||
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 os.path.join(self.base_dir, 'fig.yml')
|
||||
return os.path.join(path, winner)
|
||||
@@ -11,7 +11,7 @@ def docker_client():
|
||||
"""
|
||||
cert_path = os.environ.get('DOCKER_CERT_PATH', '')
|
||||
if cert_path == '':
|
||||
cert_path = os.path.join(os.environ.get('HOME'), '.docker')
|
||||
cert_path = os.path.join(os.environ.get('HOME', ''), '.docker')
|
||||
|
||||
base_url = os.environ.get('DOCKER_HOST')
|
||||
tls_config = None
|
||||
@@ -31,4 +31,5 @@ def docker_client():
|
||||
ca_cert=ca_cert,
|
||||
)
|
||||
|
||||
return Client(base_url=base_url, tls=tls_config)
|
||||
timeout = int(os.environ.get('DOCKER_CLIENT_TIMEOUT', 60))
|
||||
return Client(base_url=base_url, tls=tls_config, version='1.18', timeout=timeout)
|
||||
@@ -33,10 +33,7 @@ class DocoptCommand(object):
|
||||
if command is None:
|
||||
raise SystemExit(getdoc(self))
|
||||
|
||||
if not hasattr(self, command):
|
||||
raise NoSuchCommand(command, self)
|
||||
|
||||
handler = getattr(self, command)
|
||||
handler = self.get_handler(command)
|
||||
docstring = getdoc(handler)
|
||||
|
||||
if docstring is None:
|
||||
@@ -45,6 +42,14 @@ class DocoptCommand(object):
|
||||
command_options = docopt_full_help(docstring, options['ARGS'], options_first=True)
|
||||
return options, handler, command_options
|
||||
|
||||
def get_handler(self, command):
|
||||
command = command.replace('-', '_')
|
||||
|
||||
if not hasattr(self, command):
|
||||
raise NoSuchCommand(command, self)
|
||||
|
||||
return getattr(self, command)
|
||||
|
||||
|
||||
class NoSuchCommand(Exception):
|
||||
def __init__(self, command, supercommand):
|
||||
@@ -55,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 in this directory or any parent. Are you in the right directory?
|
||||
|
||||
Supported filenames: %s
|
||||
""" % ", ".join(supported_filenames))
|
||||
@@ -39,11 +39,14 @@ class LogPrinter(object):
|
||||
color_fns = cycle(colors.rainbow())
|
||||
generators = []
|
||||
|
||||
def no_color(text):
|
||||
return text
|
||||
|
||||
for container in self.containers:
|
||||
if monochrome:
|
||||
color_fn = lambda s: s
|
||||
color_fn = no_color
|
||||
else:
|
||||
color_fn = color_fns.next()
|
||||
color_fn = next(color_fns)
|
||||
generators.append(self._make_log_generator(container, color_fn))
|
||||
|
||||
return generators
|
||||
@@ -1,25 +1,25 @@
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
from inspect import getdoc
|
||||
from operator import attrgetter
|
||||
import logging
|
||||
import sys
|
||||
import re
|
||||
import signal
|
||||
from operator import attrgetter
|
||||
|
||||
from inspect import getdoc
|
||||
from fig.packages import dockerpty
|
||||
|
||||
from .. import __version__
|
||||
from ..project import NoSuchService, ConfigurationError
|
||||
from ..service import BuildError, CannotBeScaledError
|
||||
from .command import Command
|
||||
from .formatter import Formatter
|
||||
from .log_printer import LogPrinter
|
||||
from .utils import yesno
|
||||
import sys
|
||||
|
||||
from docker.errors import APIError
|
||||
from .errors import UserError
|
||||
import dockerpty
|
||||
|
||||
from .. import legacy
|
||||
from ..project import NoSuchService, ConfigurationError
|
||||
from ..service import BuildError, NeedsBuildError
|
||||
from ..config import parse_environment
|
||||
from .command import Command
|
||||
from .docopt_command import NoSuchCommand
|
||||
from .errors import UserError
|
||||
from .formatter import Formatter
|
||||
from .log_printer import LogPrinter
|
||||
from .utils import get_version_info, yesno
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -32,7 +32,7 @@ def main():
|
||||
except KeyboardInterrupt:
|
||||
log.error("\nAborting.")
|
||||
sys.exit(1)
|
||||
except (UserError, NoSuchService, ConfigurationError) as e:
|
||||
except (UserError, NoSuchService, ConfigurationError, legacy.LegacyContainersError) as e:
|
||||
log.error(e.msg)
|
||||
sys.exit(1)
|
||||
except NoSuchCommand as e:
|
||||
@@ -46,6 +46,9 @@ def main():
|
||||
except BuildError as e:
|
||||
log.error("Service '%s' failed to build: %s" % (e.service.name, e.reason))
|
||||
sys.exit(1)
|
||||
except NeedsBuildError as e:
|
||||
log.error("Service '%s' needs to be built, but --no-build was passed." % e.service.name)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def setup_logging():
|
||||
@@ -68,38 +71,39 @@ def parse_doc_section(name, source):
|
||||
|
||||
|
||||
class TopLevelCommand(Command):
|
||||
"""Punctual, lightweight development environments using Docker.
|
||||
"""Define and run multi-container applications with 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)
|
||||
--verbose Show more output
|
||||
-v, --version Print version and exit
|
||||
|
||||
Commands:
|
||||
build Build or rebuild services
|
||||
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
|
||||
build Build or rebuild services
|
||||
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
|
||||
restart Restart services
|
||||
rm Remove stopped containers
|
||||
run Run a one-off command
|
||||
scale Set number of containers for a service
|
||||
start Start services
|
||||
stop Stop services
|
||||
up Create and start containers
|
||||
migrate-to-labels Recreate containers to add labels
|
||||
|
||||
"""
|
||||
def docopt_options(self):
|
||||
options = super(TopLevelCommand, self).docopt_options()
|
||||
options['version'] = "fig %s" % __version__
|
||||
options['version'] = get_version_info()
|
||||
return options
|
||||
|
||||
def build(self, project, options):
|
||||
@@ -107,8 +111,8 @@ class TopLevelCommand(Command):
|
||||
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 `docker-compose build` to rebuild it.
|
||||
|
||||
Usage: build [options] [SERVICE...]
|
||||
|
||||
@@ -124,18 +128,22 @@ class TopLevelCommand(Command):
|
||||
|
||||
Usage: help COMMAND
|
||||
"""
|
||||
command = options['COMMAND']
|
||||
if not hasattr(self, command):
|
||||
raise NoSuchCommand(command, self)
|
||||
raise SystemExit(getdoc(getattr(self, command)))
|
||||
handler = self.get_handler(options['COMMAND'])
|
||||
raise SystemExit(getdoc(handler))
|
||||
|
||||
def kill(self, project, options):
|
||||
"""
|
||||
Force stop service containers.
|
||||
|
||||
Usage: kill [SERVICE...]
|
||||
Usage: kill [options] [SERVICE...]
|
||||
|
||||
Options:
|
||||
-s SIGNAL SIGNAL to send to the container.
|
||||
Default signal is SIGKILL.
|
||||
"""
|
||||
project.kill(service_names=options['SERVICE'])
|
||||
signal = options.get('-s', 'SIGKILL')
|
||||
|
||||
project.kill(service_names=options['SERVICE'], signal=signal)
|
||||
|
||||
def logs(self, project, options):
|
||||
"""
|
||||
@@ -159,13 +167,14 @@ class TopLevelCommand(Command):
|
||||
Usage: port [options] SERVICE PRIVATE_PORT
|
||||
|
||||
Options:
|
||||
--protocol=proto tcp or udp (defaults to tcp)
|
||||
--protocol=proto tcp or udp [default: tcp]
|
||||
--index=index index of the container if there are multiple
|
||||
instances of a service (defaults to 1)
|
||||
instances of a service [default: 1]
|
||||
"""
|
||||
index = int(options.get('--index'))
|
||||
service = project.get_service(options['SERVICE'])
|
||||
try:
|
||||
container = service.get_container(number=options.get('--index') or 1)
|
||||
container = service.get_container(number=index)
|
||||
except ValueError as e:
|
||||
raise UserError(str(e))
|
||||
print(container.get_local_port(
|
||||
@@ -232,8 +241,8 @@ class TopLevelCommand(Command):
|
||||
Usage: rm [options] [SERVICE...]
|
||||
|
||||
Options:
|
||||
--force Don't ask to confirm removal
|
||||
-v Remove volumes associated with containers
|
||||
-f, --force Don't ask to confirm removal
|
||||
-v Remove volumes associated with containers
|
||||
"""
|
||||
all_containers = project.containers(service_names=options['SERVICE'], stopped=True)
|
||||
stopped_containers = [c for c in all_containers if not c.is_running]
|
||||
@@ -255,34 +264,42 @@ class TopLevelCommand(Command):
|
||||
|
||||
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] [-e KEY=VAL...] SERVICE [COMMAND] [ARGS...]
|
||||
|
||||
Options:
|
||||
-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.
|
||||
-T Disable pseudo-tty allocation. By default `fig run`
|
||||
allocates a TTY.
|
||||
--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)
|
||||
-u, --user="" Run as specified username or uid
|
||||
--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'])
|
||||
|
||||
insecure_registry = options['--allow-insecure-ssl']
|
||||
|
||||
if not options['--no-deps']:
|
||||
deps = service.get_linked_names()
|
||||
|
||||
if len(deps) > 0:
|
||||
project.up(
|
||||
service_names=deps,
|
||||
start_links=True,
|
||||
recreate=False,
|
||||
start_deps=True,
|
||||
allow_recreate=False,
|
||||
insecure_registry=insecure_registry,
|
||||
)
|
||||
|
||||
tty = True
|
||||
@@ -298,28 +315,38 @@ class TopLevelCommand(Command):
|
||||
'command': command,
|
||||
'tty': tty,
|
||||
'stdin_open': not options['-d'],
|
||||
'detach': options['-d'],
|
||||
}
|
||||
|
||||
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
|
||||
container_options['environment'] = parse_environment(options['-e'])
|
||||
|
||||
if options['--entrypoint']:
|
||||
container_options['entrypoint'] = options.get('--entrypoint')
|
||||
|
||||
container = service.create_container(one_off=True, **container_options)
|
||||
if options['--rm']:
|
||||
container_options['restart'] = None
|
||||
|
||||
if options['--user']:
|
||||
container_options['user'] = options.get('--user')
|
||||
|
||||
if not options['--service-ports']:
|
||||
container_options['ports'] = []
|
||||
|
||||
container = service.create_container(
|
||||
quiet=True,
|
||||
one_off=True,
|
||||
insecure_registry=insecure_registry,
|
||||
**container_options
|
||||
)
|
||||
|
||||
if options['-d']:
|
||||
service.start_container(container, ports=None, one_off=True)
|
||||
service.start_container(container)
|
||||
print(container.name)
|
||||
else:
|
||||
service.start_container(container, ports=None, one_off=True)
|
||||
dockerpty.start(project.client, container.id)
|
||||
dockerpty.start(project.client, container.id, interactive=not options['-T'])
|
||||
exit_code = container.wait()
|
||||
if options['--rm']:
|
||||
log.info("Removing %s..." % container.name)
|
||||
project.client.remove_container(container.id)
|
||||
sys.exit(exit_code)
|
||||
|
||||
@@ -330,7 +357,7 @@ class TopLevelCommand(Command):
|
||||
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...]
|
||||
"""
|
||||
@@ -343,15 +370,7 @@ class TopLevelCommand(Command):
|
||||
except ValueError:
|
||||
raise UserError('Number of containers for service "%s" is not a '
|
||||
'number' % service_name)
|
||||
try:
|
||||
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)
|
||||
project.get_service(service_name).scale(num)
|
||||
|
||||
def start(self, project, options):
|
||||
"""
|
||||
@@ -365,55 +384,80 @@ class TopLevelCommand(Command):
|
||||
"""
|
||||
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...]
|
||||
Usage: stop [options] [SERVICE...]
|
||||
|
||||
Options:
|
||||
-t, --timeout TIMEOUT Specify a shutdown timeout in seconds.
|
||||
(default: 10)
|
||||
"""
|
||||
project.stop(service_names=options['SERVICE'])
|
||||
timeout = options.get('--timeout')
|
||||
params = {} if timeout is None else {'timeout': int(timeout)}
|
||||
project.stop(service_names=options['SERVICE'], **params)
|
||||
|
||||
def restart(self, project, options):
|
||||
"""
|
||||
Restart running containers.
|
||||
|
||||
Usage: restart [SERVICE...]
|
||||
Usage: restart [options] [SERVICE...]
|
||||
|
||||
Options:
|
||||
-t, --timeout TIMEOUT Specify a shutdown timeout in seconds.
|
||||
(default: 10)
|
||||
"""
|
||||
project.restart(service_names=options['SERVICE'])
|
||||
timeout = options.get('--timeout')
|
||||
params = {} if timeout is None else {'timeout': int(timeout)}
|
||||
project.restart(service_names=options['SERVICE'], **params)
|
||||
|
||||
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-color Produce monochrome output.
|
||||
--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.
|
||||
--x-smart-recreate Only recreate containers whose configuration or
|
||||
image needs to be updated. (EXPERIMENTAL)
|
||||
--no-recreate If containers already exist, don't recreate them.
|
||||
--no-build Don't build an image, even if it's missing
|
||||
-t, --timeout TIMEOUT When attached, use this timeout in seconds
|
||||
for the shutdown. (default: 10)
|
||||
|
||||
"""
|
||||
insecure_registry = options['--allow-insecure-ssl']
|
||||
detached = options['-d']
|
||||
|
||||
monochrome = options['--no-color']
|
||||
|
||||
start_links = not options['--no-deps']
|
||||
recreate = not options['--no-recreate']
|
||||
start_deps = not options['--no-deps']
|
||||
allow_recreate = not options['--no-recreate']
|
||||
smart_recreate = options['--x-smart-recreate']
|
||||
service_names = options['SERVICE']
|
||||
|
||||
project.up(
|
||||
service_names=service_names,
|
||||
start_links=start_links,
|
||||
recreate=recreate
|
||||
start_deps=start_deps,
|
||||
allow_recreate=allow_recreate,
|
||||
smart_recreate=smart_recreate,
|
||||
insecure_registry=insecure_registry,
|
||||
do_build=not options['--no-build'],
|
||||
)
|
||||
|
||||
to_attach = [c for s in project.get_services(service_names) for c in s.containers()]
|
||||
@@ -431,7 +475,35 @@ class TopLevelCommand(Command):
|
||||
signal.signal(signal.SIGINT, handler)
|
||||
|
||||
print("Gracefully stopping... (press Ctrl+C again to force)")
|
||||
project.stop(service_names=service_names)
|
||||
timeout = options.get('--timeout')
|
||||
params = {} if timeout is None else {'timeout': int(timeout)}
|
||||
project.stop(service_names=service_names, **params)
|
||||
|
||||
def migrate_to_labels(self, project, _options):
|
||||
"""
|
||||
Recreate containers to add labels
|
||||
|
||||
If you're coming from Compose 1.2 or earlier, you'll need to remove or
|
||||
migrate your existing containers after upgrading Compose. This is
|
||||
because, as of version 1.3, Compose uses Docker labels to keep track
|
||||
of containers, and so they need to be recreated with labels added.
|
||||
|
||||
If Compose detects containers that were created without labels, it
|
||||
will refuse to run so that you don't end up with two sets of them. If
|
||||
you want to keep using your existing containers (for example, because
|
||||
they have data volumes you want to preserve) you can migrate them with
|
||||
the following command:
|
||||
|
||||
docker-compose migrate-to-labels
|
||||
|
||||
Alternatively, if you're not worried about keeping them, you can
|
||||
remove them - Compose will just create new ones.
|
||||
|
||||
docker rm -f myapp_web_1 myapp_db_1 ...
|
||||
|
||||
Usage: migrate-to-labels
|
||||
"""
|
||||
legacy.migrate_project_to_labels(project)
|
||||
|
||||
|
||||
def list_containers(containers):
|
||||
@@ -5,6 +5,9 @@ import datetime
|
||||
import os
|
||||
import subprocess
|
||||
import platform
|
||||
import ssl
|
||||
|
||||
from .. import __version__
|
||||
|
||||
|
||||
def yesno(prompt, default=None):
|
||||
@@ -62,6 +65,25 @@ def mkdir(path, permissions=0o700):
|
||||
return path
|
||||
|
||||
|
||||
def find_candidates_in_parent_dirs(filenames, path):
|
||||
"""
|
||||
Given a directory path to start, looks for filenames in the
|
||||
directory, and then each parent directory successively,
|
||||
until found.
|
||||
|
||||
Returns tuple (candidates, path).
|
||||
"""
|
||||
candidates = [filename for filename in filenames
|
||||
if os.path.exists(os.path.join(path, filename))]
|
||||
|
||||
if len(candidates) == 0:
|
||||
parent_dir = os.path.join(path, '..')
|
||||
if os.path.abspath(parent_dir) != os.path.abspath(path):
|
||||
return find_candidates_in_parent_dirs(filenames, parent_dir)
|
||||
|
||||
return (candidates, path)
|
||||
|
||||
|
||||
def split_buffer(reader, separator):
|
||||
"""
|
||||
Given a generator which yields strings and a separator string,
|
||||
@@ -101,3 +123,11 @@ def is_mac():
|
||||
|
||||
def is_ubuntu():
|
||||
return platform.system() == 'Linux' and platform.linux_distribution()[0] == 'Ubuntu'
|
||||
|
||||
|
||||
def get_version_info():
|
||||
return '\n'.join([
|
||||
'docker-compose version: %s' % __version__,
|
||||
"%s version: %s" % (platform.python_implementation(), platform.python_version()),
|
||||
"OpenSSL version: %s" % ssl.OPENSSL_VERSION,
|
||||
])
|
||||
489
compose/config.py
Normal file
489
compose/config.py
Normal file
@@ -0,0 +1,489 @@
|
||||
import os
|
||||
import yaml
|
||||
import six
|
||||
|
||||
|
||||
DOCKER_CONFIG_KEYS = [
|
||||
'cap_add',
|
||||
'cap_drop',
|
||||
'cpu_shares',
|
||||
'cpuset',
|
||||
'command',
|
||||
'detach',
|
||||
'devices',
|
||||
'dns',
|
||||
'dns_search',
|
||||
'domainname',
|
||||
'entrypoint',
|
||||
'env_file',
|
||||
'environment',
|
||||
'extra_hosts',
|
||||
'read_only',
|
||||
'hostname',
|
||||
'image',
|
||||
'labels',
|
||||
'links',
|
||||
'mem_limit',
|
||||
'net',
|
||||
'log_driver',
|
||||
'pid',
|
||||
'ports',
|
||||
'privileged',
|
||||
'restart',
|
||||
'security_opt',
|
||||
'stdin_open',
|
||||
'tty',
|
||||
'user',
|
||||
'volumes',
|
||||
'volumes_from',
|
||||
'working_dir',
|
||||
]
|
||||
|
||||
ALLOWED_KEYS = DOCKER_CONFIG_KEYS + [
|
||||
'build',
|
||||
'dockerfile',
|
||||
'expose',
|
||||
'external_links',
|
||||
'name',
|
||||
]
|
||||
|
||||
DOCKER_CONFIG_HINTS = {
|
||||
'cpu_share': 'cpu_shares',
|
||||
'add_host': 'extra_hosts',
|
||||
'hosts': 'extra_hosts',
|
||||
'extra_host': 'extra_hosts',
|
||||
'device': 'devices',
|
||||
'link': 'links',
|
||||
'port': 'ports',
|
||||
'privilege': 'privileged',
|
||||
'priviliged': 'privileged',
|
||||
'privilige': 'privileged',
|
||||
'volume': 'volumes',
|
||||
'workdir': 'working_dir',
|
||||
}
|
||||
|
||||
|
||||
def load(filename):
|
||||
working_dir = os.path.dirname(filename)
|
||||
return from_dictionary(load_yaml(filename), working_dir=working_dir, filename=filename)
|
||||
|
||||
|
||||
def from_dictionary(dictionary, working_dir=None, filename=None):
|
||||
service_dicts = []
|
||||
|
||||
for service_name, service_dict in list(dictionary.items()):
|
||||
if not isinstance(service_dict, dict):
|
||||
raise ConfigurationError('Service "%s" doesn\'t have any configuration options. All top level keys in your docker-compose.yml must map to a dictionary of configuration options.' % service_name)
|
||||
loader = ServiceLoader(working_dir=working_dir, filename=filename)
|
||||
service_dict = loader.make_service_dict(service_name, service_dict)
|
||||
validate_paths(service_dict)
|
||||
service_dicts.append(service_dict)
|
||||
|
||||
return service_dicts
|
||||
|
||||
|
||||
def make_service_dict(name, service_dict, working_dir=None):
|
||||
return ServiceLoader(working_dir=working_dir).make_service_dict(name, service_dict)
|
||||
|
||||
|
||||
class ServiceLoader(object):
|
||||
def __init__(self, working_dir, filename=None, already_seen=None):
|
||||
self.working_dir = working_dir
|
||||
self.filename = filename
|
||||
self.already_seen = already_seen or []
|
||||
|
||||
def make_service_dict(self, name, service_dict):
|
||||
if self.signature(name) in self.already_seen:
|
||||
raise CircularReference(self.already_seen)
|
||||
|
||||
service_dict = service_dict.copy()
|
||||
service_dict['name'] = name
|
||||
service_dict = resolve_environment(service_dict, working_dir=self.working_dir)
|
||||
service_dict = self.resolve_extends(service_dict)
|
||||
return process_container_options(service_dict, working_dir=self.working_dir)
|
||||
|
||||
def resolve_extends(self, service_dict):
|
||||
if 'extends' not in service_dict:
|
||||
return service_dict
|
||||
|
||||
extends_options = process_extends_options(service_dict['name'], service_dict['extends'])
|
||||
|
||||
if self.working_dir is None:
|
||||
raise Exception("No working_dir passed to ServiceLoader()")
|
||||
|
||||
other_config_path = expand_path(self.working_dir, extends_options['file'])
|
||||
other_working_dir = os.path.dirname(other_config_path)
|
||||
other_already_seen = self.already_seen + [self.signature(service_dict['name'])]
|
||||
other_loader = ServiceLoader(
|
||||
working_dir=other_working_dir,
|
||||
filename=other_config_path,
|
||||
already_seen=other_already_seen,
|
||||
)
|
||||
|
||||
other_config = load_yaml(other_config_path)
|
||||
other_service_dict = other_config[extends_options['service']]
|
||||
other_service_dict = other_loader.make_service_dict(
|
||||
service_dict['name'],
|
||||
other_service_dict,
|
||||
)
|
||||
validate_extended_service_dict(
|
||||
other_service_dict,
|
||||
filename=other_config_path,
|
||||
service=extends_options['service'],
|
||||
)
|
||||
|
||||
return merge_service_dicts(other_service_dict, service_dict)
|
||||
|
||||
def signature(self, name):
|
||||
return (self.filename, name)
|
||||
|
||||
|
||||
def process_extends_options(service_name, extends_options):
|
||||
error_prefix = "Invalid 'extends' configuration for %s:" % service_name
|
||||
|
||||
if not isinstance(extends_options, dict):
|
||||
raise ConfigurationError("%s must be a dictionary" % error_prefix)
|
||||
|
||||
if 'service' not in extends_options:
|
||||
raise ConfigurationError(
|
||||
"%s you need to specify a service, e.g. 'service: web'" % error_prefix
|
||||
)
|
||||
|
||||
for k, _ in extends_options.items():
|
||||
if k not in ['file', 'service']:
|
||||
raise ConfigurationError(
|
||||
"%s unsupported configuration option '%s'" % (error_prefix, k)
|
||||
)
|
||||
|
||||
return extends_options
|
||||
|
||||
|
||||
def validate_extended_service_dict(service_dict, filename, service):
|
||||
error_prefix = "Cannot extend service '%s' in %s:" % (service, filename)
|
||||
|
||||
if 'links' in service_dict:
|
||||
raise ConfigurationError("%s services with 'links' cannot be extended" % error_prefix)
|
||||
|
||||
if 'volumes_from' in service_dict:
|
||||
raise ConfigurationError("%s services with 'volumes_from' cannot be extended" % error_prefix)
|
||||
|
||||
if 'net' in service_dict:
|
||||
if get_service_name_from_net(service_dict['net']) is not None:
|
||||
raise ConfigurationError("%s services with 'net: container' cannot be extended" % error_prefix)
|
||||
|
||||
|
||||
def process_container_options(service_dict, working_dir=None):
|
||||
for k in service_dict:
|
||||
if k not in ALLOWED_KEYS:
|
||||
msg = "Unsupported config option for %s service: '%s'" % (service_dict['name'], k)
|
||||
if k in DOCKER_CONFIG_HINTS:
|
||||
msg += " (did you mean '%s'?)" % DOCKER_CONFIG_HINTS[k]
|
||||
raise ConfigurationError(msg)
|
||||
|
||||
service_dict = service_dict.copy()
|
||||
|
||||
if 'volumes' in service_dict:
|
||||
service_dict['volumes'] = resolve_host_paths(service_dict['volumes'], working_dir=working_dir)
|
||||
|
||||
if 'build' in service_dict:
|
||||
service_dict['build'] = resolve_build_path(service_dict['build'], working_dir=working_dir)
|
||||
|
||||
if 'labels' in service_dict:
|
||||
service_dict['labels'] = parse_labels(service_dict['labels'])
|
||||
|
||||
return service_dict
|
||||
|
||||
|
||||
def merge_service_dicts(base, override):
|
||||
d = base.copy()
|
||||
|
||||
if 'environment' in base or 'environment' in override:
|
||||
d['environment'] = merge_environment(
|
||||
base.get('environment'),
|
||||
override.get('environment'),
|
||||
)
|
||||
|
||||
path_mapping_keys = ['volumes', 'devices']
|
||||
|
||||
for key in path_mapping_keys:
|
||||
if key in base or key in override:
|
||||
d[key] = merge_path_mappings(
|
||||
base.get(key),
|
||||
override.get(key),
|
||||
)
|
||||
|
||||
if 'labels' in base or 'labels' in override:
|
||||
d['labels'] = merge_labels(
|
||||
base.get('labels'),
|
||||
override.get('labels'),
|
||||
)
|
||||
|
||||
if 'image' in override and 'build' in d:
|
||||
del d['build']
|
||||
|
||||
if 'build' in override and 'image' in d:
|
||||
del d['image']
|
||||
|
||||
list_keys = ['ports', 'expose', 'external_links']
|
||||
|
||||
for key in list_keys:
|
||||
if key in base or key in override:
|
||||
d[key] = base.get(key, []) + override.get(key, [])
|
||||
|
||||
list_or_string_keys = ['dns', 'dns_search']
|
||||
|
||||
for key in list_or_string_keys:
|
||||
if key in base or key in override:
|
||||
d[key] = to_list(base.get(key)) + to_list(override.get(key))
|
||||
|
||||
already_merged_keys = ['environment', 'labels'] + path_mapping_keys + list_keys + list_or_string_keys
|
||||
|
||||
for k in set(ALLOWED_KEYS) - set(already_merged_keys):
|
||||
if k in override:
|
||||
d[k] = override[k]
|
||||
|
||||
return d
|
||||
|
||||
|
||||
def merge_environment(base, override):
|
||||
env = parse_environment(base)
|
||||
env.update(parse_environment(override))
|
||||
return env
|
||||
|
||||
|
||||
def parse_links(links):
|
||||
return dict(parse_link(l) for l in links)
|
||||
|
||||
|
||||
def parse_link(link):
|
||||
if ':' in link:
|
||||
source, alias = link.split(':', 1)
|
||||
return (alias, source)
|
||||
else:
|
||||
return (link, link)
|
||||
|
||||
|
||||
def get_env_files(options, working_dir=None):
|
||||
if 'env_file' not in options:
|
||||
return {}
|
||||
|
||||
if working_dir is None:
|
||||
raise Exception("No working_dir passed to get_env_files()")
|
||||
|
||||
env_files = options.get('env_file', [])
|
||||
if not isinstance(env_files, list):
|
||||
env_files = [env_files]
|
||||
|
||||
return [expand_path(working_dir, path) for path in env_files]
|
||||
|
||||
|
||||
def resolve_environment(service_dict, working_dir=None):
|
||||
service_dict = service_dict.copy()
|
||||
|
||||
if 'environment' not in service_dict and 'env_file' not in service_dict:
|
||||
return service_dict
|
||||
|
||||
env = {}
|
||||
|
||||
if 'env_file' in service_dict:
|
||||
for f in get_env_files(service_dict, working_dir=working_dir):
|
||||
env.update(env_vars_from_file(f))
|
||||
del service_dict['env_file']
|
||||
|
||||
env.update(parse_environment(service_dict.get('environment')))
|
||||
env = dict(resolve_env_var(k, v) for k, v in six.iteritems(env))
|
||||
|
||||
service_dict['environment'] = env
|
||||
return service_dict
|
||||
|
||||
|
||||
def parse_environment(environment):
|
||||
if not environment:
|
||||
return {}
|
||||
|
||||
if isinstance(environment, list):
|
||||
return dict(split_env(e) for e in environment)
|
||||
|
||||
if isinstance(environment, dict):
|
||||
return environment
|
||||
|
||||
raise ConfigurationError(
|
||||
"environment \"%s\" must be a list or mapping," %
|
||||
environment
|
||||
)
|
||||
|
||||
|
||||
def split_env(env):
|
||||
if '=' in env:
|
||||
return env.split('=', 1)
|
||||
else:
|
||||
return env, None
|
||||
|
||||
|
||||
def resolve_env_var(key, val):
|
||||
if val is not None:
|
||||
return key, val
|
||||
elif key in os.environ:
|
||||
return key, os.environ[key]
|
||||
else:
|
||||
return key, ''
|
||||
|
||||
|
||||
def env_vars_from_file(filename):
|
||||
"""
|
||||
Read in a line delimited file of environment variables.
|
||||
"""
|
||||
if not os.path.exists(filename):
|
||||
raise ConfigurationError("Couldn't find env file: %s" % filename)
|
||||
env = {}
|
||||
for line in open(filename, 'r'):
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#'):
|
||||
k, v = split_env(line)
|
||||
env[k] = v
|
||||
return env
|
||||
|
||||
|
||||
def resolve_host_paths(volumes, working_dir=None):
|
||||
if working_dir is None:
|
||||
raise Exception("No working_dir passed to resolve_host_paths()")
|
||||
|
||||
return [resolve_host_path(v, working_dir) for v in volumes]
|
||||
|
||||
|
||||
def resolve_host_path(volume, working_dir):
|
||||
container_path, host_path = split_path_mapping(volume)
|
||||
if host_path is not None:
|
||||
host_path = os.path.expanduser(host_path)
|
||||
host_path = os.path.expandvars(host_path)
|
||||
return "%s:%s" % (expand_path(working_dir, host_path), container_path)
|
||||
else:
|
||||
return container_path
|
||||
|
||||
|
||||
def resolve_build_path(build_path, working_dir=None):
|
||||
if working_dir is None:
|
||||
raise Exception("No working_dir passed to resolve_build_path")
|
||||
return expand_path(working_dir, build_path)
|
||||
|
||||
|
||||
def validate_paths(service_dict):
|
||||
if 'build' in service_dict:
|
||||
build_path = service_dict['build']
|
||||
if not os.path.exists(build_path) or not os.access(build_path, os.R_OK):
|
||||
raise ConfigurationError("build path %s either does not exist or is not accessible." % build_path)
|
||||
|
||||
|
||||
def merge_path_mappings(base, override):
|
||||
d = dict_from_path_mappings(base)
|
||||
d.update(dict_from_path_mappings(override))
|
||||
return path_mappings_from_dict(d)
|
||||
|
||||
|
||||
def dict_from_path_mappings(path_mappings):
|
||||
if path_mappings:
|
||||
return dict(split_path_mapping(v) for v in path_mappings)
|
||||
else:
|
||||
return {}
|
||||
|
||||
|
||||
def path_mappings_from_dict(d):
|
||||
return [join_path_mapping(v) for v in d.items()]
|
||||
|
||||
|
||||
def split_path_mapping(string):
|
||||
if ':' in string:
|
||||
(host, container) = string.split(':', 1)
|
||||
return (container, host)
|
||||
else:
|
||||
return (string, None)
|
||||
|
||||
|
||||
def join_path_mapping(pair):
|
||||
(container, host) = pair
|
||||
if host is None:
|
||||
return container
|
||||
else:
|
||||
return ":".join((host, container))
|
||||
|
||||
|
||||
def merge_labels(base, override):
|
||||
labels = parse_labels(base)
|
||||
labels.update(parse_labels(override))
|
||||
return labels
|
||||
|
||||
|
||||
def parse_labels(labels):
|
||||
if not labels:
|
||||
return {}
|
||||
|
||||
if isinstance(labels, list):
|
||||
return dict(split_label(e) for e in labels)
|
||||
|
||||
if isinstance(labels, dict):
|
||||
return labels
|
||||
|
||||
raise ConfigurationError(
|
||||
"labels \"%s\" must be a list or mapping" %
|
||||
labels
|
||||
)
|
||||
|
||||
|
||||
def split_label(label):
|
||||
if '=' in label:
|
||||
return label.split('=', 1)
|
||||
else:
|
||||
return label, ''
|
||||
|
||||
|
||||
def expand_path(working_dir, path):
|
||||
return os.path.abspath(os.path.join(working_dir, path))
|
||||
|
||||
|
||||
def to_list(value):
|
||||
if value is None:
|
||||
return []
|
||||
elif isinstance(value, six.string_types):
|
||||
return [value]
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
def get_service_name_from_net(net_config):
|
||||
if not net_config:
|
||||
return
|
||||
|
||||
if not net_config.startswith('container:'):
|
||||
return
|
||||
|
||||
_, net_name = net_config.split(':', 1)
|
||||
return net_name
|
||||
|
||||
|
||||
def load_yaml(filename):
|
||||
try:
|
||||
with open(filename, 'r') as fh:
|
||||
return yaml.safe_load(fh)
|
||||
except IOError as e:
|
||||
raise ConfigurationError(six.text_type(e))
|
||||
|
||||
|
||||
class ConfigurationError(Exception):
|
||||
def __init__(self, msg):
|
||||
self.msg = msg
|
||||
|
||||
def __str__(self):
|
||||
return self.msg
|
||||
|
||||
|
||||
class CircularReference(ConfigurationError):
|
||||
def __init__(self, trail):
|
||||
self.trail = trail
|
||||
|
||||
@property
|
||||
def msg(self):
|
||||
lines = [
|
||||
"{} in {}".format(service_name, filename)
|
||||
for (filename, service_name) in self.trail
|
||||
]
|
||||
return "Circular reference:\n {}".format("\n extends ".join(lines))
|
||||
7
compose/const.py
Normal file
7
compose/const.py
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
LABEL_CONTAINER_NUMBER = 'com.docker.compose.container-number'
|
||||
LABEL_ONE_OFF = 'com.docker.compose.oneoff'
|
||||
LABEL_PROJECT = 'com.docker.compose.project'
|
||||
LABEL_SERVICE = 'com.docker.compose.service'
|
||||
LABEL_VERSION = 'com.docker.compose.version'
|
||||
LABEL_CONFIG_HASH = 'com.docker.compose.config-hash'
|
||||
@@ -2,6 +2,9 @@ from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
import six
|
||||
from functools import reduce
|
||||
|
||||
from .const import LABEL_CONTAINER_NUMBER, LABEL_SERVICE
|
||||
|
||||
|
||||
class Container(object):
|
||||
@@ -22,10 +25,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
|
||||
@@ -45,6 +46,10 @@ class Container(object):
|
||||
def image(self):
|
||||
return self.dictionary['Image']
|
||||
|
||||
@property
|
||||
def image_config(self):
|
||||
return self.client.inspect_image(self.image)
|
||||
|
||||
@property
|
||||
def short_id(self):
|
||||
return self.id[:10]
|
||||
@@ -55,14 +60,15 @@ class Container(object):
|
||||
|
||||
@property
|
||||
def name_without_project(self):
|
||||
return '_'.join(self.dictionary['Name'].split('_')[1:])
|
||||
return '{0}_{1}'.format(self.labels.get(LABEL_SERVICE), self.number)
|
||||
|
||||
@property
|
||||
def number(self):
|
||||
try:
|
||||
return int(self.name.split('_')[-1])
|
||||
except ValueError:
|
||||
return None
|
||||
number = self.labels.get(LABEL_CONTAINER_NUMBER)
|
||||
if not number:
|
||||
raise ValueError("Container {0} does not have a {1} label".format(
|
||||
self.short_id, LABEL_CONTAINER_NUMBER))
|
||||
return int(number)
|
||||
|
||||
@property
|
||||
def ports(self):
|
||||
@@ -80,6 +86,14 @@ class Container(object):
|
||||
return ', '.join(format_port(*item)
|
||||
for item in sorted(six.iteritems(self.ports)))
|
||||
|
||||
@property
|
||||
def labels(self):
|
||||
return self.get('Config.Labels') or {}
|
||||
|
||||
@property
|
||||
def log_config(self):
|
||||
return self.get('HostConfig.LogConfig') or None
|
||||
|
||||
@property
|
||||
def human_readable_state(self):
|
||||
if self.is_running:
|
||||
@@ -124,11 +138,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 restart(self, **options):
|
||||
return self.client.restart(self.id, **options)
|
||||
|
||||
def remove(self, **options):
|
||||
return self.client.remove_container(self.id, **options)
|
||||
@@ -148,6 +162,7 @@ class Container(object):
|
||||
self.has_been_inspected = True
|
||||
return self.dictionary
|
||||
|
||||
# TODO: only used by tests, move to test module
|
||||
def links(self):
|
||||
links = []
|
||||
for container in self.client.containers():
|
||||
@@ -164,9 +179,23 @@ class Container(object):
|
||||
return self.client.attach_socket(self.id, **kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Container: %s>' % self.name
|
||||
return '<Container: %s (%s)>' % (self.name, self.id[:6])
|
||||
|
||||
def __eq__(self, other):
|
||||
if type(self) != type(other):
|
||||
return False
|
||||
return self.id == other.id
|
||||
|
||||
def __hash__(self):
|
||||
return self.id.__hash__()
|
||||
|
||||
|
||||
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]
|
||||
122
compose/legacy.py
Normal file
122
compose/legacy.py
Normal file
@@ -0,0 +1,122 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
from .container import get_container_name, Container
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# TODO: remove this section when migrate_project_to_labels is removed
|
||||
NAME_RE = re.compile(r'^([^_]+)_([^_]+)_(run_)?(\d+)$')
|
||||
|
||||
ERROR_MESSAGE_FORMAT = """
|
||||
Compose found the following containers without labels:
|
||||
|
||||
{names_list}
|
||||
|
||||
As of Compose 1.3.0, containers are identified with labels instead of naming convention. If you want to continue using these containers, run:
|
||||
|
||||
$ docker-compose migrate-to-labels
|
||||
|
||||
Alternatively, remove them:
|
||||
|
||||
$ docker rm -f {rm_args}
|
||||
"""
|
||||
|
||||
|
||||
def check_for_legacy_containers(
|
||||
client,
|
||||
project,
|
||||
services,
|
||||
stopped=False,
|
||||
one_off=False):
|
||||
"""Check if there are containers named using the old naming convention
|
||||
and warn the user that those containers may need to be migrated to
|
||||
using labels, so that compose can find them.
|
||||
"""
|
||||
containers = list(get_legacy_containers(
|
||||
client,
|
||||
project,
|
||||
services,
|
||||
stopped=stopped,
|
||||
one_off=one_off))
|
||||
|
||||
if containers:
|
||||
raise LegacyContainersError([c.name for c in containers])
|
||||
|
||||
|
||||
class LegacyContainersError(Exception):
|
||||
def __init__(self, names):
|
||||
self.names = names
|
||||
|
||||
self.msg = ERROR_MESSAGE_FORMAT.format(
|
||||
names_list="\n".join(" {}".format(name) for name in names),
|
||||
rm_args=" ".join(names),
|
||||
)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.msg
|
||||
|
||||
__str__ = __unicode__
|
||||
|
||||
|
||||
def add_labels(project, container):
|
||||
project_name, service_name, one_off, number = NAME_RE.match(container.name).groups()
|
||||
if project_name != project.name or service_name not in project.service_names:
|
||||
return
|
||||
service = project.get_service(service_name)
|
||||
service.recreate_container(container)
|
||||
|
||||
|
||||
def migrate_project_to_labels(project):
|
||||
log.info("Running migration to labels for project %s", project.name)
|
||||
|
||||
containers = get_legacy_containers(
|
||||
project.client,
|
||||
project.name,
|
||||
project.service_names,
|
||||
stopped=True,
|
||||
one_off=False)
|
||||
|
||||
for container in containers:
|
||||
add_labels(project, container)
|
||||
|
||||
|
||||
def get_legacy_containers(
|
||||
client,
|
||||
project,
|
||||
services,
|
||||
stopped=False,
|
||||
one_off=False):
|
||||
|
||||
containers = client.containers(all=stopped)
|
||||
|
||||
for service in services:
|
||||
for container in containers:
|
||||
name = get_container_name(container)
|
||||
if has_container(project, service, name, one_off=one_off):
|
||||
yield Container.from_ps(client, container)
|
||||
|
||||
|
||||
def has_container(project, service, name, one_off=False):
|
||||
if not name or not is_valid_name(name, one_off):
|
||||
return False
|
||||
container_project, container_service, _container_number = parse_name(name)
|
||||
return container_project == project and container_service == service
|
||||
|
||||
|
||||
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 (project, service_name, int(suffix))
|
||||
@@ -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]
|
||||
@@ -72,8 +74,9 @@ def print_output_event(event, stream, is_terminal):
|
||||
stream.write("%s %s%s" % (status, event['progress'], terminator))
|
||||
elif 'progressDetail' in event:
|
||||
detail = event['progressDetail']
|
||||
if 'current' in detail:
|
||||
percentage = float(detail['current']) / float(detail['total']) * 100
|
||||
total = detail.get('total')
|
||||
if 'current' in detail and total:
|
||||
percentage = float(detail['current']) / float(total) * 100
|
||||
stream.write('%s (%.1f%%)%s' % (status, percentage, terminator))
|
||||
else:
|
||||
stream.write('%s%s' % (status, terminator))
|
||||
@@ -1,10 +1,15 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
import logging
|
||||
from functools import reduce
|
||||
|
||||
from docker.errors import APIError
|
||||
|
||||
from .config import get_service_name_from_net, ConfigurationError
|
||||
from .const import LABEL_PROJECT, LABEL_SERVICE, LABEL_ONE_OFF
|
||||
from .service import Service
|
||||
from .container import Container
|
||||
from docker.errors import APIError
|
||||
from .legacy import check_for_legacy_containers
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -15,7 +20,17 @@ 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 get_service_dependents(service_dict, services):
|
||||
name = service_dict['name']
|
||||
return [
|
||||
service for service in services
|
||||
if (name in get_service_names(service.get('links', [])) or
|
||||
name in service.get('volumes_from', []) or
|
||||
name == get_service_name_from_net(service.get('net')))
|
||||
]
|
||||
|
||||
def visit(n):
|
||||
if n['name'] in temporary_marked:
|
||||
@@ -27,8 +42,7 @@ def sort_service_dicts(services):
|
||||
raise DependencyError('Circular import between %s' % ' and '.join(temporary_marked))
|
||||
if n in unmarked:
|
||||
temporary_marked.add(n['name'])
|
||||
dependents = [m for m in services if (n['name'] in get_service_names(m.get('links', []))) or (n['name'] in m.get('volumes_from', []))]
|
||||
for m in dependents:
|
||||
for m in get_service_dependents(n, services):
|
||||
visit(m)
|
||||
temporary_marked.remove(n['name'])
|
||||
unmarked.remove(n)
|
||||
@@ -49,6 +63,12 @@ class Project(object):
|
||||
self.services = services
|
||||
self.client = client
|
||||
|
||||
def labels(self, one_off=False):
|
||||
return [
|
||||
'{0}={1}'.format(LABEL_PROJECT, self.name),
|
||||
'{0}={1}'.format(LABEL_ONE_OFF, "True" if one_off else "False"),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def from_dicts(cls, name, service_dicts, client):
|
||||
"""
|
||||
@@ -58,19 +78,15 @@ class Project(object):
|
||||
for service_dict in sort_service_dicts(service_dicts):
|
||||
links = project.get_links(service_dict)
|
||||
volumes_from = project.get_volumes_from(service_dict)
|
||||
net = project.get_net(service_dict)
|
||||
|
||||
project.services.append(Service(client=client, project=name, links=links, volumes_from=volumes_from, **service_dict))
|
||||
project.services.append(Service(client=client, project=name, links=links, net=net,
|
||||
volumes_from=volumes_from, **service_dict))
|
||||
return project
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, name, config, client):
|
||||
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.')
|
||||
service['name'] = service_name
|
||||
dicts.append(service)
|
||||
return cls.from_dicts(name, dicts, client)
|
||||
@property
|
||||
def service_names(self):
|
||||
return [service.name for service in self.services]
|
||||
|
||||
def get_service(self, name):
|
||||
"""
|
||||
@@ -83,31 +99,31 @@ class Project(object):
|
||||
|
||||
raise NoSuchService(name)
|
||||
|
||||
def get_services(self, service_names=None, include_links=False):
|
||||
def get_services(self, service_names=None, include_deps=False):
|
||||
"""
|
||||
Returns a list of this project's services filtered
|
||||
by the provided list of names, or all services if service_names is None
|
||||
or [].
|
||||
|
||||
If include_links is specified, returns a list including the links for
|
||||
If include_deps is specified, returns a list including the dependencies for
|
||||
service_names, in order of dependency.
|
||||
|
||||
Preserves the original order of self.services where possible,
|
||||
reordering as needed to resolve links.
|
||||
reordering as needed to resolve dependencies.
|
||||
|
||||
Raises NoSuchService if any of the named services do not exist.
|
||||
"""
|
||||
if service_names is None or len(service_names) == 0:
|
||||
return self.get_services(
|
||||
service_names=[s.name for s in self.services],
|
||||
include_links=include_links
|
||||
service_names=self.service_names,
|
||||
include_deps=include_deps
|
||||
)
|
||||
else:
|
||||
unsorted = [self.get_service(name) for name in service_names]
|
||||
services = [s for s in self.services if s in unsorted]
|
||||
|
||||
if include_links:
|
||||
services = reduce(self._inject_links, services, [])
|
||||
if include_deps:
|
||||
services = reduce(self._inject_deps, services, [])
|
||||
|
||||
uniques = []
|
||||
[uniques.append(s) for s in services if s not in uniques]
|
||||
@@ -144,6 +160,28 @@ class Project(object):
|
||||
del service_dict['volumes_from']
|
||||
return volumes_from
|
||||
|
||||
def get_net(self, service_dict):
|
||||
if 'net' in service_dict:
|
||||
net_name = get_service_name_from_net(service_dict.get('net'))
|
||||
|
||||
if net_name:
|
||||
try:
|
||||
net = self.get_service(net_name)
|
||||
except NoSuchService:
|
||||
try:
|
||||
net = Container.from_id(self.client, net_name)
|
||||
except APIError:
|
||||
raise ConfigurationError('Service "%s" is trying to use the network of "%s", which is not the name of a service or container.' % (service_dict['name'], net_name))
|
||||
else:
|
||||
net = service_dict['net']
|
||||
|
||||
del service_dict['net']
|
||||
|
||||
else:
|
||||
net = None
|
||||
|
||||
return net
|
||||
|
||||
def start(self, service_names=None, **options):
|
||||
for service in self.get_services(service_names):
|
||||
service.start(**options)
|
||||
@@ -167,21 +205,68 @@ class Project(object):
|
||||
else:
|
||||
log.info('%s uses an image, skipping' % service.name)
|
||||
|
||||
def up(self, service_names=None, start_links=True, recreate=True):
|
||||
running_containers = []
|
||||
def up(self,
|
||||
service_names=None,
|
||||
start_deps=True,
|
||||
allow_recreate=True,
|
||||
smart_recreate=False,
|
||||
insecure_registry=False,
|
||||
do_build=True):
|
||||
|
||||
for service in self.get_services(service_names, include_links=start_links):
|
||||
if recreate:
|
||||
for (_, container) in service.recreate_containers():
|
||||
running_containers.append(container)
|
||||
services = self.get_services(service_names, include_deps=start_deps)
|
||||
|
||||
plans = self._get_convergence_plans(
|
||||
services,
|
||||
allow_recreate=allow_recreate,
|
||||
smart_recreate=smart_recreate,
|
||||
)
|
||||
|
||||
return [
|
||||
container
|
||||
for service in services
|
||||
for container in service.execute_convergence_plan(
|
||||
plans[service.name],
|
||||
insecure_registry=insecure_registry,
|
||||
do_build=do_build,
|
||||
)
|
||||
]
|
||||
|
||||
def _get_convergence_plans(self,
|
||||
services,
|
||||
allow_recreate=True,
|
||||
smart_recreate=False):
|
||||
|
||||
plans = {}
|
||||
|
||||
for service in services:
|
||||
updated_dependencies = [
|
||||
name
|
||||
for name in service.get_dependency_names()
|
||||
if name in plans
|
||||
and plans[name].action == 'recreate'
|
||||
]
|
||||
|
||||
if updated_dependencies:
|
||||
log.debug(
|
||||
'%s has upstream changes (%s)',
|
||||
service.name, ", ".join(updated_dependencies),
|
||||
)
|
||||
plan = service.convergence_plan(
|
||||
allow_recreate=allow_recreate,
|
||||
smart_recreate=False,
|
||||
)
|
||||
else:
|
||||
for container in service.start_or_create_containers():
|
||||
running_containers.append(container)
|
||||
plan = service.convergence_plan(
|
||||
allow_recreate=allow_recreate,
|
||||
smart_recreate=smart_recreate,
|
||||
)
|
||||
|
||||
return running_containers
|
||||
plans[service.name] = plan
|
||||
|
||||
return plans
|
||||
|
||||
def pull(self, service_names=None, insecure_registry=False):
|
||||
for service in self.get_services(service_names, include_links=True):
|
||||
for service in self.get_services(service_names, include_deps=True):
|
||||
service.pull(insecure_registry=insecure_registry)
|
||||
|
||||
def remove_stopped(self, service_names=None, **options):
|
||||
@@ -189,24 +274,40 @@ class Project(object):
|
||||
service.remove_stopped(**options)
|
||||
|
||||
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)]
|
||||
containers = [
|
||||
Container.from_ps(self.client, container)
|
||||
for container in self.client.containers(
|
||||
all=stopped,
|
||||
filters={'label': self.labels(one_off=one_off)})]
|
||||
|
||||
def _inject_links(self, acc, service):
|
||||
linked_names = service.get_linked_names()
|
||||
def matches_service_names(container):
|
||||
if not service_names:
|
||||
return True
|
||||
return container.labels.get(LABEL_SERVICE) in service_names
|
||||
|
||||
if len(linked_names) > 0:
|
||||
linked_services = self.get_services(
|
||||
service_names=linked_names,
|
||||
include_links=True
|
||||
if not containers:
|
||||
check_for_legacy_containers(
|
||||
self.client,
|
||||
self.name,
|
||||
self.service_names,
|
||||
stopped=stopped,
|
||||
one_off=one_off)
|
||||
|
||||
return filter(matches_service_names, containers)
|
||||
|
||||
def _inject_deps(self, acc, service):
|
||||
dep_names = service.get_dependency_names()
|
||||
|
||||
if len(dep_names) > 0:
|
||||
dep_services = self.get_services(
|
||||
service_names=list(set(dep_names)),
|
||||
include_deps=True
|
||||
)
|
||||
else:
|
||||
linked_services = []
|
||||
dep_services = []
|
||||
|
||||
linked_services.append(service)
|
||||
return acc + linked_services
|
||||
dep_services.append(service)
|
||||
return acc + dep_services
|
||||
|
||||
|
||||
class NoSuchService(Exception):
|
||||
@@ -218,13 +319,5 @@ class NoSuchService(Exception):
|
||||
return self.msg
|
||||
|
||||
|
||||
class ConfigurationError(Exception):
|
||||
def __init__(self, msg):
|
||||
self.msg = msg
|
||||
|
||||
def __str__(self):
|
||||
return self.msg
|
||||
|
||||
|
||||
class DependencyError(ConfigurationError):
|
||||
pass
|
||||
876
compose/service.py
Normal file
876
compose/service.py
Normal file
@@ -0,0 +1,876 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
from operator import attrgetter
|
||||
|
||||
import six
|
||||
from docker.errors import APIError
|
||||
from docker.utils import create_host_config, LogConfig
|
||||
|
||||
from . import __version__
|
||||
from .config import DOCKER_CONFIG_KEYS, merge_environment
|
||||
from .const import (
|
||||
LABEL_CONTAINER_NUMBER,
|
||||
LABEL_ONE_OFF,
|
||||
LABEL_PROJECT,
|
||||
LABEL_SERVICE,
|
||||
LABEL_VERSION,
|
||||
LABEL_CONFIG_HASH,
|
||||
)
|
||||
from .container import Container
|
||||
from .legacy import check_for_legacy_containers
|
||||
from .progress_stream import stream_output, StreamOutputError
|
||||
from .utils import json_hash
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
DOCKER_START_KEYS = [
|
||||
'cap_add',
|
||||
'cap_drop',
|
||||
'devices',
|
||||
'dns',
|
||||
'dns_search',
|
||||
'env_file',
|
||||
'extra_hosts',
|
||||
'read_only',
|
||||
'net',
|
||||
'log_driver',
|
||||
'pid',
|
||||
'privileged',
|
||||
'restart',
|
||||
'volumes_from',
|
||||
'security_opt',
|
||||
]
|
||||
|
||||
VALID_NAME_CHARS = '[a-zA-Z0-9]'
|
||||
|
||||
|
||||
class BuildError(Exception):
|
||||
def __init__(self, service, reason):
|
||||
self.service = service
|
||||
self.reason = reason
|
||||
|
||||
|
||||
class ConfigError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class NeedsBuildError(Exception):
|
||||
def __init__(self, service):
|
||||
self.service = service
|
||||
|
||||
|
||||
VolumeSpec = namedtuple('VolumeSpec', 'external internal mode')
|
||||
|
||||
|
||||
ServiceName = namedtuple('ServiceName', 'project service number')
|
||||
|
||||
|
||||
ConvergencePlan = namedtuple('ConvergencePlan', 'action containers')
|
||||
|
||||
|
||||
class Service(object):
|
||||
def __init__(self, name, client=None, project='default', links=None, external_links=None, volumes_from=None, net=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)
|
||||
if 'image' not in options and 'build' not in options:
|
||||
raise ConfigError('Service %s has neither an image nor a build path specified. Exactly one must be provided.' % name)
|
||||
|
||||
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.net = net or None
|
||||
self.options = options
|
||||
|
||||
def containers(self, stopped=False, one_off=False):
|
||||
containers = [
|
||||
Container.from_ps(self.client, container)
|
||||
for container in self.client.containers(
|
||||
all=stopped,
|
||||
filters={'label': self.labels(one_off=one_off)})]
|
||||
|
||||
if not containers:
|
||||
check_for_legacy_containers(
|
||||
self.client,
|
||||
self.project,
|
||||
[self.name],
|
||||
stopped=stopped,
|
||||
one_off=one_off)
|
||||
|
||||
return containers
|
||||
|
||||
def get_container(self, number=1):
|
||||
"""Return a :class:`compose.container.Container` for this service. The
|
||||
container must be active, and match `number`.
|
||||
"""
|
||||
labels = self.labels() + ['{0}={1}'.format(LABEL_CONTAINER_NUMBER, number)]
|
||||
for container in self.client.containers(filters={'label': labels}):
|
||||
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():
|
||||
log.warn('Service %s specifies a port on the host. If multiple containers '
|
||||
'for this service are created on a single host, the port will clash.'
|
||||
% self.name)
|
||||
|
||||
# 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,
|
||||
insecure_registry=False,
|
||||
do_build=True,
|
||||
previous_container=None,
|
||||
number=None,
|
||||
quiet=False,
|
||||
**override_options):
|
||||
"""
|
||||
Create a container for this service. If the image doesn't exist, attempt to pull
|
||||
it.
|
||||
"""
|
||||
self.ensure_image_exists(
|
||||
do_build=do_build,
|
||||
insecure_registry=insecure_registry,
|
||||
)
|
||||
|
||||
container_options = self._get_container_create_options(
|
||||
override_options,
|
||||
number or self._next_container_number(one_off=one_off),
|
||||
one_off=one_off,
|
||||
previous_container=previous_container,
|
||||
)
|
||||
|
||||
if 'name' in container_options and not quiet:
|
||||
log.info("Creating %s..." % container_options['name'])
|
||||
|
||||
return Container.create(self.client, **container_options)
|
||||
|
||||
def ensure_image_exists(self,
|
||||
do_build=True,
|
||||
insecure_registry=False):
|
||||
|
||||
if self.image():
|
||||
return
|
||||
|
||||
if self.can_be_built():
|
||||
if do_build:
|
||||
self.build()
|
||||
else:
|
||||
raise NeedsBuildError(self)
|
||||
else:
|
||||
self.pull(insecure_registry=insecure_registry)
|
||||
|
||||
def image(self):
|
||||
try:
|
||||
return self.client.inspect_image(self.image_name)
|
||||
except APIError as e:
|
||||
if e.response.status_code == 404 and e.explanation and 'No such image' in str(e.explanation):
|
||||
return None
|
||||
else:
|
||||
raise
|
||||
|
||||
@property
|
||||
def image_name(self):
|
||||
if self.can_be_built():
|
||||
return self.full_name
|
||||
else:
|
||||
return self.options['image']
|
||||
|
||||
def converge(self,
|
||||
allow_recreate=True,
|
||||
smart_recreate=False,
|
||||
insecure_registry=False,
|
||||
do_build=True):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
plan = self.convergence_plan(
|
||||
allow_recreate=allow_recreate,
|
||||
smart_recreate=smart_recreate,
|
||||
)
|
||||
|
||||
return self.execute_convergence_plan(
|
||||
plan,
|
||||
insecure_registry=insecure_registry,
|
||||
do_build=do_build,
|
||||
)
|
||||
|
||||
def convergence_plan(self,
|
||||
allow_recreate=True,
|
||||
smart_recreate=False):
|
||||
|
||||
containers = self.containers(stopped=True)
|
||||
|
||||
if not containers:
|
||||
return ConvergencePlan('create', [])
|
||||
|
||||
if smart_recreate and not self._containers_have_diverged(containers):
|
||||
stopped = [c for c in containers if not c.is_running]
|
||||
|
||||
if stopped:
|
||||
return ConvergencePlan('start', stopped)
|
||||
|
||||
return ConvergencePlan('noop', containers)
|
||||
|
||||
if not allow_recreate:
|
||||
return ConvergencePlan('start', containers)
|
||||
|
||||
return ConvergencePlan('recreate', containers)
|
||||
|
||||
def _containers_have_diverged(self, containers):
|
||||
config_hash = self.config_hash()
|
||||
has_diverged = False
|
||||
|
||||
for c in containers:
|
||||
container_config_hash = c.labels.get(LABEL_CONFIG_HASH, None)
|
||||
if container_config_hash != config_hash:
|
||||
log.debug(
|
||||
'%s has diverged: %s != %s',
|
||||
c.name, container_config_hash, config_hash,
|
||||
)
|
||||
has_diverged = True
|
||||
|
||||
return has_diverged
|
||||
|
||||
def execute_convergence_plan(self,
|
||||
plan,
|
||||
insecure_registry=False,
|
||||
do_build=True):
|
||||
(action, containers) = plan
|
||||
|
||||
if action == 'create':
|
||||
container = self.create_container(
|
||||
insecure_registry=insecure_registry,
|
||||
do_build=do_build,
|
||||
)
|
||||
self.start_container(container)
|
||||
|
||||
return [container]
|
||||
|
||||
elif action == 'recreate':
|
||||
return [
|
||||
self.recreate_container(
|
||||
c,
|
||||
insecure_registry=insecure_registry,
|
||||
)
|
||||
for c in containers
|
||||
]
|
||||
|
||||
elif action == 'start':
|
||||
for c in containers:
|
||||
self.start_container_if_stopped(c)
|
||||
|
||||
return containers
|
||||
|
||||
elif action == 'noop':
|
||||
for c in containers:
|
||||
log.info("%s is up-to-date" % c.name)
|
||||
|
||||
return containers
|
||||
|
||||
else:
|
||||
raise Exception("Invalid action: {}".format(action))
|
||||
|
||||
def recreate_container(self,
|
||||
container,
|
||||
insecure_registry=False):
|
||||
"""Recreate a container.
|
||||
|
||||
The original container is renamed to a temporary name so that data
|
||||
volumes can be copied to the new container, before the original
|
||||
container is removed.
|
||||
"""
|
||||
log.info("Recreating %s..." % container.name)
|
||||
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
|
||||
|
||||
# Use a hopefully unique container name by prepending the short id
|
||||
self.client.rename(
|
||||
container.id,
|
||||
'%s_%s' % (container.short_id, container.name))
|
||||
|
||||
new_container = self.create_container(
|
||||
insecure_registry=insecure_registry,
|
||||
do_build=False,
|
||||
previous_container=container,
|
||||
number=container.labels.get(LABEL_CONTAINER_NUMBER),
|
||||
quiet=True,
|
||||
)
|
||||
self.start_container(new_container)
|
||||
container.remove()
|
||||
return new_container
|
||||
|
||||
def start_container_if_stopped(self, container):
|
||||
if container.is_running:
|
||||
return container
|
||||
else:
|
||||
log.info("Starting %s..." % container.name)
|
||||
return self.start_container(container)
|
||||
|
||||
def start_container(self, container):
|
||||
container.start()
|
||||
return container
|
||||
|
||||
def config_hash(self):
|
||||
return json_hash(self.config_dict())
|
||||
|
||||
def config_dict(self):
|
||||
return {
|
||||
'options': self.options,
|
||||
'image_id': self.image()['Id'],
|
||||
}
|
||||
|
||||
def get_dependency_names(self):
|
||||
net_name = self.get_net_name()
|
||||
return (self.get_linked_names() +
|
||||
self.get_volumes_from_names() +
|
||||
([net_name] if net_name else []))
|
||||
|
||||
def get_linked_names(self):
|
||||
return [s.name for (s, _) in self.links]
|
||||
|
||||
def get_volumes_from_names(self):
|
||||
return [s.name for s in self.volumes_from if isinstance(s, Service)]
|
||||
|
||||
def get_net_name(self):
|
||||
if isinstance(self.net, Service):
|
||||
return self.net.name
|
||||
else:
|
||||
return
|
||||
|
||||
def get_container_name(self, number, one_off=False):
|
||||
# TODO: Implement issue #652 here
|
||||
return build_container_name(self.project, self.name, number, one_off)
|
||||
|
||||
# TODO: this would benefit from github.com/docker/docker/pull/11943
|
||||
# to remove the need to inspect every container
|
||||
def _next_container_number(self, one_off=False):
|
||||
numbers = [
|
||||
Container.from_ps(self.client, container).number
|
||||
for container in self.client.containers(
|
||||
all=True,
|
||||
filters={'label': self.labels(one_off=one_off)})
|
||||
]
|
||||
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):
|
||||
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)
|
||||
|
||||
return volumes_from
|
||||
|
||||
def _get_net(self):
|
||||
if not self.net:
|
||||
return None
|
||||
|
||||
if isinstance(self.net, Service):
|
||||
containers = self.net.containers()
|
||||
if len(containers) > 0:
|
||||
net = 'container:' + containers[0].id
|
||||
else:
|
||||
log.warning("Warning: Service %s is trying to use reuse the network stack "
|
||||
"of another service that is not running." % (self.net.name))
|
||||
net = None
|
||||
elif isinstance(self.net, Container):
|
||||
net = 'container:' + self.net.id
|
||||
else:
|
||||
net = self.net
|
||||
|
||||
return net
|
||||
|
||||
def _get_container_create_options(
|
||||
self,
|
||||
override_options,
|
||||
number,
|
||||
one_off=False,
|
||||
previous_container=None):
|
||||
|
||||
add_config_hash = (not one_off and not override_options)
|
||||
|
||||
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.get_container_name(number, one_off)
|
||||
|
||||
if add_config_hash:
|
||||
config_hash = self.config_hash()
|
||||
if 'labels' not in container_options:
|
||||
container_options['labels'] = {}
|
||||
container_options['labels'][LABEL_CONFIG_HASH] = config_hash
|
||||
log.debug("Added config hash: %s" % config_hash)
|
||||
|
||||
if 'detach' not in container_options:
|
||||
container_options['detach'] = True
|
||||
|
||||
# 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
|
||||
|
||||
override_options['binds'] = merge_volume_bindings(
|
||||
container_options.get('volumes') or [],
|
||||
previous_container)
|
||||
|
||||
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(
|
||||
self.options.get('environment'),
|
||||
override_options.get('environment'))
|
||||
|
||||
if previous_container:
|
||||
container_options['environment']['affinity:container'] = ('=' + previous_container.id)
|
||||
|
||||
container_options['image'] = self.image_name
|
||||
|
||||
container_options['labels'] = build_container_labels(
|
||||
container_options.get('labels', {}),
|
||||
self.labels(one_off=one_off),
|
||||
number)
|
||||
|
||||
# Delete options which are only used when starting
|
||||
for key in DOCKER_START_KEYS:
|
||||
container_options.pop(key, None)
|
||||
|
||||
container_options['host_config'] = self._get_container_host_config(
|
||||
override_options,
|
||||
one_off=one_off)
|
||||
|
||||
return container_options
|
||||
|
||||
def _get_container_host_config(self, override_options, one_off=False):
|
||||
options = dict(self.options, **override_options)
|
||||
port_bindings = build_port_bindings(options.get('ports') or [])
|
||||
|
||||
privileged = options.get('privileged', False)
|
||||
cap_add = options.get('cap_add', None)
|
||||
cap_drop = options.get('cap_drop', None)
|
||||
log_config = LogConfig(type=options.get('log_driver', 'json-file'))
|
||||
pid = options.get('pid', None)
|
||||
security_opt = options.get('security_opt', None)
|
||||
|
||||
dns = options.get('dns', None)
|
||||
if isinstance(dns, six.string_types):
|
||||
dns = [dns]
|
||||
|
||||
dns_search = options.get('dns_search', None)
|
||||
if isinstance(dns_search, six.string_types):
|
||||
dns_search = [dns_search]
|
||||
|
||||
restart = parse_restart_spec(options.get('restart', None))
|
||||
|
||||
extra_hosts = build_extra_hosts(options.get('extra_hosts', None))
|
||||
read_only = options.get('read_only', None)
|
||||
|
||||
devices = options.get('devices', None)
|
||||
|
||||
return create_host_config(
|
||||
links=self._get_links(link_to_self=one_off),
|
||||
port_bindings=port_bindings,
|
||||
binds=options.get('binds'),
|
||||
volumes_from=self._get_volumes_from(),
|
||||
privileged=privileged,
|
||||
network_mode=self._get_net(),
|
||||
devices=devices,
|
||||
dns=dns,
|
||||
dns_search=dns_search,
|
||||
restart_policy=restart,
|
||||
cap_add=cap_add,
|
||||
cap_drop=cap_drop,
|
||||
log_config=log_config,
|
||||
extra_hosts=extra_hosts,
|
||||
read_only=read_only,
|
||||
pid_mode=pid,
|
||||
security_opt=security_opt
|
||||
)
|
||||
|
||||
def build(self, no_cache=False):
|
||||
log.info('Building %s...' % self.name)
|
||||
|
||||
path = six.binary_type(self.options['build'])
|
||||
|
||||
build_output = self.client.build(
|
||||
path=path,
|
||||
tag=self.image_name,
|
||||
stream=True,
|
||||
rm=True,
|
||||
pull=False,
|
||||
nocache=no_cache,
|
||||
dockerfile=self.options.get('dockerfile', None),
|
||||
)
|
||||
|
||||
try:
|
||||
all_events = stream_output(build_output, sys.stdout)
|
||||
except StreamOutputError as e:
|
||||
raise BuildError(self, unicode(e))
|
||||
|
||||
# Ensure the HTTP connection is not reused for another
|
||||
# streaming command, as the Docker daemon can sometimes
|
||||
# complain about it
|
||||
self.client.close()
|
||||
|
||||
image_id = None
|
||||
|
||||
for event in all_events:
|
||||
if 'stream' in event:
|
||||
match = re.search(r'Successfully built ([0-9a-f]+)', event.get('stream', ''))
|
||||
if match:
|
||||
image_id = match.group(1)
|
||||
|
||||
if image_id is None:
|
||||
raise BuildError(self, event if all_events else 'Unknown')
|
||||
|
||||
return image_id
|
||||
|
||||
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 labels(self, one_off=False):
|
||||
return [
|
||||
'{0}={1}'.format(LABEL_PROJECT, self.project),
|
||||
'{0}={1}'.format(LABEL_SERVICE, self.name),
|
||||
'{0}={1}'.format(LABEL_ONE_OFF, "True" if one_off else "False")
|
||||
]
|
||||
|
||||
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' not in self.options:
|
||||
return
|
||||
|
||||
repo, tag = parse_repository_tag(self.options['image'])
|
||||
tag = tag or 'latest'
|
||||
log.info('Pulling %s (%s:%s)...' % (self.name, repo, tag))
|
||||
output = self.client.pull(
|
||||
repo,
|
||||
tag=tag,
|
||||
stream=True,
|
||||
insecure_registry=insecure_registry)
|
||||
stream_output(output, sys.stdout)
|
||||
|
||||
|
||||
# Names
|
||||
|
||||
|
||||
def build_container_name(project, service, number, one_off=False):
|
||||
bits = [project, service]
|
||||
if one_off:
|
||||
bits.append('run')
|
||||
return '_'.join(bits + [str(number)])
|
||||
|
||||
|
||||
# Images
|
||||
|
||||
|
||||
def parse_repository_tag(s):
|
||||
if ":" not in s:
|
||||
return s, ""
|
||||
repo, tag = s.rsplit(":", 1)
|
||||
if "/" in tag:
|
||||
return s, ""
|
||||
return repo, tag
|
||||
|
||||
|
||||
# Volumes
|
||||
|
||||
|
||||
def merge_volume_bindings(volumes_option, previous_container):
|
||||
"""Return a list of volume bindings for a container. Container data volumes
|
||||
are replaced by those from the previous container.
|
||||
"""
|
||||
volume_bindings = dict(
|
||||
build_volume_binding(parse_volume_spec(volume))
|
||||
for volume in volumes_option or []
|
||||
if ':' in volume)
|
||||
|
||||
if previous_container:
|
||||
volume_bindings.update(
|
||||
get_container_data_volumes(previous_container, volumes_option))
|
||||
|
||||
return volume_bindings.values()
|
||||
|
||||
|
||||
def get_container_data_volumes(container, volumes_option):
|
||||
"""Find the container data volumes that are in `volumes_option`, and return
|
||||
a mapping of volume bindings for those volumes.
|
||||
"""
|
||||
volumes = []
|
||||
|
||||
volumes_option = volumes_option or []
|
||||
container_volumes = container.get('Volumes') or {}
|
||||
image_volumes = container.image_config['ContainerConfig'].get('Volumes') or {}
|
||||
|
||||
for volume in set(volumes_option + image_volumes.keys()):
|
||||
volume = parse_volume_spec(volume)
|
||||
# No need to preserve host volumes
|
||||
if volume.external:
|
||||
continue
|
||||
|
||||
volume_path = container_volumes.get(volume.internal)
|
||||
# New volume, doesn't exist in the old container
|
||||
if not volume_path:
|
||||
continue
|
||||
|
||||
# Copy existing volume from old container
|
||||
volume = volume._replace(external=volume_path)
|
||||
volumes.append(build_volume_binding(volume))
|
||||
|
||||
return dict(volumes)
|
||||
|
||||
|
||||
def build_volume_binding(volume_spec):
|
||||
return volume_spec.internal, "{}:{}:{}".format(*volume_spec)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
# Ports
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
# Labels
|
||||
|
||||
|
||||
def build_container_labels(label_options, service_labels, number, one_off=False):
|
||||
labels = label_options or {}
|
||||
labels.update(label.split('=', 1) for label in service_labels)
|
||||
labels[LABEL_CONTAINER_NUMBER] = str(number)
|
||||
labels[LABEL_VERSION] = __version__
|
||||
return labels
|
||||
|
||||
|
||||
# Restart policy
|
||||
|
||||
|
||||
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)}
|
||||
|
||||
|
||||
# Extra hosts
|
||||
|
||||
|
||||
def build_extra_hosts(extra_hosts_config):
|
||||
if not extra_hosts_config:
|
||||
return {}
|
||||
|
||||
if isinstance(extra_hosts_config, list):
|
||||
extra_hosts_dict = {}
|
||||
for extra_hosts_line in extra_hosts_config:
|
||||
if not isinstance(extra_hosts_line, six.string_types):
|
||||
raise ConfigError(
|
||||
"extra_hosts_config \"%s\" must be either a list of strings or a string->string mapping," %
|
||||
extra_hosts_config
|
||||
)
|
||||
host, ip = extra_hosts_line.split(':')
|
||||
extra_hosts_dict.update({host.strip(): ip.strip()})
|
||||
extra_hosts_config = extra_hosts_dict
|
||||
|
||||
if isinstance(extra_hosts_config, dict):
|
||||
return extra_hosts_config
|
||||
|
||||
raise ConfigError(
|
||||
"extra_hosts_config \"%s\" must be either a list of strings or a string->string mapping," %
|
||||
extra_hosts_config
|
||||
)
|
||||
9
compose/utils.py
Normal file
9
compose/utils.py
Normal file
@@ -0,0 +1,9 @@
|
||||
import json
|
||||
import hashlib
|
||||
|
||||
|
||||
def json_hash(obj):
|
||||
dump = json.dumps(obj, sort_keys=True, separators=(',', ':'))
|
||||
h = hashlib.sha256()
|
||||
h.update(dump)
|
||||
return h.hexdigest()
|
||||
363
contrib/completion/bash/docker-compose
Normal file
363
contrib/completion/bash/docker-compose
Normal file
@@ -0,0 +1,363 @@
|
||||
#!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 "y?(a)ml"
|
||||
return
|
||||
;;
|
||||
--project-name|-p)
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=( $( compgen -W "--help -h --verbose --version -v --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() {
|
||||
case "$prev" in
|
||||
-t | --timeout)
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=( $( compgen -W "-t --timeout" -- "$cur" ) )
|
||||
;;
|
||||
*)
|
||||
__docker-compose_services_running
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
_docker-compose_rm() {
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=( $( compgen -W "--force -f -v" -- "$cur" ) )
|
||||
;;
|
||||
*)
|
||||
__docker-compose_services_stopped
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
_docker-compose_run() {
|
||||
case "$prev" in
|
||||
-e)
|
||||
COMPREPLY=( $( compgen -e -- "$cur" ) )
|
||||
compopt -o nospace
|
||||
return
|
||||
;;
|
||||
--entrypoint|--user|-u)
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=( $( compgen -W "--allow-insecure-ssl -d --entrypoint -e --no-deps --rm --service-ports -T --user -u" -- "$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() {
|
||||
case "$prev" in
|
||||
-t | --timeout)
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=( $( compgen -W "-t --timeout" -- "$cur" ) )
|
||||
;;
|
||||
*)
|
||||
__docker-compose_services_running
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
_docker-compose_up() {
|
||||
case "$prev" in
|
||||
-t | --timeout)
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=( $( compgen -W "--allow-insecure-ssl -d --no-build --no-color --no-deps --no-recreate -t --timeout --x-smart-recreate" -- "$cur" ) )
|
||||
;;
|
||||
*)
|
||||
__docker-compose_services_all
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
_docker-compose() {
|
||||
local previous_extglob_setting=$(shopt -p extglob)
|
||||
shopt -s extglob
|
||||
|
||||
local commands=(
|
||||
build
|
||||
help
|
||||
kill
|
||||
logs
|
||||
migrate-to-labels
|
||||
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
|
||||
|
||||
eval "$previous_extglob_setting"
|
||||
return 0
|
||||
}
|
||||
|
||||
complete -F _docker-compose docker-compose
|
||||
304
contrib/completion/zsh/_docker-compose
Normal file
304
contrib/completion/zsh/_docker-compose
Normal file
@@ -0,0 +1,304 @@
|
||||
#compdef docker-compose
|
||||
|
||||
# Description
|
||||
# -----------
|
||||
# zsh completion for docker-compose
|
||||
# https://github.com/sdurrheimer/docker-compose-zsh-completion
|
||||
# -------------------------------------------------------------------------
|
||||
# Version
|
||||
# -------
|
||||
# 0.1.0
|
||||
# -------------------------------------------------------------------------
|
||||
# Authors
|
||||
# -------
|
||||
# * Steve Durrheimer <s.durrheimer@gmail.com>
|
||||
# -------------------------------------------------------------------------
|
||||
# Inspiration
|
||||
# -----------
|
||||
# * @albers docker-compose bash completion script
|
||||
# * @felixr docker zsh completion script : https://github.com/felixr/docker-zsh-completion
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
# 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 docker-compose.yml.
|
||||
___docker-compose_all_services_in_compose_file() {
|
||||
local already_selected
|
||||
local -a services
|
||||
already_selected=$(echo ${words[@]} | tr " " "|")
|
||||
awk -F: '/^[a-zA-Z0-9]/{print $1}' "${compose_file:-$(__docker-compose_compose_file)}" 2>/dev/null | grep -Ev "$already_selected"
|
||||
}
|
||||
|
||||
# All services, even those without an existing container
|
||||
__docker-compose_services_all() {
|
||||
services=$(___docker-compose_all_services_in_compose_file)
|
||||
_alternative "args:services:($services)"
|
||||
}
|
||||
|
||||
# All services that have an entry with the given key in their docker-compose.yml section
|
||||
___docker-compose_services_with_key() {
|
||||
local already_selected
|
||||
local -a buildable
|
||||
already_selected=$(echo ${words[@]} | tr " " "|")
|
||||
# flatten sections to one line, then filter lines containing the key and return section name.
|
||||
awk '/^[a-zA-Z0-9]/{printf "\n"};{printf $0;next;}' "${compose_file:-$(__docker-compose_compose_file)}" 2>/dev/null | awk -F: -v key=": +$1:" '$0 ~ key {print $1}' 2>/dev/null | grep -Ev "$already_selected"
|
||||
}
|
||||
|
||||
# All services that are defined by a Dockerfile reference
|
||||
__docker-compose_services_from_build() {
|
||||
buildable=$(___docker-compose_services_with_key build)
|
||||
_alternative "args:buildable services:($buildable)"
|
||||
}
|
||||
|
||||
# All services that are defined by an image
|
||||
__docker-compose_services_from_image() {
|
||||
pullable=$(___docker-compose_services_with_key image)
|
||||
_alternative "args:pullable services:($pullable)"
|
||||
}
|
||||
|
||||
__docker-compose_get_services() {
|
||||
local kind expl
|
||||
declare -a running stopped lines args services
|
||||
|
||||
docker_status=$(docker ps > /dev/null 2>&1)
|
||||
if [ $? -ne 0 ]; then
|
||||
_message "Error! Docker is not running."
|
||||
return 1
|
||||
fi
|
||||
|
||||
kind=$1
|
||||
shift
|
||||
[[ $kind = (stopped|all) ]] && args=($args -a)
|
||||
|
||||
lines=(${(f)"$(_call_program commands docker ps ${args})"})
|
||||
services=(${(f)"$(_call_program commands docker-compose 2>/dev/null ${compose_file:+-f $compose_file} ${compose_project:+-p $compose_project} ps -q)"})
|
||||
|
||||
# Parse header line to find columns
|
||||
local i=1 j=1 k header=${lines[1]}
|
||||
declare -A begin end
|
||||
while (( $j < ${#header} - 1 )) {
|
||||
i=$(( $j + ${${header[$j,-1]}[(i)[^ ]]} - 1))
|
||||
j=$(( $i + ${${header[$i,-1]}[(i) ]} - 1))
|
||||
k=$(( $j + ${${header[$j,-1]}[(i)[^ ]]} - 2))
|
||||
begin[${header[$i,$(($j-1))]}]=$i
|
||||
end[${header[$i,$(($j-1))]}]=$k
|
||||
}
|
||||
lines=(${lines[2,-1]})
|
||||
|
||||
# Container ID
|
||||
local line s name
|
||||
local -a names
|
||||
for line in $lines; do
|
||||
if [[ $services == *"${line[${begin[CONTAINER ID]},${end[CONTAINER ID]}]%% ##}"* ]]; then
|
||||
names=(${(ps:,:)${${line[${begin[NAMES]},-1]}%% *}})
|
||||
for name in $names; do
|
||||
s="${${name%_*}#*_}:${(l:15:: :::)${${line[${begin[CREATED]},${end[CREATED]}]/ ago/}%% ##}}"
|
||||
s="$s, ${line[${begin[CONTAINER ID]},${end[CONTAINER ID]}]%% ##}"
|
||||
s="$s, ${${${line[$begin[IMAGE],$end[IMAGE]]}/:/\\:}%% ##}"
|
||||
if [[ ${line[${begin[STATUS]},${end[STATUS]}]} = Exit* ]]; then
|
||||
stopped=($stopped $s)
|
||||
else
|
||||
running=($running $s)
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
|
||||
[[ $kind = (running|all) ]] && _describe -t services-running "running services" running
|
||||
[[ $kind = (stopped|all) ]] && _describe -t services-stopped "stopped services" stopped
|
||||
}
|
||||
|
||||
__docker-compose_stoppedservices() {
|
||||
__docker-compose_get_services stopped "$@"
|
||||
}
|
||||
|
||||
__docker-compose_runningservices() {
|
||||
__docker-compose_get_services running "$@"
|
||||
}
|
||||
|
||||
__docker-compose_services () {
|
||||
__docker-compose_get_services all "$@"
|
||||
}
|
||||
|
||||
__docker-compose_caching_policy() {
|
||||
oldp=( "$1"(Nmh+1) ) # 1 hour
|
||||
(( $#oldp ))
|
||||
}
|
||||
|
||||
__docker-compose_commands () {
|
||||
local cache_policy
|
||||
|
||||
zstyle -s ":completion:${curcontext}:" cache-policy cache_policy
|
||||
if [[ -z "$cache_policy" ]]; then
|
||||
zstyle ":completion:${curcontext}:" cache-policy __docker-compose_caching_policy
|
||||
fi
|
||||
|
||||
if ( [[ ${+_docker_compose_subcommands} -eq 0 ]] || _cache_invalid docker_compose_subcommands) \
|
||||
&& ! _retrieve_cache docker_compose_subcommands;
|
||||
then
|
||||
local -a lines
|
||||
lines=(${(f)"$(_call_program commands docker-compose 2>&1)"})
|
||||
_docker_compose_subcommands=(${${${lines[$((${lines[(i)Commands:]} + 1)),${lines[(I) *]}]}## #}/ ##/:})
|
||||
_store_cache docker_compose_subcommands _docker_compose_subcommands
|
||||
fi
|
||||
_describe -t docker-compose-commands "docker-compose command" _docker_compose_subcommands
|
||||
}
|
||||
|
||||
__docker-compose_subcommand () {
|
||||
local -a _command_args
|
||||
integer ret=1
|
||||
case "$words[1]" in
|
||||
(build)
|
||||
_arguments \
|
||||
'--no-cache[Do not use cache when building the image]' \
|
||||
'*:services:__docker-compose_services_from_build' && ret=0
|
||||
;;
|
||||
(help)
|
||||
_arguments ':subcommand:__docker-compose_commands' && ret=0
|
||||
;;
|
||||
(kill)
|
||||
_arguments \
|
||||
'-s[SIGNAL to send to the container. Default signal is SIGKILL.]:signal:_signals' \
|
||||
'*:running services:__docker-compose_runningservices' && ret=0
|
||||
;;
|
||||
(logs)
|
||||
_arguments \
|
||||
'--no-color[Produce monochrome output.]' \
|
||||
'*:services:__docker-compose_services_all' && ret=0
|
||||
;;
|
||||
(migrate-to-labels)
|
||||
_arguments \
|
||||
'(-):Recreate containers to add labels' && ret=0
|
||||
;;
|
||||
(port)
|
||||
_arguments \
|
||||
'--protocol=-[tcp or udap (defaults to tcp)]:protocol:(tcp udp)' \
|
||||
'--index=-[index of the container if there are mutiple instances of a service (defaults to 1)]:index: ' \
|
||||
'1:running services:__docker-compose_runningservices' \
|
||||
'2:port:_ports' && ret=0
|
||||
;;
|
||||
(ps)
|
||||
_arguments \
|
||||
'-q[Only display IDs]' \
|
||||
'*:services:__docker-compose_services_all' && ret=0
|
||||
;;
|
||||
(pull)
|
||||
_arguments \
|
||||
'--allow-insecure-ssl[Allow insecure connections to the docker registry]' \
|
||||
'*:services:__docker-compose_services_from_image' && ret=0
|
||||
;;
|
||||
(rm)
|
||||
_arguments \
|
||||
'(-f --force)'{-f,--force}"[Don't ask to confirm removal]" \
|
||||
'-v[Remove volumes associated with containers]' \
|
||||
'*:stopped services:__docker-compose_stoppedservices' && ret=0
|
||||
;;
|
||||
(run)
|
||||
_arguments \
|
||||
'--allow-insecure-ssl[Allow insecure connections to the docker registry]' \
|
||||
'-d[Detached mode: Run container in the background, print new container name.]' \
|
||||
'--entrypoint[Overwrite the entrypoint of the image.]:entry point: ' \
|
||||
'*-e[KEY=VAL Set an environment variable (can be used multiple times)]:environment variable KEY=VAL: ' \
|
||||
'(-u --user)'{-u,--user=-}'[Run as specified username or uid]:username or uid:_users' \
|
||||
"--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.]' \
|
||||
'(-):services:__docker-compose_services' \
|
||||
'(-):command: _command_names -e' \
|
||||
'*::arguments: _normal' && ret=0
|
||||
;;
|
||||
(scale)
|
||||
_arguments '*:running services:__docker-compose_runningservices' && ret=0
|
||||
;;
|
||||
(start)
|
||||
_arguments '*:stopped services:__docker-compose_stoppedservices' && ret=0
|
||||
;;
|
||||
(stop|restart)
|
||||
_arguments \
|
||||
'(-t --timeout)'{-t,--timeout}"[Specify a shutdown timeout in seconds. (default: 10)]:seconds: " \
|
||||
'*:running services:__docker-compose_runningservices' && ret=0
|
||||
;;
|
||||
(up)
|
||||
_arguments \
|
||||
'--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]" \
|
||||
'(-t --timeout)'{-t,--timeout}"[Specify a shutdown timeout in seconds. (default: 10)]:seconds: " \
|
||||
"--x-smart-recreate[Only recreate containers whose configuration or image needs to be updated. (EXPERIMENTAL)]" \
|
||||
'*:services:__docker-compose_services_all' && ret=0
|
||||
;;
|
||||
(*)
|
||||
_message 'Unknown sub command'
|
||||
esac
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
_docker-compose () {
|
||||
# Support for subservices, which allows for `compdef _docker docker-shell=_docker_containers`.
|
||||
# Based on /usr/share/zsh/functions/Completion/Unix/_git without support for `ret`.
|
||||
if [[ $service != docker-compose ]]; then
|
||||
_call_function - _$service
|
||||
return
|
||||
fi
|
||||
|
||||
local curcontext="$curcontext" state line ret=1
|
||||
typeset -A opt_args
|
||||
|
||||
_arguments -C \
|
||||
'(- :)'{-h,--help}'[Get help]' \
|
||||
'--verbose[Show more output]' \
|
||||
'(- :)'{-v,--version}'[Print version and exit]' \
|
||||
'(-f --file)'{-f,--file}'[Specify an alternate docker-compose file (default: docker-compose.yml)]:file:_files -g "*.yml"' \
|
||||
'(-p --project-name)'{-p,--project-name}'[Specify an alternate project name (default: directory name)]:project name:' \
|
||||
'(-): :->command' \
|
||||
'(-)*:: :->option-or-argument' && ret=0
|
||||
|
||||
local counter=1
|
||||
#local compose_file compose_project
|
||||
while [ $counter -lt ${#words[@]} ]; do
|
||||
case "${words[$counter]}" in
|
||||
-f|--file)
|
||||
(( counter++ ))
|
||||
compose_file="${words[$counter]}"
|
||||
;;
|
||||
-p|--project-name)
|
||||
(( counter++ ))
|
||||
compose_project="${words[$counter]}"
|
||||
;;
|
||||
*)
|
||||
;;
|
||||
esac
|
||||
(( counter++ ))
|
||||
done
|
||||
|
||||
case $state in
|
||||
(command)
|
||||
__docker-compose_commands && ret=0
|
||||
;;
|
||||
(option-or-argument)
|
||||
curcontext=${curcontext%:*:*}:docker-compose-$words[1]:
|
||||
__docker-compose_subcommand && ret=0
|
||||
;;
|
||||
esac
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
_docker-compose "$@"
|
||||
@@ -1 +0,0 @@
|
||||
/_site
|
||||
@@ -1 +0,0 @@
|
||||
www.fig.sh
|
||||
@@ -1,10 +1,24 @@
|
||||
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:hugo
|
||||
MAINTAINER Mary Anthony <mary@docker.com> (@moxiegirl)
|
||||
|
||||
# To get the git info for this repo
|
||||
COPY . /src
|
||||
|
||||
COPY . /docs/content/compose/
|
||||
|
||||
# Sed to process GitHub Markdown
|
||||
# 1-2 Remove comment code from metadata block
|
||||
# 3 Change ](/word to ](/project/ in links
|
||||
# 4 Change ](word.md) to ](/project/word)
|
||||
# 5 Remove .md extension from link text
|
||||
# 6 Change ](../ to ](/project/word)
|
||||
# 7 Change ](../../ to ](/project/ --> not implemented
|
||||
#
|
||||
#
|
||||
RUN find /docs/content/compose -type f -name "*.md" -exec sed -i.old \
|
||||
-e '/^<!.*metadata]>/g' \
|
||||
-e '/^<!.*end-metadata.*>/g' \
|
||||
-e 's/\(\]\)\([(]\)\(\/\)/\1\2\/compose\//g' \
|
||||
-e 's/\(\][(]\)\([A-z].*\)\(\.md\)/\1\/compose\/\2/g' \
|
||||
-e 's/\([(]\)\(.*\)\(\.md\)/\1\2/g' \
|
||||
-e 's/\(\][(]\)\(\.\.\/\)/\1\/compose\//g' {} \;
|
||||
|
||||
@@ -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
|
||||
55
docs/Makefile
Normal file
55
docs/Makefile
Normal file
@@ -0,0 +1,55 @@
|
||||
.PHONY: all binary build cross default docs docs-build docs-shell shell test test-unit test-integration test-integration-cli test-docker-py validate
|
||||
|
||||
# env vars passed through directly to Docker's build scripts
|
||||
# to allow things like `make DOCKER_CLIENTONLY=1 binary` easily
|
||||
# `docs/sources/contributing/devenvironment.md ` and `project/PACKAGERS.md` have some limited documentation of some of these
|
||||
DOCKER_ENVS := \
|
||||
-e BUILDFLAGS \
|
||||
-e DOCKER_CLIENTONLY \
|
||||
-e DOCKER_EXECDRIVER \
|
||||
-e DOCKER_GRAPHDRIVER \
|
||||
-e TESTDIRS \
|
||||
-e TESTFLAGS \
|
||||
-e TIMEOUT
|
||||
# note: we _cannot_ add "-e DOCKER_BUILDTAGS" here because even if it's unset in the shell, that would shadow the "ENV DOCKER_BUILDTAGS" set in our Dockerfile, which is very important for our official builds
|
||||
|
||||
# to allow `make DOCSDIR=docs docs-shell` (to create a bind mount in docs)
|
||||
DOCS_MOUNT := $(if $(DOCSDIR),-v $(CURDIR)/$(DOCSDIR):/$(DOCSDIR))
|
||||
|
||||
# to allow `make DOCSPORT=9000 docs`
|
||||
DOCSPORT := 8000
|
||||
|
||||
# Get the IP ADDRESS
|
||||
DOCKER_IP=$(shell python -c "import urlparse ; print urlparse.urlparse('$(DOCKER_HOST)').hostname or ''")
|
||||
HUGO_BASE_URL=$(shell test -z "$(DOCKER_IP)" && echo localhost || echo "$(DOCKER_IP)")
|
||||
HUGO_BIND_IP=0.0.0.0
|
||||
|
||||
GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null)
|
||||
DOCKER_IMAGE := docker$(if $(GIT_BRANCH),:$(GIT_BRANCH))
|
||||
DOCKER_DOCS_IMAGE := docs-base$(if $(GIT_BRANCH),:$(GIT_BRANCH))
|
||||
|
||||
|
||||
DOCKER_RUN_DOCS := docker run --rm -it $(DOCS_MOUNT) -e AWS_S3_BUCKET -e NOCACHE
|
||||
|
||||
# for some docs workarounds (see below in "docs-build" target)
|
||||
GITCOMMIT := $(shell git rev-parse --short HEAD 2>/dev/null)
|
||||
|
||||
default: docs
|
||||
|
||||
docs: docs-build
|
||||
$(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" hugo server --port=$(DOCSPORT) --baseUrl=$(HUGO_BASE_URL) --bind=$(HUGO_BIND_IP)
|
||||
|
||||
docs-draft: docs-build
|
||||
$(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" hugo server --buildDrafts="true" --port=$(DOCSPORT) --baseUrl=$(HUGO_BASE_URL) --bind=$(HUGO_BIND_IP)
|
||||
|
||||
|
||||
docs-shell: docs-build
|
||||
$(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 "$(DOCKER_DOCS_IMAGE)" bash
|
||||
|
||||
|
||||
docs-build:
|
||||
# ( git remote | grep -v upstream ) || git diff --name-status upstream/release..upstream/docs ./ > ./changed-files
|
||||
# echo "$(GIT_BRANCH)" > GIT_BRANCH
|
||||
# echo "$(AWS_S3_BUCKET)" > AWS_S3_BUCKET
|
||||
# echo "$(GITCOMMIT)" > GITCOMMIT
|
||||
docker build -t "$(DOCKER_DOCS_IMAGE)" .
|
||||
77
docs/README.md
Normal file
77
docs/README.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Contributing to the Docker Compose documentation
|
||||
|
||||
The documentation in this directory is part of the [https://docs.docker.com](https://docs.docker.com) website. Docker uses [the Hugo static generator](http://gohugo.io/overview/introduction/) to convert project Markdown files to a static HTML site.
|
||||
|
||||
You don't need to be a Hugo expert to contribute to the compose documentation. If you are familiar with Markdown, you can modify the content in the `docs` files.
|
||||
|
||||
If you want to add a new file or change the location of the document in the menu, you do need to know a little more.
|
||||
|
||||
## Documentation contributing workflow
|
||||
|
||||
1. Edit a Markdown file in the tree.
|
||||
|
||||
2. Save your changes.
|
||||
|
||||
3. Make sure you in your `docs` subdirectory.
|
||||
|
||||
4. Build the documentation.
|
||||
|
||||
$ make docs
|
||||
---> ffcf3f6c4e97
|
||||
Removing intermediate container a676414185e8
|
||||
Successfully built ffcf3f6c4e97
|
||||
docker run --rm -it -e AWS_S3_BUCKET -e NOCACHE -p 8000:8000 -e DOCKERHOST "docs-base:test-tooling" hugo server --port=8000 --baseUrl=192.168.59.103 --bind=0.0.0.0
|
||||
ERROR: 2015/06/13 MenuEntry's .Url is deprecated and will be removed in Hugo 0.15. Use .URL instead.
|
||||
0 of 4 drafts rendered
|
||||
0 future content
|
||||
12 pages created
|
||||
0 paginator pages created
|
||||
0 tags created
|
||||
0 categories created
|
||||
in 55 ms
|
||||
Serving pages from /docs/public
|
||||
Web Server is available at http://0.0.0.0:8000/
|
||||
Press Ctrl+C to stop
|
||||
|
||||
5. Open the available server in your browser.
|
||||
|
||||
The documentation server has the complete menu but only the Docker Compose
|
||||
documentation resolves. You can't access the other project docs from this
|
||||
localized build.
|
||||
|
||||
## Tips on Hugo metadata and menu positioning
|
||||
|
||||
The top of each Docker Compose documentation file contains TOML metadata. The metadata is commented out to prevent it from appears in GitHub.
|
||||
|
||||
<!--[metadata]>
|
||||
+++
|
||||
title = "Extending services in Compose"
|
||||
description = "How to use Docker Compose's extends keyword to share configuration between files and projects"
|
||||
keywords = ["fig, composition, compose, docker, orchestration, documentation, docs"]
|
||||
[menu.main]
|
||||
parent="smn_workw_compose"
|
||||
weight=2
|
||||
+++
|
||||
<![end-metadata]-->
|
||||
|
||||
The metadata alone has this structure:
|
||||
|
||||
+++
|
||||
title = "Extending services in Compose"
|
||||
description = "How to use Docker Compose's extends keyword to share configuration between files and projects"
|
||||
keywords = ["fig, composition, compose, docker, orchestration, documentation, docs"]
|
||||
[menu.main]
|
||||
parent="smn_workw_compose"
|
||||
weight=2
|
||||
+++
|
||||
|
||||
The `[menu.main]` section refers to navigation defined [in the main Docker menu](https://github.com/docker/docs-base/blob/hugo/config.toml). This metadata says *add a menu item called* Extending services in Compose *to the menu with the* `smn_workdw_compose` *identifier*. If you locate the menu in the configuration, you'll find *Create multi-container applications* is the menu title.
|
||||
|
||||
You can move an article in the tree by specifying a new parent. You can shift the location of the item by changing its weight. Higher numbers are heavier and shift the item to the bottom of menu. Low or no numbers shift it up.
|
||||
|
||||
|
||||
## Other key documentation repositories
|
||||
|
||||
The `docker/docs-base` repository contains [the Hugo theme and menu configuration](https://github.com/docker/docs-base). If you open the `Dockerfile` you'll see the `make docs` relies on this as a base image for building the Compose documentation.
|
||||
|
||||
The `docker/docs.docker.com` repository contains [build system for building the Docker documentation site](https://github.com/docker/docs.docker.com). Fork this repository to build the entire documentation site.
|
||||
@@ -1,3 +0,0 @@
|
||||
markdown: redcarpet
|
||||
encoding: utf-8
|
||||
|
||||
@@ -1,71 +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/docker/fig">Fig on GitHub</a></li>
|
||||
<li><a href="http://webchat.freenode.net/?channels=%23docker-fig&uio=d4">#docker-fig on Freenode</a></li>
|
||||
</ul>
|
||||
|
||||
<p>Fig is a project from <a href="https://www.docker.com">Docker</a>.</p>
|
||||
|
||||
<div class="badges">
|
||||
<iframe src="http://ghbtns.com/github-btn.html?user=docker&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>
|
||||
164
docs/cli.md
164
docs/cli.md
@@ -1,124 +1,202 @@
|
||||
---
|
||||
layout: default
|
||||
title: Fig CLI reference
|
||||
---
|
||||
<!--[metadata]>
|
||||
+++
|
||||
title = "Compose CLI reference"
|
||||
description = "Compose CLI reference"
|
||||
keywords = ["fig, composition, compose, docker, orchestration, cli, reference"]
|
||||
[menu.main]
|
||||
identifier = "smn_install_compose"
|
||||
parent = "smn_compose_ref"
|
||||
+++
|
||||
<![end-metadata]-->
|
||||
|
||||
CLI reference
|
||||
=============
|
||||
|
||||
Most commands are run against one or more services. If the service is omitted, it will apply to all services.
|
||||
# Compose CLI reference
|
||||
|
||||
Run `fig [COMMAND] --help` for full usage.
|
||||
Most Docker Compose commands are run against one or more services. If
|
||||
the service is not specified, the command will apply to all services.
|
||||
|
||||
For full usage information, run `docker-compose [COMMAND] --help`.
|
||||
|
||||
## Commands
|
||||
|
||||
### build
|
||||
|
||||
Build or rebuild services.
|
||||
Builds or rebuilds 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.
|
||||
Services are built once and then tagged as `project_service`, e.g.,
|
||||
`composetest_db`. If you change a service's Dockerfile or the contents of its
|
||||
build directory, run `docker-compose build` to rebuild it.
|
||||
|
||||
### help
|
||||
|
||||
Get help on a command.
|
||||
Displays help and usage instructions for a command.
|
||||
|
||||
### kill
|
||||
|
||||
Force stop service containers.
|
||||
Forces running containers to stop by sending a `SIGKILL` signal. Optionally the
|
||||
signal can be passed, for example:
|
||||
|
||||
$ docker-compose kill -s SIGINT
|
||||
|
||||
### logs
|
||||
|
||||
View output from services.
|
||||
Displays log output from services.
|
||||
|
||||
### port
|
||||
|
||||
Print the public port for a port binding
|
||||
Prints the public port for a port binding
|
||||
|
||||
### ps
|
||||
|
||||
List containers.
|
||||
Lists containers.
|
||||
|
||||
### pull
|
||||
|
||||
Pulls service images.
|
||||
|
||||
### restart
|
||||
|
||||
Restarts services.
|
||||
|
||||
### rm
|
||||
|
||||
Remove stopped service containers.
|
||||
Removes stopped service containers.
|
||||
|
||||
|
||||
### run
|
||||
|
||||
Run a one-off command on a service.
|
||||
Runs a one-off command on a service.
|
||||
|
||||
For example:
|
||||
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.
|
||||
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 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.
|
||||
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:
|
||||
|
||||
Links are also created between one-off commands and the other containers for that service so you can do stuff like this:
|
||||
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`
|
||||
|
||||
$ fig run db psql -h db -U docker
|
||||
2. by default no ports will be created in case they collide with already opened
|
||||
ports.
|
||||
|
||||
If you do not want linked containers to be started when running the one-off command, specify the `--no-deps` flag:
|
||||
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
|
||||
|
||||
$ fig run --no-deps web python manage.py shell
|
||||
|
||||
### scale
|
||||
|
||||
Set number of containers to run for a service.
|
||||
Sets the number of containers to run for a service.
|
||||
|
||||
Numbers are specified in the form `service=num` as arguments.
|
||||
For example:
|
||||
Numbers are specified as arguments in the form `service=num`. For example:
|
||||
|
||||
$ fig scale web=2 worker=3
|
||||
$ docker-compose scale web=2 worker=3
|
||||
|
||||
### start
|
||||
|
||||
Start existing containers for a service.
|
||||
Starts existing containers for a service.
|
||||
|
||||
### stop
|
||||
|
||||
Stop running containers without removing them. They can be started again with `fig start`.
|
||||
Stops running containers without removing them. They can be started again with
|
||||
`docker-compose start`.
|
||||
|
||||
### up
|
||||
|
||||
Build, (re)create, start and attach to containers for a service.
|
||||
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
|
||||
|
||||
### -v, --version
|
||||
|
||||
Prints version and exits
|
||||
|
||||
### -f, --file FILE
|
||||
|
||||
Specify what file to read configuration from. If not provided, Compose will look
|
||||
for `docker-compose.yml` in the current working directory, and then each parent
|
||||
directory successively, until found.
|
||||
|
||||
|
||||
### -p, --project-name NAME
|
||||
|
||||
Specifies an alternate project name (default: current directory name)
|
||||
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Several environment variables can be used to configure Fig's behaviour.
|
||||
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.
|
||||
Variables starting with `DOCKER_` are the same as those used to configure the
|
||||
Docker command-line client. If you're using boot2docker, `eval "$(boot2docker shellinit)"`
|
||||
will set them to their correct values.
|
||||
|
||||
### FIG\_PROJECT\_NAME
|
||||
### COMPOSE\_PROJECT\_NAME
|
||||
|
||||
Set the project name, which is prepended to the name of every container started by Fig. Defaults to the `basename` of the current working directory.
|
||||
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.
|
||||
|
||||
### FIG\_FILE
|
||||
### COMPOSE\_FILE
|
||||
|
||||
Set the path to the `fig.yml` to use. Defaults to `fig.yml` in the current working directory.
|
||||
Specify what file to read configuration from. If not provided, Compose will look
|
||||
for `docker-compose.yml` in the current working directory, and then each parent
|
||||
directory successively, until found.
|
||||
|
||||
### DOCKER\_HOST
|
||||
|
||||
Set the URL to the docker daemon. Defaults to `unix:///var/run/docker.sock`, as with the docker client.
|
||||
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.
|
||||
When set to anything other than an empty string, enables TLS communication with
|
||||
the daemon.
|
||||
|
||||
### DOCKER\_CERT\_PATH
|
||||
|
||||
Configure the path to the `ca.pem`, `cert.pem` and `key.pem` files used for TLS verification. Defaults to `~/.docker`.
|
||||
Configures the path to the `ca.pem`, `cert.pem`, and `key.pem` files used for TLS verification. Defaults to `~/.docker`.
|
||||
|
||||
## Compose documentation
|
||||
|
||||
- [User guide](/)
|
||||
- [Installing Compose](install.md)
|
||||
- [Get started with Django](django.md)
|
||||
- [Get started with Rails](rails.md)
|
||||
- [Get started with Wordpress](wordpress.md)
|
||||
- [Yaml file reference](yml.md)
|
||||
- [Compose environment variables](env.md)
|
||||
- [Compose command line completion](completion.md)
|
||||
|
||||
69
docs/completion.md
Normal file
69
docs/completion.md
Normal file
@@ -0,0 +1,69 @@
|
||||
<!--[metadata]>
|
||||
+++
|
||||
title = "Command Completion"
|
||||
description = "Compose CLI reference"
|
||||
keywords = ["fig, composition, compose, docker, orchestration, cli, reference"]
|
||||
[menu.main]
|
||||
parent="smn_workw_compose"
|
||||
weight=3
|
||||
+++
|
||||
<![end-metadata]-->
|
||||
|
||||
# Command Completion
|
||||
|
||||
Compose comes with [command completion](http://en.wikipedia.org/wiki/Command-line_completion)
|
||||
for the bash and zsh shell.
|
||||
|
||||
## Installing Command Completion
|
||||
|
||||
### Bash
|
||||
|
||||
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/$(docker-compose --version | awk '{print $2}')/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose
|
||||
|
||||
Completion will be available upon next login.
|
||||
|
||||
### Zsh
|
||||
|
||||
Place the completion script in your `/path/to/zsh/completion`, using e.g. `~/.zsh/completion/`
|
||||
|
||||
mkdir -p ~/.zsh/completion
|
||||
curl -L https://raw.githubusercontent.com/docker/compose/$(docker-compose --version | awk '{print $2}')/contrib/completion/zsh/_docker-compose > ~/.zsh/completion/_docker-compose
|
||||
|
||||
Include the directory in your `$fpath`, e.g. by adding in `~/.zshrc`
|
||||
|
||||
fpath=(~/.zsh/completion $fpath)
|
||||
|
||||
Make sure `compinit` is loaded or do it by adding in `~/.zshrc`
|
||||
|
||||
autoload -Uz compinit && compinit -i
|
||||
|
||||
Then reload your shell
|
||||
|
||||
exec $SHELL -l
|
||||
|
||||
## 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
|
||||
|
||||
- [User guide](/)
|
||||
- [Installing Compose](install.md)
|
||||
- [Get started with Django](django.md)
|
||||
- [Get started with Rails](rails.md)
|
||||
- [Get started with Wordpress](wordpress.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,14 +1,29 @@
|
||||
---
|
||||
layout: default
|
||||
title: Getting started with Fig and Django
|
||||
---
|
||||
<!--[metadata]>
|
||||
+++
|
||||
title = "Quickstart Guide: Compose and Django"
|
||||
description = "Getting started with Docker Compose and Django"
|
||||
keywords = ["documentation, docs, docker, compose, orchestration, containers"]
|
||||
[menu.main]
|
||||
parent="smn_workw_compose"
|
||||
weight=4
|
||||
+++
|
||||
<![end-metadata]-->
|
||||
|
||||
Getting started with Fig 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).
|
||||
## Quickstart Guide: Compose and Django
|
||||
|
||||
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:
|
||||
|
||||
This Quick-start Guide will demonstrate how to use Compose to set up and run a
|
||||
simple Django/PostgreSQL app. Before starting, you'll need to have
|
||||
[Compose installed](install.md).
|
||||
|
||||
### Define the project
|
||||
|
||||
Start by setting up the three files you'll need to build the app. First, since
|
||||
your app is going to run inside a Docker container containing all of its
|
||||
dependencies, you'll need to define exactly what needs to be included in the
|
||||
container. This is done using a file called `Dockerfile`. To begin with, the
|
||||
Dockerfile consists of:
|
||||
|
||||
FROM python:2.7
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
@@ -18,14 +33,21 @@ Let's set up the three files that'll get us started. First, our app is going to
|
||||
RUN pip install -r requirements.txt
|
||||
ADD . /code/
|
||||
|
||||
That'll install our application inside an image with Python installed alongside all of our Python dependencies. For more information on how to write Dockerfiles, see the [Docker user guide](https://docs.docker.com/userguide/dockerimages/#building-an-image-from-a-dockerfile) and the [Dockerfile reference](http://docs.docker.com/reference/builder/).
|
||||
This Dockerfile will define an image that is used to build a container that
|
||||
includes your application and has Python installed alongside all of your Python
|
||||
dependencies. For more information on how to write Dockerfiles, see the
|
||||
[Docker user guide](https://docs.docker.com/userguide/dockerimages/#building-an-image-from-a-dockerfile) and the [Dockerfile reference](http://docs.docker.com/reference/builder/).
|
||||
|
||||
Second, we define our Python dependencies in a file called `requirements.txt`:
|
||||
Second, you'll define your 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.
|
||||
Finally, this is all tied together with a file called `docker-compose.yml`. It
|
||||
describes the services that comprise your app (here, a web server and database),
|
||||
which Docker images they use, how they link together, what volumes will be
|
||||
mounted inside the containers, and what ports they expose.
|
||||
|
||||
db:
|
||||
image: postgres
|
||||
@@ -39,20 +61,28 @@ 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
|
||||
this file works.
|
||||
|
||||
We can now start a Django project using `fig run`:
|
||||
### Build the project
|
||||
|
||||
$ fig run web django-admin.py startproject figexample .
|
||||
You can now start a Django project with `docker-compose run`:
|
||||
|
||||
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.
|
||||
$ docker-compose run web django-admin.py startproject composeexample .
|
||||
|
||||
First, Compose will build an image for the `web` service using the `Dockerfile`.
|
||||
It will then run `django-admin.py startproject composeexample .` inside a
|
||||
container built using that image.
|
||||
|
||||
This will generate a Django app inside the current directory:
|
||||
|
||||
$ ls
|
||||
Dockerfile 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:
|
||||
### Connect the database
|
||||
|
||||
Now you need to set up the database connection. Replace the `DATABASES = ...`
|
||||
definition in `composeexample/settings.py` to read:
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
@@ -64,9 +94,11 @@ First thing we need to do is set up the database connection. Replace the `DATABA
|
||||
}
|
||||
}
|
||||
|
||||
These settings are determined by the [postgres](https://registry.hub.docker.com/_/postgres/) Docker image we are using.
|
||||
These settings are determined by the
|
||||
[postgres](https://registry.hub.docker.com/_/postgres/) Docker image specified
|
||||
in the Dockerfile.
|
||||
|
||||
Then, run `fig up`:
|
||||
Then, run `docker-compose up`:
|
||||
|
||||
Recreating myapp_db_1...
|
||||
Recreating myapp_web_1...
|
||||
@@ -79,13 +111,26 @@ 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 port 8000 on your docker daemon (if you're using boot2docker, `boot2docker ip` will tell you its address).
|
||||
Your Django app should nw be running at port 8000 on your Docker daemon (if
|
||||
you're using Boot2docker, `boot2docker ip` will tell you its address).
|
||||
|
||||
You can also run management commands with Docker. To set up your database, for example, run `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
|
||||
|
||||
## More Compose documentation
|
||||
|
||||
- [User guide](/)
|
||||
- [Installing Compose](install.md)
|
||||
- [Get started with Django](django.md)
|
||||
- [Get started with Rails](rails.md)
|
||||
- [Get started with Wordpress](wordpress.md)
|
||||
- [Command line reference](cli.md)
|
||||
- [Yaml file reference](yml.md)
|
||||
- [Compose environment variables](env.md)
|
||||
- [Compose command line completion](completion.md)
|
||||
|
||||
33
docs/env.md
33
docs/env.md
@@ -1,16 +1,22 @@
|
||||
---
|
||||
layout: default
|
||||
title: Fig environment variables reference
|
||||
---
|
||||
<!--[metadata]>
|
||||
+++
|
||||
title = "Compose environment variables reference"
|
||||
description = "Compose CLI reference"
|
||||
keywords = ["fig, composition, compose, docker, orchestration, cli, reference"]
|
||||
[menu.main]
|
||||
parent="smn_compose_ref"
|
||||
weight=3
|
||||
+++
|
||||
<![end-metadata]-->
|
||||
|
||||
Environment variables reference
|
||||
# Compose environment variables reference
|
||||
===============================
|
||||
|
||||
**Note:** Environment variables are no longer the recommended method for connecting to linked services. Instead, you should use the link name (by default, the name of the linked service) as the hostname to connect to. See the [fig.yml documentation](yml.html#links) for details.
|
||||
**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.
|
||||
|
||||
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.
|
||||
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 `fig run SERVICE env`.
|
||||
To see what environment variables are available to a service, run `docker-compose run SERVICE env`.
|
||||
|
||||
<b><i>name</i>\_PORT</b><br>
|
||||
Full URL, e.g. `DB_PORT=tcp://172.17.0.5:5432`
|
||||
@@ -31,3 +37,14 @@ Protocol (tcp or udp), e.g. `DB_PORT_5432_TCP_PROTO=tcp`
|
||||
Fully qualified container name, e.g. `DB_1_NAME=/myapp_web_1/myapp_db_1`
|
||||
|
||||
[Docker links]: http://docs.docker.com/userguide/dockerlinks/
|
||||
|
||||
## Compose documentation
|
||||
|
||||
- [User guide](/)
|
||||
- [Installing Compose](install.md)
|
||||
- [Get started with Django](django.md)
|
||||
- [Get started with Rails](rails.md)
|
||||
- [Get started with Wordpress](wordpress.md)
|
||||
- [Command line reference](cli.md)
|
||||
- [Yaml file reference](yml.md)
|
||||
- [Compose command line completion](completion.md)
|
||||
|
||||
381
docs/extends.md
Normal file
381
docs/extends.md
Normal file
@@ -0,0 +1,381 @@
|
||||
<!--[metadata]>
|
||||
+++
|
||||
title = "Extending services in Compose"
|
||||
description = "How to use Docker Compose's extends keyword to share configuration between files and projects"
|
||||
keywords = ["fig, composition, compose, docker, orchestration, documentation, docs"]
|
||||
[menu.main]
|
||||
parent="smn_workw_compose"
|
||||
weight=2
|
||||
+++
|
||||
<![end-metadata]-->
|
||||
|
||||
|
||||
## Extending services in Compose
|
||||
|
||||
Docker Compose's `extends` keyword enables sharing of common configurations
|
||||
among different files, or even different projects entirely. Extending services
|
||||
is useful if you have several applications that reuse commonly-defined services.
|
||||
Using `extends` you can define a service in one place and refer to it from
|
||||
anywhere.
|
||||
|
||||
Alternatively, you can deploy the same application to multiple environments with
|
||||
a slightly different set of services in each case (or with changes to the
|
||||
configuration of some services). Moreover, you can do so without copy-pasting
|
||||
the configuration around.
|
||||
|
||||
### Understand the extends configuration
|
||||
|
||||
When defining any service in `docker-compose.yml`, you can declare that you are
|
||||
extending another service like this:
|
||||
|
||||
```yaml
|
||||
web:
|
||||
extends:
|
||||
file: common-services.yml
|
||||
service: webapp
|
||||
```
|
||||
|
||||
This instructs Compose to re-use the configuration for the `webapp` service
|
||||
defined in the `common-services.yml` file. Suppose that `common-services.yml`
|
||||
looks like this:
|
||||
|
||||
```yaml
|
||||
webapp:
|
||||
build: .
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- "/data"
|
||||
```
|
||||
|
||||
In this case, you'll get exactly the same result as if you wrote
|
||||
`docker-compose.yml` with that `build`, `ports` and `volumes` configuration
|
||||
defined directly under `web`.
|
||||
|
||||
You can go further and define (or re-define) configuration locally in
|
||||
`docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
web:
|
||||
extends:
|
||||
file: common-services.yml
|
||||
service: webapp
|
||||
environment:
|
||||
- DEBUG=1
|
||||
cpu_shares: 5
|
||||
```
|
||||
|
||||
You can also write other services and link your `web` service to them:
|
||||
|
||||
```yaml
|
||||
web:
|
||||
extends:
|
||||
file: common-services.yml
|
||||
service: webapp
|
||||
environment:
|
||||
- DEBUG=1
|
||||
cpu_shares: 5
|
||||
links:
|
||||
- db
|
||||
db:
|
||||
image: postgres
|
||||
```
|
||||
|
||||
For full details on how to use `extends`, refer to the [reference](#reference).
|
||||
|
||||
### Example use case
|
||||
|
||||
In this example, you’ll repurpose the example app from the [quick start
|
||||
guide](index.md). (If you're not familiar with Compose, it's recommended that
|
||||
you go through the quick start first.) This example assumes you want to use
|
||||
Compose both to develop an application locally and then deploy it to a
|
||||
production environment.
|
||||
|
||||
The local and production environments are similar, but there are some
|
||||
differences. In development, you mount the application code as a volume so that
|
||||
it can pick up changes; in production, the code should be immutable from the
|
||||
outside. This ensures it’s not accidentally changed. The development environment
|
||||
uses a local Redis container, but in production another team manages the Redis
|
||||
service, which is listening at `redis-production.example.com`.
|
||||
|
||||
To configure with `extends` for this sample, you must:
|
||||
|
||||
1. Define the web application as a Docker image in `Dockerfile` and a Compose
|
||||
service in `common.yml`.
|
||||
|
||||
2. Define the development environment in the standard Compose file,
|
||||
`docker-compose.yml`.
|
||||
|
||||
- Use `extends` to pull in the web service.
|
||||
- Configure a volume to enable code reloading.
|
||||
- Create an additional Redis service for the application to use locally.
|
||||
|
||||
3. Define the production environment in a third Compose file, `production.yml`.
|
||||
|
||||
- Use `extends` to pull in the web service.
|
||||
- Configure the web service to talk to the external, production Redis service.
|
||||
|
||||
#### Define the web app
|
||||
|
||||
Defining the web application requires the following:
|
||||
|
||||
1. Create an `app.py` file.
|
||||
|
||||
This file contains a simple Python application that uses Flask to serve HTTP
|
||||
and increments a counter in Redis:
|
||||
|
||||
from flask import Flask
|
||||
from redis import Redis
|
||||
import os
|
||||
|
||||
app = Flask(__name__)
|
||||
redis = Redis(host=os.environ['REDIS_HOST'], port=6379)
|
||||
|
||||
@app.route('/')
|
||||
def hello():
|
||||
redis.incr('hits')
|
||||
return 'Hello World! I have been seen %s times.\n' % redis.get('hits')
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", debug=True)
|
||||
|
||||
This code uses a `REDIS_HOST` environment variable to determine where to
|
||||
find Redis.
|
||||
|
||||
2. Define the Python dependencies in a `requirements.txt` file:
|
||||
|
||||
flask
|
||||
redis
|
||||
|
||||
3. Create a `Dockerfile` to build an image containing the app:
|
||||
|
||||
FROM python:2.7
|
||||
ADD . /code
|
||||
WORKDIR /code
|
||||
RUN pip install -r requirements.txt
|
||||
CMD python app.py
|
||||
|
||||
4. Create a Compose configuration file called `common.yml`:
|
||||
|
||||
This configuration defines how to run the app.
|
||||
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- "5000:5000"
|
||||
|
||||
Typically, you would have dropped this configuration into
|
||||
`docker-compose.yml` file, but in order to pull it into multiple files with
|
||||
`extends`, it needs to be in a separate file.
|
||||
|
||||
#### Define the development environment
|
||||
|
||||
1. Create a `docker-compose.yml` file.
|
||||
|
||||
The `extends` option pulls in the `web` service from the `common.yml` file
|
||||
you created in the previous section.
|
||||
|
||||
web:
|
||||
extends:
|
||||
file: common.yml
|
||||
service: web
|
||||
volumes:
|
||||
- .:/code
|
||||
links:
|
||||
- redis
|
||||
environment:
|
||||
- REDIS_HOST=redis
|
||||
redis:
|
||||
image: redis
|
||||
|
||||
The new addition defines a `web` service that:
|
||||
|
||||
- Fetches the base configuration for `web` out of `common.yml`.
|
||||
- Adds `volumes` and `links` configuration to the base (`common.yml`)
|
||||
configuration.
|
||||
- Sets the `REDIS_HOST` environment variable to point to the linked redis
|
||||
container. This environment uses a stock `redis` image from the Docker Hub.
|
||||
|
||||
2. Run `docker-compose up`.
|
||||
|
||||
Compose creates, links, and starts a web and redis container linked together.
|
||||
It mounts your application code inside the web container.
|
||||
|
||||
3. Verify that the code is mounted by changing the message in
|
||||
`app.py`—say, from `Hello world!` to `Hello from Compose!`.
|
||||
|
||||
Don't forget to refresh your browser to see the change!
|
||||
|
||||
#### Define the production environment
|
||||
|
||||
You are almost done. Now, define your production environment:
|
||||
|
||||
1. Create a `production.yml` file.
|
||||
|
||||
As with `docker-compose.yml`, the `extends` option pulls in the `web` service
|
||||
from `common.yml`.
|
||||
|
||||
web:
|
||||
extends:
|
||||
file: common.yml
|
||||
service: web
|
||||
environment:
|
||||
- REDIS_HOST=redis-production.example.com
|
||||
|
||||
2. Run `docker-compose -f production.yml up`.
|
||||
|
||||
Compose creates *just* a web container and configures the Redis connection via
|
||||
the `REDIS_HOST` environment variable. This variable points to the production
|
||||
Redis instance.
|
||||
|
||||
> **Note**: If you try to load up the webapp in your browser you'll get an
|
||||
> error—`redis-production.example.com` isn't actually a Redis server.
|
||||
|
||||
You've now done a basic `extends` configuration. As your application develops,
|
||||
you can make any necessary changes to the web service in `common.yml`. Compose
|
||||
picks up both the development and production environments when you next run
|
||||
`docker-compose`. You don't have to do any copy-and-paste, and you don't have to
|
||||
manually keep both environments in sync.
|
||||
|
||||
|
||||
### Reference
|
||||
|
||||
You can use `extends` on any service together with other configuration keys. It
|
||||
always expects a dictionary that should always contain two keys: `file` and
|
||||
`service`.
|
||||
|
||||
The `file` key specifies which file to look in. It can be an absolute path or a
|
||||
relative one—if relative, it's treated as relative to the current file.
|
||||
|
||||
The `service` key specifies the name of the service to extend, for example `web`
|
||||
or `database`.
|
||||
|
||||
You can extend a service that itself extends another. You can extend
|
||||
indefinitely. Compose does not support circular references and `docker-compose`
|
||||
returns an error if it encounters them.
|
||||
|
||||
#### Adding and overriding configuration
|
||||
|
||||
Compose copies configurations from the original service over to the local one,
|
||||
**except** for `links` and `volumes_from`. These exceptions exist to avoid
|
||||
implicit dependencies—you always define `links` and `volumes_from`
|
||||
locally. This ensures dependencies between services are clearly visible when
|
||||
reading the current file. Defining these locally also ensures changes to the
|
||||
referenced file don't result in breakage.
|
||||
|
||||
If a configuration option is defined in both the original service and the local
|
||||
service, the local value either *override*s or *extend*s the definition of the
|
||||
original service. This works differently for other configuration options.
|
||||
|
||||
For single-value options like `image`, `command` or `mem_limit`, the new value
|
||||
replaces the old value. **This is the default behaviour - all exceptions are
|
||||
listed below.**
|
||||
|
||||
```yaml
|
||||
# original service
|
||||
command: python app.py
|
||||
|
||||
# local service
|
||||
command: python otherapp.py
|
||||
|
||||
# result
|
||||
command: python otherapp.py
|
||||
```
|
||||
|
||||
In the case of `build` and `image`, using one in the local service causes
|
||||
Compose to discard the other, if it was defined in the original service.
|
||||
|
||||
```yaml
|
||||
# original service
|
||||
build: .
|
||||
|
||||
# local service
|
||||
image: redis
|
||||
|
||||
# result
|
||||
image: redis
|
||||
```
|
||||
|
||||
```yaml
|
||||
# original service
|
||||
image: redis
|
||||
|
||||
# local service
|
||||
build: .
|
||||
|
||||
# result
|
||||
build: .
|
||||
```
|
||||
|
||||
For the **multi-value options** `ports`, `expose`, `external_links`, `dns` and
|
||||
`dns_search`, Compose concatenates both sets of values:
|
||||
|
||||
```yaml
|
||||
# original service
|
||||
expose:
|
||||
- "3000"
|
||||
|
||||
# local service
|
||||
expose:
|
||||
- "4000"
|
||||
- "5000"
|
||||
|
||||
# result
|
||||
expose:
|
||||
- "3000"
|
||||
- "4000"
|
||||
- "5000"
|
||||
```
|
||||
|
||||
In the case of `environment` and `labels`, Compose "merges" entries together
|
||||
with locally-defined values taking precedence:
|
||||
|
||||
```yaml
|
||||
# original service
|
||||
environment:
|
||||
- FOO=original
|
||||
- BAR=original
|
||||
|
||||
# local service
|
||||
environment:
|
||||
- BAR=local
|
||||
- BAZ=local
|
||||
|
||||
# result
|
||||
environment:
|
||||
- FOO=original
|
||||
- BAR=local
|
||||
- BAZ=local
|
||||
```
|
||||
|
||||
Finally, for `volumes` and `devices`, Compose "merges" entries together with
|
||||
locally-defined bindings taking precedence:
|
||||
|
||||
```yaml
|
||||
# original service
|
||||
volumes:
|
||||
- /original-dir/foo:/foo
|
||||
- /original-dir/bar:/bar
|
||||
|
||||
# local service
|
||||
volumes:
|
||||
- /local-dir/bar:/bar
|
||||
- /local-dir/baz/:baz
|
||||
|
||||
# result
|
||||
volumes:
|
||||
- /original-dir/foo:/foo
|
||||
- /local-dir/bar:/bar
|
||||
- /local-dir/baz/:baz
|
||||
```
|
||||
|
||||
## Compose documentation
|
||||
|
||||
- [User guide](/)
|
||||
- [Installing Compose](install.md)
|
||||
- [Get started with Django](django.md)
|
||||
- [Get started with Rails](rails.md)
|
||||
- [Get started with Wordpress](wordpress.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 |
214
docs/index.md
214
docs/index.md
@@ -1,58 +1,83 @@
|
||||
---
|
||||
layout: default
|
||||
title: Fig | Fast, isolated development environments using Docker
|
||||
---
|
||||
<!--[metadata]>
|
||||
+++
|
||||
title = "Overview of Docker Compose"
|
||||
description = "Introduction and Overview of Compose"
|
||||
keywords = ["documentation, docs, docker, compose, orchestration, containers"]
|
||||
[menu.main]
|
||||
parent="smn_workw_compose"
|
||||
+++
|
||||
<![end-metadata]-->
|
||||
|
||||
<strong class="strapline">Fast, isolated development environments using Docker.</strong>
|
||||
|
||||
Define your app's environment with Docker so it can be reproduced anywhere:
|
||||
# Overview of Docker Compose
|
||||
|
||||
FROM python:2.7
|
||||
ADD . /code
|
||||
WORKDIR /code
|
||||
RUN pip install -r requirements.txt
|
||||
Compose is a tool for defining and running multi-container 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.
|
||||
|
||||
1. Define your app's environment with a `Dockerfile` so it can be
|
||||
reproduced anywhere.
|
||||
2. Define the services that make up your app in `docker-compose.yml` so
|
||||
they can be run together in an isolated environment:
|
||||
3. Lastly, run `docker-compose up` and Compose will start and run your entire app.
|
||||
|
||||
A `docker-compose.yml` looks like this:
|
||||
|
||||
```yaml
|
||||
web:
|
||||
build: .
|
||||
command: python app.py
|
||||
links:
|
||||
- db
|
||||
ports:
|
||||
- "8000:8000"
|
||||
db:
|
||||
image: postgres
|
||||
- "5000:5000"
|
||||
volumes:
|
||||
- .:/code
|
||||
links:
|
||||
- redis
|
||||
redis:
|
||||
image: redis
|
||||
```
|
||||
|
||||
(No more installing Postgres on your laptop!)
|
||||
Compose has commands for managing the whole lifecycle of your application:
|
||||
|
||||
Then type `fig up`, and Fig will start and run your entire app:
|
||||
* Start, stop and rebuild services
|
||||
* View the status of running services
|
||||
* Stream the log output of running services
|
||||
* Run a one-off command on a service
|
||||
|
||||

|
||||
## Compose documentation
|
||||
|
||||
There are commands to:
|
||||
- [Installing Compose](install.md)
|
||||
- [Get started with Django](django.md)
|
||||
- [Get started with Rails](rails.md)
|
||||
- [Get started with Wordpress](wordpress.md)
|
||||
- [Command line reference](cli.md)
|
||||
- [Yaml file reference](yml.md)
|
||||
- [Compose environment variables](env.md)
|
||||
- [Compose command line completion](completion.md)
|
||||
|
||||
- start, stop and rebuild services
|
||||
- view the status of running services
|
||||
- tail running services' log output
|
||||
- run a one-off command on a service
|
||||
## 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.
|
||||
|
||||
Quick start
|
||||
-----------
|
||||
### Installation and set-up
|
||||
|
||||
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.
|
||||
First, [install Docker and Compose](install.md).
|
||||
|
||||
First, [install Docker and Fig](install.html).
|
||||
Next, you'll want to make a directory for the project:
|
||||
|
||||
You'll want to make a directory for the project:
|
||||
$ mkdir composetest
|
||||
$ cd composetest
|
||||
|
||||
$ mkdir figtest
|
||||
$ cd figtest
|
||||
|
||||
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
|
||||
@@ -70,25 +95,41 @@ 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
|
||||
|
||||
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
|
||||
CMD python app.py
|
||||
|
||||
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:
|
||||
|
||||
We then define a set of services using `fig.yml`:
|
||||
* Build an image starting with the Python 2.7 image.
|
||||
* Add the current directory `.` into the path `/code` in the image.
|
||||
* Set the working directory to `/code`.
|
||||
* Install your Python dependencies.
|
||||
* Set the default command for the container to `python app.py`
|
||||
|
||||
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/).
|
||||
|
||||
You can test that this builds by running `docker build -t web .`.
|
||||
|
||||
### Define services
|
||||
|
||||
Next, define a set of services using `docker-compose.yml`:
|
||||
|
||||
web:
|
||||
build: .
|
||||
command: python app.py
|
||||
ports:
|
||||
- "5000:5000"
|
||||
volumes:
|
||||
@@ -100,41 +141,94 @@ We then define a set of services using `fig.yml`:
|
||||
|
||||
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 [redis](https://registry.hub.docker.com/_/redis/).
|
||||
#### web
|
||||
|
||||
Now if we run `fig up`, it'll pull a Redis image, build an image for our own code, and start everything up:
|
||||
* Builds from the `Dockerfile` in the current directory.
|
||||
* Forwards the exposed port 5000 on the container to port 5000 on the host machine.
|
||||
* Connects the web container to the Redis service via a link.
|
||||
* Mounts the current directory on the host to `/code` inside the container allowing you to modify the code without having to rebuild the image.
|
||||
|
||||
$ fig up
|
||||
#### redis
|
||||
|
||||
* Uses the public [Redis](https://registry.hub.docker.com/_/redis/) image which gets pulled from the Docker Hub registry.
|
||||
|
||||
### Build and run your app with Compose
|
||||
|
||||
Now, when you run `docker-compose up`, Compose will pull a Redis image, build an image for your code, and start everything up:
|
||||
|
||||
$ docker-compose up
|
||||
Pulling image redis...
|
||||
Building web...
|
||||
Starting 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/
|
||||
|
||||
The web app should now be listening on port 5000 on your docker daemon (if you're using boot2docker, `boot2docker ip` will tell you its address).
|
||||
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). In a browser,
|
||||
open `http://ip-from-boot2docker:5000` and you should get a message in your browser saying:
|
||||
|
||||
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:
|
||||
`Hello World! I have been seen 1 times.`
|
||||
|
||||
$ fig up -d
|
||||
Starting figtest_redis_1...
|
||||
Starting figtest_web_1...
|
||||
$ fig ps
|
||||
Name Command State Ports
|
||||
Refreshing the page will increment the number.
|
||||
|
||||
If you want to run your services in the background, you can pass the `-d` flag
|
||||
(for daemon mode) to `docker-compose up` and use `docker-compose ps` to see what
|
||||
is currently running:
|
||||
|
||||
$ docker-compose up -d
|
||||
Starting composetest_redis_1...
|
||||
Starting composetest_web_1...
|
||||
$ docker-compose ps
|
||||
Name Command State Ports
|
||||
-------------------------------------------------------------------
|
||||
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.
|
||||
|
||||
See `fig --help` other commands that are available.
|
||||
If you started Compose with `docker-compose up -d`, you'll probably want to stop
|
||||
your services once you've finished with them:
|
||||
|
||||
If you started Fig with `fig up -d`, you'll probably want to stop your services once you've finished with them:
|
||||
$ docker-compose stop
|
||||
|
||||
$ fig stop
|
||||
At this point, you have seen the basics of how Compose works.
|
||||
|
||||
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/docker/fig).
|
||||
- Next, try the quick start guide for [Django](django.md),
|
||||
[Rails](rails.md), or [Wordpress](wordpress.md).
|
||||
- See the reference guides for complete details on the [commands](cli.md), the
|
||||
[configuration file](yml.md) and [environment variables](env.md).
|
||||
|
||||
## Release Notes
|
||||
|
||||
### Version 1.2.0 (April 7, 2015)
|
||||
|
||||
For complete information on this release, see the [1.2.0 Milestone project page](https://github.com/docker/compose/wiki/1.2.0-Milestone-Project-Page).
|
||||
In addition to bug fixes and refinements, this release adds the following:
|
||||
|
||||
* The `extends` keyword, which adds the ability to extend services by sharing common configurations. For details, see
|
||||
[PR #1088](https://github.com/docker/compose/pull/1088).
|
||||
|
||||
* Better integration with Swarm. Swarm will now schedule inter-dependent
|
||||
containers on the same host. For details, see
|
||||
[PR #972](https://github.com/docker/compose/pull/972).
|
||||
|
||||
## Getting help
|
||||
|
||||
Docker Compose is still in its infancy and under active development. If you need
|
||||
help, would like to contribute, or simply want to talk about the project with
|
||||
like-minded individuals, we have a number of open channels for communication.
|
||||
|
||||
* To report bugs or file feature requests: please use the [issue tracker on Github](https://github.com/docker/compose/issues).
|
||||
|
||||
* To talk about the project with people in real time: please join the `#docker-compose` channel on IRC.
|
||||
|
||||
* To contribute code or documentation changes: please submit a [pull request on Github](https://github.com/docker/compose/pulls).
|
||||
|
||||
For more information and resources, please visit the [Getting Help project page](https://docs.docker.com/project/get-help/).
|
||||
|
||||
@@ -1,27 +1,67 @@
|
||||
---
|
||||
layout: default
|
||||
title: Installing Fig
|
||||
---
|
||||
<!--[metadata]>
|
||||
+++
|
||||
title = "Docker Compose"
|
||||
description = "How to install Docker Compose"
|
||||
keywords = ["compose, orchestration, install, installation, docker, documentation"]
|
||||
[menu.main]
|
||||
parent="mn_install"
|
||||
weight=4
|
||||
+++
|
||||
<![end-metadata]-->
|
||||
|
||||
Installing Fig
|
||||
==============
|
||||
|
||||
First, install Docker version 1.3 or greater.
|
||||
# Install Docker Compose
|
||||
|
||||
If you're on OS X, you can use the [OS X installer](https://docs.docker.com/installation/mac/) to install both Docker and boot2docker. Once boot2docker is running, set the environment variables that'll configure Docker and Fig to talk to it:
|
||||
To install Compose, you'll need to install Docker first. You'll then install
|
||||
Compose with a `curl` command.
|
||||
|
||||
$(boot2docker shellinit)
|
||||
## Install Docker
|
||||
|
||||
To persist the environment variables across shell sessions, you can add that line to your `~/.bashrc` file.
|
||||
First, install Docker version 1.6 or greater:
|
||||
|
||||
There are also guides for [Ubuntu](https://docs.docker.com/installation/ubuntulinux/) and [other platforms](https://docs.docker.com/installation/) in Docker’s documentation.
|
||||
- [Instructions for Mac OS X](http://docs.docker.com/installation/mac/)
|
||||
- [Instructions for Ubuntu](http://docs.docker.com/installation/ubuntulinux/)
|
||||
- [Instructions for other systems](http://docs.docker.com/installation/)
|
||||
|
||||
Next, install Fig:
|
||||
## Install Compose
|
||||
|
||||
curl -L https://github.com/docker/fig/releases/download/1.0.0/fig-`uname -s`-`uname -m` > /usr/local/bin/fig; chmod +x /usr/local/bin/fig
|
||||
To install Compose, run the following commands:
|
||||
|
||||
Releases are available for OS X and 64-bit Linux. Fig is also available as a Python package if you're on another platform (or if you prefer that sort of thing):
|
||||
curl -L https://github.com/docker/compose/releases/download/1.3.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
|
||||
chmod +x /usr/local/bin/docker-compose
|
||||
|
||||
$ sudo pip install -U fig
|
||||
> Note: If you get a "Permission denied" error, your `/usr/local/bin` directory probably isn't writable and you'll need to install Compose as the superuser. Run `sudo -i`, then the two commands above, then `exit`.
|
||||
|
||||
That should be all you need! Run `fig --version` to see if it worked.
|
||||
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`.
|
||||
|
||||
### Upgrading
|
||||
|
||||
If you're coming from Compose 1.2 or earlier, you'll need to remove or migrate your existing containers after upgrading Compose. This is because, as of version 1.3, Compose uses Docker labels to keep track of containers, and so they need to be recreated with labels added.
|
||||
|
||||
If Compose detects containers that were created without labels, it will refuse to run so that you don't end up with two sets of them. If you want to keep using your existing containers (for example, because they have data volumes you want to preserve) you can migrate them with the following command:
|
||||
|
||||
docker-compose migrate-to-labels
|
||||
|
||||
Alternatively, if you're not worried about keeping them, you can remove them - Compose will just create new ones.
|
||||
|
||||
docker rm -f myapp_web_1 myapp_db_1 ...
|
||||
|
||||
## Compose documentation
|
||||
|
||||
- [User guide](/)
|
||||
- [Get started with Django](django.md)
|
||||
- [Get started with Rails](rails.md)
|
||||
- [Get started with Wordpress](wordpress.md)
|
||||
- [Command line reference](cli.md)
|
||||
- [Yaml file reference](yml.md)
|
||||
- [Compose environment variables](env.md)
|
||||
- [Compose command line completion](completion.md)
|
||||
|
||||
96
docs/production.md
Normal file
96
docs/production.md
Normal file
@@ -0,0 +1,96 @@
|
||||
<!--[metadata]>
|
||||
+++
|
||||
title = "Using Compose in production"
|
||||
description = "Guide to using Docker Compose in production"
|
||||
keywords = ["documentation, docs, docker, compose, orchestration, containers, production"]
|
||||
[menu.main]
|
||||
parent="smn_workw_compose"
|
||||
weight=1
|
||||
+++
|
||||
<![end-metadata]-->
|
||||
|
||||
|
||||
## Using Compose in production
|
||||
|
||||
While **Compose is not yet considered production-ready**, if you'd like to experiment and learn more about using it in production deployments, this guide
|
||||
can help.
|
||||
The project is actively working towards becoming
|
||||
production-ready; to learn more about the progress being made, check out the
|
||||
[roadmap](https://github.com/docker/compose/blob/master/ROADMAP.md) for details
|
||||
on how it's coming along and what still needs to be done.
|
||||
|
||||
When deploying to production, you'll almost certainly want to make changes to
|
||||
your app configuration that are more appropriate to a live environment. These
|
||||
changes may include:
|
||||
|
||||
- Removing any volume bindings for application code, so that code stays inside
|
||||
the container and can't be changed from outside
|
||||
- Binding to different ports on the host
|
||||
- Setting environment variables differently (e.g., to decrease the verbosity of
|
||||
logging, or to enable email sending)
|
||||
- Specifying a restart policy (e.g., `restart: always`) to avoid downtime
|
||||
- Adding extra services (e.g., a log aggregator)
|
||||
|
||||
For this reason, you'll probably want to define a separate Compose file, say
|
||||
`production.yml`, which specifies production-appropriate configuration.
|
||||
|
||||
> **Note:** The [extends](extends.md) keyword is useful for maintaining multiple
|
||||
> Compose files which re-use common services without having to manually copy and
|
||||
> paste.
|
||||
|
||||
Once you've got an alternate configuration file, make Compose use it
|
||||
by setting the `COMPOSE_FILE` environment variable:
|
||||
|
||||
$ COMPOSE_FILE=production.yml
|
||||
$ docker-compose up -d
|
||||
|
||||
> **Note:** You can also use the file for a one-off command without setting
|
||||
> an environment variable. You do this by passing the `-f` flag, e.g.,
|
||||
> `docker-compose -f production.yml up -d`.
|
||||
|
||||
### Deploying changes
|
||||
|
||||
When you make changes to your app code, you'll need to rebuild your image and
|
||||
recreate your app's containers. To redeploy a service called
|
||||
`web`, you would use:
|
||||
|
||||
$ docker-compose build web
|
||||
$ docker-compose up --no-deps -d web
|
||||
|
||||
This will first rebuild the image for `web` and then stop, destroy, and recreate
|
||||
*just* the `web` service. The `--no-deps` flag prevents Compose from also
|
||||
recreating any services which `web` depends on.
|
||||
|
||||
### Running Compose on a single server
|
||||
|
||||
You can use Compose to deploy an app to a remote Docker host by setting the
|
||||
`DOCKER_HOST`, `DOCKER_TLS_VERIFY`, and `DOCKER_CERT_PATH` environment variables
|
||||
appropriately. For tasks like this,
|
||||
[Docker Machine](https://docs.docker.com/machine) makes managing local and
|
||||
remote Docker hosts very easy, and is recommended even if you're not deploying
|
||||
remotely.
|
||||
|
||||
Once you've set up your environment variables, all the normal `docker-compose`
|
||||
commands will work with no further configuration.
|
||||
|
||||
### Running Compose on a Swarm cluster
|
||||
|
||||
[Docker Swarm](https://docs.docker.com/swarm), a Docker-native clustering
|
||||
system, exposes the same API as a single Docker host, which means you can use
|
||||
Compose against a Swarm instance and run your apps across multiple hosts.
|
||||
|
||||
Compose/Swarm integration is still in the experimental stage, and Swarm is still
|
||||
in beta, but if you'd like to explore and experiment, check out the
|
||||
[integration guide](https://github.com/docker/compose/blob/master/SWARM.md).
|
||||
|
||||
## Compose documentation
|
||||
|
||||
- [Installing Compose](install.md)
|
||||
- [Get started with Django](django.md)
|
||||
- [Get started with Rails](rails.md)
|
||||
- [Get started with Wordpress](wordpress.md)
|
||||
- [Command line reference](cli.md)
|
||||
- [Yaml file reference](yml.md)
|
||||
- [Compose environment variables](env.md)
|
||||
- [Compose command line completion](completion.md)
|
||||
|
||||
@@ -1,16 +1,27 @@
|
||||
---
|
||||
layout: default
|
||||
title: Getting started with Fig and Rails
|
||||
---
|
||||
<!--[metadata]>
|
||||
+++
|
||||
title = "Quickstart Guide: Compose and Rails"
|
||||
description = "Getting started with Docker Compose and Rails"
|
||||
keywords = ["documentation, docs, docker, compose, orchestration, containers"]
|
||||
[menu.main]
|
||||
parent="smn_workw_compose"
|
||||
weight=5
|
||||
+++
|
||||
<![end-metadata]-->
|
||||
|
||||
Getting started with Fig and Rails
|
||||
==================================
|
||||
## Quickstart Guide: 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).
|
||||
This Quickstart guide will show you how to use Compose to set up and run a Rails/PostgreSQL app. Before starting, you'll need to have [Compose installed](install.md).
|
||||
|
||||
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:
|
||||
### Define the project
|
||||
|
||||
FROM ruby
|
||||
Start by setting up the three files you'll need to build the app. First, since
|
||||
your app is going to run inside a Docker container containing all of its
|
||||
dependencies, you'll need to define exactly what needs to be included in the
|
||||
container. This is done using a file called `Dockerfile`. To begin with, the
|
||||
Dockerfile consists of:
|
||||
|
||||
FROM ruby:2.2.0
|
||||
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev
|
||||
RUN mkdir /myapp
|
||||
WORKDIR /myapp
|
||||
@@ -18,14 +29,14 @@ Let's set up the three files that'll get us started. First, our app is going to
|
||||
RUN bundle install
|
||||
ADD . /myapp
|
||||
|
||||
That'll put our application code inside an image with Ruby, Bundler and all our dependencies. For more information on how to write Dockerfiles, see the [Docker user guide](https://docs.docker.com/userguide/dockerimages/#building-an-image-from-a-dockerfile) and the [Dockerfile reference](http://docs.docker.com/reference/builder/).
|
||||
That'll put your application code inside an image that will build a container with Ruby, Bundler and all your dependencies inside it. For more information on how to write Dockerfiles, see the [Docker user guide](https://docs.docker.com/userguide/dockerimages/#building-an-image-from-a-dockerfile) and the [Dockerfile reference](http://docs.docker.com/reference/builder/).
|
||||
|
||||
Next, we have a bootstrap `Gemfile` which just loads Rails. It'll be overwritten in a moment by `rails new`.
|
||||
Next, create a bootstrap `Gemfile` which just loads Rails. It'll be overwritten in a moment by `rails new`.
|
||||
|
||||
source 'https://rubygems.org'
|
||||
gem 'rails', '4.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. This file describes the services that comprise your app (a database and a web app), how to get each one's Docker image (the database just runs on a pre-made PostgreSQL image, and the web app is built from the current directory), and the configuration needed to link them together and expose the web app's port.
|
||||
|
||||
db:
|
||||
image: postgres
|
||||
@@ -33,7 +44,7 @@ Finally, `fig.yml` is where the magic happens. It describes what services our ap
|
||||
- "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,30 +52,44 @@ 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`:
|
||||
### Build the project
|
||||
|
||||
$ fig run web rails new . --force --database=postgresql --skip-bundle
|
||||
With those three files in place, you can now generate the Rails skeleton app
|
||||
using `docker-compose run`:
|
||||
|
||||
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:
|
||||
$ docker-compose run web rails new . --force --database=postgresql --skip-bundle
|
||||
|
||||
First, Compose will build the image for the `web` service using the
|
||||
`Dockerfile`. Then it'll run `rails new` inside a new container, using that
|
||||
image. Once it's done, you should have generated a fresh app:
|
||||
|
||||
$ ls
|
||||
Dockerfile app fig.yml tmp
|
||||
Dockerfile app docker-compose.yml tmp
|
||||
Gemfile bin lib vendor
|
||||
Gemfile.lock config log
|
||||
README.rdoc config.ru public
|
||||
Rakefile db test
|
||||
|
||||
Uncomment the line in your new `Gemfile` which loads `therubyracer`, so we've got a Javascript runtime:
|
||||
Uncomment the line in your new `Gemfile` which loads `therubyracer`, so you've
|
||||
got a Javascript runtime:
|
||||
|
||||
gem 'therubyracer', platforms: :ruby
|
||||
|
||||
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).
|
||||
Now that you've got a new `Gemfile`, you need to build the image again. (This,
|
||||
and changes to the Dockerfile itself, should be the only times you'll need to
|
||||
rebuild.)
|
||||
|
||||
$ 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 database and username to align with the defaults set by the `postgres` image.
|
||||
### Connect the database
|
||||
|
||||
Open up your newly-generated `database.yml`. Replace its contents with the following:
|
||||
The app is now bootable, but you're not quite there yet. By default, Rails
|
||||
expects a database to be running on `localhost` - so you need to point it at the
|
||||
`db` container instead. You also need to change the database and username to
|
||||
align with the defaults set by the `postgres` image.
|
||||
|
||||
Open up your newly-generated `database.yml` file. Replace its contents with the
|
||||
following:
|
||||
|
||||
development: &default
|
||||
adapter: postgresql
|
||||
@@ -79,20 +104,32 @@ Open up your newly-generated `database.yml`. Replace its contents with the follo
|
||||
<<: *default
|
||||
database: myapp_test
|
||||
|
||||
We can now boot the app.
|
||||
You can now boot the app with:
|
||||
|
||||
$ fig up
|
||||
$ docker-compose up
|
||||
|
||||
If all's well, you should see some PostgreSQL output, and then—after a few seconds—the familiar refrain:
|
||||
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:
|
||||
Finally, you 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—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).
|
||||
That's it. Your app should now be running on port 3000 on your Docker daemon (if
|
||||
you're using Boot2docker, `boot2docker ip` will tell you its address).
|
||||
|
||||

|
||||
## More Compose documentation
|
||||
|
||||
- [User guide](/)
|
||||
- [Installing Compose](install.md)
|
||||
- [Get started with Django](django.md)
|
||||
- [Get started with Rails](rails.md)
|
||||
- [Get started with Wordpress](wordpress.md)
|
||||
- [Command line reference](cli.md)
|
||||
- [Yaml file reference](yml.md)
|
||||
- [Compose environment variables](env.md)
|
||||
- [Compose command line completion](completion.md)
|
||||
|
||||
@@ -1,25 +1,47 @@
|
||||
---
|
||||
layout: default
|
||||
title: Getting started with Fig and Wordpress
|
||||
---
|
||||
<!--[metadata]>
|
||||
+++
|
||||
title = "Quickstart Guide: Compose and Wordpress"
|
||||
description = "Getting started with Compose and Wordpress"
|
||||
keywords = ["documentation, docs, docker, compose, orchestration, containers"]
|
||||
[menu.main]
|
||||
parent="smn_workw_compose"
|
||||
weight=6
|
||||
+++
|
||||
<![end-metadata]-->
|
||||
|
||||
Getting started with Fig 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:
|
||||
# Quickstart Guide: Compose and Wordpress
|
||||
|
||||
$ curl http://wordpress.org/wordpress-3.8.1.tar.gz | tar -xvzf -
|
||||
You can use Compose to easily run Wordpress in an isolated environment built
|
||||
with Docker containers.
|
||||
|
||||
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:
|
||||
## Define the project
|
||||
|
||||
First, [Install Compose](install.md) and then download Wordpress into the
|
||||
current directory:
|
||||
|
||||
$ curl https://wordpress.org/latest.tar.gz | tar -xvzf -
|
||||
|
||||
This will create a directory called `wordpress`. If you wish, you can rename it
|
||||
to the name of your project.
|
||||
|
||||
Next, inside that directory, create a `Dockerfile`, a file that defines what
|
||||
environment your app is going to run in. For more information on how to write
|
||||
Dockerfiles, see the
|
||||
[Docker user guide](https://docs.docker.com/userguide/dockerimages/#building-an-image-from-a-dockerfile) and the
|
||||
[Dockerfile reference](http://docs.docker.com/reference/builder/). In this case,
|
||||
your Dockerfile should be:
|
||||
|
||||
```
|
||||
FROM orchardup/php5
|
||||
ADD . /code
|
||||
```
|
||||
|
||||
This 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/).
|
||||
This tells Docker how to build an image defining a container that contains PHP
|
||||
and Wordpress.
|
||||
|
||||
Next up, `fig.yml` starts our web service and a separate MySQL instance:
|
||||
Next you'll create a `docker-compose.yml` file that will start your web service
|
||||
and a separate MySQL instance:
|
||||
|
||||
```
|
||||
web:
|
||||
@@ -37,7 +59,9 @@ 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, `wp-config.php` is
|
||||
the standard Wordpress config file with a single change to point the database
|
||||
configuration at the `db` container:
|
||||
|
||||
```
|
||||
<?php
|
||||
@@ -67,7 +91,7 @@ if ( !defined('ABSPATH') )
|
||||
require_once(ABSPATH . 'wp-settings.php');
|
||||
```
|
||||
|
||||
Finally, `router.php` tells PHP's built-in web server how to run Wordpress:
|
||||
Second, `router.php` tells PHP's built-in web server how to run Wordpress:
|
||||
|
||||
```
|
||||
<?php
|
||||
@@ -87,5 +111,22 @@ if(file_exists($root.$path))
|
||||
}
|
||||
}else include_once 'index.php';
|
||||
```
|
||||
### Build the project
|
||||
|
||||
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 at port 8000 on your docker daemon (if you're using boot2docker, `boot2docker ip` will tell you its address).
|
||||
With those four files in place, run `docker-compose up` inside your Wordpress
|
||||
directory and it'll pull and build the needed images, and then start the web and
|
||||
database containers. You'll then be able to visit Wordpress at port 8000 on your
|
||||
Docker daemon (if you're using Boot2docker, `boot2docker ip` will tell you its
|
||||
address).
|
||||
|
||||
## More Compose documentation
|
||||
|
||||
- [User guide](/)
|
||||
- [Installing Compose](install.md)
|
||||
- [Get started with Django](django.md)
|
||||
- [Get started with Rails](rails.md)
|
||||
- [Get started with Wordpress](wordpress.md)
|
||||
- [Command line reference](cli.md)
|
||||
- [Yaml file reference](yml.md)
|
||||
- [Compose environment variables](env.md)
|
||||
- [Compose command line completion](completion.md)
|
||||
|
||||
284
docs/yml.md
284
docs/yml.md
@@ -1,18 +1,28 @@
|
||||
---
|
||||
layout: default
|
||||
title: fig.yml reference
|
||||
---
|
||||
<!--[metadata]>
|
||||
+++
|
||||
title = "docker-compose.yml reference"
|
||||
description = "docker-compose.yml reference"
|
||||
keywords = ["fig, composition, compose, docker"]
|
||||
[menu.main]
|
||||
parent="smn_compose_ref"
|
||||
+++
|
||||
<![end-metadata]-->
|
||||
|
||||
fig.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.
|
||||
# docker-compose.yml reference
|
||||
|
||||
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`.
|
||||
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.
|
||||
|
||||
###image
|
||||
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`.
|
||||
|
||||
Tag or partial image ID. Can be local or remote - Fig will attempt to pull if it doesn't exist locally.
|
||||
### image
|
||||
|
||||
Tag or partial image ID. Can be local or remote - Compose will attempt to
|
||||
pull if it doesn't exist locally.
|
||||
|
||||
```
|
||||
image: ubuntu
|
||||
@@ -22,12 +32,26 @@ 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. When the value supplied is a
|
||||
relative path, it is interpreted as relative to the location of the yml file
|
||||
itself. This directory is also the build context that is sent to the Docker daemon.
|
||||
|
||||
Compose will build and tag it with a generated name, and use that image thereafter.
|
||||
|
||||
```
|
||||
build: /path/to/build/dir
|
||||
```
|
||||
|
||||
### dockerfile
|
||||
|
||||
Alternate Dockerfile.
|
||||
|
||||
Compose will use an alternate file to build with.
|
||||
|
||||
```
|
||||
dockerfile: Dockerfile-alternate
|
||||
```
|
||||
|
||||
### command
|
||||
|
||||
Override the default command.
|
||||
@@ -39,7 +63,9 @@ command: bundle exec thin -p 3000
|
||||
<a name="links"></a>
|
||||
### links
|
||||
|
||||
Link to containers in another service. Either specify both the service name and the link alias (`SERVICE:ALIAS`), or just the service name (which will also be used for the alias).
|
||||
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,7 +74,8 @@ links:
|
||||
- redis
|
||||
```
|
||||
|
||||
An entry with the alias' name will be created in `/etc/hosts` inside containers for this service, e.g:
|
||||
An entry with the alias' name will be created in `/etc/hosts` inside containers
|
||||
for this service, e.g:
|
||||
|
||||
```
|
||||
172.17.2.186 db
|
||||
@@ -56,13 +83,49 @@ An entry with the alias' name will be created in `/etc/hosts` inside containers
|
||||
172.17.2.187 redis
|
||||
```
|
||||
|
||||
Environment variables will also be created - see the [environment variable reference](env.html) for details.
|
||||
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
|
||||
```
|
||||
|
||||
### extra_hosts
|
||||
|
||||
Add hostname mappings. Use the same values as the docker client `--add-host` parameter.
|
||||
|
||||
```
|
||||
extra_hosts:
|
||||
- "somehost:162.242.195.82"
|
||||
- "otherhost:50.31.209.229"
|
||||
```
|
||||
|
||||
An entry with the ip address and hostname will be created in `/etc/hosts` inside containers for this service, e.g:
|
||||
|
||||
```
|
||||
162.242.195.82 somehost
|
||||
50.31.209.229 otherhost
|
||||
```
|
||||
|
||||
### 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:
|
||||
@@ -74,7 +137,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:
|
||||
@@ -108,7 +172,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:
|
||||
@@ -120,6 +185,109 @@ environment:
|
||||
- SESSION_SECRET
|
||||
```
|
||||
|
||||
### env_file
|
||||
|
||||
Add environment variables from a file. Can be a single value or a list.
|
||||
|
||||
If you have specified a Compose file with `docker-compose -f FILE`, paths in
|
||||
`env_file` are relative to the directory that file is in.
|
||||
|
||||
Environment variables specified in `environment` override these values.
|
||||
|
||||
```
|
||||
env_file: .env
|
||||
|
||||
env_file:
|
||||
- ./common.env
|
||||
- ./apps/web.env
|
||||
- /opt/secrets.env
|
||||
```
|
||||
|
||||
Compose expects each line in an env file to be in `VAR=VAL` format. Lines
|
||||
beginning with `#` (i.e. comments) are ignored, as are blank lines.
|
||||
|
||||
```
|
||||
# Set Rails/Rack environment
|
||||
RACK_ENV=development
|
||||
```
|
||||
|
||||
### extends
|
||||
|
||||
Extend another service, in the current file or another, optionally overriding
|
||||
configuration.
|
||||
|
||||
Here's a simple example. Suppose we have 2 files - **common.yml** and
|
||||
**development.yml**. We can use `extends` to define a service in
|
||||
**development.yml** which uses configuration defined in **common.yml**:
|
||||
|
||||
**common.yml**
|
||||
|
||||
```
|
||||
webapp:
|
||||
build: ./webapp
|
||||
environment:
|
||||
- DEBUG=false
|
||||
- SEND_EMAILS=false
|
||||
```
|
||||
|
||||
**development.yml**
|
||||
|
||||
```
|
||||
web:
|
||||
extends:
|
||||
file: common.yml
|
||||
service: webapp
|
||||
ports:
|
||||
- "8000:8000"
|
||||
links:
|
||||
- db
|
||||
environment:
|
||||
- DEBUG=true
|
||||
db:
|
||||
image: postgres
|
||||
```
|
||||
|
||||
Here, the `web` service in **development.yml** inherits the configuration of
|
||||
the `webapp` service in **common.yml** - the `build` and `environment` keys -
|
||||
and adds `ports` and `links` configuration. It overrides one of the defined
|
||||
environment variables (DEBUG) with a new value, and the other one
|
||||
(SEND_EMAILS) is left untouched.
|
||||
|
||||
For more on `extends`, see the [tutorial](extends.md#example) and
|
||||
[reference](extends.md#reference).
|
||||
|
||||
### labels
|
||||
|
||||
Add metadata to containers using [Docker labels](http://docs.docker.com/userguide/labels-custom-metadata/). You can use either an array or a dictionary.
|
||||
|
||||
It's recommended that you use reverse-DNS notation to prevent your labels from conflicting with those used by other software.
|
||||
|
||||
```
|
||||
labels:
|
||||
com.example.description: "Accounting webapp"
|
||||
com.example.department: "Finance"
|
||||
com.example.label-with-empty-value: ""
|
||||
|
||||
labels:
|
||||
- "com.example.description=Accounting webapp"
|
||||
- "com.example.department=Finance"
|
||||
- "com.example.label-with-empty-value"
|
||||
```
|
||||
|
||||
### log driver
|
||||
|
||||
Specify a logging driver for the service's containers, as with the ``--log-driver`` option for docker run ([documented here](http://docs.docker.com/reference/run/#logging-drivers-log-driver)).
|
||||
|
||||
Allowed values are currently ``json-file``, ``syslog`` and ``none``. The list will change over time as more drivers are added to the Docker engine.
|
||||
|
||||
The default value is json-file.
|
||||
|
||||
```
|
||||
log_driver: "json-file"
|
||||
log_driver: "syslog"
|
||||
log_driver: "none"
|
||||
```
|
||||
|
||||
### net
|
||||
|
||||
Networking mode. Use the same values as the docker client `--net` parameter.
|
||||
@@ -130,6 +298,16 @@ net: "none"
|
||||
net: "container:[name or id]"
|
||||
net: "host"
|
||||
```
|
||||
### pid
|
||||
|
||||
```
|
||||
pid: "host"
|
||||
```
|
||||
|
||||
Sets the PID mode to the host PID mode. This turns on sharing between
|
||||
container and the host operating system the PID address space. Containers
|
||||
launched with this flag will be able to access and manipulate other
|
||||
containers in the bare-metal machine's namespace and vise-versa.
|
||||
|
||||
### dns
|
||||
|
||||
@@ -142,11 +320,60 @@ 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
|
||||
```
|
||||
|
||||
### devices
|
||||
|
||||
List of device mappings. Uses the same format as the `--device` docker
|
||||
client create option.
|
||||
|
||||
```
|
||||
devices:
|
||||
- "/dev/ttyUSB0:/dev/ttyUSB0"
|
||||
```
|
||||
|
||||
### security_opt
|
||||
|
||||
Override the default labeling scheme for each container.
|
||||
|
||||
```
|
||||
security_opt:
|
||||
- label:user:USER
|
||||
- label:role:ROLE
|
||||
```
|
||||
|
||||
### working\_dir, entrypoint, user, hostname, domainname, mem\_limit, privileged, restart, stdin\_open, tty, cpu\_shares, cpuset, read\_only
|
||||
|
||||
Each of these is a single value, analogous to its
|
||||
[docker run](https://docs.docker.com/reference/run/) counterpart.
|
||||
|
||||
```
|
||||
cpu_shares: 73
|
||||
cpuset: 0,1
|
||||
|
||||
working_dir: /code
|
||||
entrypoint: /code/entrypoint.sh
|
||||
user: postgresql
|
||||
@@ -156,4 +383,21 @@ domainname: foo.com
|
||||
|
||||
mem_limit: 1000000000
|
||||
privileged: true
|
||||
|
||||
restart: always
|
||||
|
||||
stdin_open: true
|
||||
tty: true
|
||||
read_only: true
|
||||
```
|
||||
|
||||
## Compose documentation
|
||||
|
||||
- [User guide](/)
|
||||
- [Installing Compose](install.md)
|
||||
- [Get started with Django](django.md)
|
||||
- [Get started with Rails](rails.md)
|
||||
- [Get started with Wordpress](wordpress.md)
|
||||
- [Command line reference](cli.md)
|
||||
- [Compose environment variables](env.md)
|
||||
- [Compose command line completion](completion.md)
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
from .service import Service # noqa:flake8
|
||||
|
||||
__version__ = '1.0.0'
|
||||
@@ -1,27 +0,0 @@
|
||||
# dockerpty.
|
||||
#
|
||||
# Copyright 2014 Chris Corbyn <chris@w3style.co.uk>
|
||||
#
|
||||
# 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 .pty import PseudoTerminal
|
||||
|
||||
|
||||
def start(client, container):
|
||||
"""
|
||||
Present the PTY of the container inside the current process.
|
||||
|
||||
This is just a wrapper for PseudoTerminal(client, container).start()
|
||||
"""
|
||||
|
||||
PseudoTerminal(client, container).start()
|
||||
@@ -1,294 +0,0 @@
|
||||
# dockerpty: io.py
|
||||
#
|
||||
# Copyright 2014 Chris Corbyn <chris@w3style.co.uk>
|
||||
#
|
||||
# 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 os
|
||||
import fcntl
|
||||
import errno
|
||||
import struct
|
||||
import select as builtin_select
|
||||
|
||||
|
||||
def set_blocking(fd, blocking=True):
|
||||
"""
|
||||
Set the given file-descriptor blocking or non-blocking.
|
||||
|
||||
Returns the original blocking status.
|
||||
"""
|
||||
|
||||
old_flag = fcntl.fcntl(fd, fcntl.F_GETFL)
|
||||
|
||||
if blocking:
|
||||
new_flag = old_flag &~ os.O_NONBLOCK
|
||||
else:
|
||||
new_flag = old_flag | os.O_NONBLOCK
|
||||
|
||||
fcntl.fcntl(fd, fcntl.F_SETFL, new_flag)
|
||||
|
||||
return not bool(old_flag & os.O_NONBLOCK)
|
||||
|
||||
|
||||
def select(read_streams, timeout=0):
|
||||
"""
|
||||
Select the streams from `read_streams` that are ready for reading.
|
||||
|
||||
Uses `select.select()` internally but returns a flat list of streams.
|
||||
"""
|
||||
|
||||
write_streams = []
|
||||
exception_streams = []
|
||||
|
||||
try:
|
||||
return builtin_select.select(
|
||||
read_streams,
|
||||
write_streams,
|
||||
exception_streams,
|
||||
timeout,
|
||||
)[0]
|
||||
except builtin_select.error as e:
|
||||
# POSIX signals interrupt select()
|
||||
if e[0] == errno.EINTR:
|
||||
return []
|
||||
else:
|
||||
raise e
|
||||
|
||||
|
||||
class Stream(object):
|
||||
"""
|
||||
Generic Stream class.
|
||||
|
||||
This is a file-like abstraction on top of os.read() and os.write(), which
|
||||
add consistency to the reading of sockets and files alike.
|
||||
"""
|
||||
|
||||
|
||||
"""
|
||||
Recoverable IO/OS Errors.
|
||||
"""
|
||||
ERRNO_RECOVERABLE = [
|
||||
errno.EINTR,
|
||||
errno.EDEADLK,
|
||||
errno.EWOULDBLOCK,
|
||||
]
|
||||
|
||||
|
||||
def __init__(self, fd):
|
||||
"""
|
||||
Initialize the Stream for the file descriptor `fd`.
|
||||
|
||||
The `fd` object must have a `fileno()` method.
|
||||
"""
|
||||
self.fd = fd
|
||||
|
||||
|
||||
def fileno(self):
|
||||
"""
|
||||
Return the fileno() of the file descriptor.
|
||||
"""
|
||||
|
||||
return self.fd.fileno()
|
||||
|
||||
|
||||
def set_blocking(self, value):
|
||||
if hasattr(self.fd, 'setblocking'):
|
||||
self.fd.setblocking(value)
|
||||
return True
|
||||
else:
|
||||
return set_blocking(self.fd, value)
|
||||
|
||||
|
||||
def read(self, n=4096):
|
||||
"""
|
||||
Return `n` bytes of data from the Stream, or None at end of stream.
|
||||
"""
|
||||
|
||||
try:
|
||||
if hasattr(self.fd, 'recv'):
|
||||
return self.fd.recv(n)
|
||||
return os.read(self.fd.fileno(), n)
|
||||
except EnvironmentError as e:
|
||||
if e.errno not in Stream.ERRNO_RECOVERABLE:
|
||||
raise e
|
||||
|
||||
|
||||
def write(self, data):
|
||||
"""
|
||||
Write `data` to the Stream.
|
||||
"""
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
while True:
|
||||
try:
|
||||
if hasattr(self.fd, 'send'):
|
||||
self.fd.send(data)
|
||||
return len(data)
|
||||
os.write(self.fd.fileno(), data)
|
||||
return len(data)
|
||||
except EnvironmentError as e:
|
||||
if e.errno not in Stream.ERRNO_RECOVERABLE:
|
||||
raise e
|
||||
|
||||
def __repr__(self):
|
||||
return "{cls}({fd})".format(cls=type(self).__name__, fd=self.fd)
|
||||
|
||||
|
||||
class Demuxer(object):
|
||||
"""
|
||||
Wraps a multiplexed Stream to read in data demultiplexed.
|
||||
|
||||
Docker multiplexes streams together when there is no PTY attached, by
|
||||
sending an 8-byte header, followed by a chunk of data.
|
||||
|
||||
The first 4 bytes of the header denote the stream from which the data came
|
||||
(i.e. 0x01 = stdout, 0x02 = stderr). Only the first byte of these initial 4
|
||||
bytes is used.
|
||||
|
||||
The next 4 bytes indicate the length of the following chunk of data as an
|
||||
integer in big endian format. This much data must be consumed before the
|
||||
next 8-byte header is read.
|
||||
"""
|
||||
|
||||
def __init__(self, stream):
|
||||
"""
|
||||
Initialize a new Demuxer reading from `stream`.
|
||||
"""
|
||||
|
||||
self.stream = stream
|
||||
self.remain = 0
|
||||
|
||||
|
||||
def fileno(self):
|
||||
"""
|
||||
Returns the fileno() of the underlying Stream.
|
||||
|
||||
This is useful for select() to work.
|
||||
"""
|
||||
|
||||
return self.stream.fileno()
|
||||
|
||||
|
||||
def set_blocking(self, value):
|
||||
return self.stream.set_blocking(value)
|
||||
|
||||
|
||||
def read(self, n=4096):
|
||||
"""
|
||||
Read up to `n` bytes of data from the Stream, after demuxing.
|
||||
|
||||
Less than `n` bytes of data may be returned depending on the available
|
||||
payload, but the number of bytes returned will never exceed `n`.
|
||||
|
||||
Because demuxing involves scanning 8-byte headers, the actual amount of
|
||||
data read from the underlying stream may be greater than `n`.
|
||||
"""
|
||||
|
||||
size = self._next_packet_size(n)
|
||||
|
||||
if size <= 0:
|
||||
return
|
||||
else:
|
||||
return self.stream.read(size)
|
||||
|
||||
|
||||
def write(self, data):
|
||||
"""
|
||||
Delegates the the underlying Stream.
|
||||
"""
|
||||
|
||||
return self.stream.write(data)
|
||||
|
||||
|
||||
def _next_packet_size(self, n=0):
|
||||
size = 0
|
||||
|
||||
if self.remain > 0:
|
||||
size = min(n, self.remain)
|
||||
self.remain -= size
|
||||
else:
|
||||
data = self.stream.read(8)
|
||||
if data is None:
|
||||
return 0
|
||||
if len(data) == 8:
|
||||
__, actual = struct.unpack('>BxxxL', data)
|
||||
size = min(n, actual)
|
||||
self.remain = actual - size
|
||||
|
||||
return size
|
||||
|
||||
def __repr__(self):
|
||||
return "{cls}({stream})".format(cls=type(self).__name__,
|
||||
stream=self.stream)
|
||||
|
||||
|
||||
class Pump(object):
|
||||
"""
|
||||
Stream pump class.
|
||||
|
||||
A Pump wraps two Streams, reading from one and and writing its data into
|
||||
the other, much like a pipe but manually managed.
|
||||
|
||||
This abstraction is used to facilitate piping data between the file
|
||||
descriptors associated with the tty and those associated with a container's
|
||||
allocated pty.
|
||||
|
||||
Pumps are selectable based on the 'read' end of the pipe.
|
||||
"""
|
||||
|
||||
def __init__(self, from_stream, to_stream):
|
||||
"""
|
||||
Initialize a Pump with a Stream to read from and another to write to.
|
||||
"""
|
||||
|
||||
self.from_stream = from_stream
|
||||
self.to_stream = to_stream
|
||||
|
||||
|
||||
def fileno(self):
|
||||
"""
|
||||
Returns the `fileno()` of the reader end of the Pump.
|
||||
|
||||
This is useful to allow Pumps to function with `select()`.
|
||||
"""
|
||||
|
||||
return self.from_stream.fileno()
|
||||
|
||||
|
||||
def set_blocking(self, value):
|
||||
return self.from_stream.set_blocking(value)
|
||||
|
||||
|
||||
def flush(self, n=4096):
|
||||
"""
|
||||
Flush `n` bytes of data from the reader Stream to the writer Stream.
|
||||
|
||||
Returns the number of bytes that were actually flushed. A return value
|
||||
of zero is not an error.
|
||||
|
||||
If EOF has been reached, `None` is returned.
|
||||
"""
|
||||
|
||||
try:
|
||||
return self.to_stream.write(self.from_stream.read(n))
|
||||
except OSError as e:
|
||||
if e.errno != errno.EPIPE:
|
||||
raise e
|
||||
|
||||
def __repr__(self):
|
||||
return "{cls}(from={from_stream}, to={to_stream})".format(
|
||||
cls=type(self).__name__,
|
||||
from_stream=self.from_stream,
|
||||
to_stream=self.to_stream)
|
||||
@@ -1,235 +0,0 @@
|
||||
# dockerpty: pty.py
|
||||
#
|
||||
# Copyright 2014 Chris Corbyn <chris@w3style.co.uk>
|
||||
#
|
||||
# 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 sys
|
||||
import signal
|
||||
from ssl import SSLError
|
||||
|
||||
from . import io
|
||||
from . import tty
|
||||
|
||||
|
||||
class WINCHHandler(object):
|
||||
"""
|
||||
WINCH Signal handler to keep the PTY correctly sized.
|
||||
"""
|
||||
|
||||
def __init__(self, pty):
|
||||
"""
|
||||
Initialize a new WINCH handler for the given PTY.
|
||||
|
||||
Initializing a handler has no immediate side-effects. The `start()`
|
||||
method must be invoked for the signals to be trapped.
|
||||
"""
|
||||
|
||||
self.pty = pty
|
||||
self.original_handler = None
|
||||
|
||||
|
||||
def __enter__(self):
|
||||
"""
|
||||
Invoked on entering a `with` block.
|
||||
"""
|
||||
|
||||
self.start()
|
||||
return self
|
||||
|
||||
|
||||
def __exit__(self, *_):
|
||||
"""
|
||||
Invoked on exiting a `with` block.
|
||||
"""
|
||||
|
||||
self.stop()
|
||||
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Start trapping WINCH signals and resizing the PTY.
|
||||
|
||||
This method saves the previous WINCH handler so it can be restored on
|
||||
`stop()`.
|
||||
"""
|
||||
|
||||
def handle(signum, frame):
|
||||
if signum == signal.SIGWINCH:
|
||||
self.pty.resize()
|
||||
|
||||
self.original_handler = signal.signal(signal.SIGWINCH, handle)
|
||||
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stop trapping WINCH signals and restore the previous WINCH handler.
|
||||
"""
|
||||
|
||||
if self.original_handler is not None:
|
||||
signal.signal(signal.SIGWINCH, self.original_handler)
|
||||
|
||||
|
||||
class PseudoTerminal(object):
|
||||
"""
|
||||
Wraps the pseudo-TTY (PTY) allocated to a docker container.
|
||||
|
||||
The PTY is managed via the current process' TTY until it is closed.
|
||||
|
||||
Example:
|
||||
|
||||
import docker
|
||||
from dockerpty import PseudoTerminal
|
||||
|
||||
client = docker.Client()
|
||||
container = client.create_container(
|
||||
image='busybox:latest',
|
||||
stdin_open=True,
|
||||
tty=True,
|
||||
command='/bin/sh',
|
||||
)
|
||||
|
||||
# hijacks the current tty until the pty is closed
|
||||
PseudoTerminal(client, container).start()
|
||||
|
||||
Care is taken to ensure all file descriptors are restored on exit. For
|
||||
example, you can attach to a running container from within a Python REPL
|
||||
and when the container exits, the user will be returned to the Python REPL
|
||||
without adverse effects.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, client, container):
|
||||
"""
|
||||
Initialize the PTY using the docker.Client instance and container dict.
|
||||
"""
|
||||
|
||||
self.client = client
|
||||
self.container = container
|
||||
self.raw = None
|
||||
|
||||
|
||||
def start(self, **kwargs):
|
||||
"""
|
||||
Present the PTY of the container inside the current process.
|
||||
|
||||
This will take over the current process' TTY until the container's PTY
|
||||
is closed.
|
||||
"""
|
||||
|
||||
pty_stdin, pty_stdout, pty_stderr = self.sockets()
|
||||
|
||||
mappings = [
|
||||
(io.Stream(sys.stdin), pty_stdin),
|
||||
(pty_stdout, io.Stream(sys.stdout)),
|
||||
(pty_stderr, io.Stream(sys.stderr)),
|
||||
]
|
||||
|
||||
pumps = [io.Pump(a, b) for (a, b) in mappings if a and b]
|
||||
|
||||
if not self.container_info()['State']['Running']:
|
||||
self.client.start(self.container, **kwargs)
|
||||
|
||||
flags = [p.set_blocking(False) for p in pumps]
|
||||
|
||||
try:
|
||||
with WINCHHandler(self):
|
||||
self._hijack_tty(pumps)
|
||||
finally:
|
||||
if flags:
|
||||
for (pump, flag) in zip(pumps, flags):
|
||||
io.set_blocking(pump, flag)
|
||||
|
||||
|
||||
def israw(self):
|
||||
"""
|
||||
Returns True if the PTY should operate in raw mode.
|
||||
|
||||
If the container was not started with tty=True, this will return False.
|
||||
"""
|
||||
|
||||
if self.raw is None:
|
||||
info = self.container_info()
|
||||
self.raw = sys.stdout.isatty() and info['Config']['Tty']
|
||||
|
||||
return self.raw
|
||||
|
||||
|
||||
def sockets(self):
|
||||
"""
|
||||
Returns a tuple of sockets connected to the pty (stdin,stdout,stderr).
|
||||
|
||||
If any of the sockets are not attached in the container, `None` is
|
||||
returned in the tuple.
|
||||
"""
|
||||
|
||||
info = self.container_info()
|
||||
|
||||
def attach_socket(key):
|
||||
if info['Config']['Attach{0}'.format(key.capitalize())]:
|
||||
socket = self.client.attach_socket(
|
||||
self.container,
|
||||
{key: 1, 'stream': 1, 'logs': 1},
|
||||
)
|
||||
stream = io.Stream(socket)
|
||||
|
||||
if info['Config']['Tty']:
|
||||
return stream
|
||||
else:
|
||||
return io.Demuxer(stream)
|
||||
else:
|
||||
return None
|
||||
|
||||
return map(attach_socket, ('stdin', 'stdout', 'stderr'))
|
||||
|
||||
|
||||
def resize(self, size=None):
|
||||
"""
|
||||
Resize the container's PTY.
|
||||
|
||||
If `size` is not None, it must be a tuple of (height,width), otherwise
|
||||
it will be determined by the size of the current TTY.
|
||||
"""
|
||||
|
||||
if not self.israw():
|
||||
return
|
||||
|
||||
size = size or tty.size(sys.stdout)
|
||||
|
||||
if size is not None:
|
||||
rows, cols = size
|
||||
try:
|
||||
self.client.resize(self.container, height=rows, width=cols)
|
||||
except IOError: # Container already exited
|
||||
pass
|
||||
|
||||
|
||||
def container_info(self):
|
||||
"""
|
||||
Thin wrapper around client.inspect_container().
|
||||
"""
|
||||
|
||||
return self.client.inspect_container(self.container)
|
||||
|
||||
|
||||
def _hijack_tty(self, pumps):
|
||||
with tty.Terminal(sys.stdin, raw=self.israw()):
|
||||
self.resize()
|
||||
while True:
|
||||
_ready = io.select(pumps, timeout=60)
|
||||
try:
|
||||
if all([p.flush() is None for p in pumps]):
|
||||
break
|
||||
except SSLError as e:
|
||||
if 'The operation did not complete' not in e.strerror:
|
||||
raise e
|
||||
@@ -1,130 +0,0 @@
|
||||
# dockerpty: tty.py
|
||||
#
|
||||
# Copyright 2014 Chris Corbyn <chris@w3style.co.uk>
|
||||
#
|
||||
# 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 __future__ import absolute_import
|
||||
|
||||
import os
|
||||
import termios
|
||||
import tty
|
||||
import fcntl
|
||||
import struct
|
||||
|
||||
|
||||
def size(fd):
|
||||
"""
|
||||
Return a tuple (rows,cols) representing the size of the TTY `fd`.
|
||||
|
||||
The provided file descriptor should be the stdout stream of the TTY.
|
||||
|
||||
If the TTY size cannot be determined, returns None.
|
||||
"""
|
||||
|
||||
if not os.isatty(fd.fileno()):
|
||||
return None
|
||||
|
||||
try:
|
||||
dims = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, 'hhhh'))
|
||||
except:
|
||||
try:
|
||||
dims = (os.environ['LINES'], os.environ['COLUMNS'])
|
||||
except:
|
||||
return None
|
||||
|
||||
return dims
|
||||
|
||||
|
||||
class Terminal(object):
|
||||
"""
|
||||
Terminal provides wrapper functionality to temporarily make the tty raw.
|
||||
|
||||
This is useful when streaming data from a pseudo-terminal into the tty.
|
||||
|
||||
Example:
|
||||
|
||||
with Terminal(sys.stdin, raw=True):
|
||||
do_things_in_raw_mode()
|
||||
"""
|
||||
|
||||
def __init__(self, fd, raw=True):
|
||||
"""
|
||||
Initialize a terminal for the tty with stdin attached to `fd`.
|
||||
|
||||
Initializing the Terminal has no immediate side effects. The `start()`
|
||||
method must be invoked, or `with raw_terminal:` used before the
|
||||
terminal is affected.
|
||||
"""
|
||||
|
||||
self.fd = fd
|
||||
self.raw = raw
|
||||
self.original_attributes = None
|
||||
|
||||
|
||||
def __enter__(self):
|
||||
"""
|
||||
Invoked when a `with` block is first entered.
|
||||
"""
|
||||
|
||||
self.start()
|
||||
return self
|
||||
|
||||
|
||||
def __exit__(self, *_):
|
||||
"""
|
||||
Invoked when a `with` block is finished.
|
||||
"""
|
||||
|
||||
self.stop()
|
||||
|
||||
|
||||
def israw(self):
|
||||
"""
|
||||
Returns True if the TTY should operate in raw mode.
|
||||
"""
|
||||
|
||||
return self.raw
|
||||
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Saves the current terminal attributes and makes the tty raw.
|
||||
|
||||
This method returns None immediately.
|
||||
"""
|
||||
|
||||
if os.isatty(self.fd.fileno()) and self.israw():
|
||||
self.original_attributes = termios.tcgetattr(self.fd)
|
||||
tty.setraw(self.fd)
|
||||
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Restores the terminal attributes back to before setting raw mode.
|
||||
|
||||
If the raw terminal was not started, does nothing.
|
||||
"""
|
||||
|
||||
if self.original_attributes is not None:
|
||||
termios.tcsetattr(
|
||||
self.fd,
|
||||
termios.TCSADRAIN,
|
||||
self.original_attributes,
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "{cls}({fd}, raw={raw})".format(
|
||||
cls=type(self).__name__,
|
||||
fd=self.fd,
|
||||
raw=self.raw)
|
||||
522
fig/service.py
522
fig/service.py
@@ -1,522 +0,0 @@
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
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, 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):
|
||||
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:`fig.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:
|
||||
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 not containers:
|
||||
log.info("Creating %s..." % self._next_container_name(containers))
|
||||
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):
|
||||
"""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=[],
|
||||
)
|
||||
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):
|
||||
container = container or self.create_container(**override_options)
|
||||
options = dict(self.options, **override_options)
|
||||
ports = dict(split_port(port) for port in 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)
|
||||
|
||||
container.start(
|
||||
links=self._get_links(link_to_self=options.get('one_off', False)),
|
||||
port_bindings=ports,
|
||||
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 not containers:
|
||||
log.info("Creating %s..." % self._next_container_name(containers))
|
||||
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, 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))
|
||||
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'])
|
||||
|
||||
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
|
||||
|
||||
def pull(self, insecure_registry=False):
|
||||
if 'image' in self.options:
|
||||
log.info('Pulling %s (%s)...' % (self.name, self.options.get('image')))
|
||||
self.client.pull(
|
||||
self.options.get('image'),
|
||||
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 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 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 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 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 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
|
||||
nose==1.3.4
|
||||
git+https://github.com/pyinstaller/pyinstaller.git@12e40471c77f588ea5be352f7219c873ddaae056#egg=pyinstaller
|
||||
unittest2
|
||||
flake8
|
||||
unittest2==0.8.0
|
||||
flake8==2.3.0
|
||||
pep8==1.6.1
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
PyYAML==3.10
|
||||
docker-py==0.5.3
|
||||
docker-py==1.2.3
|
||||
dockerpty==0.3.4
|
||||
docopt==0.6.1
|
||||
requests==2.2.1
|
||||
requests==2.6.1
|
||||
six==1.7.3
|
||||
texttable==0.8.1
|
||||
texttable==0.8.2
|
||||
websocket-client==0.11.0
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,8 +1,12 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
mkdir -p `pwd`/dist
|
||||
chmod 777 `pwd`/dist
|
||||
docker build -t fig .
|
||||
docker run -u user -v `pwd`/dist:/code/dist fig pyinstaller -F bin/fig
|
||||
mv dist/fig dist/fig-Linux-x86_64
|
||||
docker run -u user -v `pwd`/dist:/code/dist fig dist/fig-Linux-x86_64 --version
|
||||
|
||||
TAG="docker-compose"
|
||||
docker build -t "$TAG" .
|
||||
docker run \
|
||||
--rm \
|
||||
--user=user \
|
||||
--volume="$(pwd):/code" \
|
||||
--entrypoint="script/build-linux-inner" \
|
||||
"$TAG"
|
||||
|
||||
10
script/build-linux-inner
Executable file
10
script/build-linux-inner
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
mkdir -p `pwd`/dist
|
||||
chmod 777 `pwd`/dist
|
||||
|
||||
pyinstaller -F bin/docker-compose
|
||||
mv dist/docker-compose dist/docker-compose-Linux-x86_64
|
||||
dist/docker-compose-Linux-x86_64 --version
|
||||
@@ -1,10 +1,13 @@
|
||||
#!/bin/bash
|
||||
set -ex
|
||||
|
||||
PATH="/usr/local/bin:$PATH"
|
||||
|
||||
rm -rf venv
|
||||
virtualenv venv
|
||||
virtualenv -p /usr/local/bin/python venv
|
||||
venv/bin/pip install -r requirements.txt
|
||||
venv/bin/pip install -r requirements-dev.txt
|
||||
venv/bin/pip install .
|
||||
venv/bin/pyinstaller -F bin/fig
|
||||
mv dist/fig dist/fig-Darwin-x86_64
|
||||
dist/fig-Darwin-x86_64 --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
|
||||
|
||||
15
script/ci
Executable file
15
script/ci
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
# This should be run inside a container built from the Dockerfile
|
||||
# at the root of the repo:
|
||||
#
|
||||
# $ TAG="docker-compose:$(git rev-parse --short HEAD)"
|
||||
# $ docker build -t "$TAG" .
|
||||
# $ docker run --rm --volume="/var/run/docker.sock:/var/run/docker.sock" --volume="$(pwd)/.git:/code/.git" -e "TAG=$TAG" --entrypoint="script/ci" "$TAG"
|
||||
|
||||
set -e
|
||||
|
||||
export DOCKER_VERSIONS=all
|
||||
. script/test-versions
|
||||
|
||||
>&2 echo "Building Linux binary"
|
||||
su -c script/build-linux-inner user
|
||||
@@ -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:docker/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 $@
|
||||
88
script/dind
Executable file
88
script/dind
Executable file
@@ -0,0 +1,88 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# DinD: a wrapper script which allows docker to be run inside a docker container.
|
||||
# Original version by Jerome Petazzoni <jerome@docker.com>
|
||||
# See the blog post: http://blog.docker.com/2013/09/docker-can-now-run-within-docker/
|
||||
#
|
||||
# This script should be executed inside a docker container in privilieged mode
|
||||
# ('docker run --privileged', introduced in docker 0.6).
|
||||
|
||||
# Usage: dind CMD [ARG...]
|
||||
|
||||
# apparmor sucks and Docker needs to know that it's in a container (c) @tianon
|
||||
export container=docker
|
||||
|
||||
# First, make sure that cgroups are mounted correctly.
|
||||
CGROUP=/cgroup
|
||||
|
||||
mkdir -p "$CGROUP"
|
||||
|
||||
if ! mountpoint -q "$CGROUP"; then
|
||||
mount -n -t tmpfs -o uid=0,gid=0,mode=0755 cgroup $CGROUP || {
|
||||
echo >&2 'Could not make a tmpfs mount. Did you use --privileged?'
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
if [ -d /sys/kernel/security ] && ! mountpoint -q /sys/kernel/security; then
|
||||
mount -t securityfs none /sys/kernel/security || {
|
||||
echo >&2 'Could not mount /sys/kernel/security.'
|
||||
echo >&2 'AppArmor detection and -privileged mode might break.'
|
||||
}
|
||||
fi
|
||||
|
||||
# Mount the cgroup hierarchies exactly as they are in the parent system.
|
||||
for SUBSYS in $(cut -d: -f2 /proc/1/cgroup); do
|
||||
mkdir -p "$CGROUP/$SUBSYS"
|
||||
if ! mountpoint -q $CGROUP/$SUBSYS; then
|
||||
mount -n -t cgroup -o "$SUBSYS" cgroup "$CGROUP/$SUBSYS"
|
||||
fi
|
||||
|
||||
# The two following sections address a bug which manifests itself
|
||||
# by a cryptic "lxc-start: no ns_cgroup option specified" when
|
||||
# trying to start containers withina container.
|
||||
# The bug seems to appear when the cgroup hierarchies are not
|
||||
# mounted on the exact same directories in the host, and in the
|
||||
# container.
|
||||
|
||||
# Named, control-less cgroups are mounted with "-o name=foo"
|
||||
# (and appear as such under /proc/<pid>/cgroup) but are usually
|
||||
# mounted on a directory named "foo" (without the "name=" prefix).
|
||||
# Systemd and OpenRC (and possibly others) both create such a
|
||||
# cgroup. To avoid the aforementioned bug, we symlink "foo" to
|
||||
# "name=foo". This shouldn't have any adverse effect.
|
||||
name="${SUBSYS#name=}"
|
||||
if [ "$name" != "$SUBSYS" ]; then
|
||||
ln -s "$SUBSYS" "$CGROUP/$name"
|
||||
fi
|
||||
|
||||
# Likewise, on at least one system, it has been reported that
|
||||
# systemd would mount the CPU and CPU accounting controllers
|
||||
# (respectively "cpu" and "cpuacct") with "-o cpuacct,cpu"
|
||||
# but on a directory called "cpu,cpuacct" (note the inversion
|
||||
# in the order of the groups). This tries to work around it.
|
||||
if [ "$SUBSYS" = 'cpuacct,cpu' ]; then
|
||||
ln -s "$SUBSYS" "$CGROUP/cpu,cpuacct"
|
||||
fi
|
||||
done
|
||||
|
||||
# Note: as I write those lines, the LXC userland tools cannot setup
|
||||
# a "sub-container" properly if the "devices" cgroup is not in its
|
||||
# own hierarchy. Let's detect this and issue a warning.
|
||||
if ! grep -q :devices: /proc/1/cgroup; then
|
||||
echo >&2 'WARNING: the "devices" cgroup should be in its own hierarchy.'
|
||||
fi
|
||||
if ! grep -qw devices /proc/1/cgroup; then
|
||||
echo >&2 'WARNING: it looks like the "devices" cgroup is not mounted.'
|
||||
fi
|
||||
|
||||
# Mount /tmp
|
||||
mount -t tmpfs none /tmp
|
||||
|
||||
if [ $# -gt 0 ]; then
|
||||
exec "$@"
|
||||
fi
|
||||
|
||||
echo >&2 'ERROR: No command specified.'
|
||||
echo >&2 'You probably want to run hack/make.sh, or maybe a shell?'
|
||||
11
script/docs
Executable file
11
script/docs
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
set -ex
|
||||
|
||||
# import the existing docs build cmds from docker/docker
|
||||
DOCSPORT=8000
|
||||
GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
|
||||
DOCKER_DOCS_IMAGE="compose-docs$GIT_BRANCH"
|
||||
DOCKER_RUN_DOCS="docker run --rm -it -e NOCACHE"
|
||||
|
||||
docker build -t "$DOCKER_DOCS_IMAGE" -f docs/Dockerfile .
|
||||
$DOCKER_RUN_DOCS -p $DOCSPORT:8000 "$DOCKER_DOCS_IMAGE" mkdocs serve
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/bash
|
||||
open file://`pwd`/docs/_site/index.html
|
||||
53
script/prepare-osx
Executable file
53
script/prepare-osx
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
python_version() {
|
||||
python -V 2>&1
|
||||
}
|
||||
|
||||
openssl_version() {
|
||||
python -c "import ssl; print ssl.OPENSSL_VERSION"
|
||||
}
|
||||
|
||||
desired_python_version="2.7.9"
|
||||
desired_python_brew_version="2.7.9"
|
||||
python_formula="https://raw.githubusercontent.com/Homebrew/homebrew/1681e193e4d91c9620c4901efd4458d9b6fcda8e/Library/Formula/python.rb"
|
||||
|
||||
desired_openssl_version="1.0.1j"
|
||||
desired_openssl_brew_version="1.0.1j_1"
|
||||
openssl_formula="https://raw.githubusercontent.com/Homebrew/homebrew/62fc2a1a65e83ba9dbb30b2e0a2b7355831c714b/Library/Formula/openssl.rb"
|
||||
|
||||
PATH="/usr/local/bin:$PATH"
|
||||
|
||||
if !(which brew); then
|
||||
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
|
||||
fi
|
||||
|
||||
brew update
|
||||
|
||||
if !(python_version | grep "$desired_python_version"); then
|
||||
if brew list | grep python; then
|
||||
brew unlink python
|
||||
fi
|
||||
|
||||
brew install "$python_formula"
|
||||
brew switch python "$desired_python_brew_version"
|
||||
fi
|
||||
|
||||
if !(openssl_version | grep "$desired_openssl_version"); then
|
||||
if brew list | grep openssl; then
|
||||
brew unlink openssl
|
||||
fi
|
||||
|
||||
brew install "$openssl_formula"
|
||||
brew switch openssl "$desired_openssl_brew_version"
|
||||
fi
|
||||
|
||||
echo "*** Using $(python_version)"
|
||||
echo "*** Using $(openssl_version)"
|
||||
|
||||
if !(which virtualenv); then
|
||||
pip install virtualenv
|
||||
fi
|
||||
|
||||
4
script/shell
Executable file
4
script/shell
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
set -ex
|
||||
docker build -t docker-compose .
|
||||
exec docker run -v /var/run/docker.sock:/var/run/docker.sock -v `pwd`:/code -ti --rm --entrypoint bash docker-compose
|
||||
23
script/test
23
script/test
@@ -1,12 +1,17 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
# See CONTRIBUTING.md for usage.
|
||||
|
||||
set -ex
|
||||
|
||||
target="tests"
|
||||
TAG="docker-compose:$(git rev-parse --short HEAD)"
|
||||
|
||||
if [[ -n "$@" ]]; then
|
||||
target="$@"
|
||||
fi
|
||||
|
||||
docker build -t fig .
|
||||
docker run -v /var/run/docker.sock:/var/run/docker.sock fig flake8 --exclude=packages fig
|
||||
docker run -v /var/run/docker.sock:/var/run/docker.sock fig nosetests $target
|
||||
docker build -t "$TAG" .
|
||||
docker run \
|
||||
--rm \
|
||||
--volume="/var/run/docker.sock:/var/run/docker.sock" \
|
||||
-e DOCKER_VERSIONS \
|
||||
-e "TAG=$TAG" \
|
||||
-e "affinity:image==$TAG" \
|
||||
--entrypoint="script/test-versions" \
|
||||
"$TAG" \
|
||||
"$@"
|
||||
|
||||
26
script/test-versions
Executable file
26
script/test-versions
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
# This should be run inside a container built from the Dockerfile
|
||||
# at the root of the repo - script/test will do it automatically.
|
||||
|
||||
set -e
|
||||
|
||||
>&2 echo "Running lint checks"
|
||||
flake8 compose tests setup.py
|
||||
|
||||
if [ "$DOCKER_VERSIONS" == "" ]; then
|
||||
DOCKER_VERSIONS="default"
|
||||
elif [ "$DOCKER_VERSIONS" == "all" ]; then
|
||||
DOCKER_VERSIONS="$ALL_DOCKER_VERSIONS"
|
||||
fi
|
||||
|
||||
for version in $DOCKER_VERSIONS; do
|
||||
>&2 echo "Running tests against Docker $version"
|
||||
docker run \
|
||||
--rm \
|
||||
--privileged \
|
||||
--volume="/var/lib/docker" \
|
||||
-e "DOCKER_VERSION=$version" \
|
||||
--entrypoint="script/dind" \
|
||||
"$TAG" \
|
||||
script/wrapdocker nosetests "$@"
|
||||
done
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/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
|
||||
18
script/wrapdocker
Executable file
18
script/wrapdocker
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ "$DOCKER_VERSION" != "" ] && [ "$DOCKER_VERSION" != "default" ]; then
|
||||
ln -fs "/usr/local/bin/docker-$DOCKER_VERSION" "/usr/local/bin/docker"
|
||||
fi
|
||||
|
||||
# If a pidfile is still around (for example after a container restart),
|
||||
# delete it so that docker can start.
|
||||
rm -rf /var/run/docker.pid
|
||||
docker -d $DOCKER_DAEMON_ARGS &>/var/log/docker.log &
|
||||
|
||||
>&2 echo "Waiting for Docker to start..."
|
||||
while ! docker ps &>/dev/null; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
>&2 echo ">" "$@"
|
||||
exec "$@"
|
||||
20
setup.py
20
setup.py
@@ -27,13 +27,15 @@ def find_version(*file_paths):
|
||||
install_requires = [
|
||||
'docopt >= 0.6.1, < 0.7',
|
||||
'PyYAML >= 3.10, < 4',
|
||||
'requests >= 2.2.1, < 3',
|
||||
'requests >= 2.6.1, < 2.7',
|
||||
'texttable >= 0.8.1, < 0.9',
|
||||
'websocket-client >= 0.11.0, < 0.12',
|
||||
'docker-py >= 0.5, < 0.6',
|
||||
'websocket-client >= 0.11.0, < 1.0',
|
||||
'docker-py >= 1.2.3, < 1.3',
|
||||
'dockerpty >= 0.3.4, < 0.4',
|
||||
'six >= 1.3.0, < 2',
|
||||
]
|
||||
|
||||
|
||||
tests_require = [
|
||||
'mock >= 1.0.1',
|
||||
'nose',
|
||||
@@ -47,19 +49,19 @@ if sys.version_info < (2, 7):
|
||||
|
||||
|
||||
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(exclude=[ 'tests.*', 'tests' ]),
|
||||
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
|
||||
""",
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import sys
|
||||
|
||||
if sys.version_info >= (2,7):
|
||||
import unittest
|
||||
if sys.version_info >= (2, 7):
|
||||
import unittest # NOQA
|
||||
else:
|
||||
import unittest2 as unittest
|
||||
|
||||
import unittest2 as unittest # NOQA
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
simple:
|
||||
image: busybox:latest
|
||||
command: /bin/sleep 300
|
||||
command: top
|
||||
another:
|
||||
image: busybox:latest
|
||||
command: /bin/sleep 300
|
||||
command: top
|
||||
2
tests/fixtures/build-ctx/Dockerfile
vendored
Normal file
2
tests/fixtures/build-ctx/Dockerfile
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
FROM busybox:latest
|
||||
CMD echo "success"
|
||||
2
tests/fixtures/build-path/docker-compose.yml
vendored
Normal file
2
tests/fixtures/build-path/docker-compose.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
foo:
|
||||
build: ../build-ctx/
|
||||
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 top
|
||||
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: .
|
||||
@@ -1,2 +0,0 @@
|
||||
service:
|
||||
build: tests/fixtures/dockerfile_with_entrypoint
|
||||
4
tests/fixtures/env-file/docker-compose.yml
vendored
Normal file
4
tests/fixtures/env-file/docker-compose.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
web:
|
||||
image: busybox
|
||||
command: /bin/true
|
||||
env_file: ./test.env
|
||||
1
tests/fixtures/env-file/test.env
vendored
Normal file
1
tests/fixtures/env-file/test.env
vendored
Normal file
@@ -0,0 +1 @@
|
||||
FOO=1
|
||||
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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user