mirror of
https://github.com/docker/compose.git
synced 2026-02-12 03:29:27 +08:00
Compare commits
656 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6580c5609c | ||
|
|
a07c83659d | ||
|
|
0f58b9f6b4 | ||
|
|
fed391a23e | ||
|
|
4d1b2f1547 | ||
|
|
60411e9f05 | ||
|
|
b318585f3c | ||
|
|
1820306d0a | ||
|
|
bf1c1b4c17 | ||
|
|
352062c2dc | ||
|
|
92249364b6 | ||
|
|
872a1b5a5c | ||
|
|
b969988ccb | ||
|
|
a5aac7d59e | ||
|
|
c16f4d4041 | ||
|
|
d91c458d52 | ||
|
|
475a7e19a9 | ||
|
|
f43bfaadaa | ||
|
|
2680756dd6 | ||
|
|
837f368361 | ||
|
|
9df5481066 | ||
|
|
59c7528b4e | ||
|
|
d2385e3c2c | ||
|
|
6b600faf0b | ||
|
|
431fdaa0f1 | ||
|
|
ed12f2539c | ||
|
|
0dc19cb885 | ||
|
|
62b9c64311 | ||
|
|
3408e0d463 | ||
|
|
f4b599551a | ||
|
|
267be12bb2 | ||
|
|
ec5c864cc7 | ||
|
|
cabe47a379 | ||
|
|
b4fbab4b56 | ||
|
|
6797a322b5 | ||
|
|
e04c5cb52c | ||
|
|
a3f70a9f64 | ||
|
|
f407504679 | ||
|
|
253b245a1c | ||
|
|
fac49b62b6 | ||
|
|
92ae5af019 | ||
|
|
c270e9d622 | ||
|
|
1c5194e2ec | ||
|
|
537d435a28 | ||
|
|
5d76d183b4 | ||
|
|
d4b7ed94e1 | ||
|
|
d978787fcc | ||
|
|
3535270ef0 | ||
|
|
b9eb55a225 | ||
|
|
35b217a0a4 | ||
|
|
c37dc558fb | ||
|
|
7a8f5e10fd | ||
|
|
192fce9153 | ||
|
|
fc4c35e977 | ||
|
|
648c89768b | ||
|
|
e0b0801e87 | ||
|
|
71e7103662 | ||
|
|
dbd723659b | ||
|
|
6b221d5687 | ||
|
|
ce8ef23c09 | ||
|
|
b0159e5100 | ||
|
|
ee6bb9a252 | ||
|
|
866050937a | ||
|
|
41ee65b664 | ||
|
|
7fd37c89b9 | ||
|
|
8d3c9dccc5 | ||
|
|
f5d43b6452 | ||
|
|
c48ee5caef | ||
|
|
2827786886 | ||
|
|
7ad91f3f00 | ||
|
|
24044fa704 | ||
|
|
07fa169fd2 | ||
|
|
8157f0887d | ||
|
|
6dab8c1b89 | ||
|
|
63bd05d40e | ||
|
|
e51851c884 | ||
|
|
e224c4caa4 | ||
|
|
dc857a7ad5 | ||
|
|
aca0e42178 | ||
|
|
15037ce0e5 | ||
|
|
9d55e01e2a | ||
|
|
8eee2bf913 | ||
|
|
3965db9dff | ||
|
|
294453433d | ||
|
|
22f897ed09 | ||
|
|
df7c2cc43f | ||
|
|
f2bf7f9e0d | ||
|
|
69c241ba12 | ||
|
|
59e31ff544 | ||
|
|
62a4d214e8 | ||
|
|
73bd4aca74 | ||
|
|
342ed948ec | ||
|
|
3fc7ad3291 | ||
|
|
a39460d7b2 | ||
|
|
6ab084a338 | ||
|
|
406425a6b9 | ||
|
|
b690b0d20e | ||
|
|
796df302dd | ||
|
|
1346805bef | ||
|
|
16744bc78b | ||
|
|
fc3c12ad90 | ||
|
|
bbcbe9df9f | ||
|
|
1a240f50ae | ||
|
|
47970761c5 | ||
|
|
df7bc8cbb8 | ||
|
|
ba9d744293 | ||
|
|
d5854bb625 | ||
|
|
e5f7690137 | ||
|
|
b0f398caaa | ||
|
|
0a15e7fe9c | ||
|
|
255b9419dd | ||
|
|
c63947b5e2 | ||
|
|
b5d1979d58 | ||
|
|
94aa097bc3 | ||
|
|
72ce7ce374 | ||
|
|
6d64f20ad6 | ||
|
|
506f54e9c3 | ||
|
|
90f5eda930 | ||
|
|
c0450f7df0 | ||
|
|
847ec5b559 | ||
|
|
09ffa101ed | ||
|
|
01e2b56405 | ||
|
|
2f6c763703 | ||
|
|
4caf90c581 | ||
|
|
8fdeb46430 | ||
|
|
07c47426ba | ||
|
|
a6324d6226 | ||
|
|
939406ca9d | ||
|
|
50a24bc3bf | ||
|
|
0dc55fda45 | ||
|
|
dcd8e7863f | ||
|
|
ed283fd3df | ||
|
|
393433b702 | ||
|
|
7516b67a14 | ||
|
|
5eac04d8d4 | ||
|
|
fec41d3567 | ||
|
|
c09734822e | ||
|
|
94887a28c7 | ||
|
|
262efce43e | ||
|
|
99064d17dd | ||
|
|
ed80576236 | ||
|
|
5131eaeba0 | ||
|
|
b559880a80 | ||
|
|
7f06d46827 | ||
|
|
e1a0937a61 | ||
|
|
59c976510c | ||
|
|
f189e299fd | ||
|
|
d08720247a | ||
|
|
a4df76dd3f | ||
|
|
b2e3a91098 | ||
|
|
47bbc35b74 | ||
|
|
a12f3b40d5 | ||
|
|
3386927f9f | ||
|
|
12d75a74e6 | ||
|
|
1a9c5e197d | ||
|
|
8fa85ecc05 | ||
|
|
140ced6a3b | ||
|
|
779f4bda01 | ||
|
|
0021a06468 | ||
|
|
6ead40e14c | ||
|
|
5e2308d14a | ||
|
|
9ab8f358ca | ||
|
|
d9c9b5e1f0 | ||
|
|
730de9187a | ||
|
|
bad0f45816 | ||
|
|
190ea2bbd6 | ||
|
|
91fe414522 | ||
|
|
7fb43cc85f | ||
|
|
d6657ed16c | ||
|
|
48e7c86d66 | ||
|
|
e9c2f2c5fb | ||
|
|
197fd77b99 | ||
|
|
36bef254ff | ||
|
|
8e265905d3 | ||
|
|
ef2fb77c1d | ||
|
|
dd5c2e8767 | ||
|
|
c255999fce | ||
|
|
89341013a0 | ||
|
|
f983110492 | ||
|
|
9fd296f416 | ||
|
|
bb89f85984 | ||
|
|
b573b87a92 | ||
|
|
036adb2de9 | ||
|
|
36f4e30dba | ||
|
|
9f0cfbdfd2 | ||
|
|
e117a7822d | ||
|
|
5489465905 | ||
|
|
4afcdbdb3c | ||
|
|
94d82d4acb | ||
|
|
d528f9f642 | ||
|
|
99d7a474af | ||
|
|
d1052ff666 | ||
|
|
44a91e6ba8 | ||
|
|
3996947024 | ||
|
|
b7afaba56a | ||
|
|
2ce3685e32 | ||
|
|
699bbe9ca2 | ||
|
|
4b890bffde | ||
|
|
789e1ba82b | ||
|
|
1a9614c35e | ||
|
|
d83bdd5164 | ||
|
|
e1a3fc2536 | ||
|
|
251aa7efb6 | ||
|
|
2924b9997a | ||
|
|
2a9aef1332 | ||
|
|
361294d20b | ||
|
|
9a825c5c35 | ||
|
|
944e15fa65 | ||
|
|
d04b1724ec | ||
|
|
e5916b2fae | ||
|
|
4f7cbc3812 | ||
|
|
3c48884dbb | ||
|
|
7ec63afae9 | ||
|
|
8c6b516aa0 | ||
|
|
50c588176c | ||
|
|
3770aac1af | ||
|
|
256dccc554 | ||
|
|
d0f65906ed | ||
|
|
95aa61cfe5 | ||
|
|
247691ca44 | ||
|
|
0fc9cc65d1 | ||
|
|
eb69225444 | ||
|
|
cafe68a92d | ||
|
|
723cccdae8 | ||
|
|
6b8044e92c | ||
|
|
1e7e8202af | ||
|
|
c0fdf7bd39 | ||
|
|
034b66fedb | ||
|
|
eed274c632 | ||
|
|
5b10c4811f | ||
|
|
2bd6e3d0a5 | ||
|
|
d0b5bcf26a | ||
|
|
262248d8a6 | ||
|
|
9eb3697b40 | ||
|
|
c246897af1 | ||
|
|
cfcabce593 | ||
|
|
e517061010 | ||
|
|
feb8ad7b4c | ||
|
|
1b5bf6e12a | ||
|
|
e953a32a82 | ||
|
|
f1390b3cb6 | ||
|
|
6e485df084 | ||
|
|
3a342fb25d | ||
|
|
e71e82f8ac | ||
|
|
da80eca28c | ||
|
|
1d1e23611b | ||
|
|
74e067c6e6 | ||
|
|
85b9619799 | ||
|
|
ab1fbc96c3 | ||
|
|
a04143e2a7 | ||
|
|
6c4299039a | ||
|
|
655d347ea2 | ||
|
|
94a3164248 | ||
|
|
18728a64b9 | ||
|
|
d8b0fa294e | ||
|
|
a6c8319b5d | ||
|
|
5d92f12f8e | ||
|
|
c0231bdb70 | ||
|
|
ac541e208f | ||
|
|
3d8ce448b8 | ||
|
|
949df97726 | ||
|
|
14cbe40543 | ||
|
|
9dd53ecdaa | ||
|
|
6bfe5e049d | ||
|
|
b672861ffd | ||
|
|
b081077f2b | ||
|
|
13a296049b | ||
|
|
22c531dea7 | ||
|
|
dfc74e2a77 | ||
|
|
0c12db06ec | ||
|
|
edf6b56016 | ||
|
|
8b4ed0c1a8 | ||
|
|
1b5335f409 | ||
|
|
3a2c9c1016 | ||
|
|
cf3eed2cda | ||
|
|
2ecd366905 | ||
|
|
d34dc45b78 | ||
|
|
8394e84099 | ||
|
|
adda3a7f79 | ||
|
|
52d0f4d9e7 | ||
|
|
c1a38d787d | ||
|
|
7879dfd3fd | ||
|
|
cd1c8b2f09 | ||
|
|
7a9228ad75 | ||
|
|
98ceb62202 | ||
|
|
b99bb64487 | ||
|
|
580affa5f3 | ||
|
|
d600b3498b | ||
|
|
f0eaf84cb9 | ||
|
|
257a171c0c | ||
|
|
3c5e818b49 | ||
|
|
c3c8395cef | ||
|
|
38c008e527 | ||
|
|
a3d8f7d113 | ||
|
|
65a642097c | ||
|
|
dff9aa6f0c | ||
|
|
ab145b5365 | ||
|
|
5878fe3834 | ||
|
|
715e29d7ba | ||
|
|
983337401c | ||
|
|
8251dec587 | ||
|
|
52f994cf04 | ||
|
|
3d4b5cfbfe | ||
|
|
33b057bfaf | ||
|
|
629fe771df | ||
|
|
5e6f175b5f | ||
|
|
6dc1a404d5 | ||
|
|
d1d4f47764 | ||
|
|
3abce4259f | ||
|
|
c78b952c3d | ||
|
|
fff5e51426 | ||
|
|
a724aa5717 | ||
|
|
10725136d8 | ||
|
|
8f1793dd06 | ||
|
|
fd85be2c9e | ||
|
|
24959209cc | ||
|
|
29c9763feb | ||
|
|
ca7151aeb1 | ||
|
|
6e932794f7 | ||
|
|
9e1dfcfb37 | ||
|
|
5166b2c1a8 | ||
|
|
80991f1521 | ||
|
|
b752a86589 | ||
|
|
c2acceba4e | ||
|
|
f8ee52ca2a | ||
|
|
2b245bdf9e | ||
|
|
d7e01a23f8 | ||
|
|
4e20be9c66 | ||
|
|
94e15a9985 | ||
|
|
5061875fa9 | ||
|
|
d9782b2dd1 | ||
|
|
530afba5cb | ||
|
|
4a90a7691b | ||
|
|
050f81e37c | ||
|
|
aecaf665f1 | ||
|
|
23a8938809 | ||
|
|
641c773476 | ||
|
|
97be5b4cfb | ||
|
|
76b7d0095d | ||
|
|
352ad7a38c | ||
|
|
401ea4e7a8 | ||
|
|
710cd38591 | ||
|
|
859d4bb98b | ||
|
|
a3374ac51d | ||
|
|
168b1909ae | ||
|
|
a96ab41739 | ||
|
|
0f5a56b3c2 | ||
|
|
c1d9e5156f | ||
|
|
b0ec54b6f7 | ||
|
|
2360bf0eb9 | ||
|
|
1a1a61a672 | ||
|
|
13c7380113 | ||
|
|
3a48172332 | ||
|
|
14e778b3de | ||
|
|
9164125177 | ||
|
|
2595f89519 | ||
|
|
a058c40dfb | ||
|
|
fc4b3d2771 | ||
|
|
59cc9c9b68 | ||
|
|
d1a52d2b1a | ||
|
|
5f5fbb3ea4 | ||
|
|
2d98071e55 | ||
|
|
6a3d1d06b5 | ||
|
|
6813cb86a2 | ||
|
|
f5cf9b60b2 | ||
|
|
7cec029f03 | ||
|
|
4fbad941cb | ||
|
|
d4bff56a61 | ||
|
|
f430b82b43 | ||
|
|
71533791dd | ||
|
|
65f3411320 | ||
|
|
b92651070f | ||
|
|
5be8a37b7e | ||
|
|
044c348faf | ||
|
|
465c7d569c | ||
|
|
2ca0e7954a | ||
|
|
96a92a73f1 | ||
|
|
48e7b4b0a6 | ||
|
|
8c16961afd | ||
|
|
6e9983fc6a | ||
|
|
bf87f897d7 | ||
|
|
a00ec9d1f8 | ||
|
|
be1ba818e4 | ||
|
|
b0ac88fd06 | ||
|
|
a68b4e6dde | ||
|
|
348ba0818f | ||
|
|
f79e0e588e | ||
|
|
3e7360c2c6 | ||
|
|
e6016bd5b4 | ||
|
|
343d3bb556 | ||
|
|
17b9801ec9 | ||
|
|
e550451c69 | ||
|
|
29f7b78deb | ||
|
|
431ce67f85 | ||
|
|
ba66c849b5 | ||
|
|
9550387e39 | ||
|
|
b06d37f885 | ||
|
|
bf47aa97b4 | ||
|
|
8e42d6fbb3 | ||
|
|
c07e96cf2b | ||
|
|
c2cd55e010 | ||
|
|
fb31b5fff7 | ||
|
|
41bacae171 | ||
|
|
193558a4bc | ||
|
|
e38e866626 | ||
|
|
c709251f21 | ||
|
|
9d1383ba26 | ||
|
|
c66e18c913 | ||
|
|
75c430635b | ||
|
|
b789eca421 | ||
|
|
53a5dfe26b | ||
|
|
3ed9e16558 | ||
|
|
ff1496a6a5 | ||
|
|
d7c714e1c6 | ||
|
|
d7e2a77907 | ||
|
|
07fd01ad46 | ||
|
|
496a1d3bfe | ||
|
|
0860b7ed73 | ||
|
|
229b59bd6e | ||
|
|
05e15e27ef | ||
|
|
51131813a3 | ||
|
|
1327e293f6 | ||
|
|
2edc372f41 | ||
|
|
d368e2fca9 | ||
|
|
b9c8e3e057 | ||
|
|
5d4210ceb3 | ||
|
|
4a6897ef3b | ||
|
|
fbff8983e4 | ||
|
|
6431d52a2e | ||
|
|
7faba11245 | ||
|
|
4723345473 | ||
|
|
b310516ba7 | ||
|
|
049a10136c | ||
|
|
511a9beede | ||
|
|
a8bc51b229 | ||
|
|
6bad9484be | ||
|
|
d52f73b29a | ||
|
|
edf8f14ac0 | ||
|
|
9faa7e47ed | ||
|
|
f92317c5ee | ||
|
|
fea3bc2eb1 | ||
|
|
2b89494405 | ||
|
|
2bac1c10b0 | ||
|
|
5126649de4 | ||
|
|
690a7d6dd7 | ||
|
|
19c0c9bf06 | ||
|
|
53958cf9df | ||
|
|
f94ce7767c | ||
|
|
62607f4f04 | ||
|
|
e4e9f0bc19 | ||
|
|
e0637990aa | ||
|
|
03842f92e8 | ||
|
|
8a2bbeb1eb | ||
|
|
95681246a5 | ||
|
|
80279f71d8 | ||
|
|
ae7573b9b8 | ||
|
|
ee880ca7be | ||
|
|
21837ac132 | ||
|
|
c270c13baa | ||
|
|
cbdeff99ee | ||
|
|
303e0cfd86 | ||
|
|
880bc0d8e3 | ||
|
|
89d2653662 | ||
|
|
8b75f7c7d3 | ||
|
|
9967d706e9 | ||
|
|
d5bc521ab0 | ||
|
|
d4f76ba5f9 | ||
|
|
239da2ef69 | ||
|
|
1d18d747a5 | ||
|
|
2883b5be6b | ||
|
|
e34b433d93 | ||
|
|
b6d0b8468b | ||
|
|
5e5bd939be | ||
|
|
140ccc7d4e | ||
|
|
b4cbcbefa6 | ||
|
|
9049f4e378 | ||
|
|
0e58c05361 | ||
|
|
818516f150 | ||
|
|
2abf25d827 | ||
|
|
837593e1ab | ||
|
|
274728e5fb | ||
|
|
51fb2e6149 | ||
|
|
cc29b21bfc | ||
|
|
00f5c3bb85 | ||
|
|
33795912ff | ||
|
|
92a2deb979 | ||
|
|
40fad23e0b | ||
|
|
6c8472dd67 | ||
|
|
a9838e9116 | ||
|
|
01d0e49a1c | ||
|
|
ddc721ec8a | ||
|
|
5035a10cbe | ||
|
|
fae387168f | ||
|
|
5d71c33cd7 | ||
|
|
9a5a021f91 | ||
|
|
3e7e6e7656 | ||
|
|
1bab14213d | ||
|
|
db396b81ef | ||
|
|
f60621ee1b | ||
|
|
7c9c55785d | ||
|
|
8f8b0bbd16 | ||
|
|
ddf6819a75 | ||
|
|
ee49e7055b | ||
|
|
ac90e0e939 | ||
|
|
ea93c01dfb | ||
|
|
cf18a3141f | ||
|
|
2ebec04811 | ||
|
|
b43b007b92 | ||
|
|
33aada05a4 | ||
|
|
0bdd8637db | ||
|
|
e8472be6d5 | ||
|
|
84667636a2 | ||
|
|
df9f66d437 | ||
|
|
ae67d55bf2 | ||
|
|
18525554ed | ||
|
|
64513e8d6f | ||
|
|
deb7f3c5b6 | ||
|
|
48eb5e5c82 | ||
|
|
65071aafb0 | ||
|
|
4ee87a7029 | ||
|
|
977ec7c941 | ||
|
|
40d04a076c | ||
|
|
4646ac85b0 | ||
|
|
8773bad99a | ||
|
|
084db337a0 | ||
|
|
f47f075f02 | ||
|
|
7abc4fbf3a | ||
|
|
855a9c623c | ||
|
|
7e2d86c510 | ||
|
|
405079f744 | ||
|
|
fc1bbb45b1 | ||
|
|
24a6d1d836 | ||
|
|
f3d273864d | ||
|
|
ce8ef7afe7 | ||
|
|
62bba1684b | ||
|
|
07f3c78369 | ||
|
|
cbfbb9899d | ||
|
|
b428988ef6 | ||
|
|
4c72598e68 | ||
|
|
c6e19e34f7 | ||
|
|
c0dbb1c2ec | ||
|
|
4bec39535f | ||
|
|
7c1ec74cf6 | ||
|
|
8b77b51c15 | ||
|
|
7070e06ac6 | ||
|
|
5b9c228cf8 | ||
|
|
15e8c9ffbb | ||
|
|
caccf96d3f | ||
|
|
720cc192bb | ||
|
|
573fae089f | ||
|
|
83cd16e373 | ||
|
|
74fb400fef | ||
|
|
a07b00606b | ||
|
|
65f23583ae | ||
|
|
c8a58f2547 | ||
|
|
56c6efdfce | ||
|
|
8ed86ed551 | ||
|
|
d2f4c81d62 | ||
|
|
b20190da98 | ||
|
|
5d9a5a0c84 | ||
|
|
bb7613f37b | ||
|
|
9bd54d7be2 | ||
|
|
5c8fac5993 | ||
|
|
3e2fd6a2a1 | ||
|
|
f36dd414a0 | ||
|
|
af1b0ed088 | ||
|
|
feafea2c6d | ||
|
|
c4f5ed839f | ||
|
|
b4c905dc83 | ||
|
|
804e2cdcb1 | ||
|
|
0bb5e48f53 | ||
|
|
21528f08d4 | ||
|
|
b1e7f548f4 | ||
|
|
cdcea98290 | ||
|
|
c06456da37 | ||
|
|
7b31fdf6f6 | ||
|
|
e38b403b14 | ||
|
|
ee0c4bf690 | ||
|
|
8c583d1bb2 | ||
|
|
ea4753c49a | ||
|
|
3956d85a8c | ||
|
|
8a0071d9c1 | ||
|
|
5db6c9f51b | ||
|
|
f5f9357736 | ||
|
|
bdc6b47e1f | ||
|
|
3669236aa1 | ||
|
|
207e83ac2f | ||
|
|
3c5e334d9d | ||
|
|
a8e275a432 | ||
|
|
887a30e327 | ||
|
|
7a1fb3a8d2 | ||
|
|
d4000e07a9 | ||
|
|
a3d024e11d | ||
|
|
b92e998929 | ||
|
|
f448a841c5 | ||
|
|
c9c844c279 | ||
|
|
342f187318 | ||
|
|
d063f0e00c | ||
|
|
0614e2c590 | ||
|
|
431b3dc2b2 | ||
|
|
544cd884ee | ||
|
|
c6efb45585 | ||
|
|
38008a87e8 | ||
|
|
059d240824 | ||
|
|
7a4b69edc0 | ||
|
|
8cab05feb4 | ||
|
|
892677a9d3 | ||
|
|
00a1835fae | ||
|
|
7888027425 | ||
|
|
0760ea1b00 | ||
|
|
31f0907732 | ||
|
|
9bec059cc7 | ||
|
|
f600fa8bf3 | ||
|
|
c6e91db32f | ||
|
|
b101118d1e | ||
|
|
30ea4508c3 | ||
|
|
3c91315426 | ||
|
|
bf8875d930 | ||
|
|
93b9b6fd9f | ||
|
|
f96a1a0b35 | ||
|
|
8de07ccf65 | ||
|
|
ff9fa5661d | ||
|
|
17b9cc430c | ||
|
|
d8a2a0f003 | ||
|
|
490742b892 | ||
|
|
3fa80cd974 | ||
|
|
9ede185d4b | ||
|
|
aaf90639a0 | ||
|
|
5ba7040df2 | ||
|
|
3d411ed0bb | ||
|
|
dd1f8934ad | ||
|
|
febcbcddb9 | ||
|
|
88c74d67f6 | ||
|
|
0fb915e57e | ||
|
|
e0a21e3df4 | ||
|
|
45b7bd4361 | ||
|
|
36002f95ed | ||
|
|
0a92cbfa4d | ||
|
|
5b1fd64708 | ||
|
|
9b289b6f3b | ||
|
|
6d0702e607 | ||
|
|
38478ea504 | ||
|
|
a39db86651 | ||
|
|
853d8ad280 | ||
|
|
17c6ae067a | ||
|
|
770e78fdce | ||
|
|
e5065bed16 | ||
|
|
c0676e3fa3 | ||
|
|
d4f3ed1840 | ||
|
|
9ed6538693 | ||
|
|
ff65a3e1b0 | ||
|
|
ebf9bf387c | ||
|
|
fdc1e0f2e1 | ||
|
|
f3eff9a389 | ||
|
|
2857631e90 | ||
|
|
89cd7d8db0 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,3 +1,8 @@
|
||||
*.egg-info
|
||||
*.pyc
|
||||
.tox
|
||||
/build
|
||||
/dist
|
||||
/docs/_site
|
||||
/venv
|
||||
fig.spec
|
||||
|
||||
11
.travis.yml
11
.travis.yml
@@ -1,11 +0,0 @@
|
||||
language: python
|
||||
python:
|
||||
- "2.6"
|
||||
- "2.7"
|
||||
- "3.2"
|
||||
- "3.3"
|
||||
install:
|
||||
- python setup.py install
|
||||
- pip install nose==1.3.0
|
||||
script: nosetests
|
||||
|
||||
233
CHANGES.md
Normal file
233
CHANGES.md
Normal file
@@ -0,0 +1,233 @@
|
||||
Change log
|
||||
==========
|
||||
|
||||
1.0.0 (2014-10-07)
|
||||
------------------
|
||||
|
||||
The highlights:
|
||||
|
||||
- [Fig has joined Docker.](https://www.orchardup.com/blog/orchard-is-joining-docker) Fig will continue to be maintained, but we'll also be incorporating the best bits of Fig into Docker itself.
|
||||
|
||||
This means the GitHub repository has moved to [https://github.com/docker/fig](https://github.com/docker/fig) and our IRC channel is now #docker-fig on Freenode.
|
||||
|
||||
- Fig can be used with the [official Docker OS X installer](https://docs.docker.com/installation/mac/). Boot2Docker will mount the home directory from your host machine so volumes work as expected.
|
||||
|
||||
- Fig supports Docker 1.3.
|
||||
|
||||
- It is now possible to connect to the Docker daemon using TLS by using the `DOCKER_CERT_PATH` and `DOCKER_TLS_VERIFY` environment variables.
|
||||
|
||||
- There is a new `fig port` command which outputs the host port binding of a service, in a similar way to `docker port`.
|
||||
|
||||
- There is a new `fig pull` command which pulls the latest images for a service.
|
||||
|
||||
- There is a new `fig restart` command which restarts a service's containers.
|
||||
|
||||
- Fig creates multiple containers in service by appending a number to the service name (e.g. `db_1`, `db_2`, etc). As a convenience, Fig will now give the first container an alias of the service name (e.g. `db`).
|
||||
|
||||
This link alias is also a valid hostname and added to `/etc/hosts` so you can connect to linked services using their hostname. For example, instead of resolving the environment variables `DB_PORT_5432_TCP_ADDR` and `DB_PORT_5432_TCP_PORT`, you could just use the hostname `db` and port `5432` directly.
|
||||
|
||||
- Volume definitions now support `ro` mode, expanding `~` and expanding environment variables.
|
||||
|
||||
- `.dockerignore` is supported when building.
|
||||
|
||||
- The project name can be set with the `FIG_PROJECT_NAME` environment variable.
|
||||
|
||||
- The `--env` and `--entrypoint` options have been added to `fig run`.
|
||||
|
||||
- The Fig binary for Linux is now linked against an older version of glibc so it works on CentOS 6 and Debian Wheezy.
|
||||
|
||||
Other things:
|
||||
|
||||
- `fig ps` now works on Jenkins and makes fewer API calls to the Docker daemon.
|
||||
- `--verbose` displays more useful debugging output.
|
||||
- When starting a service where `volumes_from` points to a service without any containers running, that service will now be started.
|
||||
- Lots of docs improvements. Notably, environment variables are documented and official repositories are used throughout.
|
||||
|
||||
0.5.2 (2014-07-28)
|
||||
------------------
|
||||
|
||||
- Added a `--no-cache` option to `fig build`, which bypasses the cache just like `docker build --no-cache`.
|
||||
- Fixed the `dns:` fig.yml option, which was causing fig to error out.
|
||||
- Fixed a bug where fig couldn't start under Python 2.6.
|
||||
- Fixed a log-streaming bug that occasionally caused fig to exit.
|
||||
|
||||
Thanks @dnephin and @marksteve!
|
||||
|
||||
|
||||
0.5.1 (2014-07-11)
|
||||
------------------
|
||||
|
||||
- If a service has a command defined, `fig run [service]` with no further arguments will run it.
|
||||
- The project name now defaults to the directory containing fig.yml, not the current working directory (if they're different)
|
||||
- `volumes_from` now works properly with containers as well as services
|
||||
- Fixed a race condition when recreating containers in `fig up`
|
||||
|
||||
Thanks @ryanbrainard and @d11wtq!
|
||||
|
||||
|
||||
0.5.0 (2014-07-11)
|
||||
------------------
|
||||
|
||||
- Fig now starts links when you run `fig run` or `fig up`.
|
||||
|
||||
For example, if you have a `web` service which depends on a `db` service, `fig run web ...` will start the `db` service.
|
||||
|
||||
- Environment variables can now be resolved from the environment that Fig is running in. Just specify it as a blank variable in your `fig.yml` and, if set, it'll be resolved:
|
||||
```
|
||||
environment:
|
||||
RACK_ENV: development
|
||||
SESSION_SECRET:
|
||||
```
|
||||
|
||||
- `volumes_from` is now supported in `fig.yml`. All of the volumes from the specified services and containers will be mounted:
|
||||
|
||||
```
|
||||
volumes_from:
|
||||
- service_name
|
||||
- container_name
|
||||
```
|
||||
|
||||
- A host address can now be specified in `ports`:
|
||||
|
||||
```
|
||||
ports:
|
||||
- "0.0.0.0:8000:8000"
|
||||
- "127.0.0.1:8001:8001"
|
||||
```
|
||||
|
||||
- The `net` and `workdir` options are now supported in `fig.yml`.
|
||||
- The `hostname` option now works in the same way as the Docker CLI, splitting out into a `domainname` option.
|
||||
- TTY behaviour is far more robust, and resizes are supported correctly.
|
||||
- Load YAML files safely.
|
||||
|
||||
Thanks to @d11wtq, @ryanbrainard, @rail44, @j0hnsmith, @binarin, @Elemecca, @mozz100 and @marksteve for their help with this release!
|
||||
|
||||
|
||||
0.4.2 (2014-06-18)
|
||||
------------------
|
||||
|
||||
- Fix various encoding errors when using `fig run`, `fig up` and `fig build`.
|
||||
|
||||
0.4.1 (2014-05-08)
|
||||
------------------
|
||||
|
||||
- Add support for Docker 0.11.0. (Thanks @marksteve!)
|
||||
- Make project name configurable. (Thanks @jefmathiot!)
|
||||
- Return correct exit code from `fig run`.
|
||||
|
||||
0.4.0 (2014-04-29)
|
||||
------------------
|
||||
|
||||
- Support Docker 0.9 and 0.10
|
||||
- Display progress bars correctly when pulling images (no more ski slopes)
|
||||
- `fig up` now stops all services when any container exits
|
||||
- Added support for the `privileged` config option in fig.yml (thanks @kvz!)
|
||||
- Shortened and aligned log prefixes in `fig up` output
|
||||
- Only containers started with `fig run` link back to their own service
|
||||
- Handle UTF-8 correctly when streaming `fig build/run/up` output (thanks @mauvm and @shanejonas!)
|
||||
- Error message improvements
|
||||
|
||||
0.3.2 (2014-03-05)
|
||||
------------------
|
||||
|
||||
- Added an `--rm` option to `fig run`. (Thanks @marksteve!)
|
||||
- Added an `expose` option to `fig.yml`.
|
||||
|
||||
0.3.1 (2014-03-04)
|
||||
------------------
|
||||
|
||||
- Added contribution instructions. (Thanks @kvz!)
|
||||
- Fixed `fig rm` throwing an error.
|
||||
- Fixed a bug in `fig ps` on Docker 0.8.1 when there is a container with no command.
|
||||
|
||||
0.3.0 (2014-03-03)
|
||||
------------------
|
||||
|
||||
- We now ship binaries for OS X and Linux. No more having to install with Pip!
|
||||
- Add `-f` flag to specify alternate `fig.yml` files
|
||||
- Add support for custom link names
|
||||
- Fix a bug where recreating would sometimes hang
|
||||
- Update docker-py to support Docker 0.8.0.
|
||||
- Various documentation improvements
|
||||
- Various error message improvements
|
||||
|
||||
Thanks @marksteve, @Gazler and @teozkr!
|
||||
|
||||
0.2.2 (2014-02-17)
|
||||
------------------
|
||||
|
||||
- Resolve dependencies using Cormen/Tarjan topological sort
|
||||
- Fix `fig up` not printing log output
|
||||
- Stop containers in reverse order to starting
|
||||
- Fix scale command not binding ports
|
||||
|
||||
Thanks to @barnybug and @dustinlacewell for their work on this release.
|
||||
|
||||
0.2.1 (2014-02-04)
|
||||
------------------
|
||||
|
||||
- General improvements to error reporting (#77, #79)
|
||||
|
||||
0.2.0 (2014-01-31)
|
||||
------------------
|
||||
|
||||
- Link services to themselves so run commands can access the running service. (#67)
|
||||
- Much better documentation.
|
||||
- Make service dependency resolution more reliable. (#48)
|
||||
- Load Fig configurations with a `.yaml` extension. (#58)
|
||||
|
||||
Big thanks to @cameronmaske, @mrchrisadams and @damianmoore for their help with this release.
|
||||
|
||||
0.1.4 (2014-01-27)
|
||||
------------------
|
||||
|
||||
- Add a link alias without the project name. This makes the environment variables a little shorter: `REDIS_1_PORT_6379_TCP_ADDR`. (#54)
|
||||
|
||||
0.1.3 (2014-01-23)
|
||||
------------------
|
||||
|
||||
- Fix ports sometimes being configured incorrectly. (#46)
|
||||
- Fix log output sometimes not displaying. (#47)
|
||||
|
||||
0.1.2 (2014-01-22)
|
||||
------------------
|
||||
|
||||
- Add `-T` option to `fig run` to disable pseudo-TTY. (#34)
|
||||
- Fix `fig up` requiring the ubuntu image to be pulled to recreate containers. (#33) Thanks @cameronmaske!
|
||||
- Improve reliability, fix arrow keys and fix a race condition in `fig run`. (#34, #39, #40)
|
||||
|
||||
0.1.1 (2014-01-17)
|
||||
------------------
|
||||
|
||||
- Fix bug where ports were not exposed correctly (#29). Thanks @dustinlacewell!
|
||||
|
||||
0.1.0 (2014-01-16)
|
||||
------------------
|
||||
|
||||
- Containers are recreated on each `fig up`, ensuring config is up-to-date with `fig.yml` (#2)
|
||||
- Add `fig scale` command (#9)
|
||||
- Use `DOCKER_HOST` environment variable to find Docker daemon, for consistency with the official Docker client (was previously `DOCKER_URL`) (#19)
|
||||
- Truncate long commands in `fig ps` (#18)
|
||||
- Fill out CLI help banners for commands (#15, #16)
|
||||
- Show a friendlier error when `fig.yml` is missing (#4)
|
||||
- Fix bug with `fig build` logging (#3)
|
||||
- Fix bug where builds would time out if a step took a long time without generating output (#6)
|
||||
- Fix bug where streaming container output over the Unix socket raised an error (#7)
|
||||
|
||||
Big thanks to @tomstuart, @EnTeQuAk, @schickling, @aronasorman and @GeoffreyPlitt.
|
||||
|
||||
0.0.2 (2014-01-02)
|
||||
------------------
|
||||
|
||||
- Improve documentation
|
||||
- Try to connect to Docker on `tcp://localdocker:4243` and a UNIX socket in addition to `localhost`.
|
||||
- Improve `fig up` behaviour
|
||||
- Add confirmation prompt to `fig rm`
|
||||
- Add `fig build` command
|
||||
|
||||
0.0.1 (2013-12-20)
|
||||
------------------
|
||||
|
||||
Initial release.
|
||||
|
||||
|
||||
98
CONTRIBUTING.md
Normal file
98
CONTRIBUTING.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# Contributing to Fig
|
||||
|
||||
## Development environment
|
||||
|
||||
If you're looking contribute to [Fig](http://www.fig.sh/)
|
||||
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.
|
||||
|
||||
## Running the test suite
|
||||
|
||||
$ script/test
|
||||
|
||||
## Building binaries
|
||||
|
||||
Linux:
|
||||
|
||||
$ script/build-linux
|
||||
|
||||
OS X:
|
||||
|
||||
$ script/build-osx
|
||||
|
||||
Note that this only works on Mountain Lion, not Mavericks, due to a [bug in PyInstaller](http://www.pyinstaller.org/ticket/807).
|
||||
|
||||
## Sign your work
|
||||
|
||||
The sign-off is a simple line at the end of the explanation for the
|
||||
patch, which certifies that you wrote it or otherwise have the right to
|
||||
pass it on as an open-source patch. The rules are pretty simple: if you
|
||||
can certify the below (from [developercertificate.org](http://developercertificate.org/)):
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
|
||||
then you just add a line saying
|
||||
|
||||
Signed-off-by: Random J Developer <random@developer.example.org>
|
||||
|
||||
using your real name (sorry, no pseudonyms or anonymous contributions.)
|
||||
|
||||
The easiest way to do this is to use the `--signoff` flag when committing. E.g.:
|
||||
|
||||
|
||||
$ git commit --signoff
|
||||
|
||||
|
||||
## Release process
|
||||
|
||||
1. Open pull request that:
|
||||
|
||||
- Updates version in `fig/__init__.py`
|
||||
- Updates version in `docs/install.md`
|
||||
- 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`.
|
||||
|
||||
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
|
||||
15
Dockerfile
Normal file
15
Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM debian:wheezy
|
||||
RUN apt-get update -qq && apt-get install -qy python python-pip python-dev git && apt-get clean
|
||||
RUN useradd -d /home/user -m -s /bin/bash user
|
||||
WORKDIR /code/
|
||||
|
||||
ADD requirements.txt /code/
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
ADD requirements-dev.txt /code/
|
||||
RUN pip install -r requirements-dev.txt
|
||||
|
||||
ADD . /code/
|
||||
RUN python setup.py install
|
||||
|
||||
RUN chown -R user /code/
|
||||
215
LICENSE
215
LICENSE
@@ -1,24 +1,191 @@
|
||||
Copyright (c) 2013, Orchard Laboratories Ltd.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
* The names of its contributors may not be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2014 Docker, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
4
MAINTAINERS
Normal file
4
MAINTAINERS
Normal file
@@ -0,0 +1,4 @@
|
||||
Aanand Prasad <aanand.prasad@gmail.com> (@aanand)
|
||||
Ben Firshman <ben@firshman.co.uk> (@bfirsh)
|
||||
Chris Corbyn <chris@w3style.co.uk> (@d11wtq)
|
||||
Nathan LeClaire <nathan.leclaire@gmail.com> (@nathanleclaire)
|
||||
@@ -1,3 +1,10 @@
|
||||
include Dockerfile
|
||||
include LICENSE
|
||||
include *.md
|
||||
include requirements.txt
|
||||
include requirements-dev.txt
|
||||
include tox.ini
|
||||
include *.md
|
||||
recursive-exclude tests *
|
||||
global-exclude *.pyc
|
||||
global-exclude *.pyo
|
||||
global-exclude *.un~
|
||||
|
||||
167
README.md
167
README.md
@@ -1,163 +1,46 @@
|
||||
Fig
|
||||
====
|
||||
===
|
||||
|
||||
Punctual, lightweight development environments using Docker.
|
||||
[](https://app.wercker.com/project/bykey/d5dbac3907301c3d5ce735e2d5e95a5b)
|
||||
|
||||
Fig is tool for defining and running isolated application environments. It uses simple, version-controllable YAML configuration files that look something like this:
|
||||
Fast, isolated development environments using Docker.
|
||||
|
||||
```yaml
|
||||
web:
|
||||
build: .
|
||||
links:
|
||||
- db
|
||||
ports:
|
||||
- 8000:8000
|
||||
db:
|
||||
image: orchardup/postgresql
|
||||
```
|
||||
Define your app's environment with Docker so it can be reproduced anywhere:
|
||||
|
||||
Installing
|
||||
----------
|
||||
|
||||
```bash
|
||||
$ sudo pip install fig
|
||||
```
|
||||
|
||||
Defining your app
|
||||
-----------------
|
||||
|
||||
Put a `fig.yml` in your app's directory. Each top-level key defines a "service", such as a web app, database or cache. For each service, Fig will start a Docker container, so at minimum it needs to know what image to use.
|
||||
|
||||
The simplest way to get started is to just give it an image name:
|
||||
|
||||
```yaml
|
||||
db:
|
||||
image: orchardup/postgresql
|
||||
```
|
||||
|
||||
You've now given Fig the minimal amount of configuration it needs to run:
|
||||
|
||||
```bash
|
||||
$ fig up
|
||||
Pulling image orchardup/postgresql...
|
||||
Starting myapp_db_1...
|
||||
myapp_db_1 is running at 127.0.0.1:45678
|
||||
<...output from postgresql server...>
|
||||
```
|
||||
|
||||
For each service you've defined, Fig will start a Docker container with the specified image, building or pulling it if necessary. You now have a PostgreSQL server running at `127.0.0.1:45678`.
|
||||
|
||||
By default, `fig up` will run until each container has shut down, and relay their output to the terminal. To run in the background instead, pass the `-d` flag:
|
||||
|
||||
```bash
|
||||
$ fig up -d
|
||||
Starting myapp_db_1... done
|
||||
myapp_db_1 is running at 127.0.0.1:45678
|
||||
|
||||
$ fig ps
|
||||
Name State Ports
|
||||
------------------------------------
|
||||
myapp_db_1 Up 5432->45678/tcp
|
||||
```
|
||||
|
||||
### Building services
|
||||
|
||||
Fig can automatically build images for you if your service specifies a directory with a `Dockerfile` in it (or a Git URL, as per the `docker build` command).
|
||||
|
||||
This example will build an image with `app.py` inside it:
|
||||
|
||||
#### app.py
|
||||
|
||||
```python
|
||||
print "Hello world!"
|
||||
```
|
||||
|
||||
#### fig.yml
|
||||
|
||||
```yaml
|
||||
web:
|
||||
build: .
|
||||
```
|
||||
|
||||
#### Dockerfile
|
||||
|
||||
FROM ubuntu:12.04
|
||||
RUN apt-get install python
|
||||
ADD . /opt
|
||||
WORKDIR /opt
|
||||
FROM python:2.7
|
||||
ADD . /code
|
||||
WORKDIR /code
|
||||
RUN pip install -r requirements.txt
|
||||
CMD python app.py
|
||||
|
||||
|
||||
|
||||
### Getting your code in
|
||||
|
||||
If you want to work on an application being run by Fig, you probably don't want to have to rebuild your image every time you make a change. To solve this, you can share the directory with the container using a volume so the changes are reflected immediately:
|
||||
Define the services that make up your app so they can be run together in an isolated environment:
|
||||
|
||||
```yaml
|
||||
web:
|
||||
build: .
|
||||
volumes:
|
||||
- .:/opt
|
||||
```
|
||||
|
||||
|
||||
### Communicating between containers
|
||||
|
||||
Your web app will probably need to talk to your database. You can use [Docker links](http://docs.docker.io/en/latest/use/port_redirection/#linking-a-container) to enable containers to communicate, pass in the right IP address and port via environment variables:
|
||||
|
||||
```yaml
|
||||
db:
|
||||
image: orchardup/postgresql
|
||||
|
||||
web:
|
||||
build: .
|
||||
links:
|
||||
- db
|
||||
```
|
||||
|
||||
This will pass an environment variable called `MYAPP_DB_1_PORT` into the web container, whose value will look like `tcp://172.17.0.4:45678`. Your web app's code can use that to connect to the database. To see all of the environment variables available, run `env` inside a container:
|
||||
|
||||
```bash
|
||||
$ fig up -d db
|
||||
$ fig run web env
|
||||
```
|
||||
|
||||
|
||||
### Container configuration options
|
||||
|
||||
You can pass extra configuration options to a container, much like with `docker run`:
|
||||
|
||||
```yaml
|
||||
web:
|
||||
build: .
|
||||
|
||||
-- override the default command
|
||||
command: bundle exec thin -p 3000
|
||||
|
||||
-- expose ports, optionally specifying both host and container ports (a random host port will be chosen otherwise)
|
||||
ports:
|
||||
- 3000
|
||||
- 8000:8000
|
||||
|
||||
-- map volumes
|
||||
volumes:
|
||||
- cache/:/tmp/cache
|
||||
|
||||
-- add environment variables
|
||||
environment:
|
||||
RACK_ENV: development
|
||||
- "8000:8000"
|
||||
- "49100:22"
|
||||
db:
|
||||
image: postgres
|
||||
```
|
||||
|
||||
(No more installing Postgres on your laptop!)
|
||||
|
||||
Running a one-off command
|
||||
-------------------------
|
||||
Then type `fig up`, and Fig will start and run your entire app:
|
||||
|
||||
If you want to run a management command, use `fig run` to start a one-off container:
|
||||

|
||||
|
||||
```bash
|
||||
$ fig run db createdb myapp_development
|
||||
$ fig run web rake db:migrate
|
||||
$ fig run web bash
|
||||
```
|
||||
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
|
||||
|
||||
Installation and documentation
|
||||
------------------------------
|
||||
|
||||
Full documentation is available on [Fig's website](http://www.fig.sh/).
|
||||
|
||||
3
bin/fig
Executable file
3
bin/fig
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env python
|
||||
from fig.cli.main import main
|
||||
main()
|
||||
1
docs/.gitignore-gh-pages
Normal file
1
docs/.gitignore-gh-pages
Normal file
@@ -0,0 +1 @@
|
||||
/_site
|
||||
1
docs/CNAME
Normal file
1
docs/CNAME
Normal file
@@ -0,0 +1 @@
|
||||
www.fig.sh
|
||||
10
docs/Dockerfile
Normal file
10
docs/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
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
|
||||
3
docs/Gemfile
Normal file
3
docs/Gemfile
Normal file
@@ -0,0 +1,3 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem 'github-pages'
|
||||
62
docs/Gemfile.lock
Normal file
62
docs/Gemfile.lock
Normal file
@@ -0,0 +1,62 @@
|
||||
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
|
||||
3
docs/_config.yml
Normal file
3
docs/_config.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
markdown: redcarpet
|
||||
encoding: utf-8
|
||||
|
||||
71
docs/_layouts/default.html
Normal file
71
docs/_layouts/default.html
Normal file
@@ -0,0 +1,71 @@
|
||||
<!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>
|
||||
124
docs/cli.md
Normal file
124
docs/cli.md
Normal file
@@ -0,0 +1,124 @@
|
||||
---
|
||||
layout: default
|
||||
title: Fig CLI reference
|
||||
---
|
||||
|
||||
CLI reference
|
||||
=============
|
||||
|
||||
Most commands are run against one or more services. If the service is omitted, it will apply to all services.
|
||||
|
||||
Run `fig [COMMAND] --help` for full usage.
|
||||
|
||||
## Commands
|
||||
|
||||
### build
|
||||
|
||||
Build or rebuild services.
|
||||
|
||||
Services are built once and then tagged as `project_service`, e.g. `figtest_db`. If you change a service's `Dockerfile` or the contents of its build directory, you can run `fig build` to rebuild it.
|
||||
|
||||
### help
|
||||
|
||||
Get help on a command.
|
||||
|
||||
### kill
|
||||
|
||||
Force stop service containers.
|
||||
|
||||
### logs
|
||||
|
||||
View output from services.
|
||||
|
||||
### port
|
||||
|
||||
Print the public port for a port binding
|
||||
|
||||
### ps
|
||||
|
||||
List containers.
|
||||
|
||||
### pull
|
||||
|
||||
Pulls service images.
|
||||
|
||||
### rm
|
||||
|
||||
Remove stopped service containers.
|
||||
|
||||
|
||||
### run
|
||||
|
||||
Run a one-off command on a service.
|
||||
|
||||
For example:
|
||||
|
||||
$ fig run web python manage.py shell
|
||||
|
||||
By default, linked services will 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.
|
||||
|
||||
Links are also created between one-off commands and the other containers for that service so you can do stuff like this:
|
||||
|
||||
$ fig run db psql -h db -U docker
|
||||
|
||||
If you do not want linked containers to be started when running the one-off command, specify the `--no-deps` flag:
|
||||
|
||||
$ fig run --no-deps web python manage.py shell
|
||||
|
||||
### scale
|
||||
|
||||
Set number of containers to run for a service.
|
||||
|
||||
Numbers are specified in the form `service=num` as arguments.
|
||||
For example:
|
||||
|
||||
$ fig scale web=2 worker=3
|
||||
|
||||
### start
|
||||
|
||||
Start existing containers for a service.
|
||||
|
||||
### stop
|
||||
|
||||
Stop running containers without removing them. They can be started again with `fig start`.
|
||||
|
||||
### up
|
||||
|
||||
Build, (re)create, start and attach 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 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.
|
||||
|
||||
[volumes-from]: http://docs.docker.io/en/latest/use/working_with_volumes/
|
||||
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Several environment variables can be used to configure Fig'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.
|
||||
|
||||
### FIG\_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.
|
||||
|
||||
### FIG\_FILE
|
||||
|
||||
Set the path to the `fig.yml` to use. Defaults to `fig.yml` in the current working directory.
|
||||
|
||||
### DOCKER\_HOST
|
||||
|
||||
Set the URL to the docker daemon. Defaults to `unix:///var/run/docker.sock`, as with the docker client.
|
||||
|
||||
### DOCKER\_TLS\_VERIFY
|
||||
|
||||
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`.
|
||||
7
docs/css/bootstrap.min.css
vendored
Normal file
7
docs/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
187
docs/css/fig.css
Normal file
187
docs/css/fig.css
Normal file
@@ -0,0 +1,187 @@
|
||||
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;
|
||||
}
|
||||
91
docs/django.md
Normal file
91
docs/django.md
Normal file
@@ -0,0 +1,91 @@
|
||||
---
|
||||
layout: default
|
||||
title: Getting started with Fig and Django
|
||||
---
|
||||
|
||||
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).
|
||||
|
||||
Let's set up the three files that'll get us started. First, our app is going to be running inside a Docker container which contains all of its dependencies. We can define what goes inside that Docker container using a file called `Dockerfile`. It'll contain this to start with:
|
||||
|
||||
FROM python:2.7
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
RUN mkdir /code
|
||||
WORKDIR /code
|
||||
ADD requirements.txt /code/
|
||||
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/).
|
||||
|
||||
Second, we define our Python dependencies in a file called `requirements.txt`:
|
||||
|
||||
Django
|
||||
psycopg2
|
||||
|
||||
Simple enough. Finally, this is all tied together with a file called `fig.yml`. It describes the services that our app comprises of (a web server and database), what Docker images they use, how they link together, what volumes will be mounted inside the containers and what ports they expose.
|
||||
|
||||
db:
|
||||
image: postgres
|
||||
web:
|
||||
build: .
|
||||
command: python manage.py runserver 0.0.0.0:8000
|
||||
volumes:
|
||||
- .:/code
|
||||
ports:
|
||||
- "8000:8000"
|
||||
links:
|
||||
- db
|
||||
|
||||
See the [`fig.yml` reference](yml.html) for more information on how it works.
|
||||
|
||||
We can now start a Django project using `fig run`:
|
||||
|
||||
$ fig run web django-admin.py startproject figexample .
|
||||
|
||||
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.
|
||||
|
||||
This will generate a Django app inside the current directory:
|
||||
|
||||
$ ls
|
||||
Dockerfile fig.yml figexample 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:
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||
'NAME': 'postgres',
|
||||
'USER': 'postgres',
|
||||
'HOST': 'db',
|
||||
'PORT': 5432,
|
||||
}
|
||||
}
|
||||
|
||||
These settings are determined by the [postgres](https://registry.hub.docker.com/_/postgres/) Docker image we are using.
|
||||
|
||||
Then, run `fig up`:
|
||||
|
||||
Recreating myapp_db_1...
|
||||
Recreating myapp_web_1...
|
||||
Attaching to myapp_db_1, myapp_web_1
|
||||
myapp_db_1 |
|
||||
myapp_db_1 | PostgreSQL stand-alone backend 9.1.11
|
||||
myapp_db_1 | 2014-01-27 12:17:03 UTC LOG: database system is ready to accept connections
|
||||
myapp_db_1 | 2014-01-27 12:17:03 UTC LOG: autovacuum launcher started
|
||||
myapp_web_1 | Validating models...
|
||||
myapp_web_1 |
|
||||
myapp_web_1 | 0 errors found
|
||||
myapp_web_1 | January 27, 2014 - 12:12:40
|
||||
myapp_web_1 | Django version 1.6.1, using settings 'figexample.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).
|
||||
|
||||
You can also run management commands with Docker. To set up your database, for example, run `fig up` and in another terminal run:
|
||||
|
||||
$ fig run web python manage.py syncdb
|
||||
|
||||
33
docs/env.md
Normal file
33
docs/env.md
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
layout: default
|
||||
title: Fig environment variables reference
|
||||
---
|
||||
|
||||
Environment variables reference
|
||||
===============================
|
||||
|
||||
**Note:** Environment variables are no longer the recommended method for connecting to linked services. Instead, you should use the link name (by default, the name of the linked service) as the hostname to connect to. See the [fig.yml documentation](yml.html#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.
|
||||
|
||||
To see what environment variables are available to a service, run `fig run SERVICE env`.
|
||||
|
||||
<b><i>name</i>\_PORT</b><br>
|
||||
Full URL, e.g. `DB_PORT=tcp://172.17.0.5:5432`
|
||||
|
||||
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i></b><br>
|
||||
Full URL, e.g. `DB_PORT_5432_TCP=tcp://172.17.0.5:5432`
|
||||
|
||||
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i>\_ADDR</b><br>
|
||||
Container's IP address, e.g. `DB_PORT_5432_TCP_ADDR=172.17.0.5`
|
||||
|
||||
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i>\_PORT</b><br>
|
||||
Exposed port number, e.g. `DB_PORT_5432_TCP_PORT=5432`
|
||||
|
||||
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i>\_PROTO</b><br>
|
||||
Protocol (tcp or udp), e.g. `DB_PORT_5432_TCP_PROTO=tcp`
|
||||
|
||||
<b><i>name</i>\_NAME</b><br>
|
||||
Fully qualified container name, e.g. `DB_1_NAME=/myapp_web_1/myapp_db_1`
|
||||
|
||||
[Docker links]: http://docs.docker.com/userguide/dockerlinks/
|
||||
8
docs/fig.yml
Normal file
8
docs/fig.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
jekyll:
|
||||
build: .
|
||||
ports:
|
||||
- "4000:4000"
|
||||
volumes:
|
||||
- .:/code
|
||||
environment:
|
||||
- LANG=en_US.UTF-8
|
||||
BIN
docs/img/logo.png
Normal file
BIN
docs/img/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
140
docs/index.md
Normal file
140
docs/index.md
Normal file
@@ -0,0 +1,140 @@
|
||||
---
|
||||
layout: default
|
||||
title: Fig | Fast, isolated development environments using Docker
|
||||
---
|
||||
|
||||
<strong class="strapline">Fast, isolated development environments using Docker.</strong>
|
||||
|
||||
Define your app's environment with Docker so it can be reproduced anywhere:
|
||||
|
||||
FROM python:2.7
|
||||
ADD . /code
|
||||
WORKDIR /code
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
Define the services that make up your app so they can be run together in an isolated environment:
|
||||
|
||||
```yaml
|
||||
web:
|
||||
build: .
|
||||
command: python app.py
|
||||
links:
|
||||
- db
|
||||
ports:
|
||||
- "8000:8000"
|
||||
db:
|
||||
image: postgres
|
||||
```
|
||||
|
||||
(No more installing Postgres on your laptop!)
|
||||
|
||||
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
|
||||
|
||||
|
||||
Quick start
|
||||
-----------
|
||||
|
||||
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 Fig](install.html).
|
||||
|
||||
You'll want to make a directory for the project:
|
||||
|
||||
$ 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:
|
||||
|
||||
```python
|
||||
from flask import Flask
|
||||
from redis import Redis
|
||||
import os
|
||||
app = Flask(__name__)
|
||||
redis = Redis(host='redis', port=6379)
|
||||
|
||||
@app.route('/')
|
||||
def hello():
|
||||
redis.incr('hits')
|
||||
return 'Hello World! I have been seen %s times.' % redis.get('hits')
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", debug=True)
|
||||
```
|
||||
|
||||
We define our 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`:
|
||||
|
||||
FROM python:2.7
|
||||
ADD . /code
|
||||
WORKDIR /code
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
This tells Docker to install Python, our code and our Python dependencies inside a Docker image. For more information on how to write Dockerfiles, see the [Docker user guide](https://docs.docker.com/userguide/dockerimages/#building-an-image-from-a-dockerfile) and the [Dockerfile reference](http://docs.docker.com/reference/builder/).
|
||||
|
||||
We then define a set of services using `fig.yml`:
|
||||
|
||||
web:
|
||||
build: .
|
||||
command: python app.py
|
||||
ports:
|
||||
- "5000:5000"
|
||||
volumes:
|
||||
- .:/code
|
||||
links:
|
||||
- redis
|
||||
redis:
|
||||
image: redis
|
||||
|
||||
This defines two services:
|
||||
|
||||
- `web`, which is built from `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/).
|
||||
|
||||
Now if we run `fig up`, it'll pull a Redis image, build an image for our own code, and start everything up:
|
||||
|
||||
$ fig up
|
||||
Pulling image redis...
|
||||
Building web...
|
||||
Starting figtest_redis_1...
|
||||
Starting figtest_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).
|
||||
|
||||
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:
|
||||
|
||||
$ fig up -d
|
||||
Starting figtest_redis_1...
|
||||
Starting figtest_web_1...
|
||||
$ fig 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
|
||||
|
||||
`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:
|
||||
|
||||
$ fig run web env
|
||||
|
||||
|
||||
See `fig --help` other commands that are available.
|
||||
|
||||
If you started Fig with `fig up -d`, you'll probably want to stop your services once you've finished with them:
|
||||
|
||||
$ fig stop
|
||||
|
||||
That's more-or-less how Fig works. See the reference section below for full details on the commands, configuration file and environment variables. If you have any thoughts or suggestions, [open an issue on GitHub](https://github.com/docker/fig).
|
||||
27
docs/install.md
Normal file
27
docs/install.md
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
layout: default
|
||||
title: Installing Fig
|
||||
---
|
||||
|
||||
Installing Fig
|
||||
==============
|
||||
|
||||
First, install Docker version 1.3 or greater.
|
||||
|
||||
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:
|
||||
|
||||
$(boot2docker shellinit)
|
||||
|
||||
To persist the environment variables across shell sessions, you can add that line to your `~/.bashrc` file.
|
||||
|
||||
There are also guides for [Ubuntu](https://docs.docker.com/installation/ubuntulinux/) and [other platforms](https://docs.docker.com/installation/) in Docker’s documentation.
|
||||
|
||||
Next, install Fig:
|
||||
|
||||
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
|
||||
|
||||
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):
|
||||
|
||||
$ sudo pip install -U fig
|
||||
|
||||
That should be all you need! Run `fig --version` to see if it worked.
|
||||
98
docs/rails.md
Normal file
98
docs/rails.md
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
layout: default
|
||||
title: Getting started with Fig and Rails
|
||||
---
|
||||
|
||||
Getting started with Fig 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).
|
||||
|
||||
Let's set up the three files that'll get us started. First, our app is going to be running inside a Docker container which contains all of its dependencies. We can define what goes inside that Docker container using a file called `Dockerfile`. It'll contain this to start with:
|
||||
|
||||
FROM ruby
|
||||
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev
|
||||
RUN mkdir /myapp
|
||||
WORKDIR /myapp
|
||||
ADD Gemfile /myapp/Gemfile
|
||||
RUN bundle install
|
||||
ADD . /myapp
|
||||
|
||||
That'll put 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/).
|
||||
|
||||
Next, we have a bootstrap `Gemfile` which just loads Rails. It'll be overwritten in a moment by `rails new`.
|
||||
|
||||
source 'https://rubygems.org'
|
||||
gem 'rails', '4.0.2'
|
||||
|
||||
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.
|
||||
|
||||
db:
|
||||
image: postgres
|
||||
ports:
|
||||
- "5432"
|
||||
web:
|
||||
build: .
|
||||
command: bundle exec rackup -p 3000
|
||||
volumes:
|
||||
- .:/myapp
|
||||
ports:
|
||||
- "3000:3000"
|
||||
links:
|
||||
- db
|
||||
|
||||
With those files in place, we can now generate the Rails skeleton app using `fig run`:
|
||||
|
||||
$ fig run web rails new . --force --database=postgresql --skip-bundle
|
||||
|
||||
First, Fig will build the image for the `web` service using the `Dockerfile`. Then it'll run `rails new` inside a new container, using that image. Once it's done, you should have a fresh app generated:
|
||||
|
||||
$ ls
|
||||
Dockerfile app fig.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:
|
||||
|
||||
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).
|
||||
|
||||
$ fig 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.
|
||||
|
||||
Open up your newly-generated `database.yml`. Replace its contents with the following:
|
||||
|
||||
development: &default
|
||||
adapter: postgresql
|
||||
encoding: unicode
|
||||
database: postgres
|
||||
pool: 5
|
||||
username: postgres
|
||||
password:
|
||||
host: db
|
||||
|
||||
test:
|
||||
<<: *default
|
||||
database: myapp_test
|
||||
|
||||
We can now boot the app.
|
||||
|
||||
$ fig up
|
||||
|
||||
If all's well, you should see some PostgreSQL output, and then—after a few seconds—the familiar refrain:
|
||||
|
||||
myapp_web_1 | [2014-01-17 17:16:29] INFO WEBrick 1.3.1
|
||||
myapp_web_1 | [2014-01-17 17:16:29] INFO ruby 2.0.0 (2013-11-22) [x86_64-linux-gnu]
|
||||
myapp_web_1 | [2014-01-17 17:16:29] INFO WEBrick::HTTPServer#start: pid=1 port=3000
|
||||
|
||||
Finally, we just need to create the database. In another terminal, run:
|
||||
|
||||
$ fig run web rake db:create
|
||||
|
||||
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).
|
||||
|
||||

|
||||
91
docs/wordpress.md
Normal file
91
docs/wordpress.md
Normal file
@@ -0,0 +1,91 @@
|
||||
---
|
||||
layout: default
|
||||
title: Getting started with Fig and Wordpress
|
||||
---
|
||||
|
||||
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:
|
||||
|
||||
$ curl http://wordpress.org/wordpress-3.8.1.tar.gz | tar -xvzf -
|
||||
|
||||
This will create a directory called `wordpress`, which you can rename to the name of your project if you wish. Inside that directory, we create `Dockerfile`, a file that defines what environment your app is going to run in:
|
||||
|
||||
```
|
||||
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/).
|
||||
|
||||
Next up, `fig.yml` starts our web service and a separate MySQL instance:
|
||||
|
||||
```
|
||||
web:
|
||||
build: .
|
||||
command: php -S 0.0.0.0:8000 -t /code
|
||||
ports:
|
||||
- "8000:8000"
|
||||
links:
|
||||
- db
|
||||
volumes:
|
||||
- .:/code
|
||||
db:
|
||||
image: orchardup/mysql
|
||||
environment:
|
||||
MYSQL_DATABASE: wordpress
|
||||
```
|
||||
|
||||
Two supporting files are needed to get this working - first 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:
|
||||
|
||||
```
|
||||
<?php
|
||||
define('DB_NAME', 'wordpress');
|
||||
define('DB_USER', 'root');
|
||||
define('DB_PASSWORD', '');
|
||||
define('DB_HOST', "db:3306");
|
||||
define('DB_CHARSET', 'utf8');
|
||||
define('DB_COLLATE', '');
|
||||
|
||||
define('AUTH_KEY', 'put your unique phrase here');
|
||||
define('SECURE_AUTH_KEY', 'put your unique phrase here');
|
||||
define('LOGGED_IN_KEY', 'put your unique phrase here');
|
||||
define('NONCE_KEY', 'put your unique phrase here');
|
||||
define('AUTH_SALT', 'put your unique phrase here');
|
||||
define('SECURE_AUTH_SALT', 'put your unique phrase here');
|
||||
define('LOGGED_IN_SALT', 'put your unique phrase here');
|
||||
define('NONCE_SALT', 'put your unique phrase here');
|
||||
|
||||
$table_prefix = 'wp_';
|
||||
define('WPLANG', '');
|
||||
define('WP_DEBUG', false);
|
||||
|
||||
if ( !defined('ABSPATH') )
|
||||
define('ABSPATH', dirname(__FILE__) . '/');
|
||||
|
||||
require_once(ABSPATH . 'wp-settings.php');
|
||||
```
|
||||
|
||||
Finally, `router.php` tells PHP's built-in web server how to run Wordpress:
|
||||
|
||||
```
|
||||
<?php
|
||||
|
||||
$root = $_SERVER['DOCUMENT_ROOT'];
|
||||
chdir($root);
|
||||
$path = '/'.ltrim(parse_url($_SERVER['REQUEST_URI'])['path'],'/');
|
||||
set_include_path(get_include_path().':'.__DIR__);
|
||||
if(file_exists($root.$path))
|
||||
{
|
||||
if(is_dir($root.$path) && substr($path,strlen($path) - 1, 1) !== '/')
|
||||
$path = rtrim($path,'/').'/index.php';
|
||||
if(strpos($path,'.php') === false) return false;
|
||||
else {
|
||||
chdir(dirname($root.$path));
|
||||
require_once $root.$path;
|
||||
}
|
||||
}else include_once 'index.php';
|
||||
```
|
||||
|
||||
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).
|
||||
159
docs/yml.md
Normal file
159
docs/yml.md
Normal file
@@ -0,0 +1,159 @@
|
||||
---
|
||||
layout: default
|
||||
title: fig.yml reference
|
||||
---
|
||||
|
||||
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.
|
||||
|
||||
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`.
|
||||
|
||||
###image
|
||||
|
||||
Tag or partial image ID. Can be local or remote - Fig will attempt to pull if it doesn't exist locally.
|
||||
|
||||
```
|
||||
image: ubuntu
|
||||
image: orchardup/postgresql
|
||||
image: a4bc65fd
|
||||
```
|
||||
|
||||
### build
|
||||
|
||||
Path to a directory containing a Dockerfile. Fig will build and tag it with a generated name, and use that image thereafter.
|
||||
|
||||
```
|
||||
build: /path/to/build/dir
|
||||
```
|
||||
|
||||
### command
|
||||
|
||||
Override the default command.
|
||||
|
||||
```
|
||||
command: bundle exec thin -p 3000
|
||||
```
|
||||
|
||||
<a name="links"></a>
|
||||
### links
|
||||
|
||||
Link to containers in another service. Either specify both the service name and the link alias (`SERVICE:ALIAS`), or just the service name (which will also be used for the alias).
|
||||
|
||||
```
|
||||
links:
|
||||
- db
|
||||
- db:database
|
||||
- redis
|
||||
```
|
||||
|
||||
An entry with the alias' name will be created in `/etc/hosts` inside containers for this service, e.g:
|
||||
|
||||
```
|
||||
172.17.2.186 db
|
||||
172.17.2.186 database
|
||||
172.17.2.187 redis
|
||||
```
|
||||
|
||||
Environment variables will also be created - see the [environment variable reference](env.html) for details.
|
||||
|
||||
### ports
|
||||
|
||||
Expose ports. Either specify both ports (`HOST:CONTAINER`), or just the container port (a random host port will be chosen).
|
||||
|
||||
**Note:** When mapping ports in the `HOST:CONTAINER` format, you may experience erroneous results when using a container port lower than 60, because YAML will parse numbers in the format `xx:yy` as sexagesimal (base 60). For this reason, we recommend always explicitly specifying your port mappings as strings.
|
||||
|
||||
```
|
||||
ports:
|
||||
- "3000"
|
||||
- "8000:8000"
|
||||
- "49100:22"
|
||||
- "127.0.0.1:8001:8001"
|
||||
```
|
||||
|
||||
### expose
|
||||
|
||||
Expose ports without publishing them to the host machine - they'll only be accessible to linked services. Only the internal port can be specified.
|
||||
|
||||
```
|
||||
expose:
|
||||
- "3000"
|
||||
- "8000"
|
||||
```
|
||||
|
||||
### volumes
|
||||
|
||||
Mount paths as volumes, optionally specifying a path on the host machine
|
||||
(`HOST:CONTAINER`), or an access mode (`HOST:CONTAINER:ro`).
|
||||
|
||||
```
|
||||
volumes:
|
||||
- /var/lib/mysql
|
||||
- cache/:/tmp/cache
|
||||
- ~/configs:/etc/configs/:ro
|
||||
```
|
||||
|
||||
### volumes_from
|
||||
|
||||
Mount all of the volumes from another service or container.
|
||||
|
||||
```
|
||||
volumes_from:
|
||||
- service_name
|
||||
- container_name
|
||||
```
|
||||
|
||||
### environment
|
||||
|
||||
Add environment variables. You can use either an array or a dictionary.
|
||||
|
||||
Environment variables with only a key are resolved to their values on the machine Fig is running on, which can be helpful for secret or host-specific values.
|
||||
|
||||
```
|
||||
environment:
|
||||
RACK_ENV: development
|
||||
SESSION_SECRET:
|
||||
|
||||
environment:
|
||||
- RACK_ENV=development
|
||||
- SESSION_SECRET
|
||||
```
|
||||
|
||||
### net
|
||||
|
||||
Networking mode. Use the same values as the docker client `--net` parameter.
|
||||
|
||||
```
|
||||
net: "bridge"
|
||||
net: "none"
|
||||
net: "container:[name or id]"
|
||||
net: "host"
|
||||
```
|
||||
|
||||
### dns
|
||||
|
||||
Custom DNS servers. Can be a single value or a list.
|
||||
|
||||
```
|
||||
dns: 8.8.8.8
|
||||
dns:
|
||||
- 8.8.8.8
|
||||
- 9.9.9.9
|
||||
```
|
||||
|
||||
### working\_dir, entrypoint, user, hostname, domainname, mem\_limit, privileged
|
||||
|
||||
Each of these is a single value, analogous to its [docker run](https://docs.docker.com/reference/run/) counterpart.
|
||||
|
||||
```
|
||||
working_dir: /code
|
||||
entrypoint: /code/entrypoint.sh
|
||||
user: postgresql
|
||||
|
||||
hostname: foo
|
||||
domainname: foo.com
|
||||
|
||||
mem_limit: 1000000000
|
||||
privileged: true
|
||||
```
|
||||
@@ -1,3 +1,4 @@
|
||||
from .service import Service
|
||||
from __future__ import unicode_literals
|
||||
from .service import Service # noqa:flake8
|
||||
|
||||
__version__ = '0.0.1'
|
||||
__version__ = '1.0.0'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from __future__ import unicode_literals
|
||||
NAMES = [
|
||||
'grey',
|
||||
'red',
|
||||
|
||||
@@ -1,38 +1,109 @@
|
||||
from docker import Client
|
||||
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 ..project import Project
|
||||
from ..service import ConfigError
|
||||
from .docopt_command import DocoptCommand
|
||||
from .formatter import Formatter
|
||||
from .utils import cached_property, mkdir
|
||||
from .utils import call_silently, is_mac, is_ubuntu
|
||||
from .docker_client import docker_client
|
||||
from . import verbose_proxy
|
||||
from . import errors
|
||||
from .. import __version__
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(DocoptCommand):
|
||||
@cached_property
|
||||
def client(self):
|
||||
if os.environ.get('DOCKER_URL'):
|
||||
return Client(os.environ['DOCKER_URL'])
|
||||
else:
|
||||
return Client()
|
||||
base_dir = '.'
|
||||
|
||||
@cached_property
|
||||
def project(self):
|
||||
config = yaml.load(open('fig.yml'))
|
||||
return Project.from_config(self.project_name, config, self.client)
|
||||
def dispatch(self, *args, **kwargs):
|
||||
try:
|
||||
super(Command, self).dispatch(*args, **kwargs)
|
||||
except SSLError, e:
|
||||
raise errors.UserError('SSL error: %s' % e)
|
||||
except ConnectionError:
|
||||
if call_silently(['which', 'docker']) != 0:
|
||||
if is_mac():
|
||||
raise errors.DockerNotFoundMac()
|
||||
elif is_ubuntu():
|
||||
raise errors.DockerNotFoundUbuntu()
|
||||
else:
|
||||
raise errors.DockerNotFoundGeneric()
|
||||
elif call_silently(['which', 'boot2docker']) == 0:
|
||||
raise errors.ConnectionErrorBoot2Docker()
|
||||
else:
|
||||
raise errors.ConnectionErrorGeneric(self.get_client().base_url)
|
||||
|
||||
@cached_property
|
||||
def project_name(self):
|
||||
project = os.path.basename(os.getcwd())
|
||||
project = re.sub(r'[^a-zA-Z0-9]', '', project)
|
||||
if not project:
|
||||
project = 'default'
|
||||
return project
|
||||
def perform_command(self, options, handler, command_options):
|
||||
explicit_config_path = options.get('--file') or os.environ.get('FIG_FILE')
|
||||
project = self.get_project(
|
||||
self.get_config_path(explicit_config_path),
|
||||
project_name=options.get('--project-name'),
|
||||
verbose=options.get('--verbose'))
|
||||
|
||||
@cached_property
|
||||
def formatter(self):
|
||||
return Formatter()
|
||||
handler(project, command_options)
|
||||
|
||||
def get_client(self, verbose=False):
|
||||
client = docker_client()
|
||||
if verbose:
|
||||
version_info = six.iteritems(client.version())
|
||||
log.info("Fig 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(
|
||||
self.get_project_name(config_path, project_name),
|
||||
self.get_config(config_path),
|
||||
self.get_client(verbose=verbose))
|
||||
except ConfigError as e:
|
||||
raise errors.UserError(six.text_type(e))
|
||||
|
||||
def get_project_name(self, config_path, project_name=None):
|
||||
def normalize_name(name):
|
||||
return re.sub(r'[^a-zA-Z0-9]', '', name)
|
||||
|
||||
project_name = project_name or os.environ.get('FIG_PROJECT_NAME')
|
||||
if project_name is not None:
|
||||
return normalize_name(project_name)
|
||||
|
||||
project = os.path.basename(os.path.dirname(os.path.abspath(config_path)))
|
||||
if project:
|
||||
return normalize_name(project)
|
||||
|
||||
return 'default'
|
||||
|
||||
def get_config_path(self, file_path=None):
|
||||
if file_path:
|
||||
return os.path.join(self.base_dir, file_path)
|
||||
|
||||
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 "
|
||||
"in most cases, and using .yaml can cause compatibility "
|
||||
"issues in future")
|
||||
|
||||
return os.path.join(self.base_dir, 'fig.yaml')
|
||||
|
||||
return os.path.join(self.base_dir, 'fig.yml')
|
||||
|
||||
34
fig/cli/docker_client.py
Normal file
34
fig/cli/docker_client.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from docker import Client
|
||||
from docker import tls
|
||||
import ssl
|
||||
import os
|
||||
|
||||
|
||||
def docker_client():
|
||||
"""
|
||||
Returns a docker-py client configured using environment variables
|
||||
according to the same logic as the official Docker client.
|
||||
"""
|
||||
cert_path = os.environ.get('DOCKER_CERT_PATH', '')
|
||||
if cert_path == '':
|
||||
cert_path = os.path.join(os.environ.get('HOME'), '.docker')
|
||||
|
||||
base_url = os.environ.get('DOCKER_HOST')
|
||||
tls_config = None
|
||||
|
||||
if os.environ.get('DOCKER_TLS_VERIFY', '') != '':
|
||||
parts = base_url.split('://', 1)
|
||||
base_url = '%s://%s' % ('https', parts[1])
|
||||
|
||||
client_cert = (os.path.join(cert_path, 'cert.pem'), os.path.join(cert_path, 'key.pem'))
|
||||
ca_cert = os.path.join(cert_path, 'ca.pem')
|
||||
|
||||
tls_config = tls.TLSConfig(
|
||||
ssl_version=ssl.PROTOCOL_TLSv1,
|
||||
verify=True,
|
||||
assert_hostname=False,
|
||||
client_cert=client_cert,
|
||||
ca_cert=ca_cert,
|
||||
)
|
||||
|
||||
return Client(base_url=base_url, tls=tls_config)
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
import sys
|
||||
|
||||
from inspect import getdoc
|
||||
@@ -21,7 +23,7 @@ class DocoptCommand(object):
|
||||
def dispatch(self, argv, global_options):
|
||||
self.perform_command(*self.parse(argv, global_options))
|
||||
|
||||
def perform_command(self, options, command, handler, command_options):
|
||||
def perform_command(self, options, handler, command_options):
|
||||
handler(command_options)
|
||||
|
||||
def parse(self, argv, global_options):
|
||||
@@ -41,7 +43,7 @@ class DocoptCommand(object):
|
||||
raise NoSuchCommand(command, self)
|
||||
|
||||
command_options = docopt_full_help(docstring, options['ARGS'], options_first=True)
|
||||
return (options, command, handler, command_options)
|
||||
return options, handler, command_options
|
||||
|
||||
|
||||
class NoSuchCommand(Exception):
|
||||
|
||||
@@ -1,6 +1,62 @@
|
||||
from __future__ import absolute_import
|
||||
from textwrap import dedent
|
||||
|
||||
|
||||
class UserError(Exception):
|
||||
def __init__(self, msg):
|
||||
self.msg = dedent(msg).strip()
|
||||
|
||||
def __unicode__(self):
|
||||
return self.msg
|
||||
|
||||
__str__ = __unicode__
|
||||
|
||||
|
||||
class DockerNotFoundMac(UserError):
|
||||
def __init__(self):
|
||||
super(DockerNotFoundMac, self).__init__("""
|
||||
Couldn't connect to Docker daemon. You might need to install docker-osx:
|
||||
|
||||
https://github.com/noplay/docker-osx
|
||||
""")
|
||||
|
||||
|
||||
class DockerNotFoundUbuntu(UserError):
|
||||
def __init__(self):
|
||||
super(DockerNotFoundUbuntu, self).__init__("""
|
||||
Couldn't connect to Docker daemon. You might need to install Docker:
|
||||
|
||||
http://docs.docker.io/en/latest/installation/ubuntulinux/
|
||||
""")
|
||||
|
||||
|
||||
class DockerNotFoundGeneric(UserError):
|
||||
def __init__(self):
|
||||
super(DockerNotFoundGeneric, self).__init__("""
|
||||
Couldn't connect to Docker daemon. You might need to install Docker:
|
||||
|
||||
http://docs.docker.io/en/latest/installation/
|
||||
""")
|
||||
|
||||
|
||||
class ConnectionErrorBoot2Docker(UserError):
|
||||
def __init__(self):
|
||||
super(ConnectionErrorBoot2Docker, self).__init__("""
|
||||
Couldn't connect to Docker daemon - you might need to run `boot2docker up`.
|
||||
""")
|
||||
|
||||
|
||||
class ConnectionErrorGeneric(UserError):
|
||||
def __init__(self, url):
|
||||
super(ConnectionErrorGeneric, self).__init__("""
|
||||
Couldn't connect to Docker daemon at %s - is it running?
|
||||
|
||||
If it's at a non-standard location, specify the URL with the DOCKER_HOST environment variable.
|
||||
""" % url)
|
||||
|
||||
|
||||
class FigFileNotFound(UserError):
|
||||
def __init__(self, filename):
|
||||
super(FigFileNotFound, self).__init__("""
|
||||
Can't find %s. Are you in the right directory?
|
||||
""" % filename)
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import texttable
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
import os
|
||||
import texttable
|
||||
|
||||
|
||||
def get_tty_width():
|
||||
tty_size = os.popen('stty size', 'r').read().split()
|
||||
if len(tty_size) != 2:
|
||||
return 80
|
||||
_, width = tty_size
|
||||
return int(width)
|
||||
|
||||
|
||||
class Formatter(object):
|
||||
def table(self, headers, rows):
|
||||
height, width = os.popen('stty size', 'r').read().split()
|
||||
|
||||
table = texttable.Texttable(max_width=width)
|
||||
table = texttable.Texttable(max_width=get_tty_width())
|
||||
table.set_cols_dtype(['t' for h in headers])
|
||||
table.add_rows([headers] + rows)
|
||||
table.set_deco(table.HEADER)
|
||||
|
||||
@@ -1,66 +1,79 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
import sys
|
||||
|
||||
from itertools import cycle
|
||||
|
||||
from .multiplexer import Multiplexer
|
||||
from .multiplexer import Multiplexer, STOP
|
||||
from . import colors
|
||||
from .utils import split_buffer
|
||||
|
||||
|
||||
class LogPrinter(object):
|
||||
def __init__(self, containers, attach_params=None):
|
||||
def __init__(self, containers, attach_params=None, output=sys.stdout, monochrome=False):
|
||||
self.containers = containers
|
||||
self.attach_params = attach_params or {}
|
||||
self.generators = self._make_log_generators()
|
||||
self.prefix_width = self._calculate_prefix_width(containers)
|
||||
self.generators = self._make_log_generators(monochrome)
|
||||
self.output = output
|
||||
|
||||
def run(self):
|
||||
mux = Multiplexer(self.generators)
|
||||
for line in mux.loop():
|
||||
sys.stdout.write(line)
|
||||
self.output.write(line)
|
||||
|
||||
def _make_log_generators(self):
|
||||
def _calculate_prefix_width(self, containers):
|
||||
"""
|
||||
Calculate the maximum width of container names so we can make the log
|
||||
prefixes line up like so:
|
||||
|
||||
db_1 | Listening
|
||||
web_1 | Listening
|
||||
"""
|
||||
prefix_width = 0
|
||||
for container in containers:
|
||||
prefix_width = max(prefix_width, len(container.name_without_project))
|
||||
return prefix_width
|
||||
|
||||
def _make_log_generators(self, monochrome):
|
||||
color_fns = cycle(colors.rainbow())
|
||||
generators = []
|
||||
|
||||
for container in self.containers:
|
||||
color_fn = color_fns.next()
|
||||
if monochrome:
|
||||
color_fn = lambda s: s
|
||||
else:
|
||||
color_fn = color_fns.next()
|
||||
generators.append(self._make_log_generator(container, color_fn))
|
||||
|
||||
return generators
|
||||
|
||||
def _make_log_generator(self, container, color_fn):
|
||||
prefix = color_fn(container.name + " | ")
|
||||
websocket = self._attach(container)
|
||||
return (prefix + line for line in split_buffer(read_websocket(websocket), '\n'))
|
||||
prefix = color_fn(self._generate_prefix(container)).encode('utf-8')
|
||||
# Attach to container before log printer starts running
|
||||
line_generator = split_buffer(self._attach(container), '\n')
|
||||
|
||||
for line in line_generator:
|
||||
yield prefix + line
|
||||
|
||||
exit_code = container.wait()
|
||||
yield color_fn("%s exited with code %s\n" % (container.name, exit_code))
|
||||
yield STOP
|
||||
|
||||
def _generate_prefix(self, container):
|
||||
"""
|
||||
Generate the prefix for a log line without colour
|
||||
"""
|
||||
name = container.name_without_project
|
||||
padding = ' ' * (self.prefix_width - len(name))
|
||||
return ''.join([name, padding, ' | '])
|
||||
|
||||
def _attach(self, container):
|
||||
params = {
|
||||
'stdin': False,
|
||||
'stdout': True,
|
||||
'stderr': True,
|
||||
'logs': False,
|
||||
'stream': True,
|
||||
}
|
||||
params.update(self.attach_params)
|
||||
params = dict((name, 1 if value else 0) for (name, value) in params.items())
|
||||
return container.attach_socket(params=params, ws=True)
|
||||
|
||||
def read_websocket(websocket):
|
||||
while True:
|
||||
data = websocket.recv()
|
||||
if data:
|
||||
yield data
|
||||
else:
|
||||
break
|
||||
|
||||
def split_buffer(reader, separator):
|
||||
buffered = ''
|
||||
|
||||
for data in reader:
|
||||
lines = (buffered + data).split(separator)
|
||||
for line in lines[:-1]:
|
||||
yield line + separator
|
||||
if len(lines) > 1:
|
||||
buffered = lines[-1]
|
||||
|
||||
if len(buffered) > 0:
|
||||
yield buffered
|
||||
params = dict((name, 1 if value else 0) for (name, value) in list(params.items()))
|
||||
return container.attach(**params)
|
||||
|
||||
435
fig/cli/main.py
435
fig/cli/main.py
@@ -1,25 +1,55 @@
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
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
|
||||
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
|
||||
|
||||
from docker.client import APIError
|
||||
from docker.errors import APIError
|
||||
from .errors import UserError
|
||||
from .docopt_command import NoSuchCommand
|
||||
from .socketclient import SocketClient
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def main():
|
||||
console_handler = logging.StreamHandler()
|
||||
setup_logging()
|
||||
try:
|
||||
command = TopLevelCommand()
|
||||
command.sys_dispatch()
|
||||
except KeyboardInterrupt:
|
||||
log.error("\nAborting.")
|
||||
sys.exit(1)
|
||||
except (UserError, NoSuchService, ConfigurationError) as e:
|
||||
log.error(e.msg)
|
||||
sys.exit(1)
|
||||
except NoSuchCommand as e:
|
||||
log.error("No such command: %s", e.command)
|
||||
log.error("")
|
||||
log.error("\n".join(parse_doc_section("commands:", getdoc(e.supercommand))))
|
||||
sys.exit(1)
|
||||
except APIError as e:
|
||||
log.error(e.explanation)
|
||||
sys.exit(1)
|
||||
except BuildError as e:
|
||||
log.error("Service '%s' failed to build: %s" % (e.service.name, e.reason))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def setup_logging():
|
||||
console_handler = logging.StreamHandler(sys.stderr)
|
||||
console_handler.setFormatter(logging.Formatter())
|
||||
console_handler.setLevel(logging.INFO)
|
||||
root_logger = logging.getLogger()
|
||||
@@ -29,27 +59,6 @@ def main():
|
||||
# Disable requests logging
|
||||
logging.getLogger("requests").propagate = False
|
||||
|
||||
try:
|
||||
command = TopLevelCommand()
|
||||
command.sys_dispatch()
|
||||
except KeyboardInterrupt:
|
||||
log.error("\nAborting.")
|
||||
exit(1)
|
||||
except UserError, e:
|
||||
log.error(e.msg)
|
||||
exit(1)
|
||||
except NoSuchService, e:
|
||||
log.error(e.msg)
|
||||
exit(1)
|
||||
except NoSuchCommand, e:
|
||||
log.error("No such command: %s", e.command)
|
||||
log.error("")
|
||||
log.error("\n".join(parse_doc_section("commands:", getdoc(e.supercommand))))
|
||||
exit(1)
|
||||
except APIError, e:
|
||||
log.error(e.explanation)
|
||||
exit(1)
|
||||
|
||||
|
||||
# stolen from docopt master
|
||||
def parse_doc_section(name, source):
|
||||
@@ -66,18 +75,26 @@ class TopLevelCommand(Command):
|
||||
fig -h|--help
|
||||
|
||||
Options:
|
||||
--verbose Show more output
|
||||
--version Print version and exit
|
||||
--verbose Show more output
|
||||
--version Print version and exit
|
||||
-f, --file FILE Specify an alternate fig file (default: fig.yml)
|
||||
-p, --project-name NAME Specify an alternate project name (default: directory name)
|
||||
|
||||
Commands:
|
||||
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
|
||||
rm Remove stopped containers
|
||||
run Run a one-off command
|
||||
scale Set number of containers for a service
|
||||
start Start services
|
||||
stop Stop services
|
||||
kill Kill containers
|
||||
rm Remove stopped containers
|
||||
restart Restart services
|
||||
up Create and start containers
|
||||
|
||||
"""
|
||||
def docopt_options(self):
|
||||
@@ -85,7 +102,77 @@ class TopLevelCommand(Command):
|
||||
options['version'] = "fig %s" % __version__
|
||||
return options
|
||||
|
||||
def ps(self, options):
|
||||
def build(self, project, options):
|
||||
"""
|
||||
Build or rebuild services.
|
||||
|
||||
Services are built once and then tagged as `project_service`,
|
||||
e.g. `figtest_db`. If you change a service's `Dockerfile` or the
|
||||
contents of its build directory, you can run `fig build` to rebuild it.
|
||||
|
||||
Usage: build [options] [SERVICE...]
|
||||
|
||||
Options:
|
||||
--no-cache Do not use cache when building the image.
|
||||
"""
|
||||
no_cache = bool(options.get('--no-cache', False))
|
||||
project.build(service_names=options['SERVICE'], no_cache=no_cache)
|
||||
|
||||
def help(self, project, options):
|
||||
"""
|
||||
Get help on a command.
|
||||
|
||||
Usage: help COMMAND
|
||||
"""
|
||||
command = options['COMMAND']
|
||||
if not hasattr(self, command):
|
||||
raise NoSuchCommand(command, self)
|
||||
raise SystemExit(getdoc(getattr(self, command)))
|
||||
|
||||
def kill(self, project, options):
|
||||
"""
|
||||
Force stop service containers.
|
||||
|
||||
Usage: kill [SERVICE...]
|
||||
"""
|
||||
project.kill(service_names=options['SERVICE'])
|
||||
|
||||
def logs(self, project, options):
|
||||
"""
|
||||
View output from containers.
|
||||
|
||||
Usage: logs [options] [SERVICE...]
|
||||
|
||||
Options:
|
||||
--no-color Produce monochrome output.
|
||||
"""
|
||||
containers = project.containers(service_names=options['SERVICE'], stopped=True)
|
||||
|
||||
monochrome = options['--no-color']
|
||||
print("Attaching to", list_containers(containers))
|
||||
LogPrinter(containers, attach_params={'logs': True}, monochrome=monochrome).run()
|
||||
|
||||
def port(self, project, options):
|
||||
"""
|
||||
Print the public port for a port binding.
|
||||
|
||||
Usage: port [options] SERVICE PRIVATE_PORT
|
||||
|
||||
Options:
|
||||
--protocol=proto tcp or udp (defaults to tcp)
|
||||
--index=index index of the container if there are multiple
|
||||
instances of a service (defaults to 1)
|
||||
"""
|
||||
service = project.get_service(options['SERVICE'])
|
||||
try:
|
||||
container = service.get_container(number=options.get('--index') or 1)
|
||||
except ValueError as e:
|
||||
raise UserError(str(e))
|
||||
print(container.get_local_port(
|
||||
options['PRIVATE_PORT'],
|
||||
protocol=options.get('--protocol') or 'tcp') or '')
|
||||
|
||||
def ps(self, project, options):
|
||||
"""
|
||||
List containers.
|
||||
|
||||
@@ -94,11 +181,14 @@ class TopLevelCommand(Command):
|
||||
Options:
|
||||
-q Only display IDs
|
||||
"""
|
||||
containers = self.project.containers(service_names=options['SERVICE'], stopped=True) + self.project.containers(service_names=options['SERVICE'], one_off=True)
|
||||
containers = sorted(
|
||||
project.containers(service_names=options['SERVICE'], stopped=True) +
|
||||
project.containers(service_names=options['SERVICE'], one_off=True),
|
||||
key=attrgetter('name'))
|
||||
|
||||
if options['-q']:
|
||||
for container in containers:
|
||||
print container.id
|
||||
print(container.id)
|
||||
else:
|
||||
headers = [
|
||||
'Name',
|
||||
@@ -108,146 +198,241 @@ class TopLevelCommand(Command):
|
||||
]
|
||||
rows = []
|
||||
for container in containers:
|
||||
command = container.human_readable_command
|
||||
if len(command) > 30:
|
||||
command = '%s ...' % command[:26]
|
||||
rows.append([
|
||||
container.name,
|
||||
container.human_readable_command,
|
||||
command,
|
||||
container.human_readable_state,
|
||||
container.human_readable_ports,
|
||||
])
|
||||
print Formatter().table(headers, rows)
|
||||
print(Formatter().table(headers, rows))
|
||||
|
||||
def run(self, options):
|
||||
def pull(self, project, options):
|
||||
"""
|
||||
Run a one-off command.
|
||||
Pulls images for services.
|
||||
|
||||
Usage: run [options] SERVICE COMMAND [ARGS...]
|
||||
Usage: pull [options] [SERVICE...]
|
||||
|
||||
Options:
|
||||
-d Detached mode: Run container in the background, print new container name
|
||||
--allow-insecure-ssl Allow insecure connections to the docker
|
||||
registry
|
||||
"""
|
||||
service = self.project.get_service(options['SERVICE'])
|
||||
insecure_registry = options['--allow-insecure-ssl']
|
||||
project.pull(
|
||||
service_names=options['SERVICE'],
|
||||
insecure_registry=insecure_registry
|
||||
)
|
||||
|
||||
def rm(self, project, options):
|
||||
"""
|
||||
Remove stopped service containers.
|
||||
|
||||
Usage: rm [options] [SERVICE...]
|
||||
|
||||
Options:
|
||||
--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]
|
||||
|
||||
if len(stopped_containers) > 0:
|
||||
print("Going to remove", list_containers(stopped_containers))
|
||||
if options.get('--force') \
|
||||
or yesno("Are you sure? [yN] ", default=False):
|
||||
project.remove_stopped(
|
||||
service_names=options['SERVICE'],
|
||||
v=options.get('-v', False)
|
||||
)
|
||||
else:
|
||||
print("No stopped containers")
|
||||
|
||||
def run(self, project, options):
|
||||
"""
|
||||
Run a one-off command on a service.
|
||||
|
||||
For example:
|
||||
|
||||
$ fig 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...]`.
|
||||
|
||||
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.
|
||||
"""
|
||||
service = project.get_service(options['SERVICE'])
|
||||
|
||||
if not options['--no-deps']:
|
||||
deps = service.get_linked_names()
|
||||
|
||||
if len(deps) > 0:
|
||||
project.up(
|
||||
service_names=deps,
|
||||
start_links=True,
|
||||
recreate=False,
|
||||
)
|
||||
|
||||
tty = True
|
||||
if options['-d'] or options['-T'] or not sys.stdin.isatty():
|
||||
tty = False
|
||||
|
||||
if options['COMMAND']:
|
||||
command = [options['COMMAND']] + options['ARGS']
|
||||
else:
|
||||
command = service.options.get('command')
|
||||
|
||||
container_options = {
|
||||
'command': [options['COMMAND']] + options['ARGS'],
|
||||
'tty': not options['-d'],
|
||||
'command': command,
|
||||
'tty': tty,
|
||||
'stdin_open': not 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
|
||||
|
||||
if options['--entrypoint']:
|
||||
container_options['entrypoint'] = options.get('--entrypoint')
|
||||
|
||||
container = service.create_container(one_off=True, **container_options)
|
||||
if options['-d']:
|
||||
service.start_container(container, ports=None)
|
||||
print container.name
|
||||
service.start_container(container, ports=None, one_off=True)
|
||||
print(container.name)
|
||||
else:
|
||||
with self._attach_to_container(
|
||||
container.id,
|
||||
interactive=True,
|
||||
logs=True,
|
||||
raw=True
|
||||
) as c:
|
||||
service.start_container(container, ports=None)
|
||||
c.run()
|
||||
service.start_container(container, ports=None, one_off=True)
|
||||
dockerpty.start(project.client, container.id)
|
||||
exit_code = container.wait()
|
||||
if options['--rm']:
|
||||
log.info("Removing %s..." % container.name)
|
||||
project.client.remove_container(container.id)
|
||||
sys.exit(exit_code)
|
||||
|
||||
def up(self, options):
|
||||
def scale(self, project, options):
|
||||
"""
|
||||
Create and start containers.
|
||||
Set number of containers to run for a service.
|
||||
|
||||
Usage: up [options] [SERVICE...]
|
||||
Numbers are specified in the form `service=num` as arguments.
|
||||
For example:
|
||||
|
||||
Options:
|
||||
-d Detached mode: Run containers in the background, print new container names
|
||||
$ fig scale web=2 worker=3
|
||||
|
||||
Usage: scale [SERVICE=NUM...]
|
||||
"""
|
||||
detached = options['-d']
|
||||
|
||||
unstarted = self.project.create_containers(service_names=options['SERVICE'])
|
||||
|
||||
if not detached:
|
||||
to_attach = self.project.containers(service_names=options['SERVICE']) + [c for (s, c) in unstarted]
|
||||
print "Attaching to", list_containers(to_attach)
|
||||
log_printer = LogPrinter(to_attach, attach_params={'logs': True})
|
||||
|
||||
for (s, c) in unstarted:
|
||||
s.start_container(c)
|
||||
|
||||
if detached:
|
||||
for (s, c) in unstarted:
|
||||
print c.name
|
||||
else:
|
||||
for s in options['SERVICE=NUM']:
|
||||
if '=' not in s:
|
||||
raise UserError('Arguments to scale should be in the form service=num')
|
||||
service_name, num = s.split('=', 1)
|
||||
try:
|
||||
log_printer.run()
|
||||
finally:
|
||||
self.project.kill_and_remove(unstarted)
|
||||
num = int(num)
|
||||
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)
|
||||
|
||||
def start(self, options):
|
||||
def start(self, project, options):
|
||||
"""
|
||||
Start existing containers.
|
||||
|
||||
Usage: start [SERVICE...]
|
||||
"""
|
||||
self.project.start(service_names=options['SERVICE'])
|
||||
project.start(service_names=options['SERVICE'])
|
||||
|
||||
def stop(self, options):
|
||||
def stop(self, project, options):
|
||||
"""
|
||||
Stop running containers.
|
||||
Stop running containers without removing them.
|
||||
|
||||
They can be started again with `fig start`.
|
||||
|
||||
Usage: stop [SERVICE...]
|
||||
"""
|
||||
self.project.stop(service_names=options['SERVICE'])
|
||||
project.stop(service_names=options['SERVICE'])
|
||||
|
||||
def kill(self, options):
|
||||
def restart(self, project, options):
|
||||
"""
|
||||
Kill containers.
|
||||
Restart running containers.
|
||||
|
||||
Usage: kill [SERVICE...]
|
||||
Usage: restart [SERVICE...]
|
||||
"""
|
||||
self.project.kill(service_names=options['SERVICE'])
|
||||
project.restart(service_names=options['SERVICE'])
|
||||
|
||||
def rm(self, options):
|
||||
def up(self, project, options):
|
||||
"""
|
||||
Remove stopped containers
|
||||
Build, (re)create, start and attach to containers for a service.
|
||||
|
||||
Usage: rm [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`,
|
||||
it'll start the containers in the background and leave them running.
|
||||
|
||||
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 not want existing
|
||||
containers to be recreated, `fig 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.
|
||||
"""
|
||||
self.project.remove_stopped(service_names=options['SERVICE'])
|
||||
detached = options['-d']
|
||||
|
||||
def logs(self, options):
|
||||
"""
|
||||
View output from containers.
|
||||
monochrome = options['--no-color']
|
||||
|
||||
Usage: logs [SERVICE...]
|
||||
"""
|
||||
containers = self.project.containers(service_names=options['SERVICE'], stopped=False)
|
||||
print "Attaching to", list_containers(containers)
|
||||
LogPrinter(containers, attach_params={'logs': True}).run()
|
||||
start_links = not options['--no-deps']
|
||||
recreate = not options['--no-recreate']
|
||||
service_names = options['SERVICE']
|
||||
|
||||
def _attach_to_container(self, container_id, interactive, logs=False, stream=True, raw=False):
|
||||
stdio = self.client.attach_socket(
|
||||
container_id,
|
||||
params={
|
||||
'stdin': 1 if interactive else 0,
|
||||
'stdout': 1,
|
||||
'stderr': 0,
|
||||
'logs': 1 if logs else 0,
|
||||
'stream': 1 if stream else 0
|
||||
},
|
||||
ws=True,
|
||||
project.up(
|
||||
service_names=service_names,
|
||||
start_links=start_links,
|
||||
recreate=recreate
|
||||
)
|
||||
|
||||
stderr = self.client.attach_socket(
|
||||
container_id,
|
||||
params={
|
||||
'stdin': 0,
|
||||
'stdout': 0,
|
||||
'stderr': 1,
|
||||
'logs': 1 if logs else 0,
|
||||
'stream': 1 if stream else 0
|
||||
},
|
||||
ws=True,
|
||||
)
|
||||
to_attach = [c for s in project.get_services(service_names) for c in s.containers()]
|
||||
|
||||
if not detached:
|
||||
print("Attaching to", list_containers(to_attach))
|
||||
log_printer = LogPrinter(to_attach, attach_params={"logs": True}, monochrome=monochrome)
|
||||
|
||||
try:
|
||||
log_printer.run()
|
||||
finally:
|
||||
def handler(signal, frame):
|
||||
project.kill(service_names=service_names)
|
||||
sys.exit(0)
|
||||
signal.signal(signal.SIGINT, handler)
|
||||
|
||||
print("Gracefully stopping... (press Ctrl+C again to force)")
|
||||
project.stop(service_names=service_names)
|
||||
|
||||
return SocketClient(
|
||||
socket_in=stdio,
|
||||
socket_out=stdio,
|
||||
socket_err=stderr,
|
||||
raw=raw,
|
||||
)
|
||||
|
||||
def list_containers(containers):
|
||||
return ", ".join(c.name for c in containers)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from __future__ import absolute_import
|
||||
from threading import Thread
|
||||
|
||||
try:
|
||||
@@ -6,6 +7,11 @@ except ImportError:
|
||||
from queue import Queue, Empty # Python 3.x
|
||||
|
||||
|
||||
# Yield STOP from an input generator to stop the
|
||||
# top-level loop without processing any more input.
|
||||
STOP = object()
|
||||
|
||||
|
||||
class Multiplexer(object):
|
||||
def __init__(self, generators):
|
||||
self.generators = generators
|
||||
@@ -16,7 +22,11 @@ class Multiplexer(object):
|
||||
|
||||
while True:
|
||||
try:
|
||||
yield self.queue.get(timeout=0.1)
|
||||
item = self.queue.get(timeout=0.1)
|
||||
if item is STOP:
|
||||
break
|
||||
else:
|
||||
yield item
|
||||
except Empty:
|
||||
pass
|
||||
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
# Adapted from https://github.com/benthor/remotty/blob/master/socketclient.py
|
||||
|
||||
from select import select
|
||||
import sys
|
||||
import tty
|
||||
import fcntl
|
||||
import os
|
||||
import termios
|
||||
import threading
|
||||
import errno
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SocketClient:
|
||||
def __init__(self,
|
||||
socket_in=None,
|
||||
socket_out=None,
|
||||
socket_err=None,
|
||||
raw=True,
|
||||
):
|
||||
self.socket_in = socket_in
|
||||
self.socket_out = socket_out
|
||||
self.socket_err = socket_err
|
||||
self.raw = raw
|
||||
|
||||
self.stdin_fileno = sys.stdin.fileno()
|
||||
|
||||
def __enter__(self):
|
||||
self.create()
|
||||
return self
|
||||
|
||||
def __exit__(self, type, value, trace):
|
||||
self.destroy()
|
||||
|
||||
def create(self):
|
||||
if os.isatty(sys.stdin.fileno()):
|
||||
self.settings = termios.tcgetattr(sys.stdin.fileno())
|
||||
else:
|
||||
self.settings = None
|
||||
|
||||
if self.socket_in is not None:
|
||||
self.set_blocking(sys.stdin, False)
|
||||
self.set_blocking(sys.stdout, True)
|
||||
self.set_blocking(sys.stderr, True)
|
||||
|
||||
if self.raw:
|
||||
tty.setraw(sys.stdin.fileno())
|
||||
|
||||
def set_blocking(self, file, blocking):
|
||||
fd = file.fileno()
|
||||
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
|
||||
flags = (flags & ~os.O_NONBLOCK) if blocking else (flags | os.O_NONBLOCK)
|
||||
fcntl.fcntl(fd, fcntl.F_SETFL, flags)
|
||||
|
||||
def run(self):
|
||||
if self.socket_in is not None:
|
||||
self.start_background_thread(target=self.send_ws, args=(self.socket_in, sys.stdin))
|
||||
|
||||
recv_threads = []
|
||||
|
||||
if self.socket_out is not None:
|
||||
recv_threads.append(self.start_background_thread(target=self.recv_ws, args=(self.socket_out, sys.stdout)))
|
||||
|
||||
if self.socket_err is not None:
|
||||
recv_threads.append(self.start_background_thread(target=self.recv_ws, args=(self.socket_err, sys.stderr)))
|
||||
|
||||
for t in recv_threads:
|
||||
t.join()
|
||||
|
||||
def start_background_thread(self, **kwargs):
|
||||
thread = threading.Thread(**kwargs)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
return thread
|
||||
|
||||
def recv_ws(self, socket, stream):
|
||||
try:
|
||||
while True:
|
||||
chunk = socket.recv()
|
||||
|
||||
if chunk:
|
||||
stream.write(chunk)
|
||||
stream.flush()
|
||||
else:
|
||||
break
|
||||
except Exception, e:
|
||||
log.debug(e)
|
||||
|
||||
def send_ws(self, socket, stream):
|
||||
while True:
|
||||
r, w, e = select([stream.fileno()], [], [])
|
||||
|
||||
if r:
|
||||
chunk = stream.read(1)
|
||||
|
||||
if chunk == '':
|
||||
socket.send_close()
|
||||
break
|
||||
else:
|
||||
try:
|
||||
socket.send(chunk)
|
||||
except Exception, e:
|
||||
if hasattr(e, 'errno') and e.errno == errno.EPIPE:
|
||||
break
|
||||
else:
|
||||
raise e
|
||||
|
||||
def destroy(self):
|
||||
if self.settings is not None:
|
||||
termios.tcsetattr(self.stdin_fileno, termios.TCSADRAIN, self.settings)
|
||||
|
||||
sys.stdout.flush()
|
||||
|
||||
if __name__ == '__main__':
|
||||
import websocket
|
||||
|
||||
if len(sys.argv) != 2:
|
||||
sys.stderr.write("Usage: python socketclient.py WEBSOCKET_URL\n")
|
||||
exit(1)
|
||||
|
||||
url = sys.argv[1]
|
||||
socket = websocket.create_connection(url)
|
||||
|
||||
print "connected\r"
|
||||
|
||||
with SocketClient(socket, interactive=True) as client:
|
||||
client.run()
|
||||
@@ -1,24 +1,10 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
import datetime
|
||||
import os
|
||||
|
||||
|
||||
def cached_property(f):
|
||||
"""
|
||||
returns a cached property that is calculated by function f
|
||||
http://code.activestate.com/recipes/576563-cached-property/
|
||||
"""
|
||||
def get(self):
|
||||
try:
|
||||
return self._property_cache[f]
|
||||
except AttributeError:
|
||||
self._property_cache = {}
|
||||
x = self._property_cache[f] = f(self)
|
||||
return x
|
||||
except KeyError:
|
||||
x = self._property_cache[f] = f(self)
|
||||
return x
|
||||
|
||||
return property(get)
|
||||
import subprocess
|
||||
import platform
|
||||
|
||||
|
||||
def yesno(prompt, default=None):
|
||||
@@ -60,17 +46,58 @@ def prettydate(d):
|
||||
elif s < 120:
|
||||
return '1 minute ago'
|
||||
elif s < 3600:
|
||||
return '{0} minutes ago'.format(s/60)
|
||||
return '{0} minutes ago'.format(s / 60)
|
||||
elif s < 7200:
|
||||
return '1 hour ago'
|
||||
else:
|
||||
return '{0} hours ago'.format(s/3600)
|
||||
return '{0} hours ago'.format(s / 3600)
|
||||
|
||||
|
||||
def mkdir(path, permissions=0700):
|
||||
def mkdir(path, permissions=0o700):
|
||||
if not os.path.exists(path):
|
||||
os.mkdir(path)
|
||||
|
||||
os.chmod(path, permissions)
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def split_buffer(reader, separator):
|
||||
"""
|
||||
Given a generator which yields strings and a separator string,
|
||||
joins all input, splits on the separator and yields each chunk.
|
||||
|
||||
Unlike string.split(), each chunk includes the trailing
|
||||
separator, except for the last one if none was found on the end
|
||||
of the input.
|
||||
"""
|
||||
buffered = str('')
|
||||
separator = str(separator)
|
||||
|
||||
for data in reader:
|
||||
buffered += data
|
||||
while True:
|
||||
index = buffered.find(separator)
|
||||
if index == -1:
|
||||
break
|
||||
yield buffered[:index + 1]
|
||||
buffered = buffered[index + 1:]
|
||||
|
||||
if len(buffered) > 0:
|
||||
yield buffered
|
||||
|
||||
|
||||
def call_silently(*args, **kwargs):
|
||||
"""
|
||||
Like subprocess.call(), but redirects stdout and stderr to /dev/null.
|
||||
"""
|
||||
with open(os.devnull, 'w') as shutup:
|
||||
return subprocess.call(*args, stdout=shutup, stderr=shutup, **kwargs)
|
||||
|
||||
|
||||
def is_mac():
|
||||
return platform.system() == 'Darwin'
|
||||
|
||||
|
||||
def is_ubuntu():
|
||||
return platform.system() == 'Linux' and platform.linux_distribution()[0] == 'Ubuntu'
|
||||
|
||||
58
fig/cli/verbose_proxy.py
Normal file
58
fig/cli/verbose_proxy.py
Normal file
@@ -0,0 +1,58 @@
|
||||
|
||||
import functools
|
||||
from itertools import chain
|
||||
import logging
|
||||
import pprint
|
||||
|
||||
import six
|
||||
|
||||
|
||||
def format_call(args, kwargs):
|
||||
args = (repr(a) for a in args)
|
||||
kwargs = ("{0!s}={1!r}".format(*item) for item in six.iteritems(kwargs))
|
||||
return "({0})".format(", ".join(chain(args, kwargs)))
|
||||
|
||||
|
||||
def format_return(result, max_lines):
|
||||
if isinstance(result, (list, tuple, set)):
|
||||
return "({0} with {1} items)".format(type(result).__name__, len(result))
|
||||
|
||||
if result:
|
||||
lines = pprint.pformat(result).split('\n')
|
||||
extra = '\n...' if len(lines) > max_lines else ''
|
||||
return '\n'.join(lines[:max_lines]) + extra
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class VerboseProxy(object):
|
||||
"""Proxy all function calls to another class and log method name, arguments
|
||||
and return values for each call.
|
||||
"""
|
||||
|
||||
def __init__(self, obj_name, obj, log_name=None, max_lines=10):
|
||||
self.obj_name = obj_name
|
||||
self.obj = obj
|
||||
self.max_lines = max_lines
|
||||
self.log = logging.getLogger(log_name or __name__)
|
||||
|
||||
def __getattr__(self, name):
|
||||
attr = getattr(self.obj, name)
|
||||
|
||||
if not six.callable(attr):
|
||||
return attr
|
||||
|
||||
return functools.partial(self.proxy_callable, name)
|
||||
|
||||
def proxy_callable(self, call_name, *args, **kwargs):
|
||||
self.log.info("%s %s <- %s",
|
||||
self.obj_name,
|
||||
call_name,
|
||||
format_call(args, kwargs))
|
||||
|
||||
result = getattr(self.obj, call_name)(*args, **kwargs)
|
||||
self.log.info("%s %s -> %s",
|
||||
self.obj_name,
|
||||
call_name,
|
||||
format_return(result, self.max_lines))
|
||||
return result
|
||||
103
fig/container.py
103
fig/container.py
@@ -1,10 +1,12 @@
|
||||
import logging
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
import six
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class Container(object):
|
||||
"""
|
||||
Represents a Docker container, constructed from the output of
|
||||
Represents a Docker container, constructed from the output of
|
||||
GET /containers/:id:/json.
|
||||
"""
|
||||
def __init__(self, client, dictionary, has_been_inspected=False):
|
||||
@@ -18,7 +20,7 @@ class Container(object):
|
||||
Construct a container object from the output of GET /containers/json.
|
||||
"""
|
||||
new_dictionary = {
|
||||
'ID': dictionary['Id'],
|
||||
'Id': dictionary['Id'],
|
||||
'Image': dictionary['Image'],
|
||||
}
|
||||
for name in dictionary.get('Names', []):
|
||||
@@ -37,7 +39,11 @@ class Container(object):
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self.dictionary['ID']
|
||||
return self.dictionary['Id']
|
||||
|
||||
@property
|
||||
def image(self):
|
||||
return self.dictionary['Image']
|
||||
|
||||
@property
|
||||
def short_id(self):
|
||||
@@ -48,61 +54,84 @@ class Container(object):
|
||||
return self.dictionary['Name'][1:]
|
||||
|
||||
@property
|
||||
def human_readable_ports(self):
|
||||
def name_without_project(self):
|
||||
return '_'.join(self.dictionary['Name'].split('_')[1:])
|
||||
|
||||
@property
|
||||
def number(self):
|
||||
try:
|
||||
return int(self.name.split('_')[-1])
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def ports(self):
|
||||
self.inspect_if_not_inspected()
|
||||
if not self.dictionary['NetworkSettings']['Ports']:
|
||||
return ''
|
||||
ports = []
|
||||
for private, public in self.dictionary['NetworkSettings']['Ports'].items():
|
||||
if public:
|
||||
ports.append('%s->%s' % (public[0]['HostPort'], private))
|
||||
return ', '.join(ports)
|
||||
return self.get('NetworkSettings.Ports') or {}
|
||||
|
||||
@property
|
||||
def human_readable_ports(self):
|
||||
def format_port(private, public):
|
||||
if not public:
|
||||
return private
|
||||
return '{HostIp}:{HostPort}->{private}'.format(
|
||||
private=private, **public[0])
|
||||
|
||||
return ', '.join(format_port(*item)
|
||||
for item in sorted(six.iteritems(self.ports)))
|
||||
|
||||
@property
|
||||
def human_readable_state(self):
|
||||
self.inspect_if_not_inspected()
|
||||
if self.dictionary['State']['Running']:
|
||||
if self.dictionary['State']['Ghost']:
|
||||
return 'Ghost'
|
||||
else:
|
||||
return 'Up'
|
||||
if self.is_running:
|
||||
return 'Ghost' if self.get('State.Ghost') else 'Up'
|
||||
else:
|
||||
return 'Exit %s' % self.dictionary['State']['ExitCode']
|
||||
return 'Exit %s' % self.get('State.ExitCode')
|
||||
|
||||
@property
|
||||
def human_readable_command(self):
|
||||
self.inspect_if_not_inspected()
|
||||
return ' '.join(self.dictionary['Config']['Cmd'])
|
||||
entrypoint = self.get('Config.Entrypoint') or []
|
||||
cmd = self.get('Config.Cmd') or []
|
||||
return ' '.join(entrypoint + cmd)
|
||||
|
||||
@property
|
||||
def environment(self):
|
||||
self.inspect_if_not_inspected()
|
||||
out = {}
|
||||
for var in self.dictionary.get('Config', {}).get('Env', []):
|
||||
k, v = var.split('=', 1)
|
||||
out[k] = v
|
||||
return out
|
||||
return dict(var.split("=", 1) for var in self.get('Config.Env') or [])
|
||||
|
||||
@property
|
||||
def is_running(self):
|
||||
return self.get('State.Running')
|
||||
|
||||
def get(self, key):
|
||||
"""Return a value from the container or None if the value is not set.
|
||||
|
||||
:param key: a string using dotted notation for nested dictionary
|
||||
lookups
|
||||
"""
|
||||
self.inspect_if_not_inspected()
|
||||
return self.dictionary['State']['Running']
|
||||
|
||||
def get_value(dictionary, key):
|
||||
return (dictionary or {}).get(key)
|
||||
|
||||
return reduce(get_value, key.split('.'), self.dictionary)
|
||||
|
||||
def get_local_port(self, port, protocol='tcp'):
|
||||
port = self.ports.get("%s/%s" % (port, protocol))
|
||||
return "{HostIp}:{HostPort}".format(**port[0]) if port else None
|
||||
|
||||
def start(self, **options):
|
||||
log.info("Starting %s..." % self.name)
|
||||
return self.client.start(self.id, **options)
|
||||
|
||||
def stop(self, **options):
|
||||
log.info("Stopping %s..." % self.name)
|
||||
return self.client.stop(self.id, **options)
|
||||
|
||||
def kill(self):
|
||||
log.info("Killing %s..." % self.name)
|
||||
return self.client.kill(self.id)
|
||||
|
||||
def remove(self):
|
||||
log.info("Removing %s..." % self.name)
|
||||
return self.client.remove_container(self.id)
|
||||
def restart(self):
|
||||
return self.client.restart(self.id)
|
||||
|
||||
def remove(self, **options):
|
||||
return self.client.remove_container(self.id, **options)
|
||||
|
||||
def inspect_if_not_inspected(self):
|
||||
if not self.has_been_inspected:
|
||||
@@ -116,6 +145,7 @@ class Container(object):
|
||||
|
||||
def inspect(self):
|
||||
self.dictionary = self.client.inspect_container(self.id)
|
||||
self.has_been_inspected = True
|
||||
return self.dictionary
|
||||
|
||||
def links(self):
|
||||
@@ -127,6 +157,9 @@ class Container(object):
|
||||
links.append(bits[2])
|
||||
return links
|
||||
|
||||
def attach(self, *args, **kwargs):
|
||||
return self.client.attach(self.id, *args, **kwargs)
|
||||
|
||||
def attach_socket(self, **kwargs):
|
||||
return self.client.attach_socket(self.id, **kwargs)
|
||||
|
||||
|
||||
0
fig/packages/__init__.py
Normal file
0
fig/packages/__init__.py
Normal file
27
fig/packages/dockerpty/__init__.py
Normal file
27
fig/packages/dockerpty/__init__.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# 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()
|
||||
294
fig/packages/dockerpty/io.py
Normal file
294
fig/packages/dockerpty/io.py
Normal file
@@ -0,0 +1,294 @@
|
||||
# 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)
|
||||
235
fig/packages/dockerpty/pty.py
Normal file
235
fig/packages/dockerpty/pty.py
Normal file
@@ -0,0 +1,235 @@
|
||||
# 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
|
||||
130
fig/packages/dockerpty/tty.py
Normal file
130
fig/packages/dockerpty/tty.py
Normal file
@@ -0,0 +1,130 @@
|
||||
# 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)
|
||||
83
fig/progress_stream.py
Normal file
83
fig/progress_stream.py
Normal file
@@ -0,0 +1,83 @@
|
||||
import json
|
||||
import os
|
||||
import codecs
|
||||
|
||||
|
||||
class StreamOutputError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def stream_output(output, stream):
|
||||
is_terminal = hasattr(stream, 'fileno') and os.isatty(stream.fileno())
|
||||
stream = codecs.getwriter('utf-8')(stream)
|
||||
all_events = []
|
||||
lines = {}
|
||||
diff = 0
|
||||
|
||||
for chunk in output:
|
||||
event = json.loads(chunk)
|
||||
all_events.append(event)
|
||||
|
||||
if 'progress' in event or 'progressDetail' in event:
|
||||
image_id = event['id']
|
||||
|
||||
if image_id in lines:
|
||||
diff = len(lines) - lines[image_id]
|
||||
else:
|
||||
lines[image_id] = len(lines)
|
||||
stream.write("\n")
|
||||
diff = 0
|
||||
|
||||
if is_terminal:
|
||||
# move cursor up `diff` rows
|
||||
stream.write("%c[%dA" % (27, diff))
|
||||
|
||||
print_output_event(event, stream, is_terminal)
|
||||
|
||||
if 'id' in event and is_terminal:
|
||||
# move cursor back down
|
||||
stream.write("%c[%dB" % (27, diff))
|
||||
|
||||
stream.flush()
|
||||
|
||||
return all_events
|
||||
|
||||
|
||||
def print_output_event(event, stream, is_terminal):
|
||||
if 'errorDetail' in event:
|
||||
raise StreamOutputError(event['errorDetail']['message'])
|
||||
|
||||
terminator = ''
|
||||
|
||||
if is_terminal and 'stream' not in event:
|
||||
# erase current line
|
||||
stream.write("%c[2K\r" % 27)
|
||||
terminator = "\r"
|
||||
pass
|
||||
elif 'progressDetail' in event:
|
||||
return
|
||||
|
||||
if 'time' in event:
|
||||
stream.write("[%s] " % event['time'])
|
||||
|
||||
if 'id' in event:
|
||||
stream.write("%s: " % event['id'])
|
||||
|
||||
if 'from' in event:
|
||||
stream.write("(from %s) " % event['from'])
|
||||
|
||||
status = event.get('status', '')
|
||||
|
||||
if 'progress' in event:
|
||||
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
|
||||
stream.write('%s (%.1f%%)%s' % (status, percentage, terminator))
|
||||
else:
|
||||
stream.write('%s%s' % (status, terminator))
|
||||
elif 'stream' in event:
|
||||
stream.write("%s%s" % (event['stream'], terminator))
|
||||
else:
|
||||
stream.write("%s%s\n" % (status, terminator))
|
||||
204
fig/project.py
204
fig/project.py
@@ -1,16 +1,44 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
import logging
|
||||
|
||||
from .service import Service
|
||||
from .container import Container
|
||||
from docker.errors import APIError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def sort_service_dicts(services):
|
||||
# Sort in dependency order
|
||||
def cmp(x, y):
|
||||
x_deps_y = y['name'] in x.get('links', [])
|
||||
y_deps_x = x['name'] in y.get('links', [])
|
||||
if x_deps_y and not y_deps_x:
|
||||
return 1
|
||||
elif y_deps_x and not x_deps_y:
|
||||
return -1
|
||||
return 0
|
||||
return sorted(services, cmp=cmp)
|
||||
# Topological sort (Cormen/Tarjan algorithm).
|
||||
unmarked = services[:]
|
||||
temporary_marked = set()
|
||||
sorted_services = []
|
||||
|
||||
get_service_names = lambda links: [link.split(':')[0] for link in links]
|
||||
|
||||
def visit(n):
|
||||
if n['name'] in temporary_marked:
|
||||
if n['name'] in get_service_names(n.get('links', [])):
|
||||
raise DependencyError('A service can not link to itself: %s' % n['name'])
|
||||
if n['name'] in n.get('volumes_from', []):
|
||||
raise DependencyError('A service can not mount itself as volume: %s' % n['name'])
|
||||
else:
|
||||
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:
|
||||
visit(m)
|
||||
temporary_marked.remove(n['name'])
|
||||
unmarked.remove(n)
|
||||
sorted_services.insert(0, n)
|
||||
|
||||
while unmarked:
|
||||
visit(unmarked[-1])
|
||||
|
||||
return sorted_services
|
||||
|
||||
|
||||
class Project(object):
|
||||
"""
|
||||
@@ -28,19 +56,18 @@ class Project(object):
|
||||
"""
|
||||
project = cls(name, [], client)
|
||||
for service_dict in sort_service_dicts(service_dicts):
|
||||
# Reference links by object
|
||||
links = []
|
||||
if 'links' in service_dict:
|
||||
for service_name in service_dict.get('links', []):
|
||||
links.append(project.get_service(service_name))
|
||||
del service_dict['links']
|
||||
project.services.append(Service(client=client, project=name, links=links, **service_dict))
|
||||
links = project.get_links(service_dict)
|
||||
volumes_from = project.get_volumes_from(service_dict)
|
||||
|
||||
project.services.append(Service(client=client, project=name, links=links, volumes_from=volumes_from, **service_dict))
|
||||
return project
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, name, config, client):
|
||||
dicts = []
|
||||
for service_name, service in config.items():
|
||||
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)
|
||||
@@ -56,61 +83,130 @@ class Project(object):
|
||||
|
||||
raise NoSuchService(name)
|
||||
|
||||
def get_services(self, service_names=None):
|
||||
def get_services(self, service_names=None, include_links=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 [].
|
||||
by the provided list of names, or all services if service_names is None
|
||||
or [].
|
||||
|
||||
Preserves the original order of self.services.
|
||||
If include_links is specified, returns a list including the links for
|
||||
service_names, in order of dependency.
|
||||
|
||||
Raises NoSuchService if any of the named services
|
||||
do not exist.
|
||||
Preserves the original order of self.services where possible,
|
||||
reordering as needed to resolve links.
|
||||
|
||||
Raises NoSuchService if any of the named services do not exist.
|
||||
"""
|
||||
if service_names is None or len(service_names) == 0:
|
||||
return self.services
|
||||
return self.get_services(
|
||||
service_names=[s.name for s in self.services],
|
||||
include_links=include_links
|
||||
)
|
||||
else:
|
||||
unsorted = [self.get_service(name) for name in service_names]
|
||||
return [s for s in self.services if s in unsorted]
|
||||
services = [s for s in self.services if s in unsorted]
|
||||
|
||||
def create_containers(self, service_names=None):
|
||||
"""
|
||||
Returns a list of (service, container) tuples,
|
||||
one for each service with no running containers.
|
||||
"""
|
||||
containers = []
|
||||
for service in self.get_services(service_names):
|
||||
if len(service.containers()) == 0:
|
||||
containers.append((service, service.create_container()))
|
||||
return containers
|
||||
if include_links:
|
||||
services = reduce(self._inject_links, services, [])
|
||||
|
||||
def kill_and_remove(self, tuples):
|
||||
for (service, container) in tuples:
|
||||
container.kill()
|
||||
container.remove()
|
||||
uniques = []
|
||||
[uniques.append(s) for s in services if s not in uniques]
|
||||
return uniques
|
||||
|
||||
def get_links(self, service_dict):
|
||||
links = []
|
||||
if 'links' in service_dict:
|
||||
for link in service_dict.get('links', []):
|
||||
if ':' in link:
|
||||
service_name, link_name = link.split(':', 1)
|
||||
else:
|
||||
service_name, link_name = link, None
|
||||
try:
|
||||
links.append((self.get_service(service_name), link_name))
|
||||
except NoSuchService:
|
||||
raise ConfigurationError('Service "%s" has a link to service "%s" which does not exist.' % (service_dict['name'], service_name))
|
||||
del service_dict['links']
|
||||
return links
|
||||
|
||||
def get_volumes_from(self, service_dict):
|
||||
volumes_from = []
|
||||
if 'volumes_from' in service_dict:
|
||||
for volume_name in service_dict.get('volumes_from', []):
|
||||
try:
|
||||
service = self.get_service(volume_name)
|
||||
volumes_from.append(service)
|
||||
except NoSuchService:
|
||||
try:
|
||||
container = Container.from_id(self.client, volume_name)
|
||||
volumes_from.append(container)
|
||||
except APIError:
|
||||
raise ConfigurationError('Service "%s" mounts volumes from "%s", which is not the name of a service or container.' % (service_dict['name'], volume_name))
|
||||
del service_dict['volumes_from']
|
||||
return volumes_from
|
||||
|
||||
def start(self, service_names=None, **options):
|
||||
for service in self.get_services(service_names):
|
||||
service.start(**options)
|
||||
|
||||
def stop(self, service_names=None, **options):
|
||||
for service in self.get_services(service_names):
|
||||
for service in reversed(self.get_services(service_names)):
|
||||
service.stop(**options)
|
||||
|
||||
def kill(self, service_names=None, **options):
|
||||
for service in self.get_services(service_names):
|
||||
for service in reversed(self.get_services(service_names)):
|
||||
service.kill(**options)
|
||||
|
||||
def restart(self, service_names=None, **options):
|
||||
for service in self.get_services(service_names):
|
||||
service.restart(**options)
|
||||
|
||||
def build(self, service_names=None, no_cache=False):
|
||||
for service in self.get_services(service_names):
|
||||
if service.can_be_built():
|
||||
service.build(no_cache)
|
||||
else:
|
||||
log.info('%s uses an image, skipping' % service.name)
|
||||
|
||||
def up(self, service_names=None, start_links=True, recreate=True):
|
||||
running_containers = []
|
||||
|
||||
for service in self.get_services(service_names, include_links=start_links):
|
||||
if recreate:
|
||||
for (_, container) in service.recreate_containers():
|
||||
running_containers.append(container)
|
||||
else:
|
||||
for container in service.start_or_create_containers():
|
||||
running_containers.append(container)
|
||||
|
||||
return running_containers
|
||||
|
||||
def pull(self, service_names=None, insecure_registry=False):
|
||||
for service in self.get_services(service_names, include_links=True):
|
||||
service.pull(insecure_registry=insecure_registry)
|
||||
|
||||
def remove_stopped(self, service_names=None, **options):
|
||||
for service in self.get_services(service_names):
|
||||
service.remove_stopped(**options)
|
||||
|
||||
def containers(self, service_names=None, *args, **kwargs):
|
||||
l = []
|
||||
for service in self.get_services(service_names):
|
||||
for container in service.containers(*args, **kwargs):
|
||||
l.append(container)
|
||||
return l
|
||||
def containers(self, service_names=None, stopped=False, one_off=False):
|
||||
return [Container.from_ps(self.client, container)
|
||||
for container in self.client.containers(all=stopped)
|
||||
for service in self.get_services(service_names)
|
||||
if service.has_container(container, one_off=one_off)]
|
||||
|
||||
def _inject_links(self, acc, service):
|
||||
linked_names = service.get_linked_names()
|
||||
|
||||
if len(linked_names) > 0:
|
||||
linked_services = self.get_services(
|
||||
service_names=linked_names,
|
||||
include_links=True
|
||||
)
|
||||
else:
|
||||
linked_services = []
|
||||
|
||||
linked_services.append(service)
|
||||
return acc + linked_services
|
||||
|
||||
|
||||
class NoSuchService(Exception):
|
||||
@@ -120,3 +216,15 @@ class NoSuchService(Exception):
|
||||
|
||||
def __str__(self):
|
||||
return self.msg
|
||||
|
||||
|
||||
class ConfigurationError(Exception):
|
||||
def __init__(self, msg):
|
||||
self.msg = msg
|
||||
|
||||
def __str__(self):
|
||||
return self.msg
|
||||
|
||||
|
||||
class DependencyError(ConfigurationError):
|
||||
pass
|
||||
|
||||
459
fig/service.py
459
fig/service.py
@@ -1,59 +1,171 @@
|
||||
from docker.client import APIError
|
||||
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=[], **options):
|
||||
if not re.match('^[a-zA-Z0-9]+$', name):
|
||||
raise ValueError('Invalid name: %s' % name)
|
||||
if not re.match('^[a-zA-Z0-9]+$', project):
|
||||
raise ValueError('Invalid project: %s' % project)
|
||||
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 ValueError('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)
|
||||
raise ConfigError('Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both.' % name)
|
||||
|
||||
supported_options = DOCKER_CONFIG_KEYS + ['build', 'expose']
|
||||
|
||||
for k in options:
|
||||
if k not in supported_options:
|
||||
msg = "Unsupported config option for %s service: '%s'" % (name, k)
|
||||
if k in DOCKER_CONFIG_HINTS:
|
||||
msg += " (did you mean '%s'?)" % DOCKER_CONFIG_HINTS[k]
|
||||
raise ConfigError(msg)
|
||||
|
||||
self.name = name
|
||||
self.client = client
|
||||
self.project = project
|
||||
self.links = links or []
|
||||
self.volumes_from = volumes_from or []
|
||||
self.options = options
|
||||
|
||||
def containers(self, stopped=False, one_off=False):
|
||||
l = []
|
||||
for container in self.client.containers(all=stopped):
|
||||
name = get_container_name(container)
|
||||
if not name or not is_valid_name(name, one_off):
|
||||
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
|
||||
project, name, number = parse_name(name)
|
||||
if project == self.project and name == self.name:
|
||||
l.append(Container.from_ps(self.client, container))
|
||||
return l
|
||||
_, _, 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):
|
||||
if not c.is_running:
|
||||
self.start_container(c, **options)
|
||||
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):
|
||||
@@ -61,118 +173,264 @@ class Service(object):
|
||||
Create a container for this service. If the image doesn't exist, attempt to pull
|
||||
it.
|
||||
"""
|
||||
container_options = self._get_container_options(override_options, one_off=one_off)
|
||||
container_options = self._get_container_create_options(override_options, one_off=one_off)
|
||||
try:
|
||||
return Container.create(self.client, **container_options)
|
||||
except APIError, e:
|
||||
if e.response.status_code == 404 and e.explanation and 'No such image' in e.explanation:
|
||||
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'])
|
||||
self.client.pull(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 start_container(self, container=None, **override_options):
|
||||
if container is None:
|
||||
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 = []
|
||||
|
||||
options = self.options.copy()
|
||||
options.update(override_options)
|
||||
for c in containers:
|
||||
log.info("Recreating %s..." % c.name)
|
||||
tuples.append(self.recreate_container(c, **override_options))
|
||||
|
||||
port_bindings = {}
|
||||
return tuples
|
||||
|
||||
if options.get('ports', None) is not None:
|
||||
for port in options['ports']:
|
||||
port = unicode(port)
|
||||
if ':' in port:
|
||||
internal_port, external_port = port.split(':', 1)
|
||||
port_bindings[int(internal_port)] = int(external_port)
|
||||
else:
|
||||
port_bindings[int(port)] = None
|
||||
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
|
||||
|
||||
volume_bindings = {}
|
||||
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()
|
||||
|
||||
if options.get('volumes', None) is not None:
|
||||
for volume in options['volumes']:
|
||||
external_dir, internal_dir = volume.split(':')
|
||||
volume_bindings[os.path.abspath(external_dir)] = internal_dir
|
||||
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(),
|
||||
port_bindings=port_bindings,
|
||||
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 next_container_name(self, one_off=False):
|
||||
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 + [unicode(self.next_container_number(one_off=one_off))])
|
||||
return '_'.join(bits + [str(self._next_container_number(all_containers))])
|
||||
|
||||
def next_container_number(self, one_off=False):
|
||||
numbers = [parse_name(c.name)[2] for c in self.containers(stopped=True, one_off=one_off)]
|
||||
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
|
||||
|
||||
if len(numbers) == 0:
|
||||
return 1
|
||||
else:
|
||||
return max(numbers) + 1
|
||||
|
||||
def _get_links(self):
|
||||
links = {}
|
||||
for service in self.links:
|
||||
def _get_links(self, link_to_self):
|
||||
links = []
|
||||
for service, link_name in self.links:
|
||||
for container in service.containers():
|
||||
links[container.name] = container.name
|
||||
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_container_options(self, override_options, one_off=False):
|
||||
keys = ['image', 'command', 'hostname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'volumes_from']
|
||||
container_options = dict((k, self.options[k]) for k in keys if k in self.options)
|
||||
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(one_off)
|
||||
container_options['name'] = self._next_container_name(
|
||||
self.containers(stopped=True, one_off=one_off),
|
||||
one_off)
|
||||
|
||||
if 'ports' in container_options:
|
||||
container_options['ports'] = [unicode(p).split(':')[0] for p in container_options['ports']]
|
||||
# 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((v.split(':')[1], {}) for v in container_options['volumes'])
|
||||
container_options['volumes'] = dict(
|
||||
(parse_volume_spec(v).internal, {})
|
||||
for v in container_options['volumes'])
|
||||
|
||||
if 'build' in self.options:
|
||||
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):
|
||||
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
|
||||
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 line in build_output:
|
||||
if line:
|
||||
match = re.search(r'Successfully built ([0-9a-f]+)', line)
|
||||
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)
|
||||
sys.stdout.write(line)
|
||||
|
||||
if image_id is None:
|
||||
raise BuildError()
|
||||
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+)$')
|
||||
|
||||
@@ -187,10 +445,10 @@ def is_valid_name(name, one_off=False):
|
||||
return match.group(3) is None
|
||||
|
||||
|
||||
def parse_name(name, one_off=False):
|
||||
def parse_name(name):
|
||||
match = NAME_RE.match(name)
|
||||
(project, service_name, _, suffix) = match.groups()
|
||||
return (project, service_name, int(suffix))
|
||||
return ServiceName(project, service_name, int(suffix))
|
||||
|
||||
|
||||
def get_container_name(container):
|
||||
@@ -203,3 +461,62 @@ def get_container_name(container):
|
||||
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 +1,5 @@
|
||||
mock >= 1.0.1
|
||||
nose
|
||||
git+https://github.com/pyinstaller/pyinstaller.git@12e40471c77f588ea5be352f7219c873ddaae056#egg=pyinstaller
|
||||
unittest2
|
||||
flake8
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
docker-py==0.2.3
|
||||
docopt==0.6.1
|
||||
PyYAML==3.10
|
||||
docker-py==0.5.3
|
||||
docopt==0.6.1
|
||||
requests==2.2.1
|
||||
six==1.7.3
|
||||
texttable==0.8.1
|
||||
websocket-client==0.11.0
|
||||
|
||||
33
script/.validate
Normal file
33
script/.validate
Normal file
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -z "$VALIDATE_UPSTREAM" ]; then
|
||||
# this is kind of an expensive check, so let's not do this twice if we
|
||||
# are running more than one validate bundlescript
|
||||
|
||||
VALIDATE_REPO='https://github.com/docker/fig.git'
|
||||
VALIDATE_BRANCH='master'
|
||||
|
||||
if [ "$TRAVIS" = 'true' -a "$TRAVIS_PULL_REQUEST" != 'false' ]; then
|
||||
VALIDATE_REPO="https://github.com/${TRAVIS_REPO_SLUG}.git"
|
||||
VALIDATE_BRANCH="${TRAVIS_BRANCH}"
|
||||
fi
|
||||
|
||||
VALIDATE_HEAD="$(git rev-parse --verify HEAD)"
|
||||
|
||||
git fetch -q "$VALIDATE_REPO" "refs/heads/$VALIDATE_BRANCH"
|
||||
VALIDATE_UPSTREAM="$(git rev-parse --verify FETCH_HEAD)"
|
||||
|
||||
VALIDATE_COMMIT_LOG="$VALIDATE_UPSTREAM..$VALIDATE_HEAD"
|
||||
VALIDATE_COMMIT_DIFF="$VALIDATE_UPSTREAM...$VALIDATE_HEAD"
|
||||
|
||||
validate_diff() {
|
||||
if [ "$VALIDATE_UPSTREAM" != "$VALIDATE_HEAD" ]; then
|
||||
git diff "$VALIDATE_COMMIT_DIFF" "$@"
|
||||
fi
|
||||
}
|
||||
validate_log() {
|
||||
if [ "$VALIDATE_UPSTREAM" != "$VALIDATE_HEAD" ]; then
|
||||
git log "$VALIDATE_COMMIT_LOG" "$@"
|
||||
fi
|
||||
}
|
||||
fi
|
||||
5
script/build-docs
Executable file
5
script/build-docs
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
set -ex
|
||||
pushd docs
|
||||
fig run jekyll jekyll build
|
||||
popd
|
||||
8
script/build-linux
Executable file
8
script/build-linux
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
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
|
||||
10
script/build-osx
Executable file
10
script/build-osx
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
set -ex
|
||||
rm -rf venv
|
||||
virtualenv venv
|
||||
venv/bin/pip install -r requirements.txt
|
||||
venv/bin/pip install -r requirements-dev.txt
|
||||
venv/bin/pip install .
|
||||
venv/bin/pyinstaller -F bin/fig
|
||||
mv dist/fig dist/fig-Darwin-x86_64
|
||||
dist/fig-Darwin-x86_64 --version
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/bin/sh
|
||||
find . -type f -name '*.pyc' -delete
|
||||
|
||||
rm -rf docs/_site build dist fig.egg-info
|
||||
|
||||
29
script/deploy-docs
Executable file
29
script/deploy-docs
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/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
|
||||
2
script/open-docs
Executable file
2
script/open-docs
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
open file://`pwd`/docs/_site/index.html
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -xe
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo 'pass a version as first argument'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git tag $1
|
||||
git push --tags
|
||||
python setup.py sdist upload
|
||||
|
||||
|
||||
12
script/test
12
script/test
@@ -1,2 +1,12 @@
|
||||
#!/bin/sh
|
||||
nosetests
|
||||
set -ex
|
||||
|
||||
target="tests"
|
||||
|
||||
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
|
||||
|
||||
56
script/validate-dco
Executable file
56
script/validate-dco
Executable file
@@ -0,0 +1,56 @@
|
||||
#!/bin/bash
|
||||
|
||||
source "$(dirname "$BASH_SOURCE")/.validate"
|
||||
|
||||
adds=$(validate_diff --numstat | awk '{ s += $1 } END { print s }')
|
||||
dels=$(validate_diff --numstat | awk '{ s += $2 } END { print s }')
|
||||
notDocs="$(validate_diff --numstat | awk '$3 !~ /^docs\// { print $3 }')"
|
||||
|
||||
: ${adds:=0}
|
||||
: ${dels:=0}
|
||||
|
||||
# "Username may only contain alphanumeric characters or dashes and cannot begin with a dash"
|
||||
githubUsernameRegex='[a-zA-Z0-9][a-zA-Z0-9-]+'
|
||||
|
||||
# https://github.com/docker/docker/blob/master/CONTRIBUTING.md#sign-your-work
|
||||
dcoPrefix='Signed-off-by:'
|
||||
dcoRegex="^(Docker-DCO-1.1-)?$dcoPrefix ([^<]+) <([^<>@]+@[^<>]+)>( \\(github: ($githubUsernameRegex)\\))?$"
|
||||
|
||||
check_dco() {
|
||||
grep -qE "$dcoRegex"
|
||||
}
|
||||
|
||||
if [ $adds -eq 0 -a $dels -eq 0 ]; then
|
||||
echo '0 adds, 0 deletions; nothing to validate! :)'
|
||||
elif [ -z "$notDocs" -a $adds -le 1 -a $dels -le 1 ]; then
|
||||
echo 'Congratulations! DCO small-patch-exception material!'
|
||||
else
|
||||
commits=( $(validate_log --format='format:%H%n') )
|
||||
badCommits=()
|
||||
for commit in "${commits[@]}"; do
|
||||
if [ -z "$(git log -1 --format='format:' --name-status "$commit")" ]; then
|
||||
# no content (ie, Merge commit, etc)
|
||||
continue
|
||||
fi
|
||||
if ! git log -1 --format='format:%B' "$commit" | check_dco; then
|
||||
badCommits+=( "$commit" )
|
||||
fi
|
||||
done
|
||||
if [ ${#badCommits[@]} -eq 0 ]; then
|
||||
echo "Congratulations! All commits are properly signed with the DCO!"
|
||||
else
|
||||
{
|
||||
echo "These commits do not have a proper '$dcoPrefix' marker:"
|
||||
for commit in "${badCommits[@]}"; do
|
||||
echo " - $commit"
|
||||
done
|
||||
echo
|
||||
echo 'Please amend each commit to include a properly formatted DCO marker.'
|
||||
echo
|
||||
echo 'Visit the following URL for information about the Docker DCO:'
|
||||
echo ' https://github.com/docker/docker/blob/master/CONTRIBUTING.md#sign-your-work'
|
||||
echo
|
||||
} >&2
|
||||
false
|
||||
fi
|
||||
fi
|
||||
53
setup.py
53
setup.py
@@ -1,16 +1,18 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from setuptools import setup
|
||||
import re
|
||||
import os
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from setuptools import setup, find_packages
|
||||
import codecs
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
# Borrowed from
|
||||
# https://github.com/jezdez/django_compressor/blob/develop/setup.py
|
||||
def read(*parts):
|
||||
return codecs.open(os.path.join(os.path.dirname(__file__), *parts)).read()
|
||||
path = os.path.join(os.path.dirname(__file__), *parts)
|
||||
with codecs.open(path, encoding='utf-8') as fobj:
|
||||
return fobj.read()
|
||||
|
||||
|
||||
def find_version(*file_paths):
|
||||
@@ -21,24 +23,41 @@ def find_version(*file_paths):
|
||||
return version_match.group(1)
|
||||
raise RuntimeError("Unable to find version string.")
|
||||
|
||||
with open('requirements.txt') as f:
|
||||
install_requires = f.read().splitlines()
|
||||
|
||||
with open('README.md') as f:
|
||||
long_description = f.read()
|
||||
install_requires = [
|
||||
'docopt >= 0.6.1, < 0.7',
|
||||
'PyYAML >= 3.10, < 4',
|
||||
'requests >= 2.2.1, < 3',
|
||||
'texttable >= 0.8.1, < 0.9',
|
||||
'websocket-client >= 0.11.0, < 0.12',
|
||||
'docker-py >= 0.5, < 0.6',
|
||||
'six >= 1.3.0, < 2',
|
||||
]
|
||||
|
||||
tests_require = [
|
||||
'mock >= 1.0.1',
|
||||
'nose',
|
||||
'pyinstaller',
|
||||
'flake8',
|
||||
]
|
||||
|
||||
|
||||
if sys.version_info < (2, 7):
|
||||
tests_require.append('unittest2')
|
||||
|
||||
|
||||
setup(
|
||||
name='fig',
|
||||
version=find_version("fig", "__init__.py"),
|
||||
description='Punctual, lightweight development environments using Docker',
|
||||
long_description=long_description,
|
||||
url='https://github.com/orchardup/fig',
|
||||
author='Orchard Laboratories Ltd.',
|
||||
author_email='hello@orchardup.com',
|
||||
packages=['fig', 'fig.cli'],
|
||||
package_data={},
|
||||
url='http://www.fig.sh/',
|
||||
author='Docker, Inc.',
|
||||
license='Apache License 2.0',
|
||||
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
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import sys
|
||||
|
||||
if sys.version_info >= (2,7):
|
||||
import unittest
|
||||
else:
|
||||
import unittest2 as unittest
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
from .testcases import DockerClientTestCase
|
||||
from fig.container import Container
|
||||
|
||||
class ContainerTest(DockerClientTestCase):
|
||||
def test_from_ps(self):
|
||||
container = Container.from_ps(self.client, {
|
||||
"Id":"abc",
|
||||
"Image":"ubuntu:12.04",
|
||||
"Command":"sleep 300",
|
||||
"Created":1387384730,
|
||||
"Status":"Up 8 seconds",
|
||||
"Ports":None,
|
||||
"SizeRw":0,
|
||||
"SizeRootFs":0,
|
||||
"Names":["/db_1"]
|
||||
}, has_been_inspected=True)
|
||||
self.assertEqual(container.dictionary, {
|
||||
"ID": "abc",
|
||||
"Image":"ubuntu:12.04",
|
||||
"Name": "/db_1",
|
||||
})
|
||||
|
||||
def test_environment(self):
|
||||
container = Container(self.client, {
|
||||
'ID': 'abc',
|
||||
'Config': {
|
||||
'Env': [
|
||||
'FOO=BAR',
|
||||
'BAZ=DOGE',
|
||||
]
|
||||
}
|
||||
}, has_been_inspected=True)
|
||||
self.assertEqual(container.environment, {
|
||||
'FOO': 'BAR',
|
||||
'BAZ': 'DOGE',
|
||||
})
|
||||
5
tests/fixtures/commands-figfile/fig.yml
vendored
Normal file
5
tests/fixtures/commands-figfile/fig.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
implicit:
|
||||
image: figtest_test
|
||||
explicit:
|
||||
image: figtest_test
|
||||
command: [ "/bin/true" ]
|
||||
2
tests/fixtures/dockerfile_with_entrypoint/Dockerfile
vendored
Normal file
2
tests/fixtures/dockerfile_with_entrypoint/Dockerfile
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
FROM busybox:latest
|
||||
ENTRYPOINT echo "From prebuilt entrypoint"
|
||||
2
tests/fixtures/dockerfile_with_entrypoint/fig.yml
vendored
Normal file
2
tests/fixtures/dockerfile_with_entrypoint/fig.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
service:
|
||||
build: tests/fixtures/dockerfile_with_entrypoint
|
||||
7
tests/fixtures/environment-figfile/fig.yml
vendored
Normal file
7
tests/fixtures/environment-figfile/fig.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
service:
|
||||
image: busybox:latest
|
||||
command: sleep 5
|
||||
|
||||
environment:
|
||||
foo: bar
|
||||
hello: world
|
||||
11
tests/fixtures/links-figfile/fig.yml
vendored
Normal file
11
tests/fixtures/links-figfile/fig.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
db:
|
||||
image: busybox:latest
|
||||
command: /bin/sleep 300
|
||||
web:
|
||||
image: busybox:latest
|
||||
command: /bin/sleep 300
|
||||
links:
|
||||
- db:db
|
||||
console:
|
||||
image: busybox:latest
|
||||
command: /bin/sleep 300
|
||||
3
tests/fixtures/longer-filename-figfile/fig.yaml
vendored
Normal file
3
tests/fixtures/longer-filename-figfile/fig.yaml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
definedinyamlnotyml:
|
||||
image: busybox:latest
|
||||
command: /bin/sleep 300
|
||||
6
tests/fixtures/multiple-figfiles/fig.yml
vendored
Normal file
6
tests/fixtures/multiple-figfiles/fig.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
simple:
|
||||
image: busybox:latest
|
||||
command: /bin/sleep 300
|
||||
another:
|
||||
image: busybox:latest
|
||||
command: /bin/sleep 300
|
||||
3
tests/fixtures/multiple-figfiles/fig2.yml
vendored
Normal file
3
tests/fixtures/multiple-figfiles/fig2.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
yetanother:
|
||||
image: busybox:latest
|
||||
command: /bin/sleep 300
|
||||
7
tests/fixtures/ports-figfile/fig.yml
vendored
Normal file
7
tests/fixtures/ports-figfile/fig.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
simple:
|
||||
image: busybox:latest
|
||||
command: /bin/sleep 300
|
||||
ports:
|
||||
- '3000'
|
||||
- '9999:3001'
|
||||
2
tests/fixtures/simple-dockerfile/Dockerfile
vendored
2
tests/fixtures/simple-dockerfile/Dockerfile
vendored
@@ -1,2 +1,2 @@
|
||||
FROM ubuntu
|
||||
FROM busybox:latest
|
||||
CMD echo "success"
|
||||
|
||||
2
tests/fixtures/simple-dockerfile/fig.yml
vendored
Normal file
2
tests/fixtures/simple-dockerfile/fig.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
simple:
|
||||
build: tests/fixtures/simple-dockerfile
|
||||
6
tests/fixtures/simple-figfile/fig.yml
vendored
Normal file
6
tests/fixtures/simple-figfile/fig.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
simple:
|
||||
image: busybox:latest
|
||||
command: /bin/sleep 300
|
||||
another:
|
||||
image: busybox:latest
|
||||
command: /bin/sleep 300
|
||||
0
tests/integration/__init__.py
Normal file
0
tests/integration/__init__.py
Normal file
287
tests/integration/cli_test.py
Normal file
287
tests/integration/cli_test.py
Normal file
@@ -0,0 +1,287 @@
|
||||
from __future__ import absolute_import
|
||||
import sys
|
||||
|
||||
from six import StringIO
|
||||
from mock import patch
|
||||
|
||||
from .testcases import DockerClientTestCase
|
||||
from fig.cli.main import TopLevelCommand
|
||||
|
||||
|
||||
class CLITestCase(DockerClientTestCase):
|
||||
def setUp(self):
|
||||
super(CLITestCase, self).setUp()
|
||||
self.old_sys_exit = sys.exit
|
||||
sys.exit = lambda code=0: None
|
||||
self.command = TopLevelCommand()
|
||||
self.command.base_dir = 'tests/fixtures/simple-figfile'
|
||||
|
||||
def tearDown(self):
|
||||
sys.exit = self.old_sys_exit
|
||||
self.project.kill()
|
||||
self.project.remove_stopped()
|
||||
|
||||
@property
|
||||
def project(self):
|
||||
return self.command.get_project(self.command.get_config_path())
|
||||
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
def test_ps(self, mock_stdout):
|
||||
self.project.get_service('simple').create_container()
|
||||
self.command.dispatch(['ps'], None)
|
||||
self.assertIn('simplefigfile_simple_1', mock_stdout.getvalue())
|
||||
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
def test_ps_default_figfile(self, mock_stdout):
|
||||
self.command.base_dir = 'tests/fixtures/multiple-figfiles'
|
||||
self.command.dispatch(['up', '-d'], None)
|
||||
self.command.dispatch(['ps'], None)
|
||||
|
||||
output = mock_stdout.getvalue()
|
||||
self.assertIn('multiplefigfiles_simple_1', output)
|
||||
self.assertIn('multiplefigfiles_another_1', output)
|
||||
self.assertNotIn('multiplefigfiles_yetanother_1', output)
|
||||
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
def test_ps_alternate_figfile(self, mock_stdout):
|
||||
self.command.base_dir = 'tests/fixtures/multiple-figfiles'
|
||||
self.command.dispatch(['-f', 'fig2.yml', 'up', '-d'], None)
|
||||
self.command.dispatch(['-f', 'fig2.yml', 'ps'], None)
|
||||
|
||||
output = mock_stdout.getvalue()
|
||||
self.assertNotIn('multiplefigfiles_simple_1', output)
|
||||
self.assertNotIn('multiplefigfiles_another_1', output)
|
||||
self.assertIn('multiplefigfiles_yetanother_1', output)
|
||||
|
||||
@patch('fig.service.log')
|
||||
def test_pull(self, mock_logging):
|
||||
self.command.dispatch(['pull'], None)
|
||||
mock_logging.info.assert_any_call('Pulling simple (busybox:latest)...')
|
||||
mock_logging.info.assert_any_call('Pulling another (busybox:latest)...')
|
||||
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
def test_build_no_cache(self, mock_stdout):
|
||||
self.command.base_dir = 'tests/fixtures/simple-dockerfile'
|
||||
self.command.dispatch(['build', 'simple'], None)
|
||||
|
||||
mock_stdout.truncate(0)
|
||||
cache_indicator = 'Using cache'
|
||||
self.command.dispatch(['build', 'simple'], None)
|
||||
output = mock_stdout.getvalue()
|
||||
self.assertIn(cache_indicator, output)
|
||||
|
||||
mock_stdout.truncate(0)
|
||||
self.command.dispatch(['build', '--no-cache', 'simple'], None)
|
||||
output = mock_stdout.getvalue()
|
||||
self.assertNotIn(cache_indicator, output)
|
||||
def test_up(self):
|
||||
self.command.dispatch(['up', '-d'], None)
|
||||
service = self.project.get_service('simple')
|
||||
another = self.project.get_service('another')
|
||||
self.assertEqual(len(service.containers()), 1)
|
||||
self.assertEqual(len(another.containers()), 1)
|
||||
|
||||
def test_up_with_links(self):
|
||||
self.command.base_dir = 'tests/fixtures/links-figfile'
|
||||
self.command.dispatch(['up', '-d', 'web'], None)
|
||||
web = self.project.get_service('web')
|
||||
db = self.project.get_service('db')
|
||||
console = self.project.get_service('console')
|
||||
self.assertEqual(len(web.containers()), 1)
|
||||
self.assertEqual(len(db.containers()), 1)
|
||||
self.assertEqual(len(console.containers()), 0)
|
||||
|
||||
def test_up_with_no_deps(self):
|
||||
self.command.base_dir = 'tests/fixtures/links-figfile'
|
||||
self.command.dispatch(['up', '-d', '--no-deps', 'web'], None)
|
||||
web = self.project.get_service('web')
|
||||
db = self.project.get_service('db')
|
||||
console = self.project.get_service('console')
|
||||
self.assertEqual(len(web.containers()), 1)
|
||||
self.assertEqual(len(db.containers()), 0)
|
||||
self.assertEqual(len(console.containers()), 0)
|
||||
|
||||
def test_up_with_recreate(self):
|
||||
self.command.dispatch(['up', '-d'], None)
|
||||
service = self.project.get_service('simple')
|
||||
self.assertEqual(len(service.containers()), 1)
|
||||
|
||||
old_ids = [c.id for c in service.containers()]
|
||||
|
||||
self.command.dispatch(['up', '-d'], None)
|
||||
self.assertEqual(len(service.containers()), 1)
|
||||
|
||||
new_ids = [c.id for c in service.containers()]
|
||||
|
||||
self.assertNotEqual(old_ids, new_ids)
|
||||
|
||||
def test_up_with_keep_old(self):
|
||||
self.command.dispatch(['up', '-d'], None)
|
||||
service = self.project.get_service('simple')
|
||||
self.assertEqual(len(service.containers()), 1)
|
||||
|
||||
old_ids = [c.id for c in service.containers()]
|
||||
|
||||
self.command.dispatch(['up', '-d', '--no-recreate'], None)
|
||||
self.assertEqual(len(service.containers()), 1)
|
||||
|
||||
new_ids = [c.id for c in service.containers()]
|
||||
|
||||
self.assertEqual(old_ids, new_ids)
|
||||
|
||||
@patch('fig.packages.dockerpty.start')
|
||||
def test_run_service_without_links(self, mock_stdout):
|
||||
self.command.base_dir = 'tests/fixtures/links-figfile'
|
||||
self.command.dispatch(['run', 'console', '/bin/true'], None)
|
||||
self.assertEqual(len(self.project.containers()), 0)
|
||||
|
||||
@patch('fig.packages.dockerpty.start')
|
||||
def test_run_service_with_links(self, __):
|
||||
self.command.base_dir = 'tests/fixtures/links-figfile'
|
||||
self.command.dispatch(['run', 'web', '/bin/true'], None)
|
||||
db = self.project.get_service('db')
|
||||
console = self.project.get_service('console')
|
||||
self.assertEqual(len(db.containers()), 1)
|
||||
self.assertEqual(len(console.containers()), 0)
|
||||
|
||||
@patch('fig.packages.dockerpty.start')
|
||||
def test_run_with_no_deps(self, __):
|
||||
self.command.base_dir = 'tests/fixtures/links-figfile'
|
||||
self.command.dispatch(['run', '--no-deps', 'web', '/bin/true'], None)
|
||||
db = self.project.get_service('db')
|
||||
self.assertEqual(len(db.containers()), 0)
|
||||
|
||||
@patch('fig.packages.dockerpty.start')
|
||||
def test_run_does_not_recreate_linked_containers(self, __):
|
||||
self.command.base_dir = 'tests/fixtures/links-figfile'
|
||||
self.command.dispatch(['up', '-d', 'db'], None)
|
||||
db = self.project.get_service('db')
|
||||
self.assertEqual(len(db.containers()), 1)
|
||||
|
||||
old_ids = [c.id for c in db.containers()]
|
||||
|
||||
self.command.dispatch(['run', 'web', '/bin/true'], None)
|
||||
self.assertEqual(len(db.containers()), 1)
|
||||
|
||||
new_ids = [c.id for c in db.containers()]
|
||||
|
||||
self.assertEqual(old_ids, new_ids)
|
||||
|
||||
@patch('fig.packages.dockerpty.start')
|
||||
def test_run_without_command(self, __):
|
||||
self.command.base_dir = 'tests/fixtures/commands-figfile'
|
||||
self.check_build('tests/fixtures/simple-dockerfile', tag='figtest_test')
|
||||
|
||||
for c in self.project.containers(stopped=True, one_off=True):
|
||||
c.remove()
|
||||
|
||||
self.command.dispatch(['run', 'implicit'], None)
|
||||
service = self.project.get_service('implicit')
|
||||
containers = service.containers(stopped=True, one_off=True)
|
||||
self.assertEqual(
|
||||
[c.human_readable_command for c in containers],
|
||||
[u'/bin/sh -c echo "success"'],
|
||||
)
|
||||
|
||||
self.command.dispatch(['run', 'explicit'], None)
|
||||
service = self.project.get_service('explicit')
|
||||
containers = service.containers(stopped=True, one_off=True)
|
||||
self.assertEqual(
|
||||
[c.human_readable_command for c in containers],
|
||||
[u'/bin/true'],
|
||||
)
|
||||
|
||||
@patch('fig.packages.dockerpty.start')
|
||||
def test_run_service_with_entrypoint_overridden(self, _):
|
||||
self.command.base_dir = 'tests/fixtures/dockerfile_with_entrypoint'
|
||||
name = 'service'
|
||||
self.command.dispatch(
|
||||
['run', '--entrypoint', '/bin/echo', name, 'helloworld'],
|
||||
None
|
||||
)
|
||||
service = self.project.get_service(name)
|
||||
container = service.containers(stopped=True, one_off=True)[0]
|
||||
self.assertEqual(
|
||||
container.human_readable_command,
|
||||
u'/bin/echo helloworld'
|
||||
)
|
||||
|
||||
@patch('fig.packages.dockerpty.start')
|
||||
def test_run_service_with_environement_overridden(self, _):
|
||||
name = 'service'
|
||||
self.command.base_dir = 'tests/fixtures/environment-figfile'
|
||||
self.command.dispatch(
|
||||
['run', '-e', 'foo=notbar', '-e', 'allo=moto=bobo',
|
||||
'-e', 'alpha=beta', name],
|
||||
None
|
||||
)
|
||||
service = self.project.get_service(name)
|
||||
container = service.containers(stopped=True, one_off=True)[0]
|
||||
# env overriden
|
||||
self.assertEqual('notbar', container.environment['foo'])
|
||||
# keep environement from yaml
|
||||
self.assertEqual('world', container.environment['hello'])
|
||||
# added option from command line
|
||||
self.assertEqual('beta', container.environment['alpha'])
|
||||
# make sure a value with a = don't crash out
|
||||
self.assertEqual('moto=bobo', container.environment['allo'])
|
||||
|
||||
def test_rm(self):
|
||||
service = self.project.get_service('simple')
|
||||
service.create_container()
|
||||
service.kill()
|
||||
self.assertEqual(len(service.containers(stopped=True)), 1)
|
||||
self.command.dispatch(['rm', '--force'], None)
|
||||
self.assertEqual(len(service.containers(stopped=True)), 0)
|
||||
|
||||
def test_restart(self):
|
||||
service = self.project.get_service('simple')
|
||||
container = service.create_container()
|
||||
service.start_container(container)
|
||||
started_at = container.dictionary['State']['StartedAt']
|
||||
self.command.dispatch(['restart'], None)
|
||||
container.inspect()
|
||||
self.assertNotEqual(
|
||||
container.dictionary['State']['FinishedAt'],
|
||||
'0001-01-01T00:00:00Z',
|
||||
)
|
||||
self.assertNotEqual(
|
||||
container.dictionary['State']['StartedAt'],
|
||||
started_at,
|
||||
)
|
||||
|
||||
def test_scale(self):
|
||||
project = self.project
|
||||
|
||||
self.command.scale(project, {'SERVICE=NUM': ['simple=1']})
|
||||
self.assertEqual(len(project.get_service('simple').containers()), 1)
|
||||
|
||||
self.command.scale(project, {'SERVICE=NUM': ['simple=3', 'another=2']})
|
||||
self.assertEqual(len(project.get_service('simple').containers()), 3)
|
||||
self.assertEqual(len(project.get_service('another').containers()), 2)
|
||||
|
||||
self.command.scale(project, {'SERVICE=NUM': ['simple=1', 'another=1']})
|
||||
self.assertEqual(len(project.get_service('simple').containers()), 1)
|
||||
self.assertEqual(len(project.get_service('another').containers()), 1)
|
||||
|
||||
self.command.scale(project, {'SERVICE=NUM': ['simple=1', 'another=1']})
|
||||
self.assertEqual(len(project.get_service('simple').containers()), 1)
|
||||
self.assertEqual(len(project.get_service('another').containers()), 1)
|
||||
|
||||
self.command.scale(project, {'SERVICE=NUM': ['simple=0', 'another=0']})
|
||||
self.assertEqual(len(project.get_service('simple').containers()), 0)
|
||||
self.assertEqual(len(project.get_service('another').containers()), 0)
|
||||
|
||||
def test_port(self):
|
||||
self.command.base_dir = 'tests/fixtures/ports-figfile'
|
||||
self.command.dispatch(['up', '-d'], None)
|
||||
container = self.project.get_service('simple').get_container()
|
||||
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
def get_port(number, mock_stdout):
|
||||
self.command.dispatch(['port', 'simple', str(number)], None)
|
||||
return mock_stdout.getvalue().rstrip()
|
||||
|
||||
self.assertEqual(get_port(3000), container.get_local_port(3000))
|
||||
self.assertEqual(get_port(3001), "0.0.0.0:9999")
|
||||
self.assertEqual(get_port(3002), "")
|
||||
245
tests/integration/project_test.py
Normal file
245
tests/integration/project_test.py
Normal file
@@ -0,0 +1,245 @@
|
||||
from __future__ import unicode_literals
|
||||
from fig.project import Project, ConfigurationError
|
||||
from fig.container import Container
|
||||
from .testcases import DockerClientTestCase
|
||||
|
||||
|
||||
class ProjectTest(DockerClientTestCase):
|
||||
def test_volumes_from_service(self):
|
||||
project = Project.from_config(
|
||||
name='figtest',
|
||||
config={
|
||||
'data': {
|
||||
'image': 'busybox:latest',
|
||||
'volumes': ['/var/data'],
|
||||
},
|
||||
'db': {
|
||||
'image': 'busybox:latest',
|
||||
'volumes_from': ['data'],
|
||||
},
|
||||
},
|
||||
client=self.client,
|
||||
)
|
||||
db = project.get_service('db')
|
||||
data = project.get_service('data')
|
||||
self.assertEqual(db.volumes_from, [data])
|
||||
|
||||
def test_volumes_from_container(self):
|
||||
data_container = Container.create(
|
||||
self.client,
|
||||
image='busybox:latest',
|
||||
volumes=['/var/data'],
|
||||
name='figtest_data_container',
|
||||
)
|
||||
project = Project.from_config(
|
||||
name='figtest',
|
||||
config={
|
||||
'db': {
|
||||
'image': 'busybox:latest',
|
||||
'volumes_from': ['figtest_data_container'],
|
||||
},
|
||||
},
|
||||
client=self.client,
|
||||
)
|
||||
db = project.get_service('db')
|
||||
self.assertEqual(db.volumes_from, [data_container])
|
||||
|
||||
def test_start_stop_kill_remove(self):
|
||||
web = self.create_service('web')
|
||||
db = self.create_service('db')
|
||||
project = Project('figtest', [web, db], self.client)
|
||||
|
||||
project.start()
|
||||
|
||||
self.assertEqual(len(web.containers()), 0)
|
||||
self.assertEqual(len(db.containers()), 0)
|
||||
|
||||
web_container_1 = web.create_container()
|
||||
web_container_2 = web.create_container()
|
||||
db_container = db.create_container()
|
||||
|
||||
project.start(service_names=['web'])
|
||||
self.assertEqual(set(c.name for c in project.containers()), set([web_container_1.name, web_container_2.name]))
|
||||
|
||||
project.start()
|
||||
self.assertEqual(set(c.name for c in project.containers()), set([web_container_1.name, web_container_2.name, db_container.name]))
|
||||
|
||||
project.stop(service_names=['web'], timeout=1)
|
||||
self.assertEqual(set(c.name for c in project.containers()), set([db_container.name]))
|
||||
|
||||
project.kill(service_names=['db'])
|
||||
self.assertEqual(len(project.containers()), 0)
|
||||
self.assertEqual(len(project.containers(stopped=True)), 3)
|
||||
|
||||
project.remove_stopped(service_names=['web'])
|
||||
self.assertEqual(len(project.containers(stopped=True)), 1)
|
||||
|
||||
project.remove_stopped()
|
||||
self.assertEqual(len(project.containers(stopped=True)), 0)
|
||||
|
||||
def test_project_up(self):
|
||||
web = self.create_service('web')
|
||||
db = self.create_service('db', volumes=['/var/db'])
|
||||
project = Project('figtest', [web, db], self.client)
|
||||
project.start()
|
||||
self.assertEqual(len(project.containers()), 0)
|
||||
|
||||
project.up(['db'])
|
||||
self.assertEqual(len(project.containers()), 1)
|
||||
self.assertEqual(len(db.containers()), 1)
|
||||
self.assertEqual(len(web.containers()), 0)
|
||||
|
||||
project.kill()
|
||||
project.remove_stopped()
|
||||
|
||||
def test_project_up_recreates_containers(self):
|
||||
web = self.create_service('web')
|
||||
db = self.create_service('db', volumes=['/etc'])
|
||||
project = Project('figtest', [web, db], self.client)
|
||||
project.start()
|
||||
self.assertEqual(len(project.containers()), 0)
|
||||
|
||||
project.up(['db'])
|
||||
self.assertEqual(len(project.containers()), 1)
|
||||
old_db_id = project.containers()[0].id
|
||||
db_volume_path = project.containers()[0].get('Volumes./etc')
|
||||
|
||||
project.up()
|
||||
self.assertEqual(len(project.containers()), 2)
|
||||
|
||||
db_container = [c for c in project.containers() if 'db' in c.name][0]
|
||||
self.assertNotEqual(db_container.id, old_db_id)
|
||||
self.assertEqual(db_container.get('Volumes./etc'), db_volume_path)
|
||||
|
||||
project.kill()
|
||||
project.remove_stopped()
|
||||
|
||||
def test_project_up_with_no_recreate_running(self):
|
||||
web = self.create_service('web')
|
||||
db = self.create_service('db', volumes=['/var/db'])
|
||||
project = Project('figtest', [web, db], self.client)
|
||||
project.start()
|
||||
self.assertEqual(len(project.containers()), 0)
|
||||
|
||||
project.up(['db'])
|
||||
self.assertEqual(len(project.containers()), 1)
|
||||
old_db_id = project.containers()[0].id
|
||||
db_volume_path = project.containers()[0].inspect()['Volumes']['/var/db']
|
||||
|
||||
project.up(recreate=False)
|
||||
self.assertEqual(len(project.containers()), 2)
|
||||
|
||||
db_container = [c for c in project.containers() if 'db' in c.name][0]
|
||||
self.assertEqual(db_container.id, old_db_id)
|
||||
self.assertEqual(db_container.inspect()['Volumes']['/var/db'],
|
||||
db_volume_path)
|
||||
|
||||
project.kill()
|
||||
project.remove_stopped()
|
||||
|
||||
def test_project_up_with_no_recreate_stopped(self):
|
||||
web = self.create_service('web')
|
||||
db = self.create_service('db', volumes=['/var/db'])
|
||||
project = Project('figtest', [web, db], self.client)
|
||||
project.start()
|
||||
self.assertEqual(len(project.containers()), 0)
|
||||
|
||||
project.up(['db'])
|
||||
project.stop()
|
||||
|
||||
old_containers = project.containers(stopped=True)
|
||||
|
||||
self.assertEqual(len(old_containers), 1)
|
||||
old_db_id = old_containers[0].id
|
||||
db_volume_path = old_containers[0].inspect()['Volumes']['/var/db']
|
||||
|
||||
project.up(recreate=False)
|
||||
|
||||
new_containers = project.containers(stopped=True)
|
||||
self.assertEqual(len(new_containers), 2)
|
||||
|
||||
db_container = [c for c in new_containers if 'db' in c.name][0]
|
||||
self.assertEqual(db_container.id, old_db_id)
|
||||
self.assertEqual(db_container.inspect()['Volumes']['/var/db'],
|
||||
db_volume_path)
|
||||
|
||||
project.kill()
|
||||
project.remove_stopped()
|
||||
|
||||
def test_project_up_without_all_services(self):
|
||||
console = self.create_service('console')
|
||||
db = self.create_service('db')
|
||||
project = Project('figtest', [console, db], self.client)
|
||||
project.start()
|
||||
self.assertEqual(len(project.containers()), 0)
|
||||
|
||||
project.up()
|
||||
self.assertEqual(len(project.containers()), 2)
|
||||
self.assertEqual(len(db.containers()), 1)
|
||||
self.assertEqual(len(console.containers()), 1)
|
||||
|
||||
project.kill()
|
||||
project.remove_stopped()
|
||||
|
||||
def test_project_up_starts_links(self):
|
||||
console = self.create_service('console')
|
||||
db = self.create_service('db', volumes=['/var/db'])
|
||||
web = self.create_service('web', links=[(db, 'db')])
|
||||
|
||||
project = Project('figtest', [web, db, console], self.client)
|
||||
project.start()
|
||||
self.assertEqual(len(project.containers()), 0)
|
||||
|
||||
project.up(['web'])
|
||||
self.assertEqual(len(project.containers()), 2)
|
||||
self.assertEqual(len(web.containers()), 1)
|
||||
self.assertEqual(len(db.containers()), 1)
|
||||
self.assertEqual(len(console.containers()), 0)
|
||||
|
||||
project.kill()
|
||||
project.remove_stopped()
|
||||
|
||||
def test_project_up_with_no_deps(self):
|
||||
console = self.create_service('console')
|
||||
db = self.create_service('db', volumes=['/var/db'])
|
||||
web = self.create_service('web', links=[(db, 'db')])
|
||||
|
||||
project = Project('figtest', [web, db, console], self.client)
|
||||
project.start()
|
||||
self.assertEqual(len(project.containers()), 0)
|
||||
|
||||
project.up(['web'], start_links=False)
|
||||
self.assertEqual(len(project.containers()), 1)
|
||||
self.assertEqual(len(web.containers()), 1)
|
||||
self.assertEqual(len(db.containers()), 0)
|
||||
self.assertEqual(len(console.containers()), 0)
|
||||
|
||||
project.kill()
|
||||
project.remove_stopped()
|
||||
|
||||
def test_unscale_after_restart(self):
|
||||
web = self.create_service('web')
|
||||
project = Project('figtest', [web], self.client)
|
||||
|
||||
project.start()
|
||||
|
||||
service = project.get_service('web')
|
||||
service.scale(1)
|
||||
self.assertEqual(len(service.containers()), 1)
|
||||
service.scale(3)
|
||||
self.assertEqual(len(service.containers()), 3)
|
||||
project.up()
|
||||
service = project.get_service('web')
|
||||
self.assertEqual(len(service.containers()), 3)
|
||||
service.scale(1)
|
||||
self.assertEqual(len(service.containers()), 1)
|
||||
project.up()
|
||||
service = project.get_service('web')
|
||||
self.assertEqual(len(service.containers()), 1)
|
||||
# does scale=0 ,makes any sense? after recreating at least 1 container is running
|
||||
service.scale(0)
|
||||
project.up()
|
||||
service = project.get_service('web')
|
||||
self.assertEqual(len(service.containers()), 1)
|
||||
project.kill()
|
||||
project.remove_stopped()
|
||||
391
tests/integration/service_test.py
Normal file
391
tests/integration/service_test.py
Normal file
@@ -0,0 +1,391 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
import os
|
||||
|
||||
from fig import Service
|
||||
from fig.service import CannotBeScaledError
|
||||
from fig.container import Container
|
||||
from docker.errors import APIError
|
||||
from .testcases import DockerClientTestCase
|
||||
|
||||
|
||||
class ServiceTest(DockerClientTestCase):
|
||||
def test_containers(self):
|
||||
foo = self.create_service('foo')
|
||||
bar = self.create_service('bar')
|
||||
|
||||
foo.start_container()
|
||||
|
||||
self.assertEqual(len(foo.containers()), 1)
|
||||
self.assertEqual(foo.containers()[0].name, 'figtest_foo_1')
|
||||
self.assertEqual(len(bar.containers()), 0)
|
||||
|
||||
bar.start_container()
|
||||
bar.start_container()
|
||||
|
||||
self.assertEqual(len(foo.containers()), 1)
|
||||
self.assertEqual(len(bar.containers()), 2)
|
||||
|
||||
names = [c.name for c in bar.containers()]
|
||||
self.assertIn('figtest_bar_1', names)
|
||||
self.assertIn('figtest_bar_2', names)
|
||||
|
||||
def test_containers_one_off(self):
|
||||
db = self.create_service('db')
|
||||
container = db.create_container(one_off=True)
|
||||
self.assertEqual(db.containers(stopped=True), [])
|
||||
self.assertEqual(db.containers(one_off=True, stopped=True), [container])
|
||||
|
||||
def test_project_is_added_to_container_name(self):
|
||||
service = self.create_service('web')
|
||||
service.start_container()
|
||||
self.assertEqual(service.containers()[0].name, 'figtest_web_1')
|
||||
|
||||
def test_start_stop(self):
|
||||
service = self.create_service('scalingtest')
|
||||
self.assertEqual(len(service.containers(stopped=True)), 0)
|
||||
|
||||
service.create_container()
|
||||
self.assertEqual(len(service.containers()), 0)
|
||||
self.assertEqual(len(service.containers(stopped=True)), 1)
|
||||
|
||||
service.start()
|
||||
self.assertEqual(len(service.containers()), 1)
|
||||
self.assertEqual(len(service.containers(stopped=True)), 1)
|
||||
|
||||
service.stop(timeout=1)
|
||||
self.assertEqual(len(service.containers()), 0)
|
||||
self.assertEqual(len(service.containers(stopped=True)), 1)
|
||||
|
||||
service.stop(timeout=1)
|
||||
self.assertEqual(len(service.containers()), 0)
|
||||
self.assertEqual(len(service.containers(stopped=True)), 1)
|
||||
|
||||
def test_kill_remove(self):
|
||||
service = self.create_service('scalingtest')
|
||||
|
||||
service.start_container()
|
||||
self.assertEqual(len(service.containers()), 1)
|
||||
|
||||
service.remove_stopped()
|
||||
self.assertEqual(len(service.containers()), 1)
|
||||
|
||||
service.kill()
|
||||
self.assertEqual(len(service.containers()), 0)
|
||||
self.assertEqual(len(service.containers(stopped=True)), 1)
|
||||
|
||||
service.remove_stopped()
|
||||
self.assertEqual(len(service.containers(stopped=True)), 0)
|
||||
|
||||
def test_create_container_with_one_off(self):
|
||||
db = self.create_service('db')
|
||||
container = db.create_container(one_off=True)
|
||||
self.assertEqual(container.name, 'figtest_db_run_1')
|
||||
|
||||
def test_create_container_with_one_off_when_existing_container_is_running(self):
|
||||
db = self.create_service('db')
|
||||
db.start()
|
||||
container = db.create_container(one_off=True)
|
||||
self.assertEqual(container.name, 'figtest_db_run_1')
|
||||
|
||||
def test_create_container_with_unspecified_volume(self):
|
||||
service = self.create_service('db', volumes=['/var/db'])
|
||||
container = service.create_container()
|
||||
service.start_container(container)
|
||||
self.assertIn('/var/db', container.inspect()['Volumes'])
|
||||
|
||||
def test_create_container_with_specified_volume(self):
|
||||
service = self.create_service('db', volumes=['/tmp:/host-tmp'])
|
||||
container = service.create_container()
|
||||
service.start_container(container)
|
||||
self.assertIn('/host-tmp', container.inspect()['Volumes'])
|
||||
|
||||
def test_create_container_with_volumes_from(self):
|
||||
volume_service = self.create_service('data')
|
||||
volume_container_1 = volume_service.create_container()
|
||||
volume_container_2 = Container.create(self.client, image='busybox:latest', command=["/bin/sleep", "300"])
|
||||
host_service = self.create_service('host', volumes_from=[volume_service, volume_container_2])
|
||||
host_container = host_service.create_container()
|
||||
host_service.start_container(host_container)
|
||||
self.assertIn(volume_container_1.id,
|
||||
host_container.get('HostConfig.VolumesFrom'))
|
||||
self.assertIn(volume_container_2.id,
|
||||
host_container.get('HostConfig.VolumesFrom'))
|
||||
|
||||
def test_recreate_containers(self):
|
||||
service = self.create_service(
|
||||
'db',
|
||||
environment={'FOO': '1'},
|
||||
volumes=['/etc'],
|
||||
entrypoint=['sleep'],
|
||||
command=['300']
|
||||
)
|
||||
old_container = service.create_container()
|
||||
self.assertEqual(old_container.dictionary['Config']['Entrypoint'], ['sleep'])
|
||||
self.assertEqual(old_container.dictionary['Config']['Cmd'], ['300'])
|
||||
self.assertIn('FOO=1', old_container.dictionary['Config']['Env'])
|
||||
self.assertEqual(old_container.name, 'figtest_db_1')
|
||||
service.start_container(old_container)
|
||||
volume_path = old_container.inspect()['Volumes']['/etc']
|
||||
|
||||
num_containers_before = len(self.client.containers(all=True))
|
||||
|
||||
service.options['environment']['FOO'] = '2'
|
||||
tuples = service.recreate_containers()
|
||||
self.assertEqual(len(tuples), 1)
|
||||
|
||||
intermediate_container = tuples[0][0]
|
||||
new_container = tuples[0][1]
|
||||
self.assertEqual(intermediate_container.dictionary['Config']['Entrypoint'], ['/bin/echo'])
|
||||
|
||||
self.assertEqual(new_container.dictionary['Config']['Entrypoint'], ['sleep'])
|
||||
self.assertEqual(new_container.dictionary['Config']['Cmd'], ['300'])
|
||||
self.assertIn('FOO=2', new_container.dictionary['Config']['Env'])
|
||||
self.assertEqual(new_container.name, 'figtest_db_1')
|
||||
self.assertEqual(new_container.inspect()['Volumes']['/etc'], volume_path)
|
||||
self.assertIn(intermediate_container.id, new_container.dictionary['HostConfig']['VolumesFrom'])
|
||||
|
||||
self.assertEqual(len(self.client.containers(all=True)), num_containers_before)
|
||||
self.assertNotEqual(old_container.id, new_container.id)
|
||||
self.assertRaises(APIError,
|
||||
self.client.inspect_container,
|
||||
intermediate_container.id)
|
||||
|
||||
def test_recreate_containers_when_containers_are_stopped(self):
|
||||
service = self.create_service(
|
||||
'db',
|
||||
environment={'FOO': '1'},
|
||||
volumes=['/var/db'],
|
||||
entrypoint=['sleep'],
|
||||
command=['300']
|
||||
)
|
||||
old_container = service.create_container()
|
||||
self.assertEqual(len(service.containers(stopped=True)), 1)
|
||||
service.recreate_containers()
|
||||
self.assertEqual(len(service.containers(stopped=True)), 1)
|
||||
|
||||
def test_start_container_passes_through_options(self):
|
||||
db = self.create_service('db')
|
||||
db.start_container(environment={'FOO': 'BAR'})
|
||||
self.assertEqual(db.containers()[0].environment['FOO'], 'BAR')
|
||||
|
||||
def test_start_container_inherits_options_from_constructor(self):
|
||||
db = self.create_service('db', environment={'FOO': 'BAR'})
|
||||
db.start_container()
|
||||
self.assertEqual(db.containers()[0].environment['FOO'], 'BAR')
|
||||
|
||||
def test_start_container_creates_links(self):
|
||||
db = self.create_service('db')
|
||||
web = self.create_service('web', links=[(db, None)])
|
||||
|
||||
db.start_container()
|
||||
db.start_container()
|
||||
web.start_container()
|
||||
|
||||
self.assertEqual(
|
||||
set(web.containers()[0].links()),
|
||||
set([
|
||||
'figtest_db_1', 'db_1',
|
||||
'figtest_db_2', 'db_2',
|
||||
'db',
|
||||
]),
|
||||
)
|
||||
|
||||
def test_start_container_creates_links_with_names(self):
|
||||
db = self.create_service('db')
|
||||
web = self.create_service('web', links=[(db, 'custom_link_name')])
|
||||
|
||||
db.start_container()
|
||||
db.start_container()
|
||||
web.start_container()
|
||||
|
||||
self.assertEqual(
|
||||
set(web.containers()[0].links()),
|
||||
set([
|
||||
'figtest_db_1', 'db_1',
|
||||
'figtest_db_2', 'db_2',
|
||||
'custom_link_name',
|
||||
]),
|
||||
)
|
||||
|
||||
def test_start_normal_container_does_not_create_links_to_its_own_service(self):
|
||||
db = self.create_service('db')
|
||||
|
||||
db.start_container()
|
||||
db.start_container()
|
||||
|
||||
c = db.start_container()
|
||||
self.assertEqual(set(c.links()), set([]))
|
||||
|
||||
def test_start_one_off_container_creates_links_to_its_own_service(self):
|
||||
db = self.create_service('db')
|
||||
|
||||
db.start_container()
|
||||
db.start_container()
|
||||
|
||||
c = db.start_container(one_off=True)
|
||||
|
||||
self.assertEqual(
|
||||
set(c.links()),
|
||||
set([
|
||||
'figtest_db_1', 'db_1',
|
||||
'figtest_db_2', 'db_2',
|
||||
'db',
|
||||
]),
|
||||
)
|
||||
|
||||
def test_start_container_builds_images(self):
|
||||
service = Service(
|
||||
name='test',
|
||||
client=self.client,
|
||||
build='tests/fixtures/simple-dockerfile',
|
||||
project='figtest',
|
||||
)
|
||||
container = service.start_container()
|
||||
container.wait()
|
||||
self.assertIn('success', container.logs())
|
||||
self.assertEqual(len(self.client.images(name='figtest_test')), 1)
|
||||
|
||||
def test_start_container_uses_tagged_image_if_it_exists(self):
|
||||
self.client.build('tests/fixtures/simple-dockerfile', tag='figtest_test')
|
||||
service = Service(
|
||||
name='test',
|
||||
client=self.client,
|
||||
build='this/does/not/exist/and/will/throw/error',
|
||||
project='figtest',
|
||||
)
|
||||
container = service.start_container()
|
||||
container.wait()
|
||||
self.assertIn('success', container.logs())
|
||||
|
||||
def test_start_container_creates_ports(self):
|
||||
service = self.create_service('web', ports=[8000])
|
||||
container = service.start_container().inspect()
|
||||
self.assertEqual(list(container['NetworkSettings']['Ports'].keys()), ['8000/tcp'])
|
||||
self.assertNotEqual(container['NetworkSettings']['Ports']['8000/tcp'][0]['HostPort'], '8000')
|
||||
|
||||
def test_start_container_stays_unpriviliged(self):
|
||||
service = self.create_service('web')
|
||||
container = service.start_container().inspect()
|
||||
self.assertEqual(container['HostConfig']['Privileged'], False)
|
||||
|
||||
def test_start_container_becomes_priviliged(self):
|
||||
service = self.create_service('web', privileged = True)
|
||||
container = service.start_container().inspect()
|
||||
self.assertEqual(container['HostConfig']['Privileged'], True)
|
||||
|
||||
def test_expose_does_not_publish_ports(self):
|
||||
service = self.create_service('web', expose=[8000])
|
||||
container = service.start_container().inspect()
|
||||
self.assertEqual(container['NetworkSettings']['Ports'], {'8000/tcp': None})
|
||||
|
||||
def test_start_container_creates_port_with_explicit_protocol(self):
|
||||
service = self.create_service('web', ports=['8000/udp'])
|
||||
container = service.start_container().inspect()
|
||||
self.assertEqual(list(container['NetworkSettings']['Ports'].keys()), ['8000/udp'])
|
||||
|
||||
def test_start_container_creates_fixed_external_ports(self):
|
||||
service = self.create_service('web', ports=['8000:8000'])
|
||||
container = service.start_container().inspect()
|
||||
self.assertIn('8000/tcp', container['NetworkSettings']['Ports'])
|
||||
self.assertEqual(container['NetworkSettings']['Ports']['8000/tcp'][0]['HostPort'], '8000')
|
||||
|
||||
def test_start_container_creates_fixed_external_ports_when_it_is_different_to_internal_port(self):
|
||||
service = self.create_service('web', ports=['8001:8000'])
|
||||
container = service.start_container().inspect()
|
||||
self.assertIn('8000/tcp', container['NetworkSettings']['Ports'])
|
||||
self.assertEqual(container['NetworkSettings']['Ports']['8000/tcp'][0]['HostPort'], '8001')
|
||||
|
||||
def test_port_with_explicit_interface(self):
|
||||
service = self.create_service('web', ports=[
|
||||
'127.0.0.1:8001:8000',
|
||||
'0.0.0.0:9001:9000/udp',
|
||||
])
|
||||
container = service.start_container().inspect()
|
||||
self.assertEqual(container['NetworkSettings']['Ports'], {
|
||||
'8000/tcp': [
|
||||
{
|
||||
'HostIp': '127.0.0.1',
|
||||
'HostPort': '8001',
|
||||
},
|
||||
],
|
||||
'9000/udp': [
|
||||
{
|
||||
'HostIp': '0.0.0.0',
|
||||
'HostPort': '9001',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
def test_scale(self):
|
||||
service = self.create_service('web')
|
||||
service.scale(1)
|
||||
self.assertEqual(len(service.containers()), 1)
|
||||
service.scale(3)
|
||||
self.assertEqual(len(service.containers()), 3)
|
||||
service.scale(1)
|
||||
self.assertEqual(len(service.containers()), 1)
|
||||
service.scale(0)
|
||||
self.assertEqual(len(service.containers()), 0)
|
||||
|
||||
def test_scale_on_service_that_cannot_be_scaled(self):
|
||||
service = self.create_service('web', ports=['8000:8000'])
|
||||
self.assertRaises(CannotBeScaledError, lambda: service.scale(1))
|
||||
|
||||
def test_scale_sets_ports(self):
|
||||
service = self.create_service('web', ports=['8000'])
|
||||
service.scale(2)
|
||||
containers = service.containers()
|
||||
self.assertEqual(len(containers), 2)
|
||||
for container in containers:
|
||||
self.assertEqual(list(container.inspect()['HostConfig']['PortBindings'].keys()), ['8000/tcp'])
|
||||
|
||||
def test_network_mode_none(self):
|
||||
service = self.create_service('web', net='none')
|
||||
container = service.start_container()
|
||||
self.assertEqual(container.get('HostConfig.NetworkMode'), 'none')
|
||||
|
||||
def test_network_mode_bridged(self):
|
||||
service = self.create_service('web', net='bridge')
|
||||
container = service.start_container()
|
||||
self.assertEqual(container.get('HostConfig.NetworkMode'), 'bridge')
|
||||
|
||||
def test_network_mode_host(self):
|
||||
service = self.create_service('web', net='host')
|
||||
container = service.start_container()
|
||||
self.assertEqual(container.get('HostConfig.NetworkMode'), 'host')
|
||||
|
||||
def test_dns_single_value(self):
|
||||
service = self.create_service('web', dns='8.8.8.8')
|
||||
container = service.start_container().inspect()
|
||||
self.assertEqual(container['HostConfig']['Dns'], ['8.8.8.8'])
|
||||
|
||||
def test_dns_list(self):
|
||||
service = self.create_service('web', dns=['8.8.8.8', '9.9.9.9'])
|
||||
container = service.start_container().inspect()
|
||||
self.assertEqual(container['HostConfig']['Dns'], ['8.8.8.8', '9.9.9.9'])
|
||||
|
||||
def test_working_dir_param(self):
|
||||
service = self.create_service('container', working_dir='/working/dir/sample')
|
||||
container = service.create_container().inspect()
|
||||
self.assertEqual(container['Config']['WorkingDir'], '/working/dir/sample')
|
||||
|
||||
def test_split_env(self):
|
||||
service = self.create_service('web', environment=['NORMAL=F1', 'CONTAINS_EQUALS=F=2', 'TRAILING_EQUALS='])
|
||||
env = service.start_container().environment
|
||||
for k,v in {'NORMAL': 'F1', 'CONTAINS_EQUALS': 'F=2', 'TRAILING_EQUALS': ''}.iteritems():
|
||||
self.assertEqual(env[k], v)
|
||||
|
||||
def test_resolve_env(self):
|
||||
service = self.create_service('web', environment={'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': None, 'NO_DEF': None})
|
||||
os.environ['FILE_DEF'] = 'E1'
|
||||
os.environ['FILE_DEF_EMPTY'] = 'E2'
|
||||
os.environ['ENV_DEF'] = 'E3'
|
||||
try:
|
||||
env = service.start_container().environment
|
||||
for k,v in {'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', 'NO_DEF': ''}.iteritems():
|
||||
self.assertEqual(env[k], v)
|
||||
finally:
|
||||
del os.environ['FILE_DEF']
|
||||
del os.environ['FILE_DEF_EMPTY']
|
||||
del os.environ['ENV_DEF']
|
||||
36
tests/integration/testcases.py
Normal file
36
tests/integration/testcases.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from fig.service import Service
|
||||
from fig.cli.docker_client import docker_client
|
||||
from fig.progress_stream import stream_output
|
||||
from .. import unittest
|
||||
|
||||
|
||||
class DockerClientTestCase(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.client = docker_client()
|
||||
|
||||
def setUp(self):
|
||||
for c in self.client.containers(all=True):
|
||||
if c['Names'] and 'figtest' in c['Names'][0]:
|
||||
self.client.kill(c['Id'])
|
||||
self.client.remove_container(c['Id'])
|
||||
for i in self.client.images():
|
||||
if isinstance(i.get('Tag'), basestring) and 'figtest' in i['Tag']:
|
||||
self.client.remove_image(i)
|
||||
|
||||
def create_service(self, name, **kwargs):
|
||||
if 'command' not in kwargs:
|
||||
kwargs['command'] = ["/bin/sleep", "300"]
|
||||
return Service(
|
||||
project='figtest',
|
||||
name=name,
|
||||
client=self.client,
|
||||
image="busybox:latest",
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def check_build(self, *args, **kwargs):
|
||||
build_output = self.client.build(*args, **kwargs)
|
||||
stream_output(build_output, open('/dev/null', 'w'))
|
||||
@@ -1,124 +0,0 @@
|
||||
from fig.project import Project
|
||||
from fig.service import Service
|
||||
from .testcases import DockerClientTestCase
|
||||
|
||||
|
||||
class ProjectTest(DockerClientTestCase):
|
||||
def test_from_dict(self):
|
||||
project = Project.from_dicts('test', [
|
||||
{
|
||||
'name': 'web',
|
||||
'image': 'ubuntu'
|
||||
},
|
||||
{
|
||||
'name': 'db',
|
||||
'image': 'ubuntu'
|
||||
}
|
||||
], self.client)
|
||||
self.assertEqual(len(project.services), 2)
|
||||
self.assertEqual(project.get_service('web').name, 'web')
|
||||
self.assertEqual(project.get_service('web').options['image'], 'ubuntu')
|
||||
self.assertEqual(project.get_service('db').name, 'db')
|
||||
self.assertEqual(project.get_service('db').options['image'], 'ubuntu')
|
||||
|
||||
def test_from_dict_sorts_in_dependency_order(self):
|
||||
project = Project.from_dicts('test', [
|
||||
{
|
||||
'name': 'web',
|
||||
'image': 'ubuntu',
|
||||
'links': ['db'],
|
||||
},
|
||||
{
|
||||
'name': 'db',
|
||||
'image': 'ubuntu'
|
||||
}
|
||||
], self.client)
|
||||
|
||||
self.assertEqual(project.services[0].name, 'db')
|
||||
self.assertEqual(project.services[1].name, 'web')
|
||||
|
||||
def test_get_service(self):
|
||||
web = self.create_service('web')
|
||||
project = Project('test', [web], self.client)
|
||||
self.assertEqual(project.get_service('web'), web)
|
||||
|
||||
def test_create_containers(self):
|
||||
web = self.create_service('web')
|
||||
db = self.create_service('db')
|
||||
project = Project('test', [web, db], self.client)
|
||||
|
||||
unstarted = project.create_containers(service_names=['web'])
|
||||
self.assertEqual(len(unstarted), 1)
|
||||
self.assertEqual(unstarted[0][0], web)
|
||||
self.assertEqual(len(web.containers(stopped=True)), 1)
|
||||
self.assertEqual(len(db.containers(stopped=True)), 0)
|
||||
|
||||
unstarted = project.create_containers()
|
||||
self.assertEqual(len(unstarted), 2)
|
||||
self.assertEqual(unstarted[0][0], web)
|
||||
self.assertEqual(unstarted[1][0], db)
|
||||
self.assertEqual(len(web.containers(stopped=True)), 2)
|
||||
self.assertEqual(len(db.containers(stopped=True)), 1)
|
||||
|
||||
def test_up(self):
|
||||
web = self.create_service('web')
|
||||
db = self.create_service('db')
|
||||
other = self.create_service('other')
|
||||
project = Project('test', [web, db, other], self.client)
|
||||
|
||||
web.create_container()
|
||||
|
||||
self.assertEqual(len(web.containers()), 0)
|
||||
self.assertEqual(len(db.containers()), 0)
|
||||
self.assertEqual(len(web.containers(stopped=True)), 1)
|
||||
self.assertEqual(len(db.containers(stopped=True)), 0)
|
||||
|
||||
unstarted = project.create_containers(service_names=['web', 'db'])
|
||||
self.assertEqual(len(unstarted), 2)
|
||||
self.assertEqual(unstarted[0][0], web)
|
||||
self.assertEqual(unstarted[1][0], db)
|
||||
|
||||
self.assertEqual(len(web.containers()), 0)
|
||||
self.assertEqual(len(db.containers()), 0)
|
||||
self.assertEqual(len(web.containers(stopped=True)), 2)
|
||||
self.assertEqual(len(db.containers(stopped=True)), 1)
|
||||
|
||||
project.kill_and_remove(unstarted)
|
||||
|
||||
self.assertEqual(len(web.containers()), 0)
|
||||
self.assertEqual(len(db.containers()), 0)
|
||||
self.assertEqual(len(web.containers(stopped=True)), 1)
|
||||
self.assertEqual(len(db.containers(stopped=True)), 0)
|
||||
|
||||
def test_start_stop_kill_remove(self):
|
||||
web = self.create_service('web')
|
||||
db = self.create_service('db')
|
||||
project = Project('test', [web, db], self.client)
|
||||
|
||||
project.start()
|
||||
|
||||
self.assertEqual(len(web.containers()), 0)
|
||||
self.assertEqual(len(db.containers()), 0)
|
||||
|
||||
web_container_1 = web.create_container()
|
||||
web_container_2 = web.create_container()
|
||||
db_container = db.create_container()
|
||||
|
||||
project.start(service_names=['web'])
|
||||
self.assertEqual(set(c.name for c in project.containers()), set([web_container_1.name, web_container_2.name]))
|
||||
|
||||
project.start()
|
||||
self.assertEqual(set(c.name for c in project.containers()), set([web_container_1.name, web_container_2.name, db_container.name]))
|
||||
|
||||
project.stop(service_names=['web'], timeout=1)
|
||||
self.assertEqual(set(c.name for c in project.containers()), set([db_container.name]))
|
||||
|
||||
project.kill(service_names=['db'])
|
||||
self.assertEqual(len(project.containers()), 0)
|
||||
self.assertEqual(len(project.containers(stopped=True)), 3)
|
||||
|
||||
project.remove_stopped(service_names=['web'])
|
||||
self.assertEqual(len(project.containers(stopped=True)), 1)
|
||||
|
||||
project.remove_stopped()
|
||||
self.assertEqual(len(project.containers(stopped=True)), 0)
|
||||
@@ -1,156 +0,0 @@
|
||||
from fig import Service
|
||||
from .testcases import DockerClientTestCase
|
||||
|
||||
|
||||
class ServiceTest(DockerClientTestCase):
|
||||
def test_name_validations(self):
|
||||
self.assertRaises(ValueError, lambda: Service(name=''))
|
||||
|
||||
self.assertRaises(ValueError, lambda: Service(name=' '))
|
||||
self.assertRaises(ValueError, lambda: Service(name='/'))
|
||||
self.assertRaises(ValueError, lambda: Service(name='!'))
|
||||
self.assertRaises(ValueError, lambda: Service(name='\xe2'))
|
||||
self.assertRaises(ValueError, lambda: Service(name='_'))
|
||||
self.assertRaises(ValueError, lambda: Service(name='____'))
|
||||
self.assertRaises(ValueError, lambda: Service(name='foo_bar'))
|
||||
self.assertRaises(ValueError, lambda: Service(name='__foo_bar__'))
|
||||
|
||||
Service('a')
|
||||
Service('foo')
|
||||
|
||||
def test_project_validation(self):
|
||||
self.assertRaises(ValueError, lambda: Service(name='foo', project='_'))
|
||||
Service(name='foo', project='bar')
|
||||
|
||||
def test_containers(self):
|
||||
foo = self.create_service('foo')
|
||||
bar = self.create_service('bar')
|
||||
|
||||
foo.start_container()
|
||||
|
||||
self.assertEqual(len(foo.containers()), 1)
|
||||
self.assertEqual(foo.containers()[0].name, 'default_foo_1')
|
||||
self.assertEqual(len(bar.containers()), 0)
|
||||
|
||||
bar.start_container()
|
||||
bar.start_container()
|
||||
|
||||
self.assertEqual(len(foo.containers()), 1)
|
||||
self.assertEqual(len(bar.containers()), 2)
|
||||
|
||||
names = [c.name for c in bar.containers()]
|
||||
self.assertIn('default_bar_1', names)
|
||||
self.assertIn('default_bar_2', names)
|
||||
|
||||
def test_containers_one_off(self):
|
||||
db = self.create_service('db')
|
||||
container = db.create_container(one_off=True)
|
||||
self.assertEqual(db.containers(stopped=True), [])
|
||||
self.assertEqual(db.containers(one_off=True, stopped=True), [container])
|
||||
|
||||
def test_project_is_added_to_container_name(self):
|
||||
service = self.create_service('web', project='myproject')
|
||||
service.start_container()
|
||||
self.assertEqual(service.containers()[0].name, 'myproject_web_1')
|
||||
|
||||
def test_start_stop(self):
|
||||
service = self.create_service('scalingtest')
|
||||
self.assertEqual(len(service.containers(stopped=True)), 0)
|
||||
|
||||
service.create_container()
|
||||
self.assertEqual(len(service.containers()), 0)
|
||||
self.assertEqual(len(service.containers(stopped=True)), 1)
|
||||
|
||||
service.start()
|
||||
self.assertEqual(len(service.containers()), 1)
|
||||
self.assertEqual(len(service.containers(stopped=True)), 1)
|
||||
|
||||
service.stop(timeout=1)
|
||||
self.assertEqual(len(service.containers()), 0)
|
||||
self.assertEqual(len(service.containers(stopped=True)), 1)
|
||||
|
||||
service.stop(timeout=1)
|
||||
self.assertEqual(len(service.containers()), 0)
|
||||
self.assertEqual(len(service.containers(stopped=True)), 1)
|
||||
|
||||
def test_kill_remove(self):
|
||||
service = self.create_service('scalingtest')
|
||||
|
||||
service.start_container()
|
||||
self.assertEqual(len(service.containers()), 1)
|
||||
|
||||
service.remove_stopped()
|
||||
self.assertEqual(len(service.containers()), 1)
|
||||
|
||||
service.kill()
|
||||
self.assertEqual(len(service.containers()), 0)
|
||||
self.assertEqual(len(service.containers(stopped=True)), 1)
|
||||
|
||||
service.remove_stopped()
|
||||
self.assertEqual(len(service.containers(stopped=True)), 0)
|
||||
|
||||
def test_create_container_with_one_off(self):
|
||||
db = self.create_service('db')
|
||||
container = db.create_container(one_off=True)
|
||||
self.assertEqual(container.name, 'default_db_run_1')
|
||||
|
||||
def test_create_container_with_one_off_when_existing_container_is_running(self):
|
||||
db = self.create_service('db')
|
||||
db.start()
|
||||
container = db.create_container(one_off=True)
|
||||
self.assertEqual(container.name, 'default_db_run_1')
|
||||
|
||||
def test_start_container_passes_through_options(self):
|
||||
db = self.create_service('db')
|
||||
db.start_container(environment={'FOO': 'BAR'})
|
||||
self.assertEqual(db.containers()[0].environment['FOO'], 'BAR')
|
||||
|
||||
def test_start_container_inherits_options_from_constructor(self):
|
||||
db = self.create_service('db', environment={'FOO': 'BAR'})
|
||||
db.start_container()
|
||||
self.assertEqual(db.containers()[0].environment['FOO'], 'BAR')
|
||||
|
||||
def test_start_container_creates_links(self):
|
||||
db = self.create_service('db')
|
||||
web = self.create_service('web', links=[db])
|
||||
db.start_container()
|
||||
web.start_container()
|
||||
self.assertIn('default_db_1', web.containers()[0].links())
|
||||
db.stop(timeout=1)
|
||||
web.stop(timeout=1)
|
||||
|
||||
def test_start_container_builds_images(self):
|
||||
service = Service(
|
||||
name='test',
|
||||
client=self.client,
|
||||
build='tests/fixtures/simple-dockerfile',
|
||||
)
|
||||
container = service.start_container()
|
||||
container.wait()
|
||||
self.assertIn('success', container.logs())
|
||||
self.assertEqual(len(self.client.images(name='default_test')), 1)
|
||||
|
||||
def test_start_container_uses_tagged_image_if_it_exists(self):
|
||||
self.client.build('tests/fixtures/simple-dockerfile', tag='default_test')
|
||||
service = Service(
|
||||
name='test',
|
||||
client=self.client,
|
||||
build='this/does/not/exist/and/will/throw/error',
|
||||
)
|
||||
container = service.start_container()
|
||||
container.wait()
|
||||
self.assertIn('success', container.logs())
|
||||
|
||||
def test_start_container_creates_ports(self):
|
||||
service = self.create_service('web', ports=[8000])
|
||||
container = service.start_container().inspect()
|
||||
self.assertIn('8000/tcp', container['HostConfig']['PortBindings'])
|
||||
self.assertNotEqual(container['HostConfig']['PortBindings']['8000/tcp'][0]['HostPort'], '8000')
|
||||
|
||||
def test_start_container_creates_fixed_external_ports(self):
|
||||
service = self.create_service('web', ports=['8000:8000'])
|
||||
container = service.start_container().inspect()
|
||||
self.assertIn('8000/tcp', container['HostConfig']['PortBindings'])
|
||||
self.assertEqual(container['HostConfig']['PortBindings']['8000/tcp'][0]['HostPort'], '8000')
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
from docker import Client
|
||||
from fig.service import Service
|
||||
import os
|
||||
from unittest import TestCase
|
||||
|
||||
|
||||
class DockerClientTestCase(TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if os.environ.get('DOCKER_URL'):
|
||||
cls.client = Client(os.environ['DOCKER_URL'])
|
||||
else:
|
||||
cls.client = Client()
|
||||
cls.client.pull('ubuntu')
|
||||
|
||||
def setUp(self):
|
||||
for c in self.client.containers(all=True):
|
||||
self.client.kill(c['Id'])
|
||||
self.client.remove_container(c['Id'])
|
||||
|
||||
def create_service(self, name, **kwargs):
|
||||
return Service(
|
||||
name=name,
|
||||
client=self.client,
|
||||
image="ubuntu",
|
||||
command=["/bin/sleep", "300"],
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
|
||||
0
tests/unit/__init__.py
Normal file
0
tests/unit/__init__.py
Normal file
0
tests/unit/cli/__init__.py
Normal file
0
tests/unit/cli/__init__.py
Normal file
30
tests/unit/cli/verbose_proxy_test.py
Normal file
30
tests/unit/cli/verbose_proxy_test.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from tests import unittest
|
||||
|
||||
from fig.cli import verbose_proxy
|
||||
|
||||
|
||||
class VerboseProxy(unittest.TestCase):
|
||||
|
||||
def test_format_call(self):
|
||||
expected = "(u'arg1', True, key=u'value')"
|
||||
actual = verbose_proxy.format_call(
|
||||
("arg1", True),
|
||||
{'key': 'value'})
|
||||
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_format_return_sequence(self):
|
||||
expected = "(list with 10 items)"
|
||||
actual = verbose_proxy.format_return(list(range(10)), 2)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_format_return(self):
|
||||
expected = "{u'Id': u'ok'}"
|
||||
actual = verbose_proxy.format_return({'Id': 'ok'}, 2)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_format_return_no_result(self):
|
||||
actual = verbose_proxy.format_return(None, 2)
|
||||
self.assertEqual(None, actual)
|
||||
69
tests/unit/cli_test.py
Normal file
69
tests/unit/cli_test.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
import logging
|
||||
import os
|
||||
from .. import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from fig.cli import main
|
||||
from fig.cli.main import TopLevelCommand
|
||||
from six import StringIO
|
||||
|
||||
|
||||
class CLITestCase(unittest.TestCase):
|
||||
def test_default_project_name(self):
|
||||
cwd = os.getcwd()
|
||||
|
||||
try:
|
||||
os.chdir('tests/fixtures/simple-figfile')
|
||||
command = TopLevelCommand()
|
||||
project_name = command.get_project_name(command.get_config_path())
|
||||
self.assertEquals('simplefigfile', project_name)
|
||||
finally:
|
||||
os.chdir(cwd)
|
||||
|
||||
def test_project_name_with_explicit_base_dir(self):
|
||||
command = TopLevelCommand()
|
||||
command.base_dir = 'tests/fixtures/simple-figfile'
|
||||
project_name = command.get_project_name(command.get_config_path())
|
||||
self.assertEquals('simplefigfile', project_name)
|
||||
|
||||
def test_project_name_with_explicit_project_name(self):
|
||||
command = TopLevelCommand()
|
||||
name = 'explicit-project-name'
|
||||
project_name = command.get_project_name(None, project_name=name)
|
||||
self.assertEquals('explicitprojectname', project_name)
|
||||
|
||||
def test_project_name_from_environment(self):
|
||||
command = TopLevelCommand()
|
||||
name = 'namefromenv'
|
||||
with mock.patch.dict(os.environ):
|
||||
os.environ['FIG_PROJECT_NAME'] = name
|
||||
project_name = command.get_project_name(None)
|
||||
self.assertEquals(project_name, name)
|
||||
|
||||
def test_yaml_filename_check(self):
|
||||
command = TopLevelCommand()
|
||||
command.base_dir = 'tests/fixtures/longer-filename-figfile'
|
||||
with mock.patch('fig.cli.command.log', autospec=True) as mock_log:
|
||||
self.assertTrue(command.get_config_path())
|
||||
self.assertEqual(mock_log.warning.call_count, 2)
|
||||
|
||||
def test_get_project(self):
|
||||
command = TopLevelCommand()
|
||||
command.base_dir = 'tests/fixtures/longer-filename-figfile'
|
||||
project = command.get_project(command.get_config_path())
|
||||
self.assertEqual(project.name, 'longerfilenamefigfile')
|
||||
self.assertTrue(project.client)
|
||||
self.assertTrue(project.services)
|
||||
|
||||
def test_help(self):
|
||||
command = TopLevelCommand()
|
||||
with self.assertRaises(SystemExit):
|
||||
command.dispatch(['-h'], None)
|
||||
|
||||
def test_setup_logging(self):
|
||||
main.setup_logging()
|
||||
self.assertEqual(logging.getLogger().level, logging.DEBUG)
|
||||
self.assertEqual(logging.getLogger('requests').propagate, False)
|
||||
119
tests/unit/container_test.py
Normal file
119
tests/unit/container_test.py
Normal file
@@ -0,0 +1,119 @@
|
||||
from __future__ import unicode_literals
|
||||
from .. import unittest
|
||||
|
||||
import mock
|
||||
import docker
|
||||
|
||||
from fig.container import Container
|
||||
|
||||
|
||||
class ContainerTest(unittest.TestCase):
|
||||
|
||||
|
||||
def setUp(self):
|
||||
self.container_dict = {
|
||||
"Id": "abc",
|
||||
"Image": "busybox:latest",
|
||||
"Command": "sleep 300",
|
||||
"Created": 1387384730,
|
||||
"Status": "Up 8 seconds",
|
||||
"Ports": None,
|
||||
"SizeRw": 0,
|
||||
"SizeRootFs": 0,
|
||||
"Names": ["/figtest_db_1"],
|
||||
"NetworkSettings": {
|
||||
"Ports": {},
|
||||
},
|
||||
}
|
||||
|
||||
def test_from_ps(self):
|
||||
container = Container.from_ps(None,
|
||||
self.container_dict,
|
||||
has_been_inspected=True)
|
||||
self.assertEqual(container.dictionary, {
|
||||
"Id": "abc",
|
||||
"Image":"busybox:latest",
|
||||
"Name": "/figtest_db_1",
|
||||
})
|
||||
|
||||
def test_environment(self):
|
||||
container = Container(None, {
|
||||
'Id': 'abc',
|
||||
'Config': {
|
||||
'Env': [
|
||||
'FOO=BAR',
|
||||
'BAZ=DOGE',
|
||||
]
|
||||
}
|
||||
}, has_been_inspected=True)
|
||||
self.assertEqual(container.environment, {
|
||||
'FOO': 'BAR',
|
||||
'BAZ': 'DOGE',
|
||||
})
|
||||
|
||||
def test_number(self):
|
||||
container = Container.from_ps(None,
|
||||
self.container_dict,
|
||||
has_been_inspected=True)
|
||||
self.assertEqual(container.number, 1)
|
||||
|
||||
def test_name(self):
|
||||
container = Container.from_ps(None,
|
||||
self.container_dict,
|
||||
has_been_inspected=True)
|
||||
self.assertEqual(container.name, "figtest_db_1")
|
||||
|
||||
def test_name_without_project(self):
|
||||
container = Container.from_ps(None,
|
||||
self.container_dict,
|
||||
has_been_inspected=True)
|
||||
self.assertEqual(container.name_without_project, "db_1")
|
||||
|
||||
def test_inspect_if_not_inspected(self):
|
||||
mock_client = mock.create_autospec(docker.Client)
|
||||
container = Container(mock_client, dict(Id="the_id"))
|
||||
|
||||
container.inspect_if_not_inspected()
|
||||
mock_client.inspect_container.assert_called_once_with("the_id")
|
||||
self.assertEqual(container.dictionary,
|
||||
mock_client.inspect_container.return_value)
|
||||
self.assertTrue(container.has_been_inspected)
|
||||
|
||||
container.inspect_if_not_inspected()
|
||||
self.assertEqual(mock_client.inspect_container.call_count, 1)
|
||||
|
||||
def test_human_readable_ports_none(self):
|
||||
container = Container(None, self.container_dict, has_been_inspected=True)
|
||||
self.assertEqual(container.human_readable_ports, '')
|
||||
|
||||
def test_human_readable_ports_public_and_private(self):
|
||||
self.container_dict['NetworkSettings']['Ports'].update({
|
||||
"45454/tcp": [ { "HostIp": "0.0.0.0", "HostPort": "49197" } ],
|
||||
"45453/tcp": [],
|
||||
})
|
||||
container = Container(None, self.container_dict, has_been_inspected=True)
|
||||
|
||||
expected = "45453/tcp, 0.0.0.0:49197->45454/tcp"
|
||||
self.assertEqual(container.human_readable_ports, expected)
|
||||
|
||||
def test_get_local_port(self):
|
||||
self.container_dict['NetworkSettings']['Ports'].update({
|
||||
"45454/tcp": [ { "HostIp": "0.0.0.0", "HostPort": "49197" } ],
|
||||
})
|
||||
container = Container(None, self.container_dict, has_been_inspected=True)
|
||||
|
||||
self.assertEqual(
|
||||
container.get_local_port(45454, protocol='tcp'),
|
||||
'0.0.0.0:49197')
|
||||
|
||||
def test_get(self):
|
||||
container = Container(None, {
|
||||
"Status":"Up 8 seconds",
|
||||
"HostConfig": {
|
||||
"VolumesFrom": ["volume_id",]
|
||||
},
|
||||
}, has_been_inspected=True)
|
||||
|
||||
self.assertEqual(container.get('Status'), "Up 8 seconds")
|
||||
self.assertEqual(container.get('HostConfig.VolumesFrom'), ["volume_id",])
|
||||
self.assertEqual(container.get('Foo.Bar.DoesNotExist'), None)
|
||||
69
tests/unit/log_printer_test.py
Normal file
69
tests/unit/log_printer_test.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
import os
|
||||
|
||||
from fig.cli.log_printer import LogPrinter
|
||||
from .. import unittest
|
||||
|
||||
|
||||
class LogPrinterTest(unittest.TestCase):
|
||||
def get_default_output(self, monochrome=False):
|
||||
def reader(*args, **kwargs):
|
||||
yield "hello\nworld"
|
||||
|
||||
container = MockContainer(reader)
|
||||
output = run_log_printer([container], monochrome=monochrome)
|
||||
return output
|
||||
|
||||
def test_single_container(self):
|
||||
output = self.get_default_output()
|
||||
|
||||
self.assertIn('hello', output)
|
||||
self.assertIn('world', output)
|
||||
|
||||
def test_monochrome(self):
|
||||
output = self.get_default_output(monochrome=True)
|
||||
self.assertNotIn('\033[', output)
|
||||
|
||||
def test_polychrome(self):
|
||||
output = self.get_default_output()
|
||||
self.assertIn('\033[', output)
|
||||
|
||||
def test_unicode(self):
|
||||
glyph = u'\u2022'.encode('utf-8')
|
||||
|
||||
def reader(*args, **kwargs):
|
||||
yield glyph + b'\n'
|
||||
|
||||
container = MockContainer(reader)
|
||||
output = run_log_printer([container])
|
||||
|
||||
self.assertIn(glyph, output)
|
||||
|
||||
|
||||
def run_log_printer(containers, monochrome=False):
|
||||
r, w = os.pipe()
|
||||
reader, writer = os.fdopen(r, 'r'), os.fdopen(w, 'w')
|
||||
printer = LogPrinter(containers, output=writer, monochrome=monochrome)
|
||||
printer.run()
|
||||
writer.close()
|
||||
return reader.read()
|
||||
|
||||
|
||||
class MockContainer(object):
|
||||
def __init__(self, reader):
|
||||
self._reader = reader
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return 'myapp_web_1'
|
||||
|
||||
@property
|
||||
def name_without_project(self):
|
||||
return 'web_1'
|
||||
|
||||
def attach(self, *args, **kwargs):
|
||||
return self._reader()
|
||||
|
||||
def wait(self, *args, **kwargs):
|
||||
return 0
|
||||
141
tests/unit/project_test.py
Normal file
141
tests/unit/project_test.py
Normal file
@@ -0,0 +1,141 @@
|
||||
from __future__ import unicode_literals
|
||||
from .. import unittest
|
||||
from fig.service import Service
|
||||
from fig.project import Project, ConfigurationError
|
||||
|
||||
class ProjectTest(unittest.TestCase):
|
||||
def test_from_dict(self):
|
||||
project = Project.from_dicts('figtest', [
|
||||
{
|
||||
'name': 'web',
|
||||
'image': 'busybox:latest'
|
||||
},
|
||||
{
|
||||
'name': 'db',
|
||||
'image': 'busybox:latest'
|
||||
},
|
||||
], None)
|
||||
self.assertEqual(len(project.services), 2)
|
||||
self.assertEqual(project.get_service('web').name, 'web')
|
||||
self.assertEqual(project.get_service('web').options['image'], 'busybox:latest')
|
||||
self.assertEqual(project.get_service('db').name, 'db')
|
||||
self.assertEqual(project.get_service('db').options['image'], 'busybox:latest')
|
||||
|
||||
def test_from_dict_sorts_in_dependency_order(self):
|
||||
project = Project.from_dicts('figtest', [
|
||||
{
|
||||
'name': 'web',
|
||||
'image': 'busybox:latest',
|
||||
'links': ['db'],
|
||||
},
|
||||
{
|
||||
'name': 'db',
|
||||
'image': 'busybox:latest',
|
||||
'volumes_from': ['volume']
|
||||
},
|
||||
{
|
||||
'name': 'volume',
|
||||
'image': 'busybox:latest',
|
||||
'volumes': ['/tmp'],
|
||||
}
|
||||
], None)
|
||||
|
||||
self.assertEqual(project.services[0].name, 'volume')
|
||||
self.assertEqual(project.services[1].name, 'db')
|
||||
self.assertEqual(project.services[2].name, 'web')
|
||||
|
||||
def test_from_config(self):
|
||||
project = Project.from_config('figtest', {
|
||||
'web': {
|
||||
'image': 'busybox:latest',
|
||||
},
|
||||
'db': {
|
||||
'image': 'busybox:latest',
|
||||
},
|
||||
}, None)
|
||||
self.assertEqual(len(project.services), 2)
|
||||
self.assertEqual(project.get_service('web').name, 'web')
|
||||
self.assertEqual(project.get_service('web').options['image'], 'busybox:latest')
|
||||
self.assertEqual(project.get_service('db').name, 'db')
|
||||
self.assertEqual(project.get_service('db').options['image'], 'busybox:latest')
|
||||
|
||||
def test_from_config_throws_error_when_not_dict(self):
|
||||
with self.assertRaises(ConfigurationError):
|
||||
project = Project.from_config('figtest', {
|
||||
'web': 'busybox:latest',
|
||||
}, None)
|
||||
|
||||
def test_get_service(self):
|
||||
web = Service(
|
||||
project='figtest',
|
||||
name='web',
|
||||
client=None,
|
||||
image="busybox:latest",
|
||||
)
|
||||
project = Project('test', [web], None)
|
||||
self.assertEqual(project.get_service('web'), web)
|
||||
|
||||
def test_get_services_returns_all_services_without_args(self):
|
||||
web = Service(
|
||||
project='figtest',
|
||||
name='web',
|
||||
)
|
||||
console = Service(
|
||||
project='figtest',
|
||||
name='console',
|
||||
)
|
||||
project = Project('test', [web, console], None)
|
||||
self.assertEqual(project.get_services(), [web, console])
|
||||
|
||||
def test_get_services_returns_listed_services_with_args(self):
|
||||
web = Service(
|
||||
project='figtest',
|
||||
name='web',
|
||||
)
|
||||
console = Service(
|
||||
project='figtest',
|
||||
name='console',
|
||||
)
|
||||
project = Project('test', [web, console], None)
|
||||
self.assertEqual(project.get_services(['console']), [console])
|
||||
|
||||
def test_get_services_with_include_links(self):
|
||||
db = Service(
|
||||
project='figtest',
|
||||
name='db',
|
||||
)
|
||||
web = Service(
|
||||
project='figtest',
|
||||
name='web',
|
||||
links=[(db, 'database')]
|
||||
)
|
||||
cache = Service(
|
||||
project='figtest',
|
||||
name='cache'
|
||||
)
|
||||
console = Service(
|
||||
project='figtest',
|
||||
name='console',
|
||||
links=[(web, 'web')]
|
||||
)
|
||||
project = Project('test', [web, db, cache, console], None)
|
||||
self.assertEqual(
|
||||
project.get_services(['console'], include_links=True),
|
||||
[db, web, console]
|
||||
)
|
||||
|
||||
def test_get_services_removes_duplicates_following_links(self):
|
||||
db = Service(
|
||||
project='figtest',
|
||||
name='db',
|
||||
)
|
||||
web = Service(
|
||||
project='figtest',
|
||||
name='web',
|
||||
links=[(db, 'database')]
|
||||
)
|
||||
project = Project('test', [web, db], None)
|
||||
self.assertEqual(
|
||||
project.get_services(['web', 'db'], include_links=True),
|
||||
[db, web]
|
||||
)
|
||||
218
tests/unit/service_test.py
Normal file
218
tests/unit/service_test.py
Normal file
@@ -0,0 +1,218 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
import os
|
||||
|
||||
from .. import unittest
|
||||
import mock
|
||||
|
||||
import docker
|
||||
|
||||
from fig import Service
|
||||
from fig.container import Container
|
||||
from fig.service import (
|
||||
ConfigError,
|
||||
split_port,
|
||||
parse_volume_spec,
|
||||
build_volume_binding,
|
||||
)
|
||||
|
||||
|
||||
class ServiceTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.mock_client = mock.create_autospec(docker.Client)
|
||||
|
||||
def test_name_validations(self):
|
||||
self.assertRaises(ConfigError, lambda: Service(name=''))
|
||||
|
||||
self.assertRaises(ConfigError, lambda: Service(name=' '))
|
||||
self.assertRaises(ConfigError, lambda: Service(name='/'))
|
||||
self.assertRaises(ConfigError, lambda: Service(name='!'))
|
||||
self.assertRaises(ConfigError, lambda: Service(name='\xe2'))
|
||||
self.assertRaises(ConfigError, lambda: Service(name='_'))
|
||||
self.assertRaises(ConfigError, lambda: Service(name='____'))
|
||||
self.assertRaises(ConfigError, lambda: Service(name='foo_bar'))
|
||||
self.assertRaises(ConfigError, lambda: Service(name='__foo_bar__'))
|
||||
|
||||
Service('a')
|
||||
Service('foo')
|
||||
|
||||
def test_project_validation(self):
|
||||
self.assertRaises(ConfigError, lambda: Service(name='foo', project='_'))
|
||||
Service(name='foo', project='bar')
|
||||
|
||||
def test_config_validation(self):
|
||||
self.assertRaises(ConfigError, lambda: Service(name='foo', port=['8000']))
|
||||
Service(name='foo', ports=['8000'])
|
||||
|
||||
def test_get_volumes_from_container(self):
|
||||
container_id = 'aabbccddee'
|
||||
service = Service(
|
||||
'test',
|
||||
volumes_from=[mock.Mock(id=container_id, spec=Container)])
|
||||
|
||||
self.assertEqual(service._get_volumes_from(), [container_id])
|
||||
|
||||
def test_get_volumes_from_intermediate_container(self):
|
||||
container_id = 'aabbccddee'
|
||||
service = Service('test')
|
||||
container = mock.Mock(id=container_id, spec=Container)
|
||||
|
||||
self.assertEqual(service._get_volumes_from(container), [container_id])
|
||||
|
||||
def test_get_volumes_from_service_container_exists(self):
|
||||
container_ids = ['aabbccddee', '12345']
|
||||
from_service = mock.create_autospec(Service)
|
||||
from_service.containers.return_value = [
|
||||
mock.Mock(id=container_id, spec=Container)
|
||||
for container_id in container_ids
|
||||
]
|
||||
service = Service('test', volumes_from=[from_service])
|
||||
|
||||
self.assertEqual(service._get_volumes_from(), container_ids)
|
||||
|
||||
def test_get_volumes_from_service_no_container(self):
|
||||
container_id = 'abababab'
|
||||
from_service = mock.create_autospec(Service)
|
||||
from_service.containers.return_value = []
|
||||
from_service.create_container.return_value = mock.Mock(
|
||||
id=container_id,
|
||||
spec=Container)
|
||||
service = Service('test', volumes_from=[from_service])
|
||||
|
||||
self.assertEqual(service._get_volumes_from(), [container_id])
|
||||
from_service.create_container.assert_called_once_with()
|
||||
|
||||
def test_split_port_with_host_ip(self):
|
||||
internal_port, external_port = split_port("127.0.0.1:1000:2000")
|
||||
self.assertEqual(internal_port, "2000")
|
||||
self.assertEqual(external_port, ("127.0.0.1", "1000"))
|
||||
|
||||
def test_split_port_with_protocol(self):
|
||||
internal_port, external_port = split_port("127.0.0.1:1000:2000/udp")
|
||||
self.assertEqual(internal_port, "2000/udp")
|
||||
self.assertEqual(external_port, ("127.0.0.1", "1000"))
|
||||
|
||||
def test_split_port_with_host_ip_no_port(self):
|
||||
internal_port, external_port = split_port("127.0.0.1::2000")
|
||||
self.assertEqual(internal_port, "2000")
|
||||
self.assertEqual(external_port, ("127.0.0.1", None))
|
||||
|
||||
def test_split_port_with_host_port(self):
|
||||
internal_port, external_port = split_port("1000:2000")
|
||||
self.assertEqual(internal_port, "2000")
|
||||
self.assertEqual(external_port, "1000")
|
||||
|
||||
def test_split_port_no_host_port(self):
|
||||
internal_port, external_port = split_port("2000")
|
||||
self.assertEqual(internal_port, "2000")
|
||||
self.assertEqual(external_port, None)
|
||||
|
||||
def test_split_port_invalid(self):
|
||||
with self.assertRaises(ConfigError):
|
||||
split_port("0.0.0.0:1000:2000:tcp")
|
||||
|
||||
def test_split_domainname_none(self):
|
||||
service = Service('foo', hostname='name', client=self.mock_client)
|
||||
self.mock_client.containers.return_value = []
|
||||
opts = service._get_container_create_options({})
|
||||
self.assertEqual(opts['hostname'], 'name', 'hostname')
|
||||
self.assertFalse('domainname' in opts, 'domainname')
|
||||
|
||||
def test_split_domainname_fqdn(self):
|
||||
service = Service('foo',
|
||||
hostname='name.domain.tld',
|
||||
client=self.mock_client)
|
||||
self.mock_client.containers.return_value = []
|
||||
opts = service._get_container_create_options({})
|
||||
self.assertEqual(opts['hostname'], 'name', 'hostname')
|
||||
self.assertEqual(opts['domainname'], 'domain.tld', 'domainname')
|
||||
|
||||
def test_split_domainname_both(self):
|
||||
service = Service('foo',
|
||||
hostname='name',
|
||||
domainname='domain.tld',
|
||||
client=self.mock_client)
|
||||
self.mock_client.containers.return_value = []
|
||||
opts = service._get_container_create_options({})
|
||||
self.assertEqual(opts['hostname'], 'name', 'hostname')
|
||||
self.assertEqual(opts['domainname'], 'domain.tld', 'domainname')
|
||||
|
||||
def test_split_domainname_weird(self):
|
||||
service = Service('foo',
|
||||
hostname='name.sub',
|
||||
domainname='domain.tld',
|
||||
client=self.mock_client)
|
||||
self.mock_client.containers.return_value = []
|
||||
opts = service._get_container_create_options({})
|
||||
self.assertEqual(opts['hostname'], 'name.sub', 'hostname')
|
||||
self.assertEqual(opts['domainname'], 'domain.tld', 'domainname')
|
||||
|
||||
def test_get_container_not_found(self):
|
||||
mock_client = mock.create_autospec(docker.Client)
|
||||
mock_client.containers.return_value = []
|
||||
service = Service('foo', client=mock_client)
|
||||
|
||||
self.assertRaises(ValueError, service.get_container)
|
||||
|
||||
@mock.patch('fig.service.Container', autospec=True)
|
||||
def test_get_container(self, mock_container_class):
|
||||
mock_client = mock.create_autospec(docker.Client)
|
||||
container_dict = dict(Name='default_foo_2')
|
||||
mock_client.containers.return_value = [container_dict]
|
||||
service = Service('foo', client=mock_client)
|
||||
|
||||
container = service.get_container(number=2)
|
||||
self.assertEqual(container, mock_container_class.from_ps.return_value)
|
||||
mock_container_class.from_ps.assert_called_once_with(
|
||||
mock_client, container_dict)
|
||||
|
||||
@mock.patch('fig.service.log', autospec=True)
|
||||
def test_pull_image(self, mock_log):
|
||||
service = Service('foo', client=self.mock_client, image='someimage:sometag')
|
||||
service.pull(insecure_registry=True)
|
||||
self.mock_client.pull.assert_called_once_with('someimage:sometag', insecure_registry=True)
|
||||
mock_log.info.assert_called_once_with('Pulling foo (someimage:sometag)...')
|
||||
|
||||
|
||||
class ServiceVolumesTest(unittest.TestCase):
|
||||
|
||||
def test_parse_volume_spec_only_one_path(self):
|
||||
spec = parse_volume_spec('/the/volume')
|
||||
self.assertEqual(spec, (None, '/the/volume', 'rw'))
|
||||
|
||||
def test_parse_volume_spec_internal_and_external(self):
|
||||
spec = parse_volume_spec('external:interval')
|
||||
self.assertEqual(spec, ('external', 'interval', 'rw'))
|
||||
|
||||
def test_parse_volume_spec_with_mode(self):
|
||||
spec = parse_volume_spec('external:interval:ro')
|
||||
self.assertEqual(spec, ('external', 'interval', 'ro'))
|
||||
|
||||
def test_parse_volume_spec_too_many_parts(self):
|
||||
with self.assertRaises(ConfigError):
|
||||
parse_volume_spec('one:two:three:four')
|
||||
|
||||
def test_parse_volume_bad_mode(self):
|
||||
with self.assertRaises(ConfigError):
|
||||
parse_volume_spec('one:two:notrw')
|
||||
|
||||
def test_build_volume_binding(self):
|
||||
binding = build_volume_binding(parse_volume_spec('/outside:/inside'))
|
||||
self.assertEqual(
|
||||
binding,
|
||||
('/outside', dict(bind='/inside', ro=False)))
|
||||
|
||||
@mock.patch.dict(os.environ)
|
||||
def test_build_volume_binding_with_environ(self):
|
||||
os.environ['VOLUME_PATH'] = '/opt'
|
||||
binding = build_volume_binding(parse_volume_spec('${VOLUME_PATH}:/opt'))
|
||||
self.assertEqual(binding, ('/opt', dict(bind='/opt', ro=False)))
|
||||
|
||||
@mock.patch.dict(os.environ)
|
||||
def test_building_volume_binding_with_home(self):
|
||||
os.environ['HOME'] = '/home/user'
|
||||
binding = build_volume_binding(parse_volume_spec('~:/home/user'))
|
||||
self.assertEqual(
|
||||
binding,
|
||||
('/home/user', dict(bind='/home/user', ro=False)))
|
||||
148
tests/unit/sort_service_test.py
Normal file
148
tests/unit/sort_service_test.py
Normal file
@@ -0,0 +1,148 @@
|
||||
from fig.project import sort_service_dicts, DependencyError
|
||||
from .. import unittest
|
||||
|
||||
|
||||
class SortServiceTest(unittest.TestCase):
|
||||
def test_sort_service_dicts_1(self):
|
||||
services = [
|
||||
{
|
||||
'links': ['redis'],
|
||||
'name': 'web'
|
||||
},
|
||||
{
|
||||
'name': 'grunt'
|
||||
},
|
||||
{
|
||||
'name': 'redis'
|
||||
}
|
||||
]
|
||||
|
||||
sorted_services = sort_service_dicts(services)
|
||||
self.assertEqual(len(sorted_services), 3)
|
||||
self.assertEqual(sorted_services[0]['name'], 'grunt')
|
||||
self.assertEqual(sorted_services[1]['name'], 'redis')
|
||||
self.assertEqual(sorted_services[2]['name'], 'web')
|
||||
|
||||
def test_sort_service_dicts_2(self):
|
||||
services = [
|
||||
{
|
||||
'links': ['redis', 'postgres'],
|
||||
'name': 'web'
|
||||
},
|
||||
{
|
||||
'name': 'postgres',
|
||||
'links': ['redis']
|
||||
},
|
||||
{
|
||||
'name': 'redis'
|
||||
}
|
||||
]
|
||||
|
||||
sorted_services = sort_service_dicts(services)
|
||||
self.assertEqual(len(sorted_services), 3)
|
||||
self.assertEqual(sorted_services[0]['name'], 'redis')
|
||||
self.assertEqual(sorted_services[1]['name'], 'postgres')
|
||||
self.assertEqual(sorted_services[2]['name'], 'web')
|
||||
|
||||
def test_sort_service_dicts_3(self):
|
||||
services = [
|
||||
{
|
||||
'name': 'child'
|
||||
},
|
||||
{
|
||||
'name': 'parent',
|
||||
'links': ['child']
|
||||
},
|
||||
{
|
||||
'links': ['parent'],
|
||||
'name': 'grandparent'
|
||||
},
|
||||
]
|
||||
|
||||
sorted_services = sort_service_dicts(services)
|
||||
self.assertEqual(len(sorted_services), 3)
|
||||
self.assertEqual(sorted_services[0]['name'], 'child')
|
||||
self.assertEqual(sorted_services[1]['name'], 'parent')
|
||||
self.assertEqual(sorted_services[2]['name'], 'grandparent')
|
||||
|
||||
def test_sort_service_dicts_circular_imports(self):
|
||||
services = [
|
||||
{
|
||||
'links': ['redis'],
|
||||
'name': 'web'
|
||||
},
|
||||
{
|
||||
'name': 'redis',
|
||||
'links': ['web']
|
||||
},
|
||||
]
|
||||
|
||||
try:
|
||||
sort_service_dicts(services)
|
||||
except DependencyError as e:
|
||||
self.assertIn('redis', e.msg)
|
||||
self.assertIn('web', e.msg)
|
||||
else:
|
||||
self.fail('Should have thrown an DependencyError')
|
||||
|
||||
def test_sort_service_dicts_circular_imports_2(self):
|
||||
services = [
|
||||
{
|
||||
'links': ['postgres', 'redis'],
|
||||
'name': 'web'
|
||||
},
|
||||
{
|
||||
'name': 'redis',
|
||||
'links': ['web']
|
||||
},
|
||||
{
|
||||
'name': 'postgres'
|
||||
}
|
||||
]
|
||||
|
||||
try:
|
||||
sort_service_dicts(services)
|
||||
except DependencyError as e:
|
||||
self.assertIn('redis', e.msg)
|
||||
self.assertIn('web', e.msg)
|
||||
else:
|
||||
self.fail('Should have thrown an DependencyError')
|
||||
|
||||
def test_sort_service_dicts_circular_imports_3(self):
|
||||
services = [
|
||||
{
|
||||
'links': ['b'],
|
||||
'name': 'a'
|
||||
},
|
||||
{
|
||||
'name': 'b',
|
||||
'links': ['c']
|
||||
},
|
||||
{
|
||||
'name': 'c',
|
||||
'links': ['a']
|
||||
}
|
||||
]
|
||||
|
||||
try:
|
||||
sort_service_dicts(services)
|
||||
except DependencyError as e:
|
||||
self.assertIn('a', e.msg)
|
||||
self.assertIn('b', e.msg)
|
||||
else:
|
||||
self.fail('Should have thrown an DependencyError')
|
||||
|
||||
def test_sort_service_dicts_self_imports(self):
|
||||
services = [
|
||||
{
|
||||
'links': ['web'],
|
||||
'name': 'web'
|
||||
},
|
||||
]
|
||||
|
||||
try:
|
||||
sort_service_dicts(services)
|
||||
except DependencyError as e:
|
||||
self.assertIn('web', e.msg)
|
||||
else:
|
||||
self.fail('Should have thrown an DependencyError')
|
||||
52
tests/unit/split_buffer_test.py
Normal file
52
tests/unit/split_buffer_test.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from fig.cli.utils import split_buffer
|
||||
from .. import unittest
|
||||
|
||||
class SplitBufferTest(unittest.TestCase):
|
||||
def test_single_line_chunks(self):
|
||||
def reader():
|
||||
yield b'abc\n'
|
||||
yield b'def\n'
|
||||
yield b'ghi\n'
|
||||
|
||||
self.assert_produces(reader, [b'abc\n', b'def\n', b'ghi\n'])
|
||||
|
||||
def test_no_end_separator(self):
|
||||
def reader():
|
||||
yield b'abc\n'
|
||||
yield b'def\n'
|
||||
yield b'ghi'
|
||||
|
||||
self.assert_produces(reader, [b'abc\n', b'def\n', b'ghi'])
|
||||
|
||||
def test_multiple_line_chunk(self):
|
||||
def reader():
|
||||
yield b'abc\ndef\nghi'
|
||||
|
||||
self.assert_produces(reader, [b'abc\n', b'def\n', b'ghi'])
|
||||
|
||||
def test_chunked_line(self):
|
||||
def reader():
|
||||
yield b'a'
|
||||
yield b'b'
|
||||
yield b'c'
|
||||
yield b'\n'
|
||||
yield b'd'
|
||||
|
||||
self.assert_produces(reader, [b'abc\n', b'd'])
|
||||
|
||||
def test_preserves_unicode_sequences_within_lines(self):
|
||||
string = u"a\u2022c\n".encode('utf-8')
|
||||
|
||||
def reader():
|
||||
yield string
|
||||
|
||||
self.assert_produces(reader, [string])
|
||||
|
||||
def assert_produces(self, reader, expectations):
|
||||
split = split_buffer(reader(), b'\n')
|
||||
|
||||
for (actual, expected) in zip(split, expectations):
|
||||
self.assertEqual(type(actual), type(expected))
|
||||
self.assertEqual(actual, expected)
|
||||
15
tox.ini
Normal file
15
tox.ini
Normal file
@@ -0,0 +1,15 @@
|
||||
[tox]
|
||||
envlist = py26,py27,py32,py33,pypy
|
||||
|
||||
[testenv]
|
||||
usedevelop=True
|
||||
deps =
|
||||
-rrequirements.txt
|
||||
-rrequirements-dev.txt
|
||||
commands =
|
||||
nosetests {posargs}
|
||||
flake8 fig
|
||||
|
||||
[flake8]
|
||||
# ignore line-length for now
|
||||
ignore = E501,E203
|
||||
12
wercker.yml
Normal file
12
wercker.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
box: wercker-labs/docker
|
||||
build:
|
||||
steps:
|
||||
- script:
|
||||
name: validate DCO
|
||||
code: script/validate-dco
|
||||
- script:
|
||||
name: run tests
|
||||
code: script/test
|
||||
- script:
|
||||
name: build binary
|
||||
code: script/build-linux
|
||||
Reference in New Issue
Block a user